Skip to content

Commit

Permalink
feat(sdk/skyux-eslint): add no-deprecated-classnames rule (#2953)
Browse files Browse the repository at this point in the history
  • Loading branch information
Blackbaud-SteveBrush authored Dec 18, 2024
1 parent 7f34a3f commit 5ca8de6
Show file tree
Hide file tree
Showing 11 changed files with 218 additions and 8 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
</ng-template>
<ng-template #emphasizedColumn let-value="value" let-row="row">
@if (row['jobLevel']) {
<span class="sky-margin-inline-default sky-pull-right">{{
<span class="sky-margin-inline-sm sky-pull-right">{{
row['jobLevel']
}}</span>
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="sky-padding-even-large">
<div class="sky-padding-even-xl">
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<sky-checkbox-group
data-sky-id="checkbox-group"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="sky-padding-even-large">
<div class="sky-padding-even-xl">
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<sky-checkbox-group
data-sky-id="checkbox-group"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
<div class="sky-padding-even-large">
<div class="sky-padding-even-xl">
<form [formGroup]="formGroup" (ngSubmit)="onSubmit()">
<sky-checkbox-group
headingText="Text formatting"
Expand Down
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<button
aria-haspopup="dialog"
class="sky-btn sky-btn-default sky-margin-inline-default ok-confirm-btn"
class="sky-btn sky-btn-default sky-margin-inline-sm ok-confirm-btn"
type="button"
(click)="openOKConfirm()"
>
Expand All @@ -9,7 +9,7 @@

<button
aria-haspopup="dialog"
class="sky-btn sky-btn-default sky-margin-inline-default two-action-confirm-btn"
class="sky-btn sky-btn-default sky-margin-inline-sm two-action-confirm-btn"
type="button"
(click)="openTwoActionConfirm()"
>
Expand All @@ -18,7 +18,7 @@

<button
aria-haspopup="dialog"
class="sky-btn sky-btn-default sky-margin-inline-default"
class="sky-btn sky-btn-default sky-margin-inline-sm"
type="button"
(click)="openThreeActionConfirm()"
>
Expand All @@ -27,7 +27,7 @@

<button
aria-haspopup="dialog"
class="sky-btn sky-btn-default sky-margin-inline-default"
class="sky-btn sky-btn-default sky-margin-inline-sm"
type="button"
(click)="openDeleteConfirm()"
>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
# `skyux-eslint-template/no-deprecated-classnames`

Prevents usage of deprecated CSS classnames in HTML templates.

- Type: problem

<br>

## Rule Options

The rule does not have any configuration options.

<br>

## Usage Examples

#### Default Config

```json
{
"rules": {
"skyux-eslint-template/no-deprecated-classnames": ["error"]
}
}
```

#### ❌ Invalid Code

```html
<div class="sky-margin-inline-compact"></div>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
```
1 change: 1 addition & 0 deletions libs/sdk/skyux-eslint/src/configs/template-all.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default [
{
name: 'skyux-eslint-template-all',
rules: {
'skyux-eslint-template/no-deprecated-classnames': 'error',
'skyux-eslint-template/no-deprecated-directives': 'error',
'skyux-eslint-template/no-radio-group-with-nested-list': 'error',
'skyux-eslint-template/no-unbound-id': 'error',
Expand Down
1 change: 1 addition & 0 deletions libs/sdk/skyux-eslint/src/configs/template-recommended.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ export default [
{
name: 'skyux-eslint-template-all',
rules: {
'skyux-eslint-template/no-deprecated-classnames': 'error',
'skyux-eslint-template/no-deprecated-directives': 'error',
'skyux-eslint-template/no-radio-group-with-nested-list': 'error',
'skyux-eslint-template/no-unbound-id': 'error',
Expand Down
5 changes: 5 additions & 0 deletions libs/sdk/skyux-eslint/src/plugins/template-plugin.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,10 @@
import { processors } from '@angular-eslint/eslint-plugin-template';
import type { TSESLint } from '@typescript-eslint/utils';

import {
rule as noDeprecatedClassnames,
RULE_NAME as noDeprecatedClassnamesRuleName,
} from '../rules/template/no-deprecated-classnames';
import {
rule as noDeprecatedDirectives,
RULE_NAME as noDeprecatedDirectivesRuleName,
Expand All @@ -24,6 +28,7 @@ export default {
},
processors,
rules: {
[noDeprecatedClassnamesRuleName]: noDeprecatedClassnames,
[noDeprecatedDirectivesRuleName]: noDeprecatedDirectives,
[noRadioGroupWithNestedListRuleName]: noRadioGroupWithNestedList,
[noUnboundIdRuleName]: noUnboundId,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
import { convertAnnotatedSourceToFailureCase } from '@angular-eslint/test-utils';

import { createTemplateRuleTester } from '../testing/create-template-rule-tester';

import { RULE_NAME, messageId, rule } from './no-deprecated-classnames';

const ruleTester = createTemplateRuleTester();

ruleTester.run(RULE_NAME, rule, {
valid: [``],
invalid: [
convertAnnotatedSourceToFailureCase({
description: 'should fail with one classname',
annotatedSource: `
<div class="sky-margin-inline-compact"></div>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`,
annotatedOutput: `
<div class="sky-margin-inline-xs"></div>
~
`,
messageId,
data: {
deprecated: 'sky-margin-inline-compact',
replacement: 'sky-margin-inline-xs',
},
}),
convertAnnotatedSourceToFailureCase({
description: 'should fail with multiple classnames',
annotatedSource: `
<div class="sky-margin-inline-compact sky-field-label"></div>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`,
annotatedOutput: `
<div class="sky-margin-inline-xs sky-font-data-label"></div>
~
`,
messageId,
data: {
deprecated: 'sky-margin-inline-compact, sky-field-label',
replacement: 'sky-margin-inline-xs, sky-font-data-label',
},
}),
convertAnnotatedSourceToFailureCase({
description: 'should ignore other classnames',
annotatedSource: `
<div class="sky-margin-inline-compact foobar sky-field-label"></div>
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
`,
annotatedOutput: `
<div class="sky-margin-inline-xs foobar sky-font-data-label"></div>
~
`,
messageId,
data: {
deprecated: 'sky-margin-inline-compact, sky-field-label',
replacement: 'sky-margin-inline-xs, sky-font-data-label',
},
}),
],
});
110 changes: 110 additions & 0 deletions libs/sdk/skyux-eslint/src/rules/template/no-deprecated-classnames.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,110 @@
import { TmplAstTextAttribute } from '@angular-eslint/bundled-angular-compiler';
import { getTemplateParserServices } from '@angular-eslint/utils';
import { RuleFix } from '@typescript-eslint/utils/dist/ts-eslint';

import { createESLintTemplateRule } from '../utils/create-eslint-template-rule';

export const RULE_NAME = 'no-deprecated-classnames';
export const messageId = 'noDeprecatedClassnames';

/**
* A map of deprecated classnames to their replacements.
*/
export const DEPRECATED_CLASSNAMES: Record<string, string> = {
'sky-margin-inline-compact': 'sky-margin-inline-xs',
'sky-margin-inline-default': 'sky-margin-inline-sm',
'sky-margin-stacked-compact': 'sky-margin-stacked-xs',
'sky-margin-stacked-default': 'sky-margin-stacked-lg',
'sky-margin-stacked-separate': 'sky-margin-stacked-xl',
'sky-padding-even-default': 'sky-padding-even-md',
'sky-padding-even-large': 'sky-padding-even-xl',
'sky-page-heading': 'sky-font-heading-1',
'sky-section-heading': 'sky-font-heading-2',
'sky-subsection-heading': 'sky-font-heading-3',
'sky-headline': 'sky-font-display-3',
'sky-emphasized': 'sky-font-emphasized',
'sky-deemphasized': 'sky-font-deemphasized',
'sky-field-label': 'sky-font-data-label',
};

function getDeprecatedClasses(
existingClasses: string[],
): Record<string, string> {
const found: Record<string, string> = {};

for (const existingClassname of existingClasses) {
const replacement = DEPRECATED_CLASSNAMES[existingClassname];

if (replacement) {
found[existingClassname] = DEPRECATED_CLASSNAMES[existingClassname];
}
}

return found;
}

export const rule = createESLintTemplateRule({
create(context) {
const parserServices = getTemplateParserServices(context);

return {
[`Element$1 > :matches(TextAttribute)[name="class"]`](
attr: TmplAstTextAttribute,
): void {
const existing = attr.value.split(' ');
const found = getDeprecatedClasses(existing);

const deprecatedClasses = Object.keys(found);
const replacementClasses = Object.values(found);

if (deprecatedClasses.length > 0) {
for (const deprecatedClassname of deprecatedClasses) {
const index = existing.indexOf(deprecatedClassname);

if (index > -1) {
existing[index] = found[deprecatedClassname];
}
}

context.report({
loc: parserServices.convertNodeSourceSpanToLoc(attr.sourceSpan),
messageId,
data: {
deprecated: deprecatedClasses.join(', '),
replacement: replacementClasses.join(', '),
},
fix: () => {
const fixers: RuleFix[] = [];

if (attr.valueSpan) {
fixers.push({
range: [
attr.valueSpan.start.offset,
attr.valueSpan.end.offset,
],
text: existing.join(' '),
});
}

return fixers;
},
});
}
},
};
},
defaultOptions: [],
meta: {
docs: {
description: 'Avoid using deprecated CSS classes.',
},
messages: {
[messageId]:
'The CSS classes ({{deprecated}}) are deprecated. Use ({{replacement}}) instead.',
},
schema: [],
type: 'problem',
fixable: 'code',
},
name: RULE_NAME,
});

0 comments on commit 5ca8de6

Please sign in to comment.