Skip to content

Commit 2e593cd

Browse files
authored
Eslint: Add rule to prohibit unsafe APIs (#27301)
* Replace type assertion with annotation * Update module tsconfig * Add file to ts * Add new rule * Add rule to recommended set * Allow unstable configuration * Add changelog entry * Disable for Gutenberg * fixup! Allow unstable configuration * Fix rule name in config doc * Fix rule name in test * Simplify single message * Remove from recomended configuration * Add link to unsafe API documentation * Fix changelog type
1 parent 31bb2ea commit 2e593cd

6 files changed

Lines changed: 259 additions & 5 deletions

File tree

packages/eslint-plugin/CHANGELOG.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,10 @@
22

33
## Unreleased
44

5+
### New Feature
6+
7+
- Add `no-unsafe-wp-apis` rule to discourage usage of unsafe APIs ([#27301](https://github.com/WordPress/gutenberg/pull/27301)).
8+
59
### Documentation
610

711
- Include a note about the minimum version required for `node` (10.0.0) and `npm` (6.9.0).
Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
# Prevent unsafe API usage (no-unsafe-wp-apis)
2+
3+
Prevent unsafe APIs from `@wordpress/*` packages from being imported.
4+
5+
This includes experimental and unstable APIs which are expected to change and likely to cause issues in application code.
6+
See the [documentation](https://github.com/WordPress/gutenberg/blob/master/docs/contributors/coding-guidelines.md#experimental-and-unstable-apis).
7+
8+
> **There is no support commitment for experimental and unstable APIs.** They can and will be removed or changed without advance warning, including as part of a minor or patch release. As an external consumer, you should avoid these APIs.
9+
>
10+
>
11+
> - An **experimental API** is one which is planned for eventual public availability, but is subject to further experimentation, testing, and discussion.
12+
> - An **unstable API** is one which serves as a means to an end. It is not desired to ever be converted into a public API.
13+
14+
## Rule details
15+
16+
Examples of **incorrect** code for this rule:
17+
18+
```js
19+
import { __experimentalFeature } from '@wordpress/foo';
20+
import { __unstableFeature } from '@wordpress/bar';
21+
```
22+
23+
Examples of **correct** code for this rule:
24+
25+
```js
26+
import { registerBlockType } from '@wordpress/blocks';
27+
```
28+
29+
## Options
30+
31+
The rule can be configured via an object.
32+
This should be an object where the keys are import package names and the values are arrays of allowed unsafe imports.
33+
34+
#### Example configuration
35+
36+
```json
37+
{
38+
"@wordpress/no-unsafe-wp-apis": [
39+
"error",
40+
{ "@wordpress/block-editor": [ "__experimentalBlock" ] }
41+
]
42+
}
43+
```
Lines changed: 119 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,119 @@
1+
/**
2+
* External dependencies
3+
*/
4+
import { RuleTester } from 'eslint';
5+
6+
/**
7+
* Internal dependencies
8+
*/
9+
import rule from '../no-unsafe-wp-apis';
10+
11+
const ruleTester = new RuleTester( {
12+
parserOptions: {
13+
sourceType: 'module',
14+
ecmaVersion: 6,
15+
},
16+
} );
17+
18+
const options = [
19+
{ '@wordpress/package': [ '__experimentalSafe', '__unstableSafe' ] },
20+
];
21+
22+
ruleTester.run( 'no-unsafe-wp-apis', rule, {
23+
valid: [
24+
{ code: "import _ from 'lodash';", options },
25+
{ code: "import { map } from 'lodash';", options },
26+
{ code: "import { __experimentalFoo } from 'lodash';", options },
27+
{ code: "import { __unstableFoo } from 'lodash';", options },
28+
{ code: "import _, { __unstableFoo } from 'lodash';", options },
29+
{ code: "import * as _ from 'lodash';", options },
30+
31+
{ code: "import _ from './x';", options },
32+
{ code: "import { map } from './x';", options },
33+
{ code: "import { __experimentalFoo } from './x';", options },
34+
{ code: "import { __unstableFoo } from './x';", options },
35+
{ code: "import _, { __unstableFoo } from './x';", options },
36+
{ code: "import * as _ from './x';", options },
37+
38+
{ code: "import s from '@wordpress/package';", options },
39+
{ code: "import { feature } from '@wordpress/package';", options },
40+
{
41+
code: "import { __experimentalSafe } from '@wordpress/package';",
42+
options,
43+
},
44+
{
45+
code: "import { __unstableSafe } from '@wordpress/package';",
46+
options,
47+
},
48+
{
49+
code:
50+
"import { feature, __experimentalSafe } from '@wordpress/package';",
51+
options,
52+
},
53+
{
54+
code: "import s, { __experimentalSafe } from '@wordpress/package';",
55+
options,
56+
},
57+
{ code: "import * as s from '@wordpress/package';", options },
58+
],
59+
60+
invalid: [
61+
{
62+
code: "import { __experimentalUnsafe } from '@wordpress/package';",
63+
options,
64+
errors: [
65+
{
66+
message: `Usage of \`__experimentalUnsafe\` from \`@wordpress/package\` is not allowed.
67+
See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
68+
type: 'ImportSpecifier',
69+
},
70+
],
71+
},
72+
{
73+
code: "import { __experimentalSafe } from '@wordpress/unsafe';",
74+
options,
75+
errors: [
76+
{
77+
message: `Usage of \`__experimentalSafe\` from \`@wordpress/unsafe\` is not allowed.
78+
See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
79+
type: 'ImportSpecifier',
80+
},
81+
],
82+
},
83+
{
84+
code:
85+
"import { feature, __experimentalSafe } from '@wordpress/unsafe';",
86+
options,
87+
errors: [
88+
{
89+
message: `Usage of \`__experimentalSafe\` from \`@wordpress/unsafe\` is not allowed.
90+
See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
91+
type: 'ImportSpecifier',
92+
},
93+
],
94+
},
95+
{
96+
code:
97+
"import s, { __experimentalUnsafe } from '@wordpress/package';",
98+
options,
99+
errors: [
100+
{
101+
message: `Usage of \`__experimentalUnsafe\` from \`@wordpress/package\` is not allowed.
102+
See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
103+
type: 'ImportSpecifier',
104+
},
105+
],
106+
},
107+
{
108+
code: "import { __unstableFeature } from '@wordpress/package';",
109+
options,
110+
errors: [
111+
{
112+
message: `Usage of \`__unstableFeature\` from \`@wordpress/package\` is not allowed.
113+
See https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
114+
type: 'ImportSpecifier',
115+
},
116+
],
117+
},
118+
],
119+
} );

packages/eslint-plugin/rules/dependency-group.js

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
/** @typedef {import('estree').Comment} Comment */
22
/** @typedef {import('estree').Node} Node */
33

4-
module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
4+
/** @type {import('eslint').Rule.RuleModule} */
5+
module.exports = {
56
meta: {
67
type: 'layout',
78
docs: {
@@ -254,4 +255,4 @@ module.exports = /** @type {import('eslint').Rule.RuleModule} */ ( {
254255
},
255256
};
256257
},
257-
} );
258+
};
Lines changed: 88 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,88 @@
1+
/** @type {import('eslint').Rule.RuleModule} */
2+
module.exports = {
3+
type: 'problem',
4+
meta: {
5+
schema: [
6+
{
7+
type: 'object',
8+
additionalProperties: false,
9+
patternProperties: {
10+
'^@wordpress\\/[a-zA-Z0-9_-]+$': {
11+
type: 'array',
12+
uniqueItems: true,
13+
minItems: 1,
14+
items: {
15+
type: 'string',
16+
pattern: '^(?:__experimental|__unstable)',
17+
},
18+
},
19+
},
20+
},
21+
],
22+
},
23+
create( context ) {
24+
/** @type {AllowedImportsMap} */
25+
const allowedImports =
26+
( context.options &&
27+
typeof context.options[ 0 ] === 'object' &&
28+
context.options[ 0 ] ) ||
29+
{};
30+
const reporter = makeListener( { allowedImports, context } );
31+
32+
return { ImportDeclaration: reporter };
33+
},
34+
};
35+
36+
/**
37+
* @param {Object} _
38+
* @param {AllowedImportsMap} _.allowedImports
39+
* @param {import('eslint').Rule.RuleContext} _.context
40+
*
41+
* @return {(node: Node) => void} Listener function
42+
*/
43+
function makeListener( { allowedImports, context } ) {
44+
return function reporter( node ) {
45+
if ( node.type !== 'ImportDeclaration' ) {
46+
return;
47+
}
48+
if ( typeof node.source.value !== 'string' ) {
49+
return;
50+
}
51+
52+
const sourceModule = node.source.value.trim();
53+
54+
// Ignore non-WordPress packages
55+
if ( ! sourceModule.startsWith( '@wordpress/' ) ) {
56+
return;
57+
}
58+
59+
const allowedImportNames = allowedImports[ sourceModule ] || [];
60+
61+
node.specifiers.forEach( ( specifierNode ) => {
62+
if ( specifierNode.type !== 'ImportSpecifier' ) {
63+
return;
64+
}
65+
66+
const importedName = specifierNode.imported.name;
67+
68+
if (
69+
! importedName.startsWith( '__unstable' ) &&
70+
! importedName.startsWith( '__experimental' )
71+
) {
72+
return;
73+
}
74+
75+
if ( allowedImportNames.includes( importedName ) ) {
76+
return;
77+
}
78+
79+
context.report( {
80+
message: `Usage of \`${ importedName }\` from \`${ sourceModule }\` is not allowed.\nSee https://developer.wordpress.org/block-editor/contributors/develop/coding-guidelines/#experimental-and-unstable-apis for details.`,
81+
node: specifierNode,
82+
} );
83+
} );
84+
};
85+
}
86+
87+
/** @typedef {import('estree').Node} Node */
88+
/** @typedef {Record<string, string[]|undefined>} AllowedImportsMap */
Lines changed: 2 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,12 @@
11
{
22
"extends": "../../tsconfig.base.json",
33
"compilerOptions": {
4+
"module": "CommonJS",
45
"rootDir": "rules",
56
"declarationDir": "build-types"
67
},
78
// NOTE: This package is being progressively typed. You are encouraged to
89
// expand this array with files which can be type-checked. At some point in
910
// the future, this can be simplified to an `includes` of `src/**/*`.
10-
"files": [
11-
"rules/dependency-group.js"
12-
]
11+
"files": [ "rules/dependency-group.js", "rules/no-unsafe-wp-apis.js" ]
1312
}

0 commit comments

Comments
 (0)