@wordpress/eslint-plugin 24.2.1-next.v.202602241322.0 → 24.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -84,6 +84,7 @@ The granular rulesets will not define any environment globals. As such, if they
84
84
  | [no-base-control-with-label-without-id](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-base-control-with-label-without-id.md) | Disallow the usage of BaseControl component with a label prop set but omitting the id property. | ✓ |
85
85
  | [components-no-missing-40px-size-prop](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-missing-40px-size-prop.md) | Disallow missing `__next40pxDefaultSize` prop on `@wordpress/components` components. | ✓ |
86
86
  | [components-no-unsafe-button-disabled](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/components-no-unsafe-button-disabled.md) | Disallow using `disabled` on Button without `accessibleWhenDisabled`. | ✓ |
87
+ | [no-i18n-in-save](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-i18n-in-save.md) | Disallow translation functions in block save methods. | |
87
88
  | [no-unguarded-get-range-at](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unguarded-get-range-at.md) | Disallow the usage of unguarded `getRangeAt` calls. | ✓ |
88
89
  | [no-unsafe-wp-apis](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unsafe-wp-apis.md) | Disallow the usage of unsafe APIs from `@wordpress/*` packages | ✓ |
89
90
  | [no-unused-vars-before-return](https://github.com/WordPress/gutenberg/tree/HEAD/packages/eslint-plugin/docs/rules/no-unused-vars-before-return.md) | Disallow assigning variable values if unused before a return. | ✓ |
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/eslint-plugin",
3
- "version": "24.2.1-next.v.202602241322.0+bce7cff88",
3
+ "version": "24.3.0",
4
4
  "description": "ESLint plugin for WordPress development.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -40,9 +40,9 @@
40
40
  "@babel/eslint-parser": "7.25.7",
41
41
  "@typescript-eslint/eslint-plugin": "^6.4.1",
42
42
  "@typescript-eslint/parser": "^6.4.1",
43
- "@wordpress/babel-preset-default": "^8.40.1-next.v.202602241322.0+bce7cff88",
44
- "@wordpress/prettier-config": "^4.40.1-next.v.202602241322.0+bce7cff88",
45
- "@wordpress/theme": "^0.8.1-next.v.202602241322.0+bce7cff88",
43
+ "@wordpress/babel-preset-default": "^8.41.0",
44
+ "@wordpress/prettier-config": "^4.41.0",
45
+ "@wordpress/theme": "^0.8.0",
46
46
  "cosmiconfig": "^7.0.0",
47
47
  "eslint-config-prettier": "^8.3.0",
48
48
  "eslint-import-resolver-typescript": "^4.4.4",
@@ -78,5 +78,5 @@
78
78
  "publishConfig": {
79
79
  "access": "public"
80
80
  },
81
- "gitHead": "943dde7f0b600ce238726c36284bc9f70ce0ffa4"
81
+ "gitHead": "8bfc179b9aed74c0a6dd6e8edf7a49e40e4f87cc"
82
82
  }
@@ -0,0 +1,81 @@
1
+ import { RuleTester } from 'eslint';
2
+ import rule from '../no-ds-tokens';
3
+
4
+ const ruleTester = new RuleTester( {
5
+ parserOptions: {
6
+ ecmaVersion: 6,
7
+ ecmaFeatures: {
8
+ jsx: true,
9
+ },
10
+ },
11
+ } );
12
+
13
+ ruleTester.run( 'no-ds-tokens', rule, {
14
+ valid: [
15
+ {
16
+ code: `const style = 'color: var(--my-custom-prop)';`,
17
+ },
18
+ {
19
+ code: `const style = 'color: blue';`,
20
+ },
21
+ {
22
+ code: 'const style = `border: 1px solid var(--other-prefix-token)`;',
23
+ },
24
+ {
25
+ code: `const name = 'something--wpds-color';`,
26
+ },
27
+ {
28
+ code: `<div style={ { color: 'var(--my-custom-prop)' } } />`,
29
+ },
30
+ ],
31
+ invalid: [
32
+ {
33
+ code: `const style = 'color: var(--wpds-color-fg-content-neutral)';`,
34
+ errors: [
35
+ {
36
+ messageId: 'disallowed',
37
+ },
38
+ ],
39
+ },
40
+ {
41
+ code: 'const style = `color: var(--wpds-color-fg-content-neutral)`;',
42
+ errors: [
43
+ {
44
+ messageId: 'disallowed',
45
+ },
46
+ ],
47
+ },
48
+ {
49
+ code: `<div style={ { color: 'var(--wpds-color-fg-content-neutral)' } } />`,
50
+ errors: [
51
+ {
52
+ messageId: 'disallowed',
53
+ },
54
+ ],
55
+ },
56
+ {
57
+ code: 'const style = `border: 1px solid var(--wpds-border-color, var(--wpds-border-fallback))`;',
58
+ errors: [
59
+ {
60
+ messageId: 'disallowed',
61
+ },
62
+ ],
63
+ },
64
+ {
65
+ code: `const token = '--wpds-color-fg';`,
66
+ errors: [
67
+ {
68
+ messageId: 'disallowed',
69
+ },
70
+ ],
71
+ },
72
+ {
73
+ code: 'const style = `--wpds-color-fg: red`;',
74
+ errors: [
75
+ {
76
+ messageId: 'disallowed',
77
+ },
78
+ ],
79
+ },
80
+ ],
81
+ } );
@@ -0,0 +1,270 @@
1
+ /**
2
+ * External dependencies
3
+ */
4
+ import { RuleTester } from 'eslint';
5
+
6
+ /**
7
+ * Internal dependencies
8
+ */
9
+ import rule from '../no-i18n-in-save';
10
+
11
+ const ruleTester = new RuleTester( {
12
+ parserOptions: {
13
+ ecmaVersion: 6,
14
+ sourceType: 'module',
15
+ ecmaFeatures: {
16
+ jsx: true,
17
+ },
18
+ },
19
+ } );
20
+
21
+ ruleTester.run( 'no-i18n-in-save', rule, {
22
+ valid: [
23
+ {
24
+ code: `
25
+ function edit() {
26
+ return __( 'Hello World' );
27
+ }
28
+ `,
29
+ },
30
+ {
31
+ code: `
32
+ const edit = () => {
33
+ return __( 'Hello World' );
34
+ };
35
+ `,
36
+ },
37
+ {
38
+ code: `
39
+ const settings = {
40
+ edit() {
41
+ return __( 'Hello World' );
42
+ },
43
+ };
44
+ `,
45
+ },
46
+ {
47
+ code: `
48
+ // Translation functions are fine in non-save files
49
+ function render() {
50
+ return __( 'Hello World' );
51
+ }
52
+ `,
53
+ },
54
+ {
55
+ code: `
56
+ // Translation in edit function
57
+ export default function Edit() {
58
+ return <div>{ __( 'Hello World' ) }</div>;
59
+ }
60
+ `,
61
+ },
62
+ {
63
+ code: `
64
+ // Translation in deprecated save function is allowed
65
+ function save() {
66
+ return __( 'Hello World' );
67
+ }
68
+ `,
69
+ filename: '/path/to/block/deprecated.js',
70
+ },
71
+ {
72
+ code: `
73
+ // Translation in deprecated save function with Windows path
74
+ function save() {
75
+ return __( 'Hello World' );
76
+ }
77
+ `,
78
+ filename: 'D:\\path\\to\\block\\deprecated.js',
79
+ },
80
+ ],
81
+ invalid: [
82
+ {
83
+ code: `
84
+ // Translation in save file outside save function
85
+ function render() {
86
+ return __( 'Hello World' );
87
+ }
88
+ `,
89
+ filename: '/path/to/block/save.js',
90
+ errors: [
91
+ {
92
+ messageId: 'noI18nInSave',
93
+ type: 'CallExpression',
94
+ },
95
+ ],
96
+ },
97
+ {
98
+ code: `
99
+ function save() {
100
+ return __( 'Hello World' );
101
+ }
102
+ `,
103
+ errors: [
104
+ {
105
+ messageId: 'noI18nInSave',
106
+ type: 'CallExpression',
107
+ },
108
+ ],
109
+ },
110
+ {
111
+ code: `
112
+ const save = () => {
113
+ return __( 'Hello World' );
114
+ };
115
+ `,
116
+ errors: [
117
+ {
118
+ messageId: 'noI18nInSave',
119
+ type: 'CallExpression',
120
+ },
121
+ ],
122
+ },
123
+ {
124
+ code: `
125
+ const save = function() {
126
+ return __( 'Hello World' );
127
+ };
128
+ `,
129
+ errors: [
130
+ {
131
+ messageId: 'noI18nInSave',
132
+ type: 'CallExpression',
133
+ },
134
+ ],
135
+ },
136
+ {
137
+ code: `
138
+ export default function save() {
139
+ return <span>{ __( 'Hello World' ) }</span>;
140
+ }
141
+ `,
142
+ errors: [
143
+ {
144
+ messageId: 'noI18nInSave',
145
+ type: 'CallExpression',
146
+ },
147
+ ],
148
+ },
149
+ {
150
+ code: `
151
+ const settings = {
152
+ save() {
153
+ return __( 'Hello World' );
154
+ },
155
+ };
156
+ `,
157
+ errors: [
158
+ {
159
+ messageId: 'noI18nInSave',
160
+ type: 'CallExpression',
161
+ },
162
+ ],
163
+ },
164
+ {
165
+ code: `
166
+ const settings = {
167
+ save: () => __( 'Hello World' ),
168
+ };
169
+ `,
170
+ errors: [
171
+ {
172
+ messageId: 'noI18nInSave',
173
+ type: 'CallExpression',
174
+ },
175
+ ],
176
+ },
177
+ {
178
+ code: `
179
+ function save() {
180
+ return _x( 'Hello', 'greeting' );
181
+ }
182
+ `,
183
+ errors: [
184
+ {
185
+ messageId: 'noI18nInSave',
186
+ type: 'CallExpression',
187
+ },
188
+ ],
189
+ },
190
+ {
191
+ code: `
192
+ function save() {
193
+ const count = 5;
194
+ return _n( 'One item', 'Multiple items', count );
195
+ }
196
+ `,
197
+ errors: [
198
+ {
199
+ messageId: 'noI18nInSave',
200
+ type: 'CallExpression',
201
+ },
202
+ ],
203
+ },
204
+ {
205
+ code: `
206
+ function save() {
207
+ const count = 5;
208
+ return _nx( 'One item', 'Multiple items', count, 'context' );
209
+ }
210
+ `,
211
+ errors: [
212
+ {
213
+ messageId: 'noI18nInSave',
214
+ type: 'CallExpression',
215
+ },
216
+ ],
217
+ },
218
+ {
219
+ code: `
220
+ function save() {
221
+ return (
222
+ <button>
223
+ <span>{ __( 'Click me' ) }</span>
224
+ </button>
225
+ );
226
+ }
227
+ `,
228
+ errors: [
229
+ {
230
+ messageId: 'noI18nInSave',
231
+ type: 'CallExpression',
232
+ },
233
+ ],
234
+ },
235
+ {
236
+ code: `
237
+ // Multiple translation calls in save
238
+ function save() {
239
+ const label = __( 'Label' );
240
+ return <div title={ _x( 'Title', 'context' ) }>{ label }</div>;
241
+ }
242
+ `,
243
+ errors: [
244
+ {
245
+ messageId: 'noI18nInSave',
246
+ type: 'CallExpression',
247
+ },
248
+ {
249
+ messageId: 'noI18nInSave',
250
+ type: 'CallExpression',
251
+ },
252
+ ],
253
+ },
254
+ {
255
+ code: `
256
+ // Translation after a nested inner function named save must still be caught
257
+ function save() {
258
+ function save() {}
259
+ return __( 'Hello World' );
260
+ }
261
+ `,
262
+ errors: [
263
+ {
264
+ messageId: 'noI18nInSave',
265
+ type: 'CallExpression',
266
+ },
267
+ ],
268
+ },
269
+ ],
270
+ } );
@@ -27,6 +27,18 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
27
27
  {
28
28
  code: '<div style={ { color: `var(--wpds-color-fg-content-neutral)` } } />',
29
29
  },
30
+ {
31
+ code: `const token = 'var(--wpds-color-fg-content-neutral)';`,
32
+ },
33
+ {
34
+ code: `const name = 'something--wpds-color';`,
35
+ },
36
+ {
37
+ code: '`${ prefix }: var(--wpds-color-fg-content-neutral)`',
38
+ },
39
+ {
40
+ code: '`var(--wpds-color-fg-content-neutral) ${ suffix }`',
41
+ },
30
42
  ],
31
43
  invalid: [
32
44
  {
@@ -34,6 +46,9 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
34
46
  errors: [
35
47
  {
36
48
  messageId: 'onlyKnownTokens',
49
+ data: {
50
+ tokenNames: "'--wpds-nonexistent-token'",
51
+ },
37
52
  },
38
53
  ],
39
54
  },
@@ -53,6 +68,77 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
53
68
  errors: [
54
69
  {
55
70
  messageId: 'onlyKnownTokens',
71
+ data: {
72
+ tokenNames: "'--wpds-nonexistent'",
73
+ },
74
+ },
75
+ ],
76
+ },
77
+ {
78
+ code: `const token = 'var(--wpds-nonexistent-token)';`,
79
+ errors: [
80
+ {
81
+ messageId: 'onlyKnownTokens',
82
+ data: {
83
+ tokenNames: "'--wpds-nonexistent-token'",
84
+ },
85
+ },
86
+ ],
87
+ },
88
+ {
89
+ code: 'const token = `var(--wpds-nonexistent-token)`;',
90
+ errors: [
91
+ {
92
+ messageId: 'onlyKnownTokens',
93
+ data: {
94
+ tokenNames: "'--wpds-nonexistent-token'",
95
+ },
96
+ },
97
+ ],
98
+ },
99
+ {
100
+ code: 'const token = `var(--wpds-dimension-gap-${ size })`;',
101
+ errors: [
102
+ {
103
+ messageId: 'dynamicToken',
104
+ },
105
+ ],
106
+ },
107
+ {
108
+ code: '<div style={ { gap: `var(--wpds-dimension-gap-${ size })` } } />',
109
+ errors: [
110
+ {
111
+ messageId: 'dynamicToken',
112
+ },
113
+ ],
114
+ },
115
+ {
116
+ code: `const token = '--wpds-nonexistent-token';`,
117
+ errors: [
118
+ {
119
+ messageId: 'onlyKnownTokens',
120
+ data: {
121
+ tokenNames: "'--wpds-nonexistent-token'",
122
+ },
123
+ },
124
+ ],
125
+ },
126
+ {
127
+ code: 'const style = `--wpds-dimension-gap-${ size }`;',
128
+ errors: [
129
+ {
130
+ messageId: 'dynamicToken',
131
+ },
132
+ ],
133
+ },
134
+ {
135
+ code: '`${ prefix }: var(--wpds-nonexistent-token)`',
136
+ errors: [
137
+ {
138
+ messageId: 'onlyKnownTokens',
139
+ data: {
140
+ tokenNames: "'--wpds-nonexistent-token'",
141
+ },
56
142
  },
57
143
  ],
58
144
  },
@@ -0,0 +1,29 @@
1
+ const DS_TOKEN_PREFIX = 'wpds-';
2
+
3
+ const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );
4
+
5
+ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
6
+ meta: {
7
+ type: 'problem',
8
+ docs: {
9
+ description: 'Disallow any usage of --wpds-* CSS custom properties',
10
+ },
11
+ schema: [],
12
+ messages: {
13
+ disallowed:
14
+ 'Design System tokens (--wpds-*) should not be used in this context.',
15
+ },
16
+ },
17
+ create( context ) {
18
+ const selector = `:matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral TemplateElement[value.raw=${ wpdsTokensRegex }])`;
19
+ return {
20
+ /** @param {import('estree').Literal | import('estree').TemplateElement} node */
21
+ [ selector ]( node ) {
22
+ context.report( {
23
+ node,
24
+ messageId: 'disallowed',
25
+ } );
26
+ },
27
+ };
28
+ },
29
+ } );
@@ -0,0 +1,121 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ const {
5
+ TRANSLATION_FUNCTIONS,
6
+ getTranslateFunctionName,
7
+ } = require( '../utils' );
8
+
9
+ module.exports = {
10
+ meta: {
11
+ type: 'problem',
12
+ schema: [],
13
+ messages: {
14
+ noI18nInSave:
15
+ 'Translation functions should not be used in block save functions. Translated content is saved to the database and will not update if the language changes.',
16
+ },
17
+ docs: {
18
+ description: 'Disallow translation functions in block save methods',
19
+ category: 'Best Practices',
20
+ recommended: false,
21
+ },
22
+ },
23
+ create( context ) {
24
+ let saveFunctionDepth = 0;
25
+ const filename = context.getFilename();
26
+
27
+ // Skip deprecated files as they preserve old behavior including translation functions
28
+ const normalizedFilename = filename.replace( /\\/g, '/' );
29
+ const isDeprecatedFile =
30
+ normalizedFilename.includes( '/deprecated.js' ) ||
31
+ normalizedFilename.includes( '/deprecated.ts' ) ||
32
+ normalizedFilename.includes( '/deprecated.jsx' ) ||
33
+ normalizedFilename.includes( '/deprecated.tsx' );
34
+
35
+ if ( isDeprecatedFile ) {
36
+ return {};
37
+ }
38
+
39
+ const isSaveFile =
40
+ normalizedFilename.endsWith( '/save.js' ) ||
41
+ normalizedFilename.endsWith( '/save.ts' ) ||
42
+ normalizedFilename.endsWith( '/save.jsx' ) ||
43
+ normalizedFilename.endsWith( '/save.tsx' );
44
+
45
+ return {
46
+ // Track when we enter a function named 'save'
47
+ FunctionDeclaration( node ) {
48
+ if ( node.id && node.id.name === 'save' ) {
49
+ saveFunctionDepth++;
50
+ }
51
+ },
52
+ 'FunctionDeclaration:exit'( node ) {
53
+ if ( node.id && node.id.name === 'save' ) {
54
+ saveFunctionDepth--;
55
+ }
56
+ },
57
+
58
+ // Track arrow functions assigned to 'save'
59
+ VariableDeclarator( node ) {
60
+ if (
61
+ node.id &&
62
+ node.id.name === 'save' &&
63
+ node.init &&
64
+ ( node.init.type === 'ArrowFunctionExpression' ||
65
+ node.init.type === 'FunctionExpression' )
66
+ ) {
67
+ saveFunctionDepth++;
68
+ }
69
+ },
70
+ 'VariableDeclarator:exit'( node ) {
71
+ if (
72
+ node.id &&
73
+ node.id.name === 'save' &&
74
+ node.init &&
75
+ ( node.init.type === 'ArrowFunctionExpression' ||
76
+ node.init.type === 'FunctionExpression' )
77
+ ) {
78
+ saveFunctionDepth--;
79
+ }
80
+ },
81
+
82
+ // Track object properties named 'save'
83
+ 'Property[key.name="save"]'( node ) {
84
+ if (
85
+ node.value &&
86
+ ( node.value.type === 'FunctionExpression' ||
87
+ node.value.type === 'ArrowFunctionExpression' )
88
+ ) {
89
+ saveFunctionDepth++;
90
+ }
91
+ },
92
+ 'Property[key.name="save"]:exit'( node ) {
93
+ if (
94
+ node.value &&
95
+ ( node.value.type === 'FunctionExpression' ||
96
+ node.value.type === 'ArrowFunctionExpression' )
97
+ ) {
98
+ saveFunctionDepth--;
99
+ }
100
+ },
101
+
102
+ // Check for translation function calls
103
+ CallExpression( node ) {
104
+ const { callee } = node;
105
+ const functionName = getTranslateFunctionName( callee );
106
+
107
+ if ( ! TRANSLATION_FUNCTIONS.has( functionName ) ) {
108
+ return;
109
+ }
110
+
111
+ // Report if we're in a save file or inside a save function
112
+ if ( isSaveFile || saveFunctionDepth > 0 ) {
113
+ context.report( {
114
+ node,
115
+ messageId: 'noI18nInSave',
116
+ } );
117
+ }
118
+ },
119
+ };
120
+ },
121
+ };
@@ -36,7 +36,7 @@ function extractCSSVariables( value, prefix = '' ) {
36
36
  }
37
37
 
38
38
  const knownTokens = new Set( tokenList );
39
- const wpdsTokensRegex = new RegExp( `[^\\w]--${ DS_TOKEN_PREFIX }`, 'i' );
39
+ const wpdsTokensRegex = new RegExp( `(?:^|[^\\w])--${ DS_TOKEN_PREFIX }`, 'i' );
40
40
 
41
41
  module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
42
42
  meta: {
@@ -48,13 +48,84 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
48
48
  messages: {
49
49
  onlyKnownTokens:
50
50
  'The following CSS variables are not valid Design System tokens: {{ tokenNames }}',
51
+ dynamicToken:
52
+ 'Design System tokens must not be dynamically constructed, as they cannot be statically verified for correctness or processed automatically to inject fallbacks.',
51
53
  },
52
54
  },
53
55
  create( context ) {
54
- const disallowedTokensAST = `JSXAttribute[name.name="style"] :matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral TemplateElement[value.raw=${ wpdsTokensRegex }])`;
56
+ const dynamicTemplateLiteralAST = `TemplateLiteral[expressions.length>0]:has(TemplateElement[value.raw=${ wpdsTokensRegex }])`;
57
+ const staticTokensAST = `:matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral[expressions.length=0] TemplateElement[value.raw=${ wpdsTokensRegex }])`;
58
+ const dynamicTokenEndRegex = new RegExp(
59
+ `--${ DS_TOKEN_PREFIX }[\\w-]*$`
60
+ );
61
+
55
62
  return {
63
+ /**
64
+ * For template literals with expressions, check each quasi
65
+ * individually: flag as dynamic only when a `--wpds-*` token
66
+ * name is split across a quasi/expression boundary, and
67
+ * validate any complete static tokens normally.
68
+ *
69
+ * @param {import('estree').TemplateLiteral} node
70
+ */
71
+ [ dynamicTemplateLiteralAST ]( node ) {
72
+ let hasDynamic = false;
73
+ const unknownTokens = [];
74
+
75
+ for ( const quasi of node.quasis ) {
76
+ const raw = quasi.value.raw;
77
+ const value = quasi.value.cooked ?? raw;
78
+ const isFollowedByExpression = ! quasi.tail;
79
+
80
+ if (
81
+ isFollowedByExpression &&
82
+ dynamicTokenEndRegex.test( raw )
83
+ ) {
84
+ hasDynamic = true;
85
+ }
86
+
87
+ const tokens = extractCSSVariables(
88
+ value,
89
+ DS_TOKEN_PREFIX
90
+ );
91
+
92
+ // Remove the trailing incomplete token — it's the one
93
+ // being dynamically constructed by the next expression.
94
+ if ( isFollowedByExpression ) {
95
+ const endMatch = value.match( /(--([\w-]+))$/ );
96
+ if ( endMatch ) {
97
+ tokens.delete( endMatch[ 1 ] );
98
+ }
99
+ }
100
+
101
+ for ( const token of tokens ) {
102
+ if ( ! knownTokens.has( token ) ) {
103
+ unknownTokens.push( token );
104
+ }
105
+ }
106
+ }
107
+
108
+ if ( hasDynamic ) {
109
+ context.report( {
110
+ node,
111
+ messageId: 'dynamicToken',
112
+ } );
113
+ }
114
+
115
+ if ( unknownTokens.length > 0 ) {
116
+ context.report( {
117
+ node,
118
+ messageId: 'onlyKnownTokens',
119
+ data: {
120
+ tokenNames: unknownTokens
121
+ .map( ( token ) => `'${ token }'` )
122
+ .join( ', ' ),
123
+ },
124
+ } );
125
+ }
126
+ },
56
127
  /** @param {import('estree').Literal | import('estree').TemplateElement} node */
57
- [ disallowedTokensAST ]( node ) {
128
+ [ staticTokensAST ]( node ) {
58
129
  let computedValue;
59
130
 
60
131
  if ( ! node.value ) {
@@ -62,13 +133,11 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
62
133
  }
63
134
 
64
135
  if ( typeof node.value === 'string' ) {
65
- // Get the node's value when it's a "string"
66
136
  computedValue = node.value;
67
137
  } else if (
68
138
  typeof node.value === 'object' &&
69
139
  'raw' in node.value
70
140
  ) {
71
- // Get the node's value when it's a `template literal`
72
141
  computedValue = node.value.cooked ?? node.value.raw;
73
142
  }
74
143