Skip to content

Commit 644d32b

Browse files
committed
feat(lint): no deprecated global attribute value
Signed-off-by: Cory Rylan <crylan@nvidia.com>
1 parent 410c452 commit 644d32b

10 files changed

Lines changed: 362 additions & 81 deletions

File tree

projects/internals/tools/src/context/index.test.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -83,14 +83,6 @@ describe('prompts', () => {
8383
expect(result?.messages[0].content.text).toContain('api_');
8484
});
8585

86-
it('should have "playground" prompt with authoring guidelines', () => {
87-
const playgroundPrompt = prompts.find(p => p.name === 'playground');
88-
expect(playgroundPrompt).toBeDefined();
89-
90-
const result = playgroundPrompt?.handler({});
91-
expect(result?.messages[0].content.text).toContain('playground');
92-
});
93-
9486
it('should have "create-project" prompt for starter projects', () => {
9587
const createProjectPrompt = prompts.find(p => p.name === 'create-project');
9688
expect(createProjectPrompt).toBeDefined();

projects/lint/README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -64,6 +64,7 @@ export default [
6464
| ---- | ----------- | -------- | -------- |
6565
| `@nvidia-elements/lint/no-complex-popovers` | Disallow excessive DOM complexity inside popover elements. | HTML | `error` |
6666
| `@nvidia-elements/lint/no-deprecated-attributes` | Disallow use of deprecated attributes in HTML. | HTML | `error` |
67+
| `@nvidia-elements/lint/no-deprecated-global-attribute-value` | Disallow use of deprecated attribute values for nve-* utility attributes. | HTML | `error` |
6768
| `@nvidia-elements/lint/no-deprecated-css-imports` | Disallow use of deprecated CSS import paths. | CSS | `error` |
6869
| `@nvidia-elements/lint/no-deprecated-css-variable` | Disallow use of deprecated --mlv-* CSS theme variables. | CSS | `error` |
6970
| `@nvidia-elements/lint/no-deprecated-global-attributes` | Disallow use of deprecated global utility attributes in HTML. | HTML | `error` |

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

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import noDeprecatedIconNames from '../rules/no-deprecated-icon-names.js';
1010
import noDeprecatedPopoverAttributes from '../rules/no-deprecated-popover-attributes.js';
1111
import noUnexpectedGlobalAttributeValue from '../rules/no-unexpected-global-attribute-value.js';
1212
import noUnexpectedStyleCustomization from '../rules/no-unexpected-style-customization.js';
13+
import noDeprecatedGlobalAttributeValue from '../rules/no-deprecated-global-attribute-value.js';
1314
import noDeprecatedGlobalAttributes from '../rules/no-deprecated-global-attributes.js';
1415
import noRestrictedAttributes from '../rules/no-restricted-attributes.js';
1516
import noDeprecatedSlots from '../rules/no-deprecated-slots.js';
@@ -61,6 +62,7 @@ export const elementsHtmlConfig: Linter.Config = {
6162
'no-deprecated-attributes': noDeprecatedAttributes,
6263
'no-deprecated-icon-names': noDeprecatedIconNames,
6364
'no-deprecated-popover-attributes': noDeprecatedPopoverAttributes,
65+
'no-deprecated-global-attribute-value': noDeprecatedGlobalAttributeValue,
6466
'no-deprecated-global-attributes': noDeprecatedGlobalAttributes,
6567
'no-deprecated-slots': noDeprecatedSlots,
6668
'no-missing-slotted-elements': noMissingSlottedElements,
@@ -91,6 +93,7 @@ export const elementsHtmlConfig: Linter.Config = {
9193
'@nvidia-elements/lint/no-deprecated-attributes': ['error'],
9294
'@nvidia-elements/lint/no-deprecated-icon-names': ['error'],
9395
'@nvidia-elements/lint/no-deprecated-popover-attributes': ['error'],
96+
'@nvidia-elements/lint/no-deprecated-global-attribute-value': ['error'],
9497
'@nvidia-elements/lint/no-deprecated-global-attributes': ['error'],
9598
'@nvidia-elements/lint/no-deprecated-slots': ['error'],
9699
'@nvidia-elements/lint/no-missing-slotted-elements': ['error'],

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

Lines changed: 57 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,25 +3,54 @@
33

44
import { globalAttributes } from './metadata.js';
55

6-
export const ATTRIBUTE_EXCEPTIONS = ['debug', 'mkd', 'md']; // internal scopes
7-
86
export const VALUE_BINDINGS = ['${', '{', '{{', '{%'];
97

8+
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']);
11+
1012
export const VALID_NVE_TEXT_VALUES = new Set([
1113
...(globalAttributes.find(attribute => attribute.name === 'nve-text')?.values?.map(value => value.name) ?? []),
1214
...ATTRIBUTE_EXCEPTIONS
1315
]);
1416

15-
export const VALID_NVE_LAYOUT_VALUES = [
17+
export const VALID_NVE_LAYOUT_VALUES = new Set([
1618
...(globalAttributes.find(attribute => attribute.name === 'nve-layout')?.values?.map(value => value.name) ?? []),
1719
...ATTRIBUTE_EXCEPTIONS
18-
];
20+
]);
1921

2022
export const VALID_NVE_DISPLAY_VALUES = new Set([
2123
...(globalAttributes.find(attribute => attribute.name === 'nve-display')?.values?.map(value => value.name) ?? []),
2224
...ATTRIBUTE_EXCEPTIONS
2325
]);
2426

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+
);
30+
31+
export const DISTILLED_NVE_LAYOUT_VALUES = new Set(
32+
[...VALID_NVE_LAYOUT_VALUES].filter(v => !isComplexAttributeValue(v) && !DEPRECATED_NVE_LAYOUT_VALUES.has(v))
33+
);
34+
35+
export const DISTILLED_NVE_DISPLAY_VALUES = new Set(
36+
[...VALID_NVE_DISPLAY_VALUES].filter(v => !isComplexAttributeValue(v))
37+
);
38+
39+
// also used in @internals/metadata, these are values that often confuse agents due to complexity in playground template generation within the same context window
40+
export function isComplexAttributeValue(value: string) {
41+
return (
42+
value.includes('|') ||
43+
value.includes('@') ||
44+
value.includes('&') ||
45+
value.includes('xx') ||
46+
value.includes('-y:') ||
47+
value.includes(':none') ||
48+
value.includes('debug') ||
49+
value.includes('mkd') ||
50+
value.includes('md')
51+
);
52+
}
53+
2554
export function recommendedNveTextValue(attributeValue: string): string | null {
2655
if (VALUE_BINDINGS.some(binding => attributeValue.includes(binding))) {
2756
return attributeValue;
@@ -57,7 +86,9 @@ export function recommendedNveLayoutValue(attributeValue: string, invalidSymbols
5786
}
5887

5988
const values = getAttributeValueSegments(attributeValue);
60-
const validValues = new Set(VALID_NVE_LAYOUT_VALUES.filter(v => !invalidSymbols.some(symbol => v.includes(symbol))));
89+
const validValues = new Set(
90+
[...VALID_NVE_LAYOUT_VALUES].filter(v => !invalidSymbols.some(symbol => v.includes(symbol)))
91+
);
6192

6293
const repairs: [RegExp, string][] = [
6394
[/^default$/, 'column'],
@@ -101,6 +132,27 @@ export function recommendedNveLayoutValue(attributeValue: string, invalidSymbols
101132
}
102133
}
103134

135+
export function recommendedNveDisplayValue(attributeValue: string): string | null {
136+
if (VALUE_BINDINGS.some(binding => attributeValue.includes(binding))) {
137+
return attributeValue;
138+
}
139+
140+
const values = getAttributeValueSegments(attributeValue);
141+
const validValues = new Set(VALID_NVE_DISPLAY_VALUES);
142+
143+
const repairs: [RegExp, string][] = [
144+
[/^hidden$/, 'hide'],
145+
[/^visisble$/, 'show']
146+
];
147+
148+
const result: string[] = repairAttributeValueSegments(values, repairs);
149+
if (result.some(value => !validValues.has(value))) {
150+
return null;
151+
} else {
152+
return result.join(' ');
153+
}
154+
}
155+
104156
function getAttributeValueSegments(value: string) {
105157
return value
106158
.toLowerCase()

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,7 @@ export async function lintPlaygroundTemplate(code: string): Promise<TemplateLint
2828
// extra restrictions/rules as agents rarely use these advanced APIs correctly for playground generation out of context of an established project
2929
// '@nvidia-elements/lint/no-unexpected-style-customization': ['error']
3030
const rules: Partial<Linter.RulesRecord> = {
31-
'@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { 'nve-layout': ['@', '|', '&', 'xx'] }],
31+
'@nvidia-elements/lint/no-unexpected-global-attribute-value': ['error', { distilled: true }],
3232
'@nvidia-elements/lint/no-missing-slotted-elements': ['error', { 'nve-card': { required: ['nve-card-content'] } }],
3333
'@nvidia-elements/lint/no-missing-gap-space': ['error']
3434
};
Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,118 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import { describe, it, expect, beforeEach } from 'vitest';
5+
import { RuleTester } from 'eslint';
6+
import type { JSRuleDefinition } from 'eslint';
7+
import htmlParser from '@html-eslint/parser';
8+
import noDeprecatedAttributeValue, { DEPRECATED_ATTRIBUTE_VALUES } from './no-deprecated-global-attribute-value.js';
9+
import noDeprecatedGlobalAttributeValue from './no-deprecated-global-attribute-value.js';
10+
11+
const rule = noDeprecatedGlobalAttributeValue as unknown as JSRuleDefinition;
12+
13+
const MIGRATE_PROMPT_DEPRECATIONS: { attribute: string; before: string; after: string }[] = [
14+
{ attribute: 'nve-text', before: 'eyebrow', after: 'label sm' },
15+
{ attribute: 'nve-layout', before: 'grow', after: 'full' }
16+
];
17+
18+
describe('noDeprecatedGlobalAttributeValue', () => {
19+
let tester: RuleTester;
20+
21+
beforeEach(() => {
22+
tester = new RuleTester({
23+
languageOptions: {
24+
parser: htmlParser,
25+
parserOptions: {
26+
frontmatter: true
27+
}
28+
}
29+
});
30+
});
31+
32+
it('should define rule metadata', () => {
33+
expect(noDeprecatedAttributeValue.meta).toBeDefined();
34+
expect(noDeprecatedAttributeValue.meta.type).toBe('problem');
35+
expect(noDeprecatedAttributeValue.meta.fixable).toBe('code');
36+
expect(noDeprecatedAttributeValue.meta.hasSuggestions).toBe(true);
37+
expect(noDeprecatedAttributeValue.meta.docs.recommended).toBe(true);
38+
expect(noDeprecatedAttributeValue.meta.docs.url).toContain('/docs/lint/');
39+
});
40+
41+
it('should cover every deprecation documented in the migrate prompt', () => {
42+
MIGRATE_PROMPT_DEPRECATIONS.forEach(({ attribute, before, after }) => {
43+
expect(DEPRECATED_ATTRIBUTE_VALUES[attribute]?.[before]).toBe(after);
44+
});
45+
});
46+
47+
it('should allow non-deprecated values', () => {
48+
tester.run('non-deprecated values', rule, {
49+
valid: [
50+
'<div nve-text="body"></div>',
51+
'<div nve-text="label sm"></div>',
52+
'<div nve-layout="row"></div>',
53+
'<div nve-layout="full"></div>',
54+
'<div></div>'
55+
],
56+
invalid: []
57+
});
58+
});
59+
60+
it('should ignore template binding expressions', () => {
61+
tester.run('template binding expressions', rule, {
62+
valid: [
63+
'<div nve-text="${value}"></div>',
64+
'<div nve-text="{{ value }}"></div>',
65+
'<div nve-layout="{value}"></div>'
66+
],
67+
invalid: []
68+
});
69+
});
70+
71+
it('should report and auto-fix every migrate-prompt deprecation', () => {
72+
tester.run('migrate prompt deprecations', rule, {
73+
valid: [],
74+
invalid: MIGRATE_PROMPT_DEPRECATIONS.map(({ attribute, before, after }) => ({
75+
code: `<div ${attribute}="${before}"></div>`,
76+
output: `<div ${attribute}="${after}"></div>`,
77+
errors: [
78+
{
79+
messageId: 'unexpected-deprecated-global-attribute-value',
80+
data: { attribute, value: before, alternative: after },
81+
suggestions: [
82+
{
83+
messageId: 'suggest-replace-deprecated-global-attribute-value',
84+
data: { value: before, alternative: after },
85+
output: `<div ${attribute}="${after}"></div>`
86+
}
87+
]
88+
}
89+
]
90+
}))
91+
});
92+
});
93+
94+
it('should rewrite deprecated tokens within a multi-token value', () => {
95+
tester.run('multi-token replacement', rule, {
96+
valid: [],
97+
invalid: [
98+
{
99+
code: '<div nve-layout="grow column"></div>',
100+
output: '<div nve-layout="full column"></div>',
101+
errors: [
102+
{
103+
messageId: 'unexpected-deprecated-global-attribute-value',
104+
data: { attribute: 'nve-layout', value: 'grow column', alternative: 'full column' },
105+
suggestions: [
106+
{
107+
messageId: 'suggest-replace-deprecated-global-attribute-value',
108+
data: { value: 'grow column', alternative: 'full column' },
109+
output: '<div nve-layout="full column"></div>'
110+
}
111+
]
112+
}
113+
]
114+
}
115+
]
116+
});
117+
});
118+
});
Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// SPDX-FileCopyrightText: Copyright (c) 2026 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
2+
// SPDX-License-Identifier: Apache-2.0
3+
4+
import type { Rule } from 'eslint';
5+
import { createVisitors } from '@html-eslint/eslint-plugin/lib/rules/utils/visitors.js';
6+
import { findAttr } from '@html-eslint/eslint-plugin/lib/rules/utils/node.js';
7+
import { VALUE_BINDINGS } from '../internals/attributes.js';
8+
import type { HtmlTagNode } from '../rule-types.js';
9+
10+
declare const __ELEMENTS_PAGES_BASE_URL__: string;
11+
12+
export const DEPRECATED_ATTRIBUTE_VALUES: Record<string, Record<string, string>> = {
13+
'nve-text': {
14+
eyebrow: 'label sm'
15+
},
16+
'nve-layout': {
17+
grow: 'full'
18+
}
19+
};
20+
21+
function rewriteValue(value: string, deprecations: Record<string, string>): string | null {
22+
const tokens = value.split(/\s+/).filter(Boolean);
23+
const replaced = tokens.map(token => deprecations[token] ?? token);
24+
if (tokens.every((token, i) => token === replaced[i])) return null;
25+
return Array.from(new Set(replaced.flatMap(t => t.split(/\s+/)))).join(' ');
26+
}
27+
28+
const rule = {
29+
meta: {
30+
type: 'problem' as const,
31+
fixable: 'code' as const,
32+
hasSuggestions: true,
33+
docs: {
34+
description: 'Disallow use of deprecated attribute values for nve-* utility attributes.',
35+
category: 'Best Practice',
36+
recommended: true,
37+
url: `${__ELEMENTS_PAGES_BASE_URL__}/docs/lint/`
38+
},
39+
schema: [],
40+
messages: {
41+
['unexpected-deprecated-global-attribute-value']:
42+
'Unexpected use of deprecated value "{{value}}" in "{{attribute}}". Use "{{alternative}}" instead.',
43+
['suggest-replace-deprecated-global-attribute-value']: 'Replace "{{value}}" with "{{alternative}}"'
44+
}
45+
},
46+
create(context: Rule.RuleContext) {
47+
return createVisitors(context, {
48+
Tag(node: HtmlTagNode) {
49+
Object.entries(DEPRECATED_ATTRIBUTE_VALUES).forEach(([attribute, deprecations]) => {
50+
const attr = findAttr(node, attribute);
51+
const value = attr?.value?.value ?? '';
52+
if (!attr || !value) return;
53+
if (VALUE_BINDINGS.some(binding => value.includes(binding))) return;
54+
const alternative = rewriteValue(value, deprecations);
55+
if (alternative === null) return;
56+
57+
context.report({
58+
node: attr,
59+
data: { attribute, value, alternative },
60+
messageId: 'unexpected-deprecated-global-attribute-value',
61+
suggest: [
62+
{
63+
messageId: 'suggest-replace-deprecated-global-attribute-value',
64+
data: { value, alternative },
65+
fix: fixer =>
66+
fixer.replaceText(
67+
attr,
68+
`${attribute}=${attr.startWrapper.value}${alternative}${attr.endWrapper.value}`
69+
)
70+
}
71+
],
72+
fix: fixer =>
73+
fixer.replaceText(attr, `${attribute}=${attr.startWrapper.value}${alternative}${attr.endWrapper.value}`)
74+
});
75+
});
76+
}
77+
});
78+
}
79+
} as const;
80+
81+
export default rule;

0 commit comments

Comments
 (0)