@wordpress/eslint-plugin 24.4.1-next.v.202603161435.0 → 24.5.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.
@@ -39,6 +39,9 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
39
39
  {
40
40
  code: '`var(--wpds-color-fg-content-neutral) ${ suffix }`',
41
41
  },
42
+ {
43
+ code: `const style = { '--wpds-color-fg-content-neutral': 'red' };`,
44
+ },
42
45
  ],
43
46
  invalid: [
44
47
  {
@@ -142,5 +145,60 @@ ruleTester.run( 'no-unknown-ds-tokens', rule, {
142
145
  },
143
146
  ],
144
147
  },
148
+ {
149
+ code: `const token = '--wpds-color-fg-content-neutral';`,
150
+ errors: [
151
+ {
152
+ messageId: 'bareToken',
153
+ data: {
154
+ tokenNames: "'--wpds-color-fg-content-neutral'",
155
+ },
156
+ },
157
+ ],
158
+ },
159
+ {
160
+ code: 'const token = `--wpds-color-fg-content-neutral`;',
161
+ errors: [
162
+ {
163
+ messageId: 'bareToken',
164
+ data: {
165
+ tokenNames: "'--wpds-color-fg-content-neutral'",
166
+ },
167
+ },
168
+ ],
169
+ },
170
+ {
171
+ code: '<div style={ { gap: `--wpds-color-fg-content-neutral` } } />',
172
+ errors: [
173
+ {
174
+ messageId: 'bareToken',
175
+ data: {
176
+ tokenNames: "'--wpds-color-fg-content-neutral'",
177
+ },
178
+ },
179
+ ],
180
+ },
181
+ {
182
+ code: '`${ prefix }: --wpds-color-fg-content-neutral`',
183
+ errors: [
184
+ {
185
+ messageId: 'bareToken',
186
+ data: {
187
+ tokenNames: "'--wpds-color-fg-content-neutral'",
188
+ },
189
+ },
190
+ ],
191
+ },
192
+ {
193
+ code: '`var(--wpds-color-fg-content-neutral) --wpds-color-fg-content-neutral ${ x }`',
194
+ errors: [
195
+ {
196
+ messageId: 'bareToken',
197
+ data: {
198
+ tokenNames: "'--wpds-color-fg-content-neutral'",
199
+ },
200
+ },
201
+ ],
202
+ },
145
203
  ],
146
204
  } );
@@ -0,0 +1,174 @@
1
+ import { RuleTester } from 'eslint';
2
+ import rule from '../no-unmerged-classname';
3
+
4
+ const ruleTester = new RuleTester( {
5
+ parserOptions: {
6
+ sourceType: 'module',
7
+ ecmaVersion: 2018,
8
+ ecmaFeatures: {
9
+ jsx: true,
10
+ },
11
+ },
12
+ } );
13
+
14
+ ruleTester.run( 'no-unmerged-classname', rule, {
15
+ valid: [
16
+ {
17
+ // className destructured and merged via clsx
18
+ code: `
19
+ function Foo( { className, ...restProps } ) {
20
+ return <div className={ clsx( styles.foo, className ) } { ...restProps } />;
21
+ }
22
+ `,
23
+ },
24
+ {
25
+ // className destructured and used directly
26
+ code: `
27
+ function Foo( { className, ...restProps } ) {
28
+ return <div className={ className } { ...restProps } />;
29
+ }
30
+ `,
31
+ },
32
+ {
33
+ // className merged via template literal
34
+ code: `
35
+ function Foo( { className, ...restProps } ) {
36
+ return <div className={ \`\${styles.foo} \${className}\` } { ...restProps } />;
37
+ }
38
+ `,
39
+ },
40
+ {
41
+ // className merged via string concatenation
42
+ code: `
43
+ function Foo( { className, ...restProps } ) {
44
+ return <div className={ styles.foo + ' ' + className } { ...restProps } />;
45
+ }
46
+ `,
47
+ },
48
+ {
49
+ // No spread — internal elements are fine
50
+ code: `
51
+ function Foo( { ...restProps } ) {
52
+ return <div className={ styles.foo } />;
53
+ }
54
+ `,
55
+ },
56
+ {
57
+ // No className attribute at all
58
+ code: `
59
+ function Foo( { ...restProps } ) {
60
+ return <div { ...restProps } />;
61
+ }
62
+ `,
63
+ },
64
+ {
65
+ // No rest element in destructuring
66
+ code: `
67
+ function Foo( { padding } ) {
68
+ return <div className={ styles.foo } { ...someOtherSpread } />;
69
+ }
70
+ `,
71
+ },
72
+ {
73
+ // Arrow function component
74
+ code: `
75
+ const Foo = ( { className, ...restProps } ) => (
76
+ <div className={ clsx( styles.foo, className ) } { ...restProps } />
77
+ );
78
+ `,
79
+ },
80
+ {
81
+ // className in conditional expression
82
+ code: `
83
+ function Foo( { className, isActive, ...restProps } ) {
84
+ return <div className={ isActive ? className : styles.inactive } { ...restProps } />;
85
+ }
86
+ `,
87
+ },
88
+ {
89
+ // className in logical expression
90
+ code: `
91
+ function Foo( { className, ...restProps } ) {
92
+ return <div className={ className || styles.default } { ...restProps } />;
93
+ }
94
+ `,
95
+ },
96
+ {
97
+ // className forwarded on a different element than the one with spread
98
+ code: `
99
+ function Foo( { className, style, ...props } ) {
100
+ return (
101
+ <Outer className={ clsx( styles.outer, className ) } style={ style }>
102
+ <Inner className={ styles.inner } { ...props } />
103
+ </Outer>
104
+ );
105
+ }
106
+ `,
107
+ },
108
+ {
109
+ // className destructured but not referenced — not this rule's concern
110
+ // (would be caught by no-unused-vars)
111
+ code: `
112
+ function Foo( { className, ...restProps } ) {
113
+ return <div className={ clsx( styles.foo ) } { ...restProps } />;
114
+ }
115
+ `,
116
+ },
117
+ ],
118
+ invalid: [
119
+ {
120
+ // className not destructured, sits in rest props
121
+ code: `
122
+ function Foo( { padding, ...rest } ) {
123
+ return <div className={ clsx( styles.foo ) } { ...rest } />;
124
+ }
125
+ `,
126
+ errors: [ { messageId: 'noUnmergedClassname' } ],
127
+ },
128
+ {
129
+ // className not destructured, no other named props
130
+ code: `
131
+ function Foo( { ...others } ) {
132
+ return <div className={ styles.foo } { ...others } />;
133
+ }
134
+ `,
135
+ errors: [ { messageId: 'noUnmergedClassname' } ],
136
+ },
137
+ {
138
+ // Arrow function, className not destructured
139
+ code: `
140
+ const Foo = ( { padding, ...props } ) => (
141
+ <div className={ styles.foo } { ...props } />
142
+ );
143
+ `,
144
+ errors: [ { messageId: 'noUnmergedClassname' } ],
145
+ },
146
+ {
147
+ // Named function expression
148
+ code: `
149
+ const Foo = function Foo( { padding, ...rest } ) {
150
+ return <div className={ styles.foo } { ...rest } />;
151
+ };
152
+ `,
153
+ errors: [ { messageId: 'noUnmergedClassname' } ],
154
+ },
155
+ {
156
+ // forwardRef pattern — className not destructured
157
+ code: `
158
+ const Foo = forwardRef( function Foo( { padding, ...restProps }, ref ) {
159
+ return <div ref={ ref } className={ clsx( styles.foo ) } { ...restProps } />;
160
+ } );
161
+ `,
162
+ errors: [ { messageId: 'noUnmergedClassname' } ],
163
+ },
164
+ {
165
+ // Props not destructured — spread of entire props object
166
+ code: `
167
+ function Foo( props ) {
168
+ return <div className={ styles.foo } { ...props } />;
169
+ }
170
+ `,
171
+ errors: [ { messageId: 'noUnmergedClassname' } ],
172
+ },
173
+ ],
174
+ } );
@@ -64,7 +64,6 @@ ruleTester.run( 'no-unsafe-wp-apis', rule, {
64
64
  {
65
65
  message: `Usage of \`__experimentalUnsafe\` from \`@wordpress/package\` is not allowed.
66
66
  See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
67
- type: 'ImportSpecifier',
68
67
  },
69
68
  ],
70
69
  },
@@ -75,7 +74,6 @@ See https://developer.wordpress.org/block-editor/contributors/develop/coding-gui
75
74
  {
76
75
  message: `Usage of \`__experimentalSafe\` from \`@wordpress/unsafe\` is not allowed.
77
76
  See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
78
- type: 'ImportSpecifier',
79
77
  },
80
78
  ],
81
79
  },
@@ -86,7 +84,6 @@ See https://developer.wordpress.org/block-editor/contributors/develop/coding-gui
86
84
  {
87
85
  message: `Usage of \`__experimentalSafe\` from \`@wordpress/unsafe\` is not allowed.
88
86
  See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
89
- type: 'ImportSpecifier',
90
87
  },
91
88
  ],
92
89
  },
@@ -97,7 +94,6 @@ See https://developer.wordpress.org/block-editor/contributors/develop/coding-gui
97
94
  {
98
95
  message: `Usage of \`__experimentalUnsafe\` from \`@wordpress/package\` is not allowed.
99
96
  See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
100
- type: 'ImportSpecifier',
101
97
  },
102
98
  ],
103
99
  },
@@ -108,7 +104,6 @@ See https://developer.wordpress.org/block-editor/contributors/develop/coding-gui
108
104
  {
109
105
  message: `Usage of \`__unstableFeature\` from \`@wordpress/package\` is not allowed.
110
106
  See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
111
- type: 'ImportSpecifier',
112
107
  },
113
108
  ],
114
109
  },
@@ -41,7 +41,6 @@ ruleTester.run( 'use-recommended-components', rule, {
41
41
  {
42
42
  message:
43
43
  '`SomeComponent` from `@wordpress/ui` is not yet recommended for use in a WordPress environment.',
44
- type: 'ImportSpecifier',
45
44
  },
46
45
  ],
47
46
  },
@@ -51,12 +50,10 @@ ruleTester.run( 'use-recommended-components', rule, {
51
50
  {
52
51
  message:
53
52
  '`Foo` from `@wordpress/ui` is not yet recommended for use in a WordPress environment.',
54
- type: 'ImportSpecifier',
55
53
  },
56
54
  {
57
55
  message:
58
56
  '`Bar` from `@wordpress/ui` is not yet recommended for use in a WordPress environment.',
59
- type: 'ImportSpecifier',
60
57
  },
61
58
  ],
62
59
  },
@@ -67,7 +64,6 @@ ruleTester.run( 'use-recommended-components', rule, {
67
64
  {
68
65
  message:
69
66
  '__experimentalZStack is planned for deprecation. Write your own CSS instead.',
70
- type: 'ImportSpecifier',
71
67
  },
72
68
  ],
73
69
  },
@@ -77,7 +73,6 @@ ruleTester.run( 'use-recommended-components', rule, {
77
73
  {
78
74
  message:
79
75
  '__experimentalZStack is planned for deprecation. Write your own CSS instead.',
80
- type: 'ImportSpecifier',
81
76
  },
82
77
  ],
83
78
  },
@@ -31,7 +31,7 @@ function arrayLast( array ) {
31
31
  function getReferences( context, specifiers ) {
32
32
  const variables = specifiers.reduce(
33
33
  ( acc, specifier ) =>
34
- acc.concat( context.getDeclaredVariables( specifier ) ),
34
+ acc.concat( context.sourceCode.getDeclaredVariables( specifier ) ),
35
35
  []
36
36
  );
37
37
  const references = variables.reduce(
@@ -58,7 +58,9 @@ function collectAllNodesFromCallbackFunctions( context, node ) {
58
58
  ( acc, { identifier: { parent } } ) =>
59
59
  parent && parent.arguments && parent.arguments.length > 0
60
60
  ? acc.concat(
61
- context.getDeclaredVariables( parent.arguments[ 0 ] )
61
+ context.sourceCode.getDeclaredVariables(
62
+ parent.arguments[ 0 ]
63
+ )
62
64
  )
63
65
  : acc,
64
66
  []
@@ -153,9 +155,10 @@ function getFixes( fixer, context, callNode ) {
153
155
  fixer.replaceText( callNode.arguments[ 0 ], variableName ),
154
156
  ];
155
157
 
156
- const imports = context
157
- .getAncestors()[ 0 ]
158
- .body.filter( ( node ) => node.type === 'ImportDeclaration' );
158
+ const ancestors = context.sourceCode.getAncestors( callNode );
159
+ const imports = ancestors[ 0 ].body.filter(
160
+ ( node ) => node.type === 'ImportDeclaration'
161
+ );
159
162
  const packageImports = imports.filter(
160
163
  ( node ) => node.source.value === importName
161
164
  );
@@ -22,7 +22,8 @@ module.exports = {
22
22
  },
23
23
  create( context ) {
24
24
  const mode = context.options[ 0 ] || 'always';
25
- const comments = context.getSourceCode().getAllComments();
25
+ const sourceCode = context.sourceCode;
26
+ const comments = sourceCode.getAllComments();
26
27
 
27
28
  /**
28
29
  * Locality classification of an import, one of "External",
@@ -194,9 +195,7 @@ module.exports = {
194
195
  return null;
195
196
  }
196
197
 
197
- const text = context
198
- .getSourceCode()
199
- .getText();
198
+ const text = sourceCode.getText();
200
199
 
201
200
  // Trim preceding and trailing newlines.
202
201
  let [ start, end ] = comment.range;
@@ -61,6 +61,7 @@ function extractTranslatorKeys( commentText ) {
61
61
  module.exports = {
62
62
  meta: {
63
63
  type: 'problem',
64
+ schema: [],
64
65
  messages: {
65
66
  missing:
66
67
  'Translation function with placeholders is missing preceding translator comment',
@@ -71,6 +72,7 @@ module.exports = {
71
72
  },
72
73
  },
73
74
  create( context ) {
75
+ const sourceCode = context.sourceCode;
74
76
  return {
75
77
  CallExpression( node ) {
76
78
  const {
@@ -107,7 +109,7 @@ module.exports = {
107
109
  return;
108
110
  }
109
111
 
110
- const comments = context.getCommentsBefore( node ).slice();
112
+ const comments = sourceCode.getCommentsBefore( node ).slice();
111
113
 
112
114
  let parentNode = parent;
113
115
 
@@ -123,7 +125,9 @@ module.exports = {
123
125
  parentNode.type !== 'Program' &&
124
126
  Math.abs( parentNode.loc.start.line - currentLine ) <= 1
125
127
  ) {
126
- comments.push( ...context.getCommentsBefore( parentNode ) );
128
+ comments.push(
129
+ ...sourceCode.getCommentsBefore( parentNode )
130
+ );
127
131
  parentNode = parentNode.parent;
128
132
  }
129
133
 
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ const { createDOMGlobalRule } = require( '../utils/dom-globals' );
5
+
6
+ module.exports = createDOMGlobalRule( {
7
+ description: 'Disallow use of DOM globals in class constructors',
8
+ message:
9
+ "Use of DOM global '{{name}}' is forbidden in class constructors, consider moving this to componentDidMount() or equivalent for non React components",
10
+ test( scope ) {
11
+ if ( scope.block?.parent ) {
12
+ const { type, kind } = scope.block.parent;
13
+ return type === 'MethodDefinition' && kind === 'constructor';
14
+ }
15
+ return false;
16
+ },
17
+ } );
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ const { createDOMGlobalRule } = require( '../utils/dom-globals' );
5
+
6
+ module.exports = createDOMGlobalRule( {
7
+ description: 'Disallow use of DOM globals in module scope',
8
+ message: "Use of DOM global '{{name}}' is forbidden in module scope",
9
+ test( scope ) {
10
+ return scope.type === 'module' || scope.type === 'global';
11
+ },
12
+ } );
@@ -0,0 +1,26 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ const {
5
+ createDOMGlobalRule,
6
+ isReturnValueJSX,
7
+ } = require( '../utils/dom-globals' );
8
+
9
+ module.exports = createDOMGlobalRule( {
10
+ description:
11
+ 'Disallow use of DOM globals in render() method of a React class component',
12
+ message:
13
+ "Use of DOM global '{{name}}' is forbidden in render(), consider moving this to componentDidMount()",
14
+ test( scope ) {
15
+ if ( scope.block?.parent ) {
16
+ const { type, kind, key } = scope.block.parent;
17
+ return (
18
+ type === 'MethodDefinition' &&
19
+ kind === 'method' &&
20
+ key.name === 'render' &&
21
+ isReturnValueJSX( scope )
22
+ );
23
+ }
24
+ return false;
25
+ },
26
+ } );
@@ -0,0 +1,17 @@
1
+ /**
2
+ * Internal dependencies
3
+ */
4
+ const {
5
+ createDOMGlobalRule,
6
+ isReturnValueJSX,
7
+ } = require( '../utils/dom-globals' );
8
+
9
+ module.exports = createDOMGlobalRule( {
10
+ description:
11
+ 'Disallow use of DOM globals in the render cycle of a React function component',
12
+ message:
13
+ "Use of DOM global '{{name}}' is forbidden in the render-cycle of a React FC, consider moving this inside useEffect()",
14
+ test( scope ) {
15
+ return isReturnValueJSX( scope );
16
+ },
17
+ } );
@@ -22,7 +22,7 @@ module.exports = {
22
22
  },
23
23
  create( context ) {
24
24
  let saveFunctionDepth = 0;
25
- const filename = context.getFilename();
25
+ const filename = context.filename;
26
26
 
27
27
  // Skip deprecated files as they preserve old behavior including translation functions
28
28
  const normalizedFilename = filename.replace( /\\/g, '/' );
@@ -3,7 +3,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
3
3
  type: 'problem',
4
4
  docs: {
5
5
  description:
6
- 'Disallow setting any CSS custom property beginning with --wpds- in inline styles',
6
+ 'Disallow setting any CSS custom property beginning with --wpds-',
7
7
  },
8
8
  schema: [],
9
9
  messages: {
@@ -14,9 +14,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
14
14
  create( context ) {
15
15
  return {
16
16
  /** @param {import('estree').Property} node */
17
- 'JSXAttribute[name.name="style"] ObjectExpression > Property[key.value=/^--wpds-/]'(
18
- node
19
- ) {
17
+ 'ObjectExpression > Property[key.value=/^--wpds-/]'( node ) {
20
18
  context.report( {
21
19
  node: node.key,
22
20
  messageId: 'disallowedSet',
@@ -4,35 +4,41 @@ const tokenList = tokenListModule.default || tokenListModule;
4
4
  const DS_TOKEN_PREFIX = 'wpds-';
5
5
 
6
6
  /**
7
- * Extracts all unique CSS custom properties (variables) from a given CSS value string,
8
- * including those in fallback positions, optionally filtering by a specific prefix.
7
+ * Single-pass extraction that finds all `--prefix-*` tokens in a CSS value
8
+ * string and classifies each occurrence as `var()`-wrapped or bare.
9
9
  *
10
- * @param {string} value - The CSS value string to search for variables.
10
+ * @param {string} value - The CSS value string to search.
11
11
  * @param {string} [prefix=''] - Optional prefix to filter variables (e.g., 'wpds-').
12
- * @return {Set<string>} A Set of unique matched CSS variable names (e.g., Set { '--wpds-token' }).
12
+ * @return {{ tokens: Set<string>, bare: Set<string> }}
13
+ * `tokens` — every unique matched token;
14
+ * `bare` — the subset that appeared at least once without a `var()` wrapper.
13
15
  *
14
16
  * @example
15
- * extractCSSVariables(
16
- * 'border: 1px solid var(--wpds-border-color, var(--wpds-border-fallback)); ' +
17
- * 'color: var(--wpds-color-fg, black); ' +
18
- * 'background: var(--unrelated-bg);',
19
- * 'wpds'
17
+ * classifyTokens(
18
+ * 'var(--wpds-color-fg) --wpds-color-bg',
19
+ * 'wpds-'
20
20
  * );
21
- * // → Set { '--wpds-border-color', '--wpds-border-fallback', '--wpds-color-fg' }
21
+ * // → { tokens: Set {'--wpds-color-fg','--wpds-color-bg'},
22
+ * // bare: Set {'--wpds-color-bg'} }
22
23
  */
23
- function extractCSSVariables( value, prefix = '' ) {
24
- const regex = /--[\w-]+/g;
25
- const variables = new Set();
24
+ function classifyTokens( value, prefix = '' ) {
25
+ const regex = new RegExp(
26
+ `(?:^|[^\\w])(var\\(\\s*)?(--${ prefix }[\\w-]+)`,
27
+ 'g'
28
+ );
29
+ const tokens = new Set();
30
+ const bare = new Set();
26
31
 
27
32
  let match;
28
33
  while ( ( match = regex.exec( value ) ) !== null ) {
29
- const variableName = match[ 0 ];
30
- if ( variableName.startsWith( `--${ prefix }` ) ) {
31
- variables.add( variableName );
34
+ const token = match[ 2 ];
35
+ tokens.add( token );
36
+ if ( ! match[ 1 ] ) {
37
+ bare.add( token );
32
38
  }
33
39
  }
34
40
 
35
- return variables;
41
+ return { tokens, bare };
36
42
  }
37
43
 
38
44
  const knownTokens = new Set( tokenList );
@@ -50,6 +56,8 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
50
56
  'The following CSS variables are not valid Design System tokens: {{ tokenNames }}',
51
57
  dynamicToken:
52
58
  'Design System tokens must not be dynamically constructed, as they cannot be statically verified for correctness or processed automatically to inject fallbacks.',
59
+ bareToken:
60
+ 'Design System tokens must be wrapped in `var()` for build-time fallback injection to work: {{ tokenNames }}',
53
61
  },
54
62
  },
55
63
  create( context ) {
@@ -71,6 +79,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
71
79
  [ dynamicTemplateLiteralAST ]( node ) {
72
80
  let hasDynamic = false;
73
81
  const unknownTokens = [];
82
+ const bareTokens = [];
74
83
 
75
84
  for ( const quasi of node.quasis ) {
76
85
  const raw = quasi.value.raw;
@@ -84,7 +93,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
84
93
  hasDynamic = true;
85
94
  }
86
95
 
87
- const tokens = extractCSSVariables(
96
+ const { tokens, bare } = classifyTokens(
88
97
  value,
89
98
  DS_TOKEN_PREFIX
90
99
  );
@@ -95,12 +104,15 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
95
104
  const endMatch = value.match( /(--([\w-]+))$/ );
96
105
  if ( endMatch ) {
97
106
  tokens.delete( endMatch[ 1 ] );
107
+ bare.delete( endMatch[ 1 ] );
98
108
  }
99
109
  }
100
110
 
101
111
  for ( const token of tokens ) {
102
112
  if ( ! knownTokens.has( token ) ) {
103
113
  unknownTokens.push( token );
114
+ } else if ( bare.has( token ) ) {
115
+ bareTokens.push( token );
104
116
  }
105
117
  }
106
118
  }
@@ -123,6 +135,18 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
123
135
  },
124
136
  } );
125
137
  }
138
+
139
+ if ( bareTokens.length > 0 ) {
140
+ context.report( {
141
+ node,
142
+ messageId: 'bareToken',
143
+ data: {
144
+ tokenNames: bareTokens
145
+ .map( ( token ) => `'${ token }'` )
146
+ .join( ', ' ),
147
+ },
148
+ } );
149
+ }
126
150
  },
127
151
  /** @param {import('estree').Literal | import('estree').TemplateElement} node */
128
152
  [ staticTokensAST ]( node ) {
@@ -145,7 +169,7 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
145
169
  return;
146
170
  }
147
171
 
148
- const usedTokens = extractCSSVariables(
172
+ const { tokens: usedTokens, bare } = classifyTokens(
149
173
  computedValue,
150
174
  DS_TOKEN_PREFIX
151
175
  );
@@ -164,6 +188,31 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
164
188
  },
165
189
  } );
166
190
  }
191
+
192
+ // Skip bare-token check for property keys
193
+ // (e.g. `{ '--wpds-token': value }` declaring a custom property).
194
+ const isPropertyKey =
195
+ node.parent?.type === 'Property' &&
196
+ node.parent.key === node;
197
+
198
+ if ( ! isPropertyKey ) {
199
+ const bareTokens = [ ...usedTokens ].filter(
200
+ ( token ) =>
201
+ knownTokens.has( token ) && bare.has( token )
202
+ );
203
+
204
+ if ( bareTokens.length > 0 ) {
205
+ context.report( {
206
+ node,
207
+ messageId: 'bareToken',
208
+ data: {
209
+ tokenNames: bareTokens
210
+ .map( ( token ) => `'${ token }'` )
211
+ .join( ', ' ),
212
+ },
213
+ } );
214
+ }
215
+ }
167
216
  },
168
217
  };
169
218
  },