@wordpress/eslint-plugin 24.3.1-next.v.202602271551.0 → 24.4.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 +56 -0
- package/rules/__tests__/no-dom-globals-in-module-scope.js +117 -0
- package/rules/__tests__/no-dom-globals-in-react-cc-render.js +51 -0
- package/rules/__tests__/no-dom-globals-in-react-fc.js +77 -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-unsafe-wp-apis.js +0 -5
- package/rules/__tests__/use-recommended-components.js +91 -0
- 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-unused-vars-before-return.js +7 -6
- package/rules/react-no-unsafe-timeout.js +1 -1
- package/rules/use-recommended-components.js +114 -0
- package/utils/dom-globals.js +156 -0
|
@@ -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(
|
|
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
|
|
157
|
-
|
|
158
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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(
|
|
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
|
+
} );
|
package/rules/no-i18n-in-save.js
CHANGED
|
@@ -22,7 +22,7 @@ module.exports = {
|
|
|
22
22
|
},
|
|
23
23
|
create( context ) {
|
|
24
24
|
let saveFunctionDepth = 0;
|
|
25
|
-
const filename = context.
|
|
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-
|
|
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
|
-
'
|
|
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
|
-
*
|
|
8
|
-
*
|
|
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
|
|
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
|
|
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
|
-
*
|
|
16
|
-
* '
|
|
17
|
-
* '
|
|
18
|
-
* 'background: var(--unrelated-bg);',
|
|
19
|
-
* 'wpds'
|
|
17
|
+
* classifyTokens(
|
|
18
|
+
* 'var(--wpds-color-fg) --wpds-color-bg',
|
|
19
|
+
* 'wpds-'
|
|
20
20
|
* );
|
|
21
|
-
* // → Set {
|
|
21
|
+
* // → { tokens: Set {'--wpds-color-fg','--wpds-color-bg'},
|
|
22
|
+
* // bare: Set {'--wpds-color-bg'} }
|
|
22
23
|
*/
|
|
23
|
-
function
|
|
24
|
-
const regex =
|
|
25
|
-
|
|
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
|
|
30
|
-
|
|
31
|
-
|
|
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
|
|
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 =
|
|
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 =
|
|
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
|
},
|
|
@@ -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,114 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Allowlist: only the listed components are permitted from these packages.
|
|
3
|
+
* Any other named import will be flagged with the package's message.
|
|
4
|
+
*
|
|
5
|
+
* `message` supports `{{ name }}` and `{{ source }}` placeholders.
|
|
6
|
+
*
|
|
7
|
+
* @type {Record<string, { allowed: string[], message?: string }>}
|
|
8
|
+
*/
|
|
9
|
+
const ALLOWLIST = {
|
|
10
|
+
'@wordpress/ui': {
|
|
11
|
+
allowed: [ 'Badge', 'Stack' ],
|
|
12
|
+
message:
|
|
13
|
+
'`{{ name }}` from `{{ source }}` is not yet recommended for use in a WordPress environment.',
|
|
14
|
+
},
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Denylist: the listed components are flagged with a message pointing
|
|
19
|
+
* to a recommended alternative.
|
|
20
|
+
*
|
|
21
|
+
* Messages support `{{ name }}` and `{{ source }}` placeholders.
|
|
22
|
+
*
|
|
23
|
+
* @type {Record<string, Record<string, string>>}
|
|
24
|
+
*/
|
|
25
|
+
const DENYLIST = {
|
|
26
|
+
'@wordpress/components': {
|
|
27
|
+
__experimentalZStack:
|
|
28
|
+
'{{ name }} is planned for deprecation. Write your own CSS instead.',
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** @type {import('eslint').Rule.RuleModule} */
|
|
33
|
+
const rule = {
|
|
34
|
+
meta: {
|
|
35
|
+
type: 'suggestion',
|
|
36
|
+
docs: {
|
|
37
|
+
description:
|
|
38
|
+
'Encourage the use of recommended UI components in a WordPress environment.',
|
|
39
|
+
url: 'https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/use-recommended-components.md',
|
|
40
|
+
},
|
|
41
|
+
schema: [],
|
|
42
|
+
},
|
|
43
|
+
create( context ) {
|
|
44
|
+
return {
|
|
45
|
+
/** @param {import('estree').ImportDeclaration} node */
|
|
46
|
+
ImportDeclaration( node ) {
|
|
47
|
+
if ( typeof node.source.value !== 'string' ) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
const source = node.source.value;
|
|
52
|
+
|
|
53
|
+
const allowlistEntry = ALLOWLIST[ source ];
|
|
54
|
+
const denylistEntry = DENYLIST[ source ];
|
|
55
|
+
|
|
56
|
+
if ( ! allowlistEntry && ! denylistEntry ) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
node.specifiers.forEach( ( specifier ) => {
|
|
61
|
+
if ( specifier.type !== 'ImportSpecifier' ) {
|
|
62
|
+
return;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const name = specifier.imported.name;
|
|
66
|
+
|
|
67
|
+
if (
|
|
68
|
+
allowlistEntry &&
|
|
69
|
+
! allowlistEntry.allowed.includes( name )
|
|
70
|
+
) {
|
|
71
|
+
context.report( {
|
|
72
|
+
node: specifier,
|
|
73
|
+
message: resolveMessage(
|
|
74
|
+
allowlistEntry.message,
|
|
75
|
+
name,
|
|
76
|
+
source
|
|
77
|
+
),
|
|
78
|
+
} );
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
if ( denylistEntry?.hasOwnProperty( name ) ) {
|
|
82
|
+
context.report( {
|
|
83
|
+
node: specifier,
|
|
84
|
+
message: resolveMessage(
|
|
85
|
+
denylistEntry[ name ],
|
|
86
|
+
name,
|
|
87
|
+
source
|
|
88
|
+
),
|
|
89
|
+
} );
|
|
90
|
+
}
|
|
91
|
+
} );
|
|
92
|
+
},
|
|
93
|
+
};
|
|
94
|
+
},
|
|
95
|
+
};
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* @param {string|undefined} template
|
|
99
|
+
* @param {string} name
|
|
100
|
+
* @param {string} source
|
|
101
|
+
* @return {string} Resolved message string.
|
|
102
|
+
*/
|
|
103
|
+
function resolveMessage( template, name, source ) {
|
|
104
|
+
if ( ! template ) {
|
|
105
|
+
return `\`${ name }\` from \`${ source }\` is not recommended.`;
|
|
106
|
+
}
|
|
107
|
+
return template
|
|
108
|
+
.replace( /\{\{\s*name\s*\}\}/g, name )
|
|
109
|
+
.replace( /\{\{\s*source\s*\}\}/g, source );
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
module.exports = rule;
|
|
113
|
+
module.exports.ALLOWLIST = ALLOWLIST;
|
|
114
|
+
module.exports.DENYLIST = DENYLIST;
|
|
@@ -0,0 +1,156 @@
|
|
|
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
|
+
);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Creates an ESLint rule that reports DOM global usage based on a scope
|
|
79
|
+
* predicate.
|
|
80
|
+
*
|
|
81
|
+
* @param {Object} options Rule options.
|
|
82
|
+
* @param {string} options.description Rule description.
|
|
83
|
+
* @param {string} options.message Error message template (use {{name}} for the global name).
|
|
84
|
+
* @param {(scope: import('eslint').Scope.Scope) => boolean} options.test Predicate that receives the reference's scope and
|
|
85
|
+
* returns true if usage should be reported.
|
|
86
|
+
* @return {import('eslint').Rule.RuleModule} ESLint rule module.
|
|
87
|
+
*/
|
|
88
|
+
function createDOMGlobalRule( { description, message, test } ) {
|
|
89
|
+
return {
|
|
90
|
+
meta: {
|
|
91
|
+
type: 'problem',
|
|
92
|
+
schema: [],
|
|
93
|
+
docs: {
|
|
94
|
+
description,
|
|
95
|
+
},
|
|
96
|
+
messages: {
|
|
97
|
+
defaultMessage: message,
|
|
98
|
+
},
|
|
99
|
+
},
|
|
100
|
+
create( context ) {
|
|
101
|
+
return {
|
|
102
|
+
Program( node ) {
|
|
103
|
+
const scope = context.sourceCode.getScope( node );
|
|
104
|
+
|
|
105
|
+
// Report variables declared elsewhere (e.g. variables
|
|
106
|
+
// defined as "global" by eslint).
|
|
107
|
+
for ( const variable of scope.variables ) {
|
|
108
|
+
if (
|
|
109
|
+
variable.defs.length === 0 &&
|
|
110
|
+
isDOMGlobal( variable.name )
|
|
111
|
+
) {
|
|
112
|
+
for ( const reference of variable.references ) {
|
|
113
|
+
if (
|
|
114
|
+
! shouldSkipReference( reference ) &&
|
|
115
|
+
test( reference.from )
|
|
116
|
+
) {
|
|
117
|
+
context.report( {
|
|
118
|
+
node: reference.identifier,
|
|
119
|
+
messageId: 'defaultMessage',
|
|
120
|
+
data: {
|
|
121
|
+
name: reference.identifier.name,
|
|
122
|
+
},
|
|
123
|
+
} );
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// Report variables not declared at all.
|
|
130
|
+
for ( const reference of scope.through ) {
|
|
131
|
+
if (
|
|
132
|
+
isDOMGlobal( reference.identifier.name ) &&
|
|
133
|
+
! shouldSkipReference( reference ) &&
|
|
134
|
+
test( reference.from )
|
|
135
|
+
) {
|
|
136
|
+
context.report( {
|
|
137
|
+
node: reference.identifier,
|
|
138
|
+
messageId: 'defaultMessage',
|
|
139
|
+
data: {
|
|
140
|
+
name: reference.identifier.name,
|
|
141
|
+
},
|
|
142
|
+
} );
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
};
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
module.exports = {
|
|
152
|
+
isDOMGlobal,
|
|
153
|
+
isReturnValueJSX,
|
|
154
|
+
shouldSkipReference,
|
|
155
|
+
createDOMGlobalRule,
|
|
156
|
+
};
|