@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.
- package/README.md +3 -1
- package/configs/custom.js +2 -0
- package/package.json +5 -4
- package/rules/__tests__/components-no-missing-40px-size-prop.js +353 -0
- package/rules/__tests__/components-no-unsafe-button-disabled.js +235 -0
- package/rules/__tests__/dependency-group.js +33 -0
- package/rules/__tests__/no-setting-ds-tokens.js +46 -0
- package/rules/__tests__/no-unknown-ds-tokens.js +60 -0
- package/rules/components-no-missing-40px-size-prop.js +327 -0
- package/rules/components-no-unsafe-button-disabled.js +126 -0
- package/rules/dependency-group.js +64 -1
- package/rules/no-setting-ds-tokens.js +27 -0
- package/rules/no-unknown-ds-tokens.js +101 -0
- package/utils/has-truthy-jsx-attribute.js +50 -0
- package/utils/index.js +2 -0
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../no-setting-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-setting-ds-tokens', rule, {
|
|
14
|
+
valid: [
|
|
15
|
+
{
|
|
16
|
+
code: `<div style={ { '--my-custom-prop': 'value' } } />`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: `<div style={ { color: 'var(--wpds-color-fg-content-neutral)' } } />`,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: `<div style={ { '--other-prefix-token': '10px' } } />`,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: `<div style={ { margin: '10px' } } />`,
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
invalid: [
|
|
29
|
+
{
|
|
30
|
+
code: `<div style={ { '--wpds-color-fg-content-neutral': 'red' } } />`,
|
|
31
|
+
errors: [
|
|
32
|
+
{
|
|
33
|
+
messageId: 'disallowedSet',
|
|
34
|
+
},
|
|
35
|
+
],
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
code: `<div style={ { '--wpds-font-size-md': '10px', color: 'blue' } } />`,
|
|
39
|
+
errors: [
|
|
40
|
+
{
|
|
41
|
+
messageId: 'disallowedSet',
|
|
42
|
+
},
|
|
43
|
+
],
|
|
44
|
+
},
|
|
45
|
+
],
|
|
46
|
+
} );
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import { RuleTester } from 'eslint';
|
|
2
|
+
import rule from '../no-unknown-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-unknown-ds-tokens', rule, {
|
|
14
|
+
valid: [
|
|
15
|
+
{
|
|
16
|
+
code: `<div style={ { color: 'var(--my-custom-prop)' } } />`,
|
|
17
|
+
},
|
|
18
|
+
{
|
|
19
|
+
code: `<div style={ { color: 'blue' } } />`,
|
|
20
|
+
},
|
|
21
|
+
{
|
|
22
|
+
code: `<div style={ { color: 'var(--other-prefix-token)' } } />`,
|
|
23
|
+
},
|
|
24
|
+
{
|
|
25
|
+
code: `<div style={ { color: 'var(--wpds-color-fg-content-neutral)' } } />`,
|
|
26
|
+
},
|
|
27
|
+
{
|
|
28
|
+
code: '<div style={ { color: `var(--wpds-color-fg-content-neutral)` } } />',
|
|
29
|
+
},
|
|
30
|
+
],
|
|
31
|
+
invalid: [
|
|
32
|
+
{
|
|
33
|
+
code: `<div style={ { color: 'var(--wpds-nonexistent-token)' } } />`,
|
|
34
|
+
errors: [
|
|
35
|
+
{
|
|
36
|
+
messageId: 'onlyKnownTokens',
|
|
37
|
+
},
|
|
38
|
+
],
|
|
39
|
+
},
|
|
40
|
+
{
|
|
41
|
+
code: `<div style={ { color: 'var(--wpds-fake-color, var(--wpds-also-fake))' } } />`,
|
|
42
|
+
errors: [
|
|
43
|
+
{
|
|
44
|
+
messageId: 'onlyKnownTokens',
|
|
45
|
+
data: {
|
|
46
|
+
tokenNames: "'--wpds-fake-color', '--wpds-also-fake'",
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
code: '<div style={ { color: `var(--wpds-nonexistent)` } } />',
|
|
53
|
+
errors: [
|
|
54
|
+
{
|
|
55
|
+
messageId: 'onlyKnownTokens',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
],
|
|
60
|
+
} );
|
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
const { hasTruthyJsxAttribute } = require( '../utils' );
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enforces that specific components from @wordpress/components include the
|
|
5
|
+
* `__next40pxDefaultSize` prop.
|
|
6
|
+
*
|
|
7
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Components that require the __next40pxDefaultSize prop.
|
|
12
|
+
* These can be exempted if they have a non-default `size` prop.
|
|
13
|
+
*/
|
|
14
|
+
const COMPONENTS_REQUIRING_40PX = new Set( [
|
|
15
|
+
'BorderBoxControl',
|
|
16
|
+
'BorderControl',
|
|
17
|
+
'BoxControl',
|
|
18
|
+
'Button',
|
|
19
|
+
'ClipboardButton',
|
|
20
|
+
'ComboboxControl',
|
|
21
|
+
'CustomSelectControl',
|
|
22
|
+
'FontAppearanceControl',
|
|
23
|
+
'FontFamilyControl',
|
|
24
|
+
'FontSizePicker',
|
|
25
|
+
'FormTokenField',
|
|
26
|
+
'IconButton',
|
|
27
|
+
'InputControl',
|
|
28
|
+
'LetterSpacingControl',
|
|
29
|
+
'LineHeightControl',
|
|
30
|
+
'NumberControl',
|
|
31
|
+
'Radio',
|
|
32
|
+
'RangeControl',
|
|
33
|
+
'SelectControl',
|
|
34
|
+
'TextControl',
|
|
35
|
+
'TreeSelect',
|
|
36
|
+
'ToggleGroupControl',
|
|
37
|
+
'UnitControl',
|
|
38
|
+
] );
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Components that can use the `render` prop as an alternative to __next40pxDefaultSize.
|
|
42
|
+
*/
|
|
43
|
+
const COMPONENTS_WITH_RENDER_EXEMPTION = new Set( [ 'FormFileUpload' ] );
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* All tracked component names for path-based detection.
|
|
47
|
+
*/
|
|
48
|
+
const ALL_TRACKED_COMPONENTS = new Set( [
|
|
49
|
+
...COMPONENTS_REQUIRING_40PX,
|
|
50
|
+
...COMPONENTS_WITH_RENDER_EXEMPTION,
|
|
51
|
+
] );
|
|
52
|
+
|
|
53
|
+
module.exports = {
|
|
54
|
+
meta: {
|
|
55
|
+
type: 'problem',
|
|
56
|
+
schema: [
|
|
57
|
+
{
|
|
58
|
+
type: 'object',
|
|
59
|
+
properties: {
|
|
60
|
+
checkLocalImports: {
|
|
61
|
+
type: 'boolean',
|
|
62
|
+
description:
|
|
63
|
+
'When true, also checks components imported from relative paths (for use inside @wordpress/components package).',
|
|
64
|
+
},
|
|
65
|
+
},
|
|
66
|
+
additionalProperties: false,
|
|
67
|
+
},
|
|
68
|
+
],
|
|
69
|
+
messages: {
|
|
70
|
+
missingProp:
|
|
71
|
+
'{{ component }} should have the `__next40pxDefaultSize` prop when using the default size.',
|
|
72
|
+
missingPropFormFileUpload:
|
|
73
|
+
'FormFileUpload should have the `__next40pxDefaultSize` prop to opt-in to the new default size.',
|
|
74
|
+
},
|
|
75
|
+
},
|
|
76
|
+
create( context ) {
|
|
77
|
+
const checkLocalImports =
|
|
78
|
+
context.options[ 0 ]?.checkLocalImports ?? false;
|
|
79
|
+
|
|
80
|
+
// Track local names of components imported from @wordpress/components
|
|
81
|
+
// Map: localName -> importedName
|
|
82
|
+
const trackedImports = new Map();
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Check if the import source should be tracked.
|
|
86
|
+
*
|
|
87
|
+
* @param {string} source - The import source path
|
|
88
|
+
* @return {boolean} Whether to track imports from this source
|
|
89
|
+
*/
|
|
90
|
+
function shouldTrackImportSource( source ) {
|
|
91
|
+
if ( source === '@wordpress/components' ) {
|
|
92
|
+
return true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// When checkLocalImports is enabled, also track relative imports
|
|
96
|
+
if ( checkLocalImports ) {
|
|
97
|
+
return source.startsWith( '.' ) || source.startsWith( '/' );
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return false;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Try to infer component name from import path.
|
|
105
|
+
* e.g., '../button' -> 'Button', '../input-control' -> 'InputControl'
|
|
106
|
+
*
|
|
107
|
+
* @param {string} source - The import source path
|
|
108
|
+
* @return {string|null} The inferred component name or null
|
|
109
|
+
*/
|
|
110
|
+
function inferComponentNameFromPath( source ) {
|
|
111
|
+
// Get the last segment of the path
|
|
112
|
+
const lastSegment = source.split( '/' ).pop();
|
|
113
|
+
if ( ! lastSegment ) {
|
|
114
|
+
return null;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Convert kebab-case to PascalCase
|
|
118
|
+
const pascalCase = lastSegment
|
|
119
|
+
.split( '-' )
|
|
120
|
+
.map(
|
|
121
|
+
( part ) => part.charAt( 0 ).toUpperCase() + part.slice( 1 )
|
|
122
|
+
)
|
|
123
|
+
.join( '' );
|
|
124
|
+
|
|
125
|
+
// Check if it's one of our tracked components
|
|
126
|
+
if ( ALL_TRACKED_COMPONENTS.has( pascalCase ) ) {
|
|
127
|
+
return pascalCase;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Check if the `size` prop has a non-default value.
|
|
135
|
+
*
|
|
136
|
+
* @param {Array} attributes - JSX attributes array
|
|
137
|
+
* @return {boolean} Whether size has a non-default value
|
|
138
|
+
*/
|
|
139
|
+
function hasNonDefaultSize( attributes ) {
|
|
140
|
+
const sizeAttr = attributes.find(
|
|
141
|
+
( a ) =>
|
|
142
|
+
a.type === 'JSXAttribute' &&
|
|
143
|
+
a.name &&
|
|
144
|
+
a.name.name === 'size'
|
|
145
|
+
);
|
|
146
|
+
|
|
147
|
+
if ( ! sizeAttr ) {
|
|
148
|
+
return false;
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
// String value like `size="small"` or `size="compact"`
|
|
152
|
+
if (
|
|
153
|
+
sizeAttr.value &&
|
|
154
|
+
sizeAttr.value.type === 'Literal' &&
|
|
155
|
+
typeof sizeAttr.value.value === 'string'
|
|
156
|
+
) {
|
|
157
|
+
return sizeAttr.value.value !== 'default';
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
// Expression - could be non-default, so don't report
|
|
161
|
+
if (
|
|
162
|
+
sizeAttr.value &&
|
|
163
|
+
sizeAttr.value.type === 'JSXExpressionContainer'
|
|
164
|
+
) {
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
* Check if the `render` prop exists.
|
|
173
|
+
*
|
|
174
|
+
* @param {Array} attributes - JSX attributes array
|
|
175
|
+
* @return {boolean} Whether render prop exists
|
|
176
|
+
*/
|
|
177
|
+
function hasRenderProp( attributes ) {
|
|
178
|
+
return attributes.some(
|
|
179
|
+
( a ) =>
|
|
180
|
+
a.type === 'JSXAttribute' &&
|
|
181
|
+
a.name &&
|
|
182
|
+
a.name.name === 'render'
|
|
183
|
+
);
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Check if the `variant` prop has the value "link".
|
|
188
|
+
* Button with variant="link" doesn't need __next40pxDefaultSize.
|
|
189
|
+
*
|
|
190
|
+
* @param {Array} attributes - JSX attributes array
|
|
191
|
+
* @return {boolean} Whether variant is "link"
|
|
192
|
+
*/
|
|
193
|
+
function hasLinkVariant( attributes ) {
|
|
194
|
+
const variantAttr = attributes.find(
|
|
195
|
+
( a ) =>
|
|
196
|
+
a.type === 'JSXAttribute' &&
|
|
197
|
+
a.name &&
|
|
198
|
+
a.name.name === 'variant'
|
|
199
|
+
);
|
|
200
|
+
|
|
201
|
+
if ( ! variantAttr ) {
|
|
202
|
+
return false;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// String value like `variant="link"`
|
|
206
|
+
if (
|
|
207
|
+
variantAttr.value &&
|
|
208
|
+
variantAttr.value.type === 'Literal' &&
|
|
209
|
+
variantAttr.value.value === 'link'
|
|
210
|
+
) {
|
|
211
|
+
return true;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
return {
|
|
218
|
+
ImportDeclaration( node ) {
|
|
219
|
+
const source = node.source.value;
|
|
220
|
+
|
|
221
|
+
if ( ! shouldTrackImportSource( source ) ) {
|
|
222
|
+
return;
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// Handle named imports
|
|
226
|
+
node.specifiers.forEach( ( specifier ) => {
|
|
227
|
+
if ( specifier.type !== 'ImportSpecifier' ) {
|
|
228
|
+
return;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
const importedName = specifier.imported.name;
|
|
232
|
+
const localName = specifier.local.name;
|
|
233
|
+
|
|
234
|
+
// Track components that require the prop
|
|
235
|
+
if (
|
|
236
|
+
COMPONENTS_REQUIRING_40PX.has( importedName ) ||
|
|
237
|
+
COMPONENTS_WITH_RENDER_EXEMPTION.has( importedName )
|
|
238
|
+
) {
|
|
239
|
+
trackedImports.set( localName, importedName );
|
|
240
|
+
}
|
|
241
|
+
} );
|
|
242
|
+
|
|
243
|
+
// Handle default imports when checking local imports
|
|
244
|
+
// e.g., import InputControl from '../input-control'
|
|
245
|
+
if ( checkLocalImports ) {
|
|
246
|
+
node.specifiers.forEach( ( specifier ) => {
|
|
247
|
+
if ( specifier.type === 'ImportDefaultSpecifier' ) {
|
|
248
|
+
const localName = specifier.local.name;
|
|
249
|
+
const inferredName =
|
|
250
|
+
inferComponentNameFromPath( source );
|
|
251
|
+
if ( inferredName ) {
|
|
252
|
+
trackedImports.set( localName, inferredName );
|
|
253
|
+
return;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// Support patterns like `import ClipboardButton from '.';`
|
|
257
|
+
// (common in component folder examples/tests).
|
|
258
|
+
// If the local name matches a tracked component, treat it as such.
|
|
259
|
+
if ( ALL_TRACKED_COMPONENTS.has( localName ) ) {
|
|
260
|
+
trackedImports.set( localName, localName );
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
} );
|
|
264
|
+
}
|
|
265
|
+
},
|
|
266
|
+
|
|
267
|
+
JSXOpeningElement( node ) {
|
|
268
|
+
// Only check simple JSX element names (not member expressions)
|
|
269
|
+
if ( node.name.type !== 'JSXIdentifier' ) {
|
|
270
|
+
return;
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
const elementName = node.name.name;
|
|
274
|
+
const importedName = trackedImports.get( elementName );
|
|
275
|
+
|
|
276
|
+
// Only check if this is a tracked component from @wordpress/components
|
|
277
|
+
if ( ! importedName ) {
|
|
278
|
+
return;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
const attributes = node.attributes;
|
|
282
|
+
|
|
283
|
+
// Check if __next40pxDefaultSize has a truthy value
|
|
284
|
+
if (
|
|
285
|
+
hasTruthyJsxAttribute( attributes, '__next40pxDefaultSize' )
|
|
286
|
+
) {
|
|
287
|
+
return;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
// Handle FormFileUpload special case
|
|
291
|
+
if ( COMPONENTS_WITH_RENDER_EXEMPTION.has( importedName ) ) {
|
|
292
|
+
// FormFileUpload is valid if it has a `render` prop
|
|
293
|
+
if ( hasRenderProp( attributes ) ) {
|
|
294
|
+
return;
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
context.report( {
|
|
298
|
+
node,
|
|
299
|
+
messageId: 'missingPropFormFileUpload',
|
|
300
|
+
} );
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// For other components, check if size prop has a non-default value
|
|
305
|
+
if ( hasNonDefaultSize( attributes ) ) {
|
|
306
|
+
return;
|
|
307
|
+
}
|
|
308
|
+
|
|
309
|
+
// Button with variant="link" doesn't need __next40pxDefaultSize
|
|
310
|
+
if (
|
|
311
|
+
importedName === 'Button' &&
|
|
312
|
+
hasLinkVariant( attributes )
|
|
313
|
+
) {
|
|
314
|
+
return;
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
context.report( {
|
|
318
|
+
node,
|
|
319
|
+
messageId: 'missingProp',
|
|
320
|
+
data: {
|
|
321
|
+
component: importedName,
|
|
322
|
+
},
|
|
323
|
+
} );
|
|
324
|
+
},
|
|
325
|
+
};
|
|
326
|
+
},
|
|
327
|
+
};
|
|
@@ -0,0 +1,126 @@
|
|
|
1
|
+
const { hasTruthyJsxAttribute } = require( '../utils' );
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Enforces that Button from @wordpress/components includes `accessibleWhenDisabled`
|
|
5
|
+
* when `disabled` is set.
|
|
6
|
+
*
|
|
7
|
+
* @type {import('eslint').Rule.RuleModule}
|
|
8
|
+
*/
|
|
9
|
+
module.exports = {
|
|
10
|
+
meta: {
|
|
11
|
+
type: 'problem',
|
|
12
|
+
schema: [
|
|
13
|
+
{
|
|
14
|
+
type: 'object',
|
|
15
|
+
properties: {
|
|
16
|
+
checkLocalImports: {
|
|
17
|
+
type: 'boolean',
|
|
18
|
+
description:
|
|
19
|
+
'When true, also checks components imported from relative paths (for use inside @wordpress/components package).',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
additionalProperties: false,
|
|
23
|
+
},
|
|
24
|
+
],
|
|
25
|
+
messages: {
|
|
26
|
+
missingAccessibleWhenDisabled:
|
|
27
|
+
'`disabled` used without the `accessibleWhenDisabled` prop. Disabling a control without maintaining focusability can cause accessibility issues, by hiding their presence from screen reader users, or preventing focus from returning to a trigger element. (Ignore this error if you truly mean to disable.)',
|
|
28
|
+
},
|
|
29
|
+
},
|
|
30
|
+
create( context ) {
|
|
31
|
+
const checkLocalImports =
|
|
32
|
+
context.options[ 0 ]?.checkLocalImports ?? false;
|
|
33
|
+
|
|
34
|
+
// Track local names of Button imported from @wordpress/components
|
|
35
|
+
const wpComponentsButtons = new Set();
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Check if the import source should be tracked.
|
|
39
|
+
*
|
|
40
|
+
* @param {string} source - The import source path
|
|
41
|
+
* @return {boolean} Whether to track imports from this source
|
|
42
|
+
*/
|
|
43
|
+
function shouldTrackImportSource( source ) {
|
|
44
|
+
if ( source === '@wordpress/components' ) {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
// When checkLocalImports is enabled, also track relative imports
|
|
49
|
+
if ( checkLocalImports ) {
|
|
50
|
+
return source.startsWith( '.' ) || source.startsWith( '/' );
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
return false;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return {
|
|
57
|
+
ImportDeclaration( node ) {
|
|
58
|
+
if ( ! shouldTrackImportSource( node.source.value ) ) {
|
|
59
|
+
return;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
node.specifiers.forEach( ( specifier ) => {
|
|
63
|
+
if ( specifier.type !== 'ImportSpecifier' ) {
|
|
64
|
+
return;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
const importedName = specifier.imported.name;
|
|
68
|
+
if ( importedName === 'Button' ) {
|
|
69
|
+
// Track the local name (handles aliased imports)
|
|
70
|
+
wpComponentsButtons.add( specifier.local.name );
|
|
71
|
+
}
|
|
72
|
+
} );
|
|
73
|
+
|
|
74
|
+
// Also handle default imports when checking local imports
|
|
75
|
+
// e.g., import Button from './button'
|
|
76
|
+
if ( checkLocalImports ) {
|
|
77
|
+
node.specifiers.forEach( ( specifier ) => {
|
|
78
|
+
if ( specifier.type === 'ImportDefaultSpecifier' ) {
|
|
79
|
+
const localName = specifier.local.name;
|
|
80
|
+
// Check if the import path suggests it's a Button component
|
|
81
|
+
const source = node.source.value;
|
|
82
|
+
if (
|
|
83
|
+
source.endsWith( '/button' ) ||
|
|
84
|
+
source.endsWith( '/Button' )
|
|
85
|
+
) {
|
|
86
|
+
wpComponentsButtons.add( localName );
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
} );
|
|
90
|
+
}
|
|
91
|
+
},
|
|
92
|
+
|
|
93
|
+
JSXOpeningElement( node ) {
|
|
94
|
+
// Only check simple JSX element names (not member expressions)
|
|
95
|
+
if ( node.name.type !== 'JSXIdentifier' ) {
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const elementName = node.name.name;
|
|
100
|
+
|
|
101
|
+
// Only check if this is a Button from @wordpress/components
|
|
102
|
+
if ( ! wpComponentsButtons.has( elementName ) ) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
if ( ! hasTruthyJsxAttribute( node.attributes, 'disabled' ) ) {
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
const hasAccessibleWhenDisabled = node.attributes.some(
|
|
111
|
+
( attr ) =>
|
|
112
|
+
attr.type === 'JSXAttribute' &&
|
|
113
|
+
attr.name &&
|
|
114
|
+
attr.name.name === 'accessibleWhenDisabled'
|
|
115
|
+
);
|
|
116
|
+
|
|
117
|
+
if ( ! hasAccessibleWhenDisabled ) {
|
|
118
|
+
context.report( {
|
|
119
|
+
node,
|
|
120
|
+
messageId: 'missingAccessibleWhenDisabled',
|
|
121
|
+
} );
|
|
122
|
+
}
|
|
123
|
+
},
|
|
124
|
+
};
|
|
125
|
+
},
|
|
126
|
+
};
|
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
/** @typedef {import('estree').Comment} Comment */
|
|
2
2
|
/** @typedef {import('estree').Node} Node */
|
|
3
|
+
/** @typedef {import('estree').SourceLocation} SourceLocation */
|
|
4
|
+
|
|
5
|
+
const DEPENDENCY_BLOCK_PATTERN =
|
|
6
|
+
/^\*?\n \* (External|Node|WordPress|Internal) dependencies\n $/;
|
|
3
7
|
|
|
4
8
|
/** @type {import('eslint').Rule.RuleModule} */
|
|
5
9
|
module.exports = {
|
|
@@ -9,10 +13,15 @@ module.exports = {
|
|
|
9
13
|
description: 'Enforce dependencies docblocks formatting',
|
|
10
14
|
url: 'https://github.com/WordPress/gutenberg/blob/HEAD/packages/eslint-plugin/docs/rules/dependency-group.md',
|
|
11
15
|
},
|
|
12
|
-
schema: [
|
|
16
|
+
schema: [
|
|
17
|
+
{
|
|
18
|
+
enum: [ 'always', 'never' ],
|
|
19
|
+
},
|
|
20
|
+
],
|
|
13
21
|
fixable: 'code',
|
|
14
22
|
},
|
|
15
23
|
create( context ) {
|
|
24
|
+
const mode = context.options[ 0 ] || 'always';
|
|
16
25
|
const comments = context.getSourceCode().getAllComments();
|
|
17
26
|
|
|
18
27
|
/**
|
|
@@ -94,6 +103,20 @@ module.exports = {
|
|
|
94
103
|
return pattern.test( value );
|
|
95
104
|
}
|
|
96
105
|
|
|
106
|
+
/**
|
|
107
|
+
* Returns true if the given comment node is any dependency block comment.
|
|
108
|
+
*
|
|
109
|
+
* @param {Comment} node Comment node to check.
|
|
110
|
+
*
|
|
111
|
+
* @return {boolean} Whether comment node is a dependency block.
|
|
112
|
+
*/
|
|
113
|
+
function isDependencyBlock( node ) {
|
|
114
|
+
return (
|
|
115
|
+
node.type === 'Block' &&
|
|
116
|
+
DEPENDENCY_BLOCK_PATTERN.test( node.value )
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
97
120
|
/**
|
|
98
121
|
* Returns true if the given node occurs prior in code to a reference,
|
|
99
122
|
* or false otherwise.
|
|
@@ -157,6 +180,46 @@ module.exports = {
|
|
|
157
180
|
* @param {import('estree').Program} node Program node.
|
|
158
181
|
*/
|
|
159
182
|
Program( node ) {
|
|
183
|
+
if ( mode === 'never' ) {
|
|
184
|
+
for ( const comment of comments ) {
|
|
185
|
+
if ( isDependencyBlock( comment ) ) {
|
|
186
|
+
context.report( {
|
|
187
|
+
loc: /** @type {SourceLocation} */ (
|
|
188
|
+
comment.loc
|
|
189
|
+
),
|
|
190
|
+
message:
|
|
191
|
+
'Unexpected dependency group comment block',
|
|
192
|
+
fix( fixer ) {
|
|
193
|
+
if ( ! comment.range ) {
|
|
194
|
+
return null;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
const text = context
|
|
198
|
+
.getSourceCode()
|
|
199
|
+
.getText();
|
|
200
|
+
|
|
201
|
+
// Trim preceding and trailing newlines.
|
|
202
|
+
let [ start, end ] = comment.range;
|
|
203
|
+
while (
|
|
204
|
+
start > 1 &&
|
|
205
|
+
text[ start - 1 ] === '\n' &&
|
|
206
|
+
text[ start - 2 ] === '\n'
|
|
207
|
+
) {
|
|
208
|
+
start--;
|
|
209
|
+
}
|
|
210
|
+
while ( text[ end ] === '\n' ) {
|
|
211
|
+
end++;
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
return fixer.removeRange( [ start, end ] );
|
|
215
|
+
},
|
|
216
|
+
} );
|
|
217
|
+
}
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
return;
|
|
221
|
+
}
|
|
222
|
+
|
|
160
223
|
/**
|
|
161
224
|
* The set of package localities which have been reported for
|
|
162
225
|
* the current program. Each locality is reported at most one
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
|
|
2
|
+
meta: {
|
|
3
|
+
type: 'problem',
|
|
4
|
+
docs: {
|
|
5
|
+
description:
|
|
6
|
+
'Disallow setting any CSS custom property beginning with --wpds- in inline styles',
|
|
7
|
+
},
|
|
8
|
+
schema: [],
|
|
9
|
+
messages: {
|
|
10
|
+
disallowedSet:
|
|
11
|
+
'Do not set CSS custom properties using the Design System tokens namespace (i.e. beginning with --wpds-*).',
|
|
12
|
+
},
|
|
13
|
+
},
|
|
14
|
+
create( context ) {
|
|
15
|
+
return {
|
|
16
|
+
/** @param {import('estree').Property} node */
|
|
17
|
+
'JSXAttribute[name.name="style"] ObjectExpression > Property[key.value=/^--wpds-/]'(
|
|
18
|
+
node
|
|
19
|
+
) {
|
|
20
|
+
context.report( {
|
|
21
|
+
node: node.key,
|
|
22
|
+
messageId: 'disallowedSet',
|
|
23
|
+
} );
|
|
24
|
+
},
|
|
25
|
+
};
|
|
26
|
+
},
|
|
27
|
+
} );
|