@wordpress/eslint-plugin 24.0.1-next.ba3aee3a2.0 → 24.1.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.
@@ -0,0 +1,101 @@
1
+ const tokenListModule = require( '@wordpress/theme/design-tokens.js' );
2
+ const tokenList = tokenListModule.default || tokenListModule;
3
+
4
+ const DS_TOKEN_PREFIX = 'wpds-';
5
+
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.
9
+ *
10
+ * @param {string} value - The CSS value string to search for variables.
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' }).
13
+ *
14
+ * @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'
20
+ * );
21
+ * // → Set { '--wpds-border-color', '--wpds-border-fallback', '--wpds-color-fg' }
22
+ */
23
+ function extractCSSVariables( value, prefix = '' ) {
24
+ const regex = /--[\w-]+/g;
25
+ const variables = new Set();
26
+
27
+ let match;
28
+ while ( ( match = regex.exec( value ) ) !== null ) {
29
+ const variableName = match[ 0 ];
30
+ if ( variableName.startsWith( `--${ prefix }` ) ) {
31
+ variables.add( variableName );
32
+ }
33
+ }
34
+
35
+ return variables;
36
+ }
37
+
38
+ const knownTokens = new Set( tokenList );
39
+ const wpdsTokensRegex = new RegExp( `[^\\w]--${ DS_TOKEN_PREFIX }`, 'i' );
40
+
41
+ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
42
+ meta: {
43
+ type: 'problem',
44
+ docs: {
45
+ description: 'Prevent use of non-existing --wpds-* variables',
46
+ },
47
+ schema: [],
48
+ messages: {
49
+ onlyKnownTokens:
50
+ 'The following CSS variables are not valid Design System tokens: {{ tokenNames }}',
51
+ },
52
+ },
53
+ create( context ) {
54
+ const disallowedTokensAST = `JSXAttribute[name.name="style"] :matches(Literal[value=${ wpdsTokensRegex }], TemplateLiteral TemplateElement[value.raw=${ wpdsTokensRegex }])`;
55
+ return {
56
+ /** @param {import('estree').Literal | import('estree').TemplateElement} node */
57
+ [ disallowedTokensAST ]( node ) {
58
+ let computedValue;
59
+
60
+ if ( ! node.value ) {
61
+ return;
62
+ }
63
+
64
+ if ( typeof node.value === 'string' ) {
65
+ // Get the node's value when it's a "string"
66
+ computedValue = node.value;
67
+ } else if (
68
+ typeof node.value === 'object' &&
69
+ 'raw' in node.value
70
+ ) {
71
+ // Get the node's value when it's a `template literal`
72
+ computedValue = node.value.cooked ?? node.value.raw;
73
+ }
74
+
75
+ if ( ! computedValue ) {
76
+ return;
77
+ }
78
+
79
+ const usedTokens = extractCSSVariables(
80
+ computedValue,
81
+ DS_TOKEN_PREFIX
82
+ );
83
+ const unknownTokens = [ ...usedTokens ].filter(
84
+ ( token ) => ! knownTokens.has( token )
85
+ );
86
+
87
+ if ( unknownTokens.length > 0 ) {
88
+ context.report( {
89
+ node,
90
+ messageId: 'onlyKnownTokens',
91
+ data: {
92
+ tokenNames: unknownTokens
93
+ .map( ( token ) => `'${ token }'` )
94
+ .join( ', ' ),
95
+ },
96
+ } );
97
+ }
98
+ },
99
+ };
100
+ },
101
+ } );
@@ -0,0 +1,50 @@
1
+ /**
2
+ * Check if a JSX/React attribute exists and has a truthy value.
3
+ *
4
+ * This utility analyzes JSX attribute nodes from an ESLint AST to determine
5
+ * if a specific attribute is present with a truthy value.
6
+ *
7
+ * Handles the following patterns:
8
+ * - Boolean shorthand: `<Component prop />` → truthy
9
+ * - Explicit true: `<Component prop={true} />` → truthy
10
+ * - Explicit false: `<Component prop={false} />` → NOT truthy
11
+ * - String values: `<Component prop="value" />` → truthy (if non-empty)
12
+ * - Dynamic expressions: `<Component prop={someVar} />` → assumed truthy
13
+ *
14
+ * @param {Array} attributes - Array of JSX attribute nodes from the ESLint AST
15
+ * @param {string} attrName - The attribute name to check
16
+ * @return {boolean} Whether the attribute exists with a truthy value
17
+ */
18
+ function hasTruthyJsxAttribute( attributes, attrName ) {
19
+ const attr = attributes.find(
20
+ ( a ) => a.type === 'JSXAttribute' && a.name && a.name.name === attrName
21
+ );
22
+
23
+ if ( ! attr ) {
24
+ return false;
25
+ }
26
+
27
+ // Boolean attribute without value (e.g., `<Button disabled />`)
28
+ if ( attr.value === null ) {
29
+ return true;
30
+ }
31
+
32
+ // Expression like `prop={true}` or `prop={false}`
33
+ if (
34
+ attr.value.type === 'JSXExpressionContainer' &&
35
+ attr.value.expression.type === 'Literal'
36
+ ) {
37
+ return attr.value.expression.value !== false;
38
+ }
39
+
40
+ // String value - truthy if not empty
41
+ if ( attr.value.type === 'Literal' ) {
42
+ return Boolean( attr.value.value );
43
+ }
44
+
45
+ // For any other expression (variables, function calls, etc.),
46
+ // assume it could be truthy since we can't statically analyze it
47
+ return true;
48
+ }
49
+
50
+ module.exports = { hasTruthyJsxAttribute };
package/utils/index.js CHANGED
@@ -9,6 +9,7 @@ const {
9
9
  const { getTranslateFunctionArgs } = require( './get-translate-function-args' );
10
10
  const { getTextContentFromNode } = require( './get-text-content-from-node' );
11
11
  const { getTranslateFunctionName } = require( './get-translate-function-name' );
12
+ const { hasTruthyJsxAttribute } = require( './has-truthy-jsx-attribute' );
12
13
  const isPackageInstalled = require( './is-package-installed' );
13
14
 
14
15
  module.exports = {
@@ -18,5 +19,6 @@ module.exports = {
18
19
  getTranslateFunctionArgs,
19
20
  getTextContentFromNode,
20
21
  getTranslateFunctionName,
22
+ hasTruthyJsxAttribute,
21
23
  isPackageInstalled,
22
24
  };