@wordpress/eslint-plugin 25.2.1-next.v.202605131032.0 → 25.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@wordpress/eslint-plugin",
3
- "version": "25.2.1-next.v.202605131032.0+f6d6e7149",
3
+ "version": "25.3.0",
4
4
  "description": "ESLint plugin for WordPress development.",
5
5
  "author": "The WordPress Contributors",
6
6
  "license": "GPL-2.0-or-later",
@@ -44,9 +44,9 @@
44
44
  "@babel/eslint-parser": "^7.28.6",
45
45
  "@eslint-community/eslint-plugin-eslint-comments": "^4.4.0",
46
46
  "@eslint/compat": "^2.0.0",
47
- "@wordpress/babel-preset-default": "^8.45.1-next.v.202605131032.0+f6d6e7149",
48
- "@wordpress/prettier-config": "^4.45.1-next.v.202605131032.0+f6d6e7149",
49
- "@wordpress/theme": "^0.13.1-next.v.202605131032.0+f6d6e7149",
47
+ "@wordpress/babel-preset-default": "^8.47.0",
48
+ "@wordpress/prettier-config": "^4.47.0",
49
+ "@wordpress/theme": "^0.14.0",
50
50
  "cosmiconfig": "^7.0.0",
51
51
  "eslint-config-prettier": "^10.0.0",
52
52
  "eslint-import-resolver-typescript": "^4.4.4",
@@ -83,5 +83,5 @@
83
83
  "publishConfig": {
84
84
  "access": "public"
85
85
  },
86
- "gitHead": "0e198c7ac7ca634e73ded9220ce048c0302174dd"
86
+ "gitHead": "d653c5fd6161571a0c2ebde28553d6e25624eacc"
87
87
  }
@@ -66,10 +66,6 @@ ruleTester.run( 'use-import-as', rule, {
66
66
  code: "import { VisuallyHidden as WCVisuallyHidden } from '@wordpress/components';",
67
67
  options,
68
68
  },
69
- {
70
- code: 'import { "VisuallyHidden" as WCVisuallyHidden } from \'@wordpress/components\';',
71
- options,
72
- },
73
69
  {
74
70
  code: "import { Button, VisuallyHidden as WCVisuallyHidden } from '@wordpress/components';",
75
71
  options,
@@ -95,9 +91,9 @@ ruleTester.run( 'use-import-as', rule, {
95
91
  {
96
92
  code: `
97
93
  import { privateApis } from '@wordpress/components';
98
- import { unlock as open } from '../../lock-unlock';
94
+ import { unlock } from '../../lock-unlock';
99
95
 
100
- const { Badge: WCBadge = fallbackBadge } = open( privateApis );
96
+ const { Badge: WCBadge = fallbackBadge } = unlock( privateApis );
101
97
  `,
102
98
  options,
103
99
  },
@@ -161,17 +157,6 @@ ruleTester.run( 'use-import-as', rule, {
161
157
  ),
162
158
  ],
163
159
  },
164
- {
165
- code: 'import { "VisuallyHidden" as Hidden } from \'@wordpress/components\';',
166
- options,
167
- errors: [
168
- withSuggestions(
169
- '`VisuallyHidden` from `@wordpress/components` must be imported as `WCVisuallyHidden`.',
170
- 'import { "VisuallyHidden" as WCVisuallyHidden } from \'@wordpress/components\';',
171
- 'Import as `WCVisuallyHidden`.'
172
- ),
173
- ],
174
- },
175
160
  {
176
161
  code: "import { Button, VisuallyHidden } from '@wordpress/components';",
177
162
  options,
@@ -1,11 +1,4 @@
1
- /**
2
- * External dependencies
3
- */
4
1
  import { RuleTester } from 'eslint';
5
-
6
- /**
7
- * Internal dependencies
8
- */
9
2
  import rule, { ALLOWLIST, DENYLIST } from '../use-recommended-components';
10
3
 
11
4
  const ruleTester = new RuleTester( {
@@ -27,10 +20,28 @@ ruleTester.run( 'use-recommended-components', rule, {
27
20
 
28
21
  // Allowed @wordpress/ui components.
29
22
  "import { Badge } from '@wordpress/ui';",
23
+ "import { Icon } from '@wordpress/ui';",
30
24
  "import { Link } from '@wordpress/ui';",
31
25
  "import { Stack } from '@wordpress/ui';",
32
26
  "import { Text } from '@wordpress/ui';",
33
- "import { Badge, Link, Stack, Text } from '@wordpress/ui';",
27
+ "import { Badge, Icon, Link, Stack, Tabs, Text } from '@wordpress/ui';",
28
+
29
+ // Unlocked private APIs are only checked for denied names.
30
+ "import { privateApis } from '@wordpress/components'; import { unlock } from '../../lock-unlock'; const { SomethingElse } = unlock( privateApis );",
31
+ `
32
+ import { privateApis } from '@wordpress/components';
33
+ import { unlock } from '../../lock-unlock';
34
+
35
+ function test() {
36
+ function unlock( value ) {
37
+ return value;
38
+ }
39
+
40
+ const { Tabs } = unlock( privateApis );
41
+
42
+ return Tabs;
43
+ }
44
+ `,
34
45
  ],
35
46
 
36
47
  invalid: [
@@ -76,6 +87,33 @@ ruleTester.run( 'use-recommended-components', rule, {
76
87
  },
77
88
  ],
78
89
  },
90
+ {
91
+ code: "import { Tabs, TabPanel } from '@wordpress/components';",
92
+ errors: [
93
+ {
94
+ message: 'Use `Tabs` from `@wordpress/ui` instead.',
95
+ },
96
+ {
97
+ message: 'Use `Tabs` from `@wordpress/ui` instead.',
98
+ },
99
+ ],
100
+ },
101
+ {
102
+ code: "import { privateApis } from '@wordpress/components'; import { unlock } from '../../lock-unlock'; const { Tabs } = unlock( privateApis );",
103
+ errors: [
104
+ {
105
+ message: 'Use `Tabs` from `@wordpress/ui` instead.',
106
+ },
107
+ ],
108
+ },
109
+ {
110
+ code: "import { privateApis as componentsPrivateApis } from '@wordpress/components'; import { unlock } from '../../lock-unlock'; const { Tabs: WCTabs } = unlock( componentsPrivateApis );",
111
+ errors: [
112
+ {
113
+ message: 'Use `Tabs` from `@wordpress/ui` instead.',
114
+ },
115
+ ],
116
+ },
79
117
  ],
80
118
  } );
81
119
 
@@ -1,3 +1,10 @@
1
+ const {
2
+ createPrivateApisState,
3
+ trackPrivateApisSpecifier,
4
+ getPropertyName,
5
+ getUnlockDestructuring,
6
+ } = require( '../utils/private-apis' );
7
+
1
8
  /** @type {import('eslint').Rule.RuleModule} */
2
9
  const rule = {
3
10
  meta: {
@@ -34,8 +41,7 @@ const rule = {
34
41
  typeof context.options[ 0 ] === 'object'
35
42
  ? context.options[ 0 ]
36
43
  : {};
37
- const privateApisSources = new Map();
38
- const trackedUnlockImports = new Set();
44
+ const privateApisState = createPrivateApisState();
39
45
 
40
46
  return {
41
47
  /** @param {import('estree').ImportDeclaration} node */
@@ -52,21 +58,24 @@ const rule = {
52
58
  return;
53
59
  }
54
60
 
55
- const importedName = getImportedName( specifier );
56
- if ( importedName === 'unlock' ) {
57
- trackedUnlockImports.add( specifier.local.name );
58
- }
61
+ const importedName = specifier.imported.name;
62
+ trackPrivateApisSpecifier(
63
+ privateApisState,
64
+ specifier,
65
+ source,
66
+ !! sourceMap
67
+ );
59
68
 
60
69
  if ( ! sourceMap ) {
61
70
  return;
62
71
  }
63
72
 
64
- if ( importedName === 'privateApis' ) {
65
- privateApisSources.set( specifier.local.name, source );
73
+ if ( ! sourceMap.hasOwnProperty( importedName ) ) {
74
+ return;
66
75
  }
67
- const localName = sourceMap[ importedName ];
68
76
 
69
- if ( ! localName || specifier.local.name === localName ) {
77
+ const localName = sourceMap[ importedName ];
78
+ if ( specifier.local.name === localName ) {
70
79
  return;
71
80
  }
72
81
 
@@ -101,50 +110,31 @@ const rule = {
101
110
  },
102
111
  /** @param {import('estree').VariableDeclarator} node */
103
112
  VariableDeclarator( node ) {
104
- if (
105
- node.parent.type !== 'VariableDeclaration' ||
106
- node.parent.kind !== 'const' ||
107
- node.id.type !== 'ObjectPattern' ||
108
- ! isUnlockCall(
109
- node.init,
110
- context.sourceCode,
111
- trackedUnlockImports
112
- )
113
- ) {
114
- return;
115
- }
116
-
117
- const privateApisIdentifier = node.init.arguments[ 0 ];
118
- if ( privateApisIdentifier.type !== 'Identifier' ) {
119
- return;
120
- }
121
-
122
- const source = privateApisSources.get(
123
- privateApisIdentifier.name
113
+ const unlockDestructuring = getUnlockDestructuring(
114
+ node,
115
+ context.sourceCode,
116
+ privateApisState
124
117
  );
125
- if ( ! source ) {
118
+ if ( ! unlockDestructuring ) {
126
119
  return;
127
120
  }
128
121
 
122
+ const { source, properties } = unlockDestructuring;
129
123
  const sourceMap = importAsMap[ source ];
130
124
  if ( ! sourceMap ) {
131
125
  return;
132
126
  }
133
127
 
134
- node.id.properties.forEach( ( property ) => {
135
- if ( property.type !== 'Property' || property.computed ) {
136
- return;
137
- }
138
-
128
+ properties.forEach( ( property ) => {
139
129
  const importedName = getPropertyName( property.key );
140
- if ( ! importedName ) {
130
+ if (
131
+ ! importedName ||
132
+ ! sourceMap.hasOwnProperty( importedName )
133
+ ) {
141
134
  return;
142
135
  }
143
136
 
144
137
  const localName = sourceMap[ importedName ];
145
- if ( ! localName ) {
146
- return;
147
- }
148
138
 
149
139
  const propertyLocalName = getPropertyLocalName(
150
140
  property.value
@@ -189,16 +179,6 @@ const rule = {
189
179
  },
190
180
  };
191
181
 
192
- /**
193
- * @param {import('estree').ImportSpecifier} specifier
194
- * @return {string} Imported name.
195
- */
196
- function getImportedName( specifier ) {
197
- return specifier.imported.type === 'Identifier'
198
- ? specifier.imported.name
199
- : String( specifier.imported.value );
200
- }
201
-
202
182
  /**
203
183
  * @param {import('estree').ImportSpecifier} specifier
204
184
  * @param {import('eslint').SourceCode} sourceCode
@@ -209,52 +189,6 @@ function getImportSpecifierSuggestionText( specifier, sourceCode, localName ) {
209
189
  return `${ sourceCode.getText( specifier.imported ) } as ${ localName }`;
210
190
  }
211
191
 
212
- /**
213
- * @param {import('estree').CallExpression|import('estree').Expression|null} node
214
- * @param {import('eslint').SourceCode} sourceCode
215
- * @param {ReadonlySet<string>} trackedUnlockImports
216
- * @return {node is import('estree').CallExpression} Whether this is an `unlock()` call with one argument.
217
- */
218
- function isUnlockCall( node, sourceCode, trackedUnlockImports ) {
219
- if (
220
- node &&
221
- node.type === 'CallExpression' &&
222
- node.callee.type === 'Identifier' &&
223
- node.arguments.length === 1
224
- ) {
225
- if ( ! trackedUnlockImports.has( node.callee.name ) ) {
226
- return false;
227
- }
228
-
229
- const { references } = sourceCode.getScope( node.callee );
230
- const reference = references.find(
231
- ( currentReference ) => currentReference.identifier === node.callee
232
- );
233
-
234
- return !! reference?.resolved?.defs.some(
235
- ( definition ) => definition.type === 'ImportBinding'
236
- );
237
- }
238
-
239
- return false;
240
- }
241
-
242
- /**
243
- * @param {import('estree').Expression|import('estree').PrivateIdentifier} key
244
- * @return {string|null} Property name.
245
- */
246
- function getPropertyName( key ) {
247
- if ( key.type === 'Identifier' ) {
248
- return key.name;
249
- }
250
-
251
- if ( key.type === 'Literal' ) {
252
- return String( key.value );
253
- }
254
-
255
- return null;
256
- }
257
-
258
192
  /**
259
193
  * @param {import('estree').Property} property
260
194
  * @param {import('eslint').SourceCode} sourceCode
@@ -1,3 +1,10 @@
1
+ const {
2
+ createPrivateApisState,
3
+ trackPrivateApisSpecifier,
4
+ getPropertyName,
5
+ getUnlockDestructuring,
6
+ } = require( '../utils/private-apis' );
7
+
1
8
  /**
2
9
  * Allowlist: only the listed components are permitted from these packages.
3
10
  * Any other named import will be flagged with the package's message.
@@ -14,8 +21,10 @@ const ALLOWLIST = {
14
21
  'Collapsible',
15
22
  'CollapsibleCard',
16
23
  'EmptyState',
24
+ 'Icon',
17
25
  'Link',
18
26
  'Stack',
27
+ 'Tabs',
19
28
  'Text',
20
29
  'VisuallyHidden',
21
30
  ],
@@ -49,6 +58,8 @@ const DENYLIST = {
49
58
  CardHeader:
50
59
  'Use `Card.Header` (and optionally `Card.Title`) from `@wordpress/ui` instead.',
51
60
  CardMedia: 'Use `Card.FullBleed` from `@wordpress/ui` instead.',
61
+ TabPanel: 'Use `Tabs` from `@wordpress/ui` instead.',
62
+ Tabs: 'Use `Tabs` from `@wordpress/ui` instead.',
52
63
  VisuallyHidden: 'Use `{{ name }}` from `@wordpress/ui` instead.',
53
64
  },
54
65
  };
@@ -65,6 +76,8 @@ const rule = {
65
76
  schema: [],
66
77
  },
67
78
  create( context ) {
79
+ const privateApisState = createPrivateApisState();
80
+
68
81
  return {
69
82
  /** @param {import('estree').ImportDeclaration} node */
70
83
  ImportDeclaration( node ) {
@@ -77,16 +90,22 @@ const rule = {
77
90
  const allowlistEntry = ALLOWLIST[ source ];
78
91
  const denylistEntry = DENYLIST[ source ];
79
92
 
80
- if ( ! allowlistEntry && ! denylistEntry ) {
81
- return;
82
- }
83
-
84
93
  node.specifiers.forEach( ( specifier ) => {
85
94
  if ( specifier.type !== 'ImportSpecifier' ) {
86
95
  return;
87
96
  }
88
97
 
89
98
  const name = specifier.imported.name;
99
+ trackPrivateApisSpecifier(
100
+ privateApisState,
101
+ specifier,
102
+ source,
103
+ !! denylistEntry
104
+ );
105
+
106
+ if ( ! allowlistEntry && ! denylistEntry ) {
107
+ return;
108
+ }
90
109
 
91
110
  if (
92
111
  allowlistEntry &&
@@ -114,6 +133,39 @@ const rule = {
114
133
  }
115
134
  } );
116
135
  },
136
+ /** @param {import('estree').VariableDeclarator} node */
137
+ VariableDeclarator( node ) {
138
+ const unlockDestructuring = getUnlockDestructuring(
139
+ node,
140
+ context.sourceCode,
141
+ privateApisState
142
+ );
143
+ if ( ! unlockDestructuring ) {
144
+ return;
145
+ }
146
+
147
+ const { source, properties } = unlockDestructuring;
148
+ const denylistEntry = DENYLIST[ source ];
149
+ if ( ! denylistEntry ) {
150
+ return;
151
+ }
152
+
153
+ properties.forEach( ( property ) => {
154
+ const name = getPropertyName( property.key );
155
+ if ( ! name || ! denylistEntry.hasOwnProperty( name ) ) {
156
+ return;
157
+ }
158
+
159
+ context.report( {
160
+ node: property.key,
161
+ message: resolveMessage(
162
+ denylistEntry[ name ],
163
+ name,
164
+ source
165
+ ),
166
+ } );
167
+ } );
168
+ },
117
169
  };
118
170
  },
119
171
  };
@@ -0,0 +1,118 @@
1
+ /**
2
+ * Shared helpers for ESLint rules that inspect `unlock( privateApis )` usage.
3
+ */
4
+
5
+ /**
6
+ * @typedef {{ privateApisSources: Map<string, string> }} PrivateApisState
7
+ */
8
+
9
+ /**
10
+ * @return {PrivateApisState} Mutable state for tracking `unlock` and `privateApis` imports.
11
+ */
12
+ function createPrivateApisState() {
13
+ return {
14
+ privateApisSources: new Map(),
15
+ };
16
+ }
17
+
18
+ /**
19
+ * Records imported `privateApis` → package source when requested.
20
+ *
21
+ * @param {PrivateApisState} state
22
+ * @param {import('estree').ImportSpecifier} specifier
23
+ * @param {string} source
24
+ * @param {boolean} trackPrivateApis
25
+ */
26
+ function trackPrivateApisSpecifier(
27
+ state,
28
+ specifier,
29
+ source,
30
+ trackPrivateApis
31
+ ) {
32
+ const name = specifier.imported.name;
33
+ if ( trackPrivateApis && name === 'privateApis' ) {
34
+ state.privateApisSources.set( specifier.local.name, source );
35
+ }
36
+ }
37
+
38
+ /**
39
+ * @param {import('estree').CallExpression|import('estree').Expression|null} node
40
+ * @param {import('eslint').SourceCode} sourceCode
41
+ * @return {node is import('estree').CallExpression} Whether this is an `unlock()` call with one argument.
42
+ */
43
+ function isUnlockCall( node, sourceCode ) {
44
+ if (
45
+ node &&
46
+ node.type === 'CallExpression' &&
47
+ node.callee.type === 'Identifier' &&
48
+ node.callee.name === 'unlock' &&
49
+ node.arguments.length === 1
50
+ ) {
51
+ const { references } = sourceCode.getScope( node.callee );
52
+ const reference = references.find(
53
+ ( currentReference ) => currentReference.identifier === node.callee
54
+ );
55
+
56
+ return !! reference?.resolved?.defs.some(
57
+ ( definition ) => definition.type === 'ImportBinding'
58
+ );
59
+ }
60
+
61
+ return false;
62
+ }
63
+
64
+ /**
65
+ * @param {import('estree').Expression|import('estree').PrivateIdentifier} key
66
+ * @return {string|null} Static name of an object pattern property key.
67
+ */
68
+ function getPropertyName( key ) {
69
+ if ( key.type === 'Identifier' ) {
70
+ return key.name;
71
+ }
72
+
73
+ if ( key.type === 'Literal' ) {
74
+ return String( key.value );
75
+ }
76
+
77
+ return null;
78
+ }
79
+
80
+ /**
81
+ * Parses `const { … } = unlock( privateApis )` and returns the package source and properties.
82
+ *
83
+ * @param {import('estree').VariableDeclarator} node
84
+ * @param {import('eslint').SourceCode} sourceCode
85
+ * @param {PrivateApisState} state
86
+ * @return {{ source: string, properties: import('estree').Property[] }|null} Unlock destructuring context.
87
+ */
88
+ function getUnlockDestructuring( node, sourceCode, state ) {
89
+ if (
90
+ node.id.type !== 'ObjectPattern' ||
91
+ ! isUnlockCall( node.init, sourceCode )
92
+ ) {
93
+ return null;
94
+ }
95
+
96
+ const privateApisIdentifier = node.init.arguments[ 0 ];
97
+ if ( privateApisIdentifier.type !== 'Identifier' ) {
98
+ return null;
99
+ }
100
+
101
+ const source = state.privateApisSources.get( privateApisIdentifier.name );
102
+ if ( ! source ) {
103
+ return null;
104
+ }
105
+
106
+ const properties = node.id.properties.filter(
107
+ ( property ) => property.type === 'Property' && ! property.computed
108
+ );
109
+
110
+ return { source, properties };
111
+ }
112
+
113
+ module.exports = {
114
+ createPrivateApisState,
115
+ trackPrivateApisSpecifier,
116
+ getPropertyName,
117
+ getUnlockDestructuring,
118
+ };