Skip to content

Commit 8cf8337

Browse files
committed
feat(lint): deprecation rules
Signed-off-by: Cory Rylan <crylan@nvidia.com>
1 parent 50b89f8 commit 8cf8337

20 files changed

Lines changed: 1010 additions & 155 deletions

projects/lint/README.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -66,7 +66,7 @@ export default [
6666
| `@nvidia-elements/lint/no-deprecated-attributes` | Disallow use of deprecated attributes in HTML. | HTML | `error` |
6767
| `@nvidia-elements/lint/no-deprecated-global-attribute-value` | Disallow use of deprecated attribute values for nve-* utility attributes. | HTML | `error` |
6868
| `@nvidia-elements/lint/no-deprecated-css-imports` | Disallow use of deprecated CSS import paths. | CSS | `error` |
69-
| `@nvidia-elements/lint/no-deprecated-css-variable` | Disallow use of deprecated --mlv-* CSS theme variables. | CSS | `error` |
69+
| `@nvidia-elements/lint/no-deprecated-css-variable` | Disallow use of deprecated CSS custom properties. | CSS/HTML | `error` |
7070
| `@nvidia-elements/lint/no-deprecated-global-attributes` | Disallow use of deprecated global utility attributes in HTML. | HTML | `error` |
7171
| `@nvidia-elements/lint/no-deprecated-icon-names` | Disallow use of deprecated icon names. | HTML | `error` |
7272
| `@nvidia-elements/lint/no-deprecated-packages` | Disallow usage of deprecated packages. | JSON | `error` |
@@ -80,6 +80,7 @@ export default [
8080
| `@nvidia-elements/lint/no-missing-icon-name` | Require icon elements to have an icon name attribute. | HTML | `error` |
8181
| `@nvidia-elements/lint/no-missing-popover-trigger` | Require popover elements to have a corresponding trigger element. | HTML | `error` |
8282
| `@nvidia-elements/lint/no-missing-slotted-elements` | Disallow use of missing slotted elements. | HTML | `error` |
83+
| `@nvidia-elements/lint/no-misprefixed-tags` | Disallow misprefixed Elements tags that resolve to a known nve-* element. | HTML | `error` |
8384
| `@nvidia-elements/lint/no-nested-container-types` | Require nested container components to use flat container mode. | HTML | `error` |
8485
| `@nvidia-elements/lint/no-restricted-attributes` | Disallow use of invalid API attributes or utility attributes on custom HTML element tags. | HTML | `error` |
8586
| `@nvidia-elements/lint/no-restricted-page-sizing` | Disallow custom height or width styles on nve-page. | HTML | `error` |

projects/lint/src/eslint/configs/html.ts

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import noDeprecatedPopoverAttributes from '../rules/no-deprecated-popover-attrib
1111
import noUnexpectedGlobalAttributeValue from '../rules/no-unexpected-global-attribute-value.js';
1212
import noUnexpectedStyleCustomization from '../rules/no-unexpected-style-customization.js';
1313
import noDeprecatedGlobalAttributeValue from '../rules/no-deprecated-global-attribute-value.js';
14+
import noDeprecatedCssVariable from '../rules/no-deprecated-css-variable.js';
1415
import noDeprecatedGlobalAttributes from '../rules/no-deprecated-global-attributes.js';
1516
import noRestrictedAttributes from '../rules/no-restricted-attributes.js';
1617
import noSlottedPopovers from '../rules/no-slotted-popovers.js';
@@ -19,6 +20,7 @@ import noMissingSlottedElements from '../rules/no-missing-slotted-elements.js';
1920
import noMissingControlLabel from '../rules/no-missing-control-label.js';
2021
import noMissingIconName from '../rules/no-missing-icon-name.js';
2122
import noMissingPopoverTrigger from '../rules/no-missing-popover-trigger.js';
23+
import noMisprefixedTags from '../rules/no-misprefixed-tags.js';
2224
import noUnexpectedSlotValue from '../rules/no-unexpected-slot-value.js';
2325
import noUnknownTags from '../rules/no-unknown-tags.js';
2426
import noUnexpectedAttributeValue from '../rules/no-unexpected-attribute-value.js';
@@ -64,13 +66,15 @@ export const elementsHtmlConfig: Linter.Config = {
6466
'no-deprecated-attributes': noDeprecatedAttributes,
6567
'no-deprecated-icon-names': noDeprecatedIconNames,
6668
'no-deprecated-popover-attributes': noDeprecatedPopoverAttributes,
69+
'no-deprecated-css-variable': noDeprecatedCssVariable,
6770
'no-deprecated-global-attribute-value': noDeprecatedGlobalAttributeValue,
6871
'no-deprecated-global-attributes': noDeprecatedGlobalAttributes,
6972
'no-deprecated-slots': noDeprecatedSlots,
7073
'no-missing-slotted-elements': noMissingSlottedElements,
7174
'no-missing-control-label': noMissingControlLabel,
7275
'no-missing-icon-name': noMissingIconName,
7376
'no-missing-popover-trigger': noMissingPopoverTrigger,
77+
'no-misprefixed-tags': noMisprefixedTags,
7478
'no-restricted-attributes': noRestrictedAttributes,
7579
'no-restricted-page-sizing': noRestrictedPageSizing,
7680
'no-slotted-popovers': noSlottedPopovers,
@@ -96,13 +100,15 @@ export const elementsHtmlConfig: Linter.Config = {
96100
'@nvidia-elements/lint/no-deprecated-attributes': ['error'],
97101
'@nvidia-elements/lint/no-deprecated-icon-names': ['error'],
98102
'@nvidia-elements/lint/no-deprecated-popover-attributes': ['error'],
103+
'@nvidia-elements/lint/no-deprecated-css-variable': ['error'],
99104
'@nvidia-elements/lint/no-deprecated-global-attribute-value': ['error'],
100105
'@nvidia-elements/lint/no-deprecated-global-attributes': ['error'],
101106
'@nvidia-elements/lint/no-deprecated-slots': ['error'],
102107
'@nvidia-elements/lint/no-missing-slotted-elements': ['error'],
103108
'@nvidia-elements/lint/no-missing-control-label': ['error'],
104109
'@nvidia-elements/lint/no-missing-icon-name': ['error'],
105110
'@nvidia-elements/lint/no-missing-popover-trigger': ['error'],
111+
'@nvidia-elements/lint/no-misprefixed-tags': ['error'],
106112
'@nvidia-elements/lint/no-restricted-attributes': ['error'],
107113
'@nvidia-elements/lint/no-restricted-page-sizing': ['error'],
108114
'@nvidia-elements/lint/no-slotted-popovers': ['error'],

projects/lint/src/eslint/internals/attributes.ts

Lines changed: 6 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -6,8 +6,6 @@ import { globalAttributes } from './metadata.js';
66
export const VALUE_BINDINGS = ['${', '{', '{{', '{%'];
77

88
const ATTRIBUTE_EXCEPTIONS = ['debug', 'mkd', 'md']; // internal scopes
9-
const DEPRECATED_NVE_TEXT_VALUES = new Set(['eyebrow']);
10-
const DEPRECATED_NVE_LAYOUT_VALUES = new Set(['grow']);
119

1210
const VALID_NVE_TEXT_VALUES = new Set([
1311
...(globalAttributes.find(attribute => attribute.name === 'nve-text')?.values?.map(value => value.name) ?? []),
@@ -24,12 +22,10 @@ const VALID_NVE_DISPLAY_VALUES = new Set([
2422
...ATTRIBUTE_EXCEPTIONS
2523
]);
2624

27-
export const DISTILLED_NVE_TEXT_VALUES = new Set(
28-
[...VALID_NVE_TEXT_VALUES].filter(v => !isComplexAttributeValue(v) && !DEPRECATED_NVE_TEXT_VALUES.has(v))
29-
);
25+
export const DISTILLED_NVE_TEXT_VALUES = new Set([...VALID_NVE_TEXT_VALUES].filter(v => !isComplexAttributeValue(v)));
3026

3127
export const DISTILLED_NVE_LAYOUT_VALUES = new Set(
32-
[...VALID_NVE_LAYOUT_VALUES].filter(v => !isComplexAttributeValue(v) && !DEPRECATED_NVE_LAYOUT_VALUES.has(v))
28+
[...VALID_NVE_LAYOUT_VALUES].filter(v => !isComplexAttributeValue(v))
3329
);
3430

3531
export const DISTILLED_NVE_DISPLAY_VALUES = new Set(
@@ -68,7 +64,8 @@ export function recommendedNveTextValue(attributeValue: string): string | null {
6864
[/^heading-1$/, 'heading'],
6965
[/^heading:1$/, 'heading'],
7066
[/^heading-2$/, 'heading'],
71-
[/^heading:2$/, 'heading']
67+
[/^heading:2$/, 'heading'],
68+
[/^eyebrow$/, 'label sm']
7269
];
7370

7471
const result: string[] = repairAttributeValueSegments(values, repairs);
@@ -94,6 +91,7 @@ export function recommendedNveLayoutValue(attributeValue: string, invalidSymbols
9491
[/^default$/, 'column'],
9592
[/^stack$/, 'column'],
9693
[/^col$/, 'column'],
94+
[/^grow$/, 'full'],
9795
[/^inline$/, 'row'],
9896
[/^center$/, 'align:center'],
9997
[/^wrap$/, 'align:wrap'],
@@ -161,7 +159,7 @@ function getAttributeValueSegments(value: string) {
161159
}
162160

163161
function repairAttributeValueSegments(values: string[], repairs: [RegExp, string][]) {
164-
return values.map(value => repairStringValue(value, repairs));
162+
return values.flatMap(value => repairStringValue(value, repairs).split(/\s+/).filter(Boolean));
165163
}
166164

167165
function repairStringValue(value: string, repairs: [RegExp, string][]) {

projects/lint/src/eslint/rule-types.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,9 @@ export interface HtmlTagNode {
1010
attributes?: HtmlAttribute[];
1111
value?: string;
1212
openStart: { range: [number, number] };
13+
openEnd?: { range: [number, number] };
14+
close?: { range: [number, number]; value: string } | null;
15+
selfClosing?: boolean;
1316
loc?: { start: { line: number; column: number }; end: { line: number; column: number } };
1417
range?: [number, number];
1518
[key: string]: unknown;

projects/lint/src/eslint/rules/no-deprecated-attributes.test.ts

Lines changed: 87 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -39,6 +39,9 @@ describe('noDeprecatedAttributes', () => {
3939
expect(noDeprecatedAttributes.meta.messages['unexpected-deprecated-attribute-replacement']).toBe(
4040
'Unexpected use of deprecated attribute "{{attribute}}". Use {{replacement}} instead.'
4141
);
42+
expect(noDeprecatedAttributes.meta.messages['unexpected-deprecated-attribute-value-replacement']).toBe(
43+
'Unexpected use of deprecated value "{{value}}" in attribute "{{attribute}}". Use {{replacement}} instead.'
44+
);
4245
});
4346

4447
it('should allow valid use of attributes', () => {
@@ -47,8 +50,13 @@ describe('noDeprecatedAttributes', () => {
4750
'<nve-badge></nve-badge>',
4851
'<nve-badge status="success"></nve-badge>',
4952
`<nve-badge status=${'success'}></nve-badge>`,
53+
'<nve-button interaction="emphasis"></nve-button>',
54+
'<nve-button container="flat" interaction="destructive"></nve-button>',
5055
'<nve-combobox tag-layout="hidden"></nve-combobox>',
51-
'<nve-combobox tag-layout="wrap"></nve-combobox>'
56+
'<nve-combobox tag-layout="wrap"></nve-combobox>',
57+
'<nve-icon-button interaction="destructive"></nve-icon-button>',
58+
'<nve-toast status="success"></nve-toast>',
59+
'<nve-toast prominence="muted"></nve-toast>'
5260
],
5361
invalid: []
5462
});
@@ -72,6 +80,74 @@ describe('noDeprecatedAttributes', () => {
7280
{ messageId: 'unexpected-deprecated-attribute', data: { attribute: 'status', value: 'trend-neutral' } }
7381
]
7482
},
83+
{
84+
code: '<nve-button interaction="emphasize"></nve-button>',
85+
output: '<nve-button interaction="emphasis"></nve-button>',
86+
errors: [
87+
{
88+
messageId: 'unexpected-deprecated-attribute-value-replacement',
89+
data: { attribute: 'interaction', value: 'emphasize', replacement: 'interaction="emphasis"' }
90+
}
91+
]
92+
},
93+
{
94+
code: '<nve-button interaction="inverse"></nve-button>',
95+
errors: [
96+
{ messageId: 'unexpected-deprecated-attribute', data: { attribute: 'interaction', value: 'inverse' } }
97+
]
98+
},
99+
{
100+
code: '<nve-button interaction="flat"></nve-button>',
101+
output: '<nve-button container="flat"></nve-button>',
102+
errors: [
103+
{
104+
messageId: 'unexpected-deprecated-attribute-value-replacement',
105+
data: { attribute: 'interaction', value: 'flat', replacement: 'container="flat"' }
106+
}
107+
]
108+
},
109+
{
110+
code: '<nve-button interaction="flat-destructive"></nve-button>',
111+
output: '<nve-button container="flat" interaction="destructive"></nve-button>',
112+
errors: [
113+
{
114+
messageId: 'unexpected-deprecated-attribute-value-replacement',
115+
data: {
116+
attribute: 'interaction',
117+
value: 'flat-destructive',
118+
replacement: 'container="flat" interaction="destructive"'
119+
}
120+
}
121+
]
122+
},
123+
{
124+
code: '<nve-button interaction="flat-emphasis"></nve-button>',
125+
output: '<nve-button container="flat" interaction="emphasis"></nve-button>',
126+
errors: [
127+
{
128+
messageId: 'unexpected-deprecated-attribute-value-replacement',
129+
data: {
130+
attribute: 'interaction',
131+
value: 'flat-emphasis',
132+
replacement: 'container="flat" interaction="emphasis"'
133+
}
134+
}
135+
]
136+
},
137+
{
138+
code: '<nve-icon-button interaction="flat-emphasize"></nve-icon-button>',
139+
output: '<nve-icon-button container="flat" interaction="emphasis"></nve-icon-button>',
140+
errors: [
141+
{
142+
messageId: 'unexpected-deprecated-attribute-value-replacement',
143+
data: {
144+
attribute: 'interaction',
145+
value: 'flat-emphasize',
146+
replacement: 'container="flat" interaction="emphasis"'
147+
}
148+
}
149+
]
150+
},
75151
{
76152
code: '<nve-combobox notags></nve-combobox>',
77153
output: '<nve-combobox tag-layout="hidden"></nve-combobox>',
@@ -91,6 +167,16 @@ describe('noDeprecatedAttributes', () => {
91167
data: { attribute: 'notags', replacement: 'tag-layout="hidden"' }
92168
}
93169
]
170+
},
171+
{
172+
code: '<nve-toast status="muted"></nve-toast>',
173+
output: '<nve-toast prominence="muted"></nve-toast>',
174+
errors: [
175+
{
176+
messageId: 'unexpected-deprecated-attribute-value-replacement',
177+
data: { attribute: 'status', value: 'muted', replacement: 'prominence="muted"' }
178+
}
179+
]
94180
}
95181
]
96182
});

projects/lint/src/eslint/rules/no-deprecated-attributes.ts

Lines changed: 52 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ declare const __ELEMENTS_PAGES_BASE_URL__: string;
1010

1111
interface DeprecatedAttributeConfig {
1212
replacement?: string;
13-
values?: string[];
13+
values?: readonly string[] | Record<string, string | undefined>;
1414
}
1515

1616
interface DeprecatedAttributeReport {
@@ -19,30 +19,72 @@ interface DeprecatedAttributeReport {
1919
config: DeprecatedAttributeConfig;
2020
}
2121

22+
function isDeprecatedAttributeValueMap(
23+
values: DeprecatedAttributeConfig['values']
24+
): values is Record<string, string | undefined> {
25+
return !!values && !Array.isArray(values);
26+
}
27+
28+
const DEPRECATED_BUTTON_INTERACTION_VALUES = {
29+
emphasize: 'interaction="emphasis"',
30+
inverse: undefined,
31+
flat: 'container="flat"',
32+
'flat-destructive': 'container="flat" interaction="destructive"',
33+
'flat-emphasis': 'container="flat" interaction="emphasis"',
34+
'flat-emphasize': 'container="flat" interaction="emphasis"'
35+
} as const;
36+
2237
const DEPRECATED_ATTRIBUTES: Record<string, Record<string, DeprecatedAttributeConfig>> = {
2338
'nve-badge': {
2439
status: { values: ['trend-up', 'trend-down', 'trend-neutral'] }
2540
},
41+
'nve-button': {
42+
interaction: { values: DEPRECATED_BUTTON_INTERACTION_VALUES }
43+
},
2644
'nve-combobox': {
2745
notags: { replacement: 'tag-layout="hidden"' }
46+
},
47+
'nve-icon-button': {
48+
interaction: { values: DEPRECATED_BUTTON_INTERACTION_VALUES }
49+
},
50+
'nve-toast': {
51+
status: { values: { muted: 'prominence="muted"' } }
2852
}
2953
};
3054

3155
function attributeValueIsDeprecated(config: DeprecatedAttributeConfig, value?: string) {
32-
return !config.values || (!!value && config.values.includes(value));
56+
if (!config.values) return true;
57+
if (!value) return false;
58+
return Array.isArray(config.values) ? config.values.includes(value) : Object.hasOwn(config.values, value);
59+
}
60+
61+
export function attributeValueIsDeprecatedForTag(tagName: string, attribute: string, value?: string) {
62+
const config = DEPRECATED_ATTRIBUTES[tagName]?.[attribute];
63+
return !!config && attributeValueIsDeprecated(config, value);
64+
}
65+
66+
function getDeprecatedAttributeReplacement(config: DeprecatedAttributeConfig, value?: string) {
67+
const values = config.values;
68+
if (!isDeprecatedAttributeValueMap(values) || !value || !Object.hasOwn(values, value)) {
69+
return config.replacement;
70+
}
71+
return values[value];
3372
}
3473

3574
function reportDeprecatedAttribute(context: Rule.RuleContext, { attr, attribute, config }: DeprecatedAttributeReport) {
36-
const replacement = config.replacement;
37-
const messageId = config.replacement
38-
? 'unexpected-deprecated-attribute-replacement'
75+
const value = attr.value?.value;
76+
const replacement = getDeprecatedAttributeReplacement(config, value);
77+
const messageId = replacement
78+
? config.values
79+
? 'unexpected-deprecated-attribute-value-replacement'
80+
: 'unexpected-deprecated-attribute-replacement'
3981
: 'unexpected-deprecated-attribute';
4082
const report: Rule.ReportDescriptor = {
4183
node: attr,
4284
data: {
4385
attribute,
44-
replacement: config.replacement ?? '',
45-
value: attr.value?.value ?? ''
86+
replacement: replacement ?? '',
87+
value: value ?? ''
4688
},
4789
messageId
4890
};
@@ -67,7 +109,9 @@ const rule = {
67109
['unexpected-deprecated-attribute']:
68110
'Unexpected use of deprecated value "{{value}}" in attribute "{{attribute}}"',
69111
['unexpected-deprecated-attribute-replacement']:
70-
'Unexpected use of deprecated attribute "{{attribute}}". Use {{replacement}} instead.'
112+
'Unexpected use of deprecated attribute "{{attribute}}". Use {{replacement}} instead.',
113+
['unexpected-deprecated-attribute-value-replacement']:
114+
'Unexpected use of deprecated value "{{value}}" in attribute "{{attribute}}". Use {{replacement}} instead.'
71115
}
72116
},
73117
create(context: Rule.RuleContext) {

0 commit comments

Comments
 (0)