@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.
- package/README.md +5 -0
- package/package.json +5 -5
- package/rules/__tests__/no-dom-globals-in-constructor.js +103 -0
- package/rules/__tests__/no-dom-globals-in-module-scope.js +160 -0
- package/rules/__tests__/no-dom-globals-in-react-cc-render.js +98 -0
- package/rules/__tests__/no-dom-globals-in-react-fc.js +125 -0
- package/rules/__tests__/no-i18n-in-save.js +0 -14
- package/rules/__tests__/no-setting-ds-tokens.js +33 -0
- package/rules/__tests__/no-unknown-ds-tokens.js +58 -0
- package/rules/__tests__/no-unmerged-classname.js +174 -0
- package/rules/__tests__/no-unsafe-wp-apis.js +0 -5
- package/rules/__tests__/use-recommended-components.js +0 -5
- package/rules/data-no-store-string-literals.js +8 -5
- package/rules/dependency-group.js +3 -4
- package/rules/i18n-translator-comments.js +6 -2
- package/rules/no-dom-globals-in-constructor.js +17 -0
- package/rules/no-dom-globals-in-module-scope.js +12 -0
- package/rules/no-dom-globals-in-react-cc-render.js +26 -0
- package/rules/no-dom-globals-in-react-fc.js +17 -0
- package/rules/no-i18n-in-save.js +1 -1
- package/rules/no-setting-ds-tokens.js +2 -4
- package/rules/no-unknown-ds-tokens.js +68 -19
- package/rules/no-unmerged-classname.js +107 -0
- package/rules/no-unused-vars-before-return.js +7 -6
- package/rules/react-no-unsafe-timeout.js +1 -1
- package/utils/dom-globals.js +157 -0
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
2
|
+
module.exports = {
|
|
3
|
+
meta: {
|
|
4
|
+
type: 'problem',
|
|
5
|
+
schema: [],
|
|
6
|
+
messages: {
|
|
7
|
+
noUnmergedClassname:
|
|
8
|
+
'The `className` prop should be destructured from props and merged into the `className` attribute to ensure it is forwarded to the underlying element.',
|
|
9
|
+
},
|
|
10
|
+
},
|
|
11
|
+
create( context ) {
|
|
12
|
+
return {
|
|
13
|
+
JSXOpeningElement( node ) {
|
|
14
|
+
const classNameAttr = node.attributes.find(
|
|
15
|
+
( attr ) =>
|
|
16
|
+
attr.type === 'JSXAttribute' &&
|
|
17
|
+
attr.name?.name === 'className'
|
|
18
|
+
);
|
|
19
|
+
|
|
20
|
+
if ( ! classNameAttr ) {
|
|
21
|
+
return;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
const hasSpread = node.attributes.some(
|
|
25
|
+
( attr ) => attr.type === 'JSXSpreadAttribute'
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
if ( ! hasSpread ) {
|
|
29
|
+
return;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const func = getEnclosingFunction( node );
|
|
33
|
+
if ( ! func ) {
|
|
34
|
+
return;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const propsParam = func.params[ 0 ];
|
|
38
|
+
if ( ! propsParam ) {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Props passed as a plain identifier (e.g. `function Foo( props )`)
|
|
43
|
+
// and spread directly — className can never be merged.
|
|
44
|
+
if ( propsParam.type === 'Identifier' ) {
|
|
45
|
+
const spreadMatchesParam = node.attributes.some(
|
|
46
|
+
( attr ) =>
|
|
47
|
+
attr.type === 'JSXSpreadAttribute' &&
|
|
48
|
+
attr.argument.type === 'Identifier' &&
|
|
49
|
+
attr.argument.name === propsParam.name
|
|
50
|
+
);
|
|
51
|
+
if ( spreadMatchesParam ) {
|
|
52
|
+
context.report( {
|
|
53
|
+
node: classNameAttr,
|
|
54
|
+
messageId: 'noUnmergedClassname',
|
|
55
|
+
} );
|
|
56
|
+
}
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
if ( propsParam.type !== 'ObjectPattern' ) {
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
const hasRestElement = propsParam.properties.some(
|
|
65
|
+
( prop ) => prop.type === 'RestElement'
|
|
66
|
+
);
|
|
67
|
+
if ( ! hasRestElement ) {
|
|
68
|
+
return;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const classNameDestructured = propsParam.properties.some(
|
|
72
|
+
( prop ) =>
|
|
73
|
+
prop.type === 'Property' &&
|
|
74
|
+
prop.key?.name === 'className'
|
|
75
|
+
);
|
|
76
|
+
|
|
77
|
+
if ( ! classNameDestructured ) {
|
|
78
|
+
context.report( {
|
|
79
|
+
node: classNameAttr,
|
|
80
|
+
messageId: 'noUnmergedClassname',
|
|
81
|
+
} );
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
};
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* Walk up the AST to find the enclosing function node.
|
|
90
|
+
*
|
|
91
|
+
* @param {import('estree').Node} node
|
|
92
|
+
* @return {import('estree').Function | null} The enclosing function, or null.
|
|
93
|
+
*/
|
|
94
|
+
function getEnclosingFunction( node ) {
|
|
95
|
+
let current = node;
|
|
96
|
+
while ( current.parent ) {
|
|
97
|
+
current = current.parent;
|
|
98
|
+
if (
|
|
99
|
+
current.type === 'FunctionDeclaration' ||
|
|
100
|
+
current.type === 'FunctionExpression' ||
|
|
101
|
+
current.type === 'ArrowFunctionExpression'
|
|
102
|
+
) {
|
|
103
|
+
return current;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
return null;
|
|
107
|
+
}
|
|
@@ -11,15 +11,16 @@
|
|
|
11
11
|
const FUNCTION_SCOPE_JSX_IDENTIFIERS = new WeakMap();
|
|
12
12
|
|
|
13
13
|
/**
|
|
14
|
-
* Returns the closest function scope for the
|
|
15
|
-
*
|
|
14
|
+
* Returns the closest function scope for the given node, or undefined if it
|
|
15
|
+
* cannot be determined.
|
|
16
16
|
*
|
|
17
17
|
* @param {ESLintRuleContext} context ESLint context object.
|
|
18
|
+
* @param {ESTreeNode} node Current AST node.
|
|
18
19
|
*
|
|
19
20
|
* @return {ESLintScope|undefined} Function scope, if known.
|
|
20
21
|
*/
|
|
21
|
-
function getClosestFunctionScope( context ) {
|
|
22
|
-
let functionScope = context.getScope();
|
|
22
|
+
function getClosestFunctionScope( context, node ) {
|
|
23
|
+
let functionScope = context.sourceCode.getScope( node );
|
|
23
24
|
while ( functionScope.type !== 'function' && functionScope.upper ) {
|
|
24
25
|
functionScope = functionScope.upper;
|
|
25
26
|
}
|
|
@@ -73,7 +74,7 @@ module.exports = /** @type {import('eslint').Rule} */ ( {
|
|
|
73
74
|
// identifiers. Account for this by visiting JSX identifiers
|
|
74
75
|
// first, and tracking them in a map per function scope, which
|
|
75
76
|
// is later merged with the known variable references.
|
|
76
|
-
const functionScope = getClosestFunctionScope( context );
|
|
77
|
+
const functionScope = getClosestFunctionScope( context, node );
|
|
77
78
|
if ( ! functionScope ) {
|
|
78
79
|
return;
|
|
79
80
|
}
|
|
@@ -88,7 +89,7 @@ module.exports = /** @type {import('eslint').Rule} */ ( {
|
|
|
88
89
|
FUNCTION_SCOPE_JSX_IDENTIFIERS.get( functionScope ).add( node );
|
|
89
90
|
},
|
|
90
91
|
'ReturnStatement:exit'( node ) {
|
|
91
|
-
const functionScope = getClosestFunctionScope( context );
|
|
92
|
+
const functionScope = getClosestFunctionScope( context, node );
|
|
92
93
|
if ( ! functionScope ) {
|
|
93
94
|
return;
|
|
94
95
|
}
|
|
@@ -71,7 +71,7 @@ module.exports = {
|
|
|
71
71
|
// Consider whether `setTimeout` is a reference to the global
|
|
72
72
|
// by checking references to see if `setTimeout` resolves to a
|
|
73
73
|
// variable in scope.
|
|
74
|
-
const { references } = context.getScope();
|
|
74
|
+
const { references } = context.sourceCode.getScope( node );
|
|
75
75
|
const hasResolvedReference = references.some(
|
|
76
76
|
( reference ) =>
|
|
77
77
|
reference.identifier.name === 'setTimeout' &&
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Shared utilities for SSR-safety rules that prevent DOM global usage in
|
|
3
|
+
* unsafe contexts.
|
|
4
|
+
*
|
|
5
|
+
* These rules replace the unmaintained `eslint-plugin-ssr-friendly` package.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { browser: browserGlobals, node: nodeGlobals } = require( 'globals' );
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns true if the given name is a browser-only DOM global (exists in
|
|
12
|
+
* browser globals but not in node globals).
|
|
13
|
+
*
|
|
14
|
+
* @param {string} name Identifier name to check.
|
|
15
|
+
* @return {boolean} Whether the name is a DOM-only global.
|
|
16
|
+
*/
|
|
17
|
+
function isDOMGlobal( name ) {
|
|
18
|
+
return name in browserGlobals && ! ( name in nodeGlobals );
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Returns true if the given scope's block returns JSX (heuristic for
|
|
23
|
+
* detecting React render functions).
|
|
24
|
+
*
|
|
25
|
+
* @param {import('eslint').Scope.Scope} scope Scope to check.
|
|
26
|
+
* @return {boolean} Whether the scope returns JSX.
|
|
27
|
+
*/
|
|
28
|
+
function isReturnValueJSX( scope ) {
|
|
29
|
+
if ( scope.type !== 'function' ) {
|
|
30
|
+
return false;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
const { block } = scope;
|
|
34
|
+
|
|
35
|
+
// Concise arrow function: const C = () => <div />
|
|
36
|
+
if (
|
|
37
|
+
block.type === 'ArrowFunctionExpression' &&
|
|
38
|
+
block.body.type !== 'BlockStatement'
|
|
39
|
+
) {
|
|
40
|
+
return (
|
|
41
|
+
block.body.type === 'JSXElement' ||
|
|
42
|
+
block.body.type === 'JSXFragment'
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const body = block?.body?.body;
|
|
47
|
+
if ( ! body || typeof body.find !== 'function' ) {
|
|
48
|
+
return false;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
return body.some(
|
|
52
|
+
( statement ) =>
|
|
53
|
+
statement?.type === 'ReturnStatement' &&
|
|
54
|
+
statement.argument &&
|
|
55
|
+
( statement.argument.type === 'JSXElement' ||
|
|
56
|
+
statement.argument.type === 'JSXFragment' )
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Returns true if the reference's identifier should be skipped from
|
|
62
|
+
* reporting. Allows `typeof <global>` checks and TypeScript type references.
|
|
63
|
+
*
|
|
64
|
+
* @param {import('eslint').Scope.Reference} reference Variable reference.
|
|
65
|
+
* @return {boolean} Whether the reference should be skipped.
|
|
66
|
+
*/
|
|
67
|
+
function shouldSkipReference( reference ) {
|
|
68
|
+
const { parent } = reference.identifier;
|
|
69
|
+
return (
|
|
70
|
+
( parent.type === 'UnaryExpression' && parent.operator === 'typeof' ) ||
|
|
71
|
+
parent.type === 'TSTypeReference' ||
|
|
72
|
+
parent.type === 'TSInterfaceHeritage' ||
|
|
73
|
+
parent.type === 'TSTypeQuery' ||
|
|
74
|
+
parent.type === 'TSQualifiedName'
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Creates an ESLint rule that reports DOM global usage based on a scope
|
|
80
|
+
* predicate.
|
|
81
|
+
*
|
|
82
|
+
* @param {Object} options Rule options.
|
|
83
|
+
* @param {string} options.description Rule description.
|
|
84
|
+
* @param {string} options.message Error message template (use {{name}} for the global name).
|
|
85
|
+
* @param {(scope: import('eslint').Scope.Scope) => boolean} options.test Predicate that receives the reference's scope and
|
|
86
|
+
* returns true if usage should be reported.
|
|
87
|
+
* @return {import('eslint').Rule.RuleModule} ESLint rule module.
|
|
88
|
+
*/
|
|
89
|
+
function createDOMGlobalRule( { description, message, test } ) {
|
|
90
|
+
return {
|
|
91
|
+
meta: {
|
|
92
|
+
type: 'problem',
|
|
93
|
+
schema: [],
|
|
94
|
+
docs: {
|
|
95
|
+
description,
|
|
96
|
+
},
|
|
97
|
+
messages: {
|
|
98
|
+
defaultMessage: message,
|
|
99
|
+
},
|
|
100
|
+
},
|
|
101
|
+
create( context ) {
|
|
102
|
+
return {
|
|
103
|
+
Program( node ) {
|
|
104
|
+
const scope = context.sourceCode.getScope( node );
|
|
105
|
+
|
|
106
|
+
// Report variables declared elsewhere (e.g. variables
|
|
107
|
+
// defined as "global" by eslint).
|
|
108
|
+
for ( const variable of scope.variables ) {
|
|
109
|
+
if (
|
|
110
|
+
variable.defs.length === 0 &&
|
|
111
|
+
isDOMGlobal( variable.name )
|
|
112
|
+
) {
|
|
113
|
+
for ( const reference of variable.references ) {
|
|
114
|
+
if (
|
|
115
|
+
! shouldSkipReference( reference ) &&
|
|
116
|
+
test( reference.from )
|
|
117
|
+
) {
|
|
118
|
+
context.report( {
|
|
119
|
+
node: reference.identifier,
|
|
120
|
+
messageId: 'defaultMessage',
|
|
121
|
+
data: {
|
|
122
|
+
name: reference.identifier.name,
|
|
123
|
+
},
|
|
124
|
+
} );
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Report variables not declared at all.
|
|
131
|
+
for ( const reference of scope.through ) {
|
|
132
|
+
if (
|
|
133
|
+
isDOMGlobal( reference.identifier.name ) &&
|
|
134
|
+
! shouldSkipReference( reference ) &&
|
|
135
|
+
test( reference.from )
|
|
136
|
+
) {
|
|
137
|
+
context.report( {
|
|
138
|
+
node: reference.identifier,
|
|
139
|
+
messageId: 'defaultMessage',
|
|
140
|
+
data: {
|
|
141
|
+
name: reference.identifier.name,
|
|
142
|
+
},
|
|
143
|
+
} );
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
};
|
|
148
|
+
},
|
|
149
|
+
};
|
|
150
|
+
}
|
|
151
|
+
|
|
152
|
+
module.exports = {
|
|
153
|
+
isDOMGlobal,
|
|
154
|
+
isReturnValueJSX,
|
|
155
|
+
shouldSkipReference,
|
|
156
|
+
createDOMGlobalRule,
|
|
157
|
+
};
|