Skip to content

Commit d39bfa3

Browse files
authored
Add id-match rule (#3109)
1 parent d14d0fd commit d39bfa3

5 files changed

Lines changed: 338 additions & 0 deletions

File tree

docs/rules/id-match.md

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
# id-match
2+
3+
📝 Require identifiers to match a specified regular expression.
4+
5+
🚫 This rule is _disabled_ in the following [configs](https://github.com/sindresorhus/eslint-plugin-unicorn#recommended-config): ✅ `recommended`, ☑️ `unopinionated`.
6+
7+
<!-- end auto-generated rule header -->
8+
<!-- Do not manually modify this header. Run: `npm run fix:eslint-docs` -->
9+
10+
This rule is the same as the built-in ESLint [`id-match`](https://eslint.org/docs/latest/rules/id-match) rule, but with an additional `checkNamedSpecifiers` option.
11+
12+
## Examples
13+
14+
```js
15+
/* eslint unicorn/id-match: ["error", "^[a-z]+$"] */
16+
17+
//
18+
const foo$ = 1;
19+
20+
//
21+
const foo = 1;
22+
```
23+
24+
## Options
25+
26+
This rule supports the same options as ESLint `id-match`.
27+
28+
### `properties`
29+
30+
Set `properties` to `true` to check object and member property names.
31+
32+
### `classFields`
33+
34+
Set `classFields` to `true` to check class field names.
35+
36+
### `onlyDeclarations`
37+
38+
Set `onlyDeclarations` to `true` to only check declared identifiers.
39+
40+
### `ignoreDestructuring`
41+
42+
Set `ignoreDestructuring` to `true` to ignore identifiers in destructuring patterns.
43+
44+
### `checkNamedSpecifiers`
45+
46+
Set `checkNamedSpecifiers` to `false` to ignore named import specifiers and external named export specifiers:
47+
48+
```js
49+
/* eslint unicorn/id-match: ["error", "^[a-z]+$", {"checkNamedSpecifiers": false}] */
50+
51+
//
52+
import {foo$} from 'module';
53+
54+
//
55+
export {foo$} from 'module';
56+
```
57+
58+
Only named import specifiers and external named export specifiers are ignored. Default imports, namespace imports, namespace re-exports, local export specifiers, and later references to imported bindings are still checked:
59+
60+
```js
61+
/* eslint unicorn/id-match: ["error", "^[a-z]+$", {"checkNamedSpecifiers": false}] */
62+
63+
//
64+
import foo$ from 'module';
65+
66+
//
67+
import * as foo$ from 'module';
68+
69+
//
70+
export * as foo$ from 'module';
71+
72+
//
73+
const foo = 1;
74+
export {foo as bar$};
75+
76+
//
77+
import {bar$} from 'module';
78+
79+
//
80+
const foo = bar$;
81+
```

readme.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,7 @@ export default [
8181
| [expiring-todo-comments](docs/rules/expiring-todo-comments.md) | Add expiration conditions to TODO comments. | ✅ ☑️ | | |
8282
| [explicit-length-check](docs/rules/explicit-length-check.md) | Enforce explicitly comparing the `length` or `size` property of a value. || 🔧 | 💡 |
8383
| [filename-case](docs/rules/filename-case.md) | Enforce a case style for filenames and directory names. || | |
84+
| [id-match](docs/rules/id-match.md) | Require identifiers to match a specified regular expression. | | | |
8485
| [import-style](docs/rules/import-style.md) | Enforce specific import styles per module. | ✅ ☑️ | | |
8586
| [isolated-functions](docs/rules/isolated-functions.md) | Prevent usage of variables from outside the scope of isolated functions. || | |
8687
| [new-for-builtins](docs/rules/new-for-builtins.md) | Enforce the use of `new` for all builtins, except `String`, `Number`, `Boolean`, `Symbol` and `BigInt`. | ✅ ☑️ | 🔧 | 💡 |

rules/id-match.js

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
import {getBuiltinRule} from './utils/index.js';
2+
3+
const baseRule = getBuiltinRule('id-match');
4+
5+
const schema = structuredClone(baseRule.meta.schema);
6+
schema[1].properties.checkNamedSpecifiers = {
7+
type: 'boolean',
8+
description: 'Whether to check named import specifiers and external named export specifiers.',
9+
};
10+
11+
const isNamedSpecifierReport = problem => {
12+
const {parent} = problem.node;
13+
14+
return parent?.type === 'ImportSpecifier'
15+
|| (
16+
parent?.type === 'ExportSpecifier'
17+
&& parent.parent.source
18+
);
19+
};
20+
21+
const shouldIgnoreNamedSpecifierReport = (problem, checkNamedSpecifiers) =>
22+
!checkNamedSpecifiers
23+
&& isNamedSpecifierReport(problem);
24+
25+
/**
26+
@param {import('eslint').Rule.RuleContext} context
27+
*/
28+
const create = context => {
29+
const checkNamedSpecifiers = context.options[1]?.checkNamedSpecifiers !== false;
30+
const fakeContext = Object.create(context, {
31+
report: {
32+
value(problem) {
33+
if (shouldIgnoreNamedSpecifierReport(problem, checkNamedSpecifiers)) {
34+
return;
35+
}
36+
37+
context.report(problem);
38+
},
39+
},
40+
});
41+
const listeners = baseRule.create(fakeContext);
42+
const {
43+
Program: onProgram,
44+
Identifier: onIdentifier,
45+
PrivateIdentifier: onPrivateIdentifier,
46+
} = listeners;
47+
48+
context.on('Program', node => onProgram(node));
49+
context.on('Identifier', node => onIdentifier(node));
50+
context.on('PrivateIdentifier', node => onPrivateIdentifier(node));
51+
};
52+
53+
/** @type {import('eslint').Rule.RuleModule} */
54+
const config = {
55+
create,
56+
meta: {
57+
type: 'suggestion',
58+
docs: {
59+
description: 'Require identifiers to match a specified regular expression.',
60+
recommended: false,
61+
},
62+
schema,
63+
defaultOptions: [
64+
baseRule.meta.defaultOptions[0],
65+
{
66+
...baseRule.meta.defaultOptions[1],
67+
checkNamedSpecifiers: true,
68+
},
69+
],
70+
messages: baseRule.meta.messages,
71+
languages: [
72+
'js/js',
73+
],
74+
},
75+
};
76+
77+
export default config;

rules/index.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,6 +20,7 @@ export {default as 'escape-case'} from './escape-case.js';
2020
export {default as 'expiring-todo-comments'} from './expiring-todo-comments.js';
2121
export {default as 'explicit-length-check'} from './explicit-length-check.js';
2222
export {default as 'filename-case'} from './filename-case.js';
23+
export {default as 'id-match'} from './id-match.js';
2324
export {default as 'import-style'} from './import-style.js';
2425
export {default as 'isolated-functions'} from './isolated-functions.js';
2526
export {default as 'new-for-builtins'} from './new-for-builtins.js';

test/id-match.js

Lines changed: 178 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,178 @@
1+
import {getTester} from './utils/test.js';
2+
import parsers from './utils/parsers.js';
3+
4+
const {test} = getTester(import.meta);
5+
6+
const options = ['^[a-z]+$'];
7+
const optionsCheckNamedSpecifiers = ['^[a-z]+$', {checkNamedSpecifiers: true}];
8+
const optionsDoNotCheckNamedSpecifiers = ['^[a-z]+$', {checkNamedSpecifiers: false}];
9+
const optionsCheckProperties = ['^[a-z]+$', {properties: true}];
10+
const optionsCheckClassFields = ['^[a-z]+$', {classFields: true}];
11+
const optionsOnlyDeclarations = ['^[a-z]+$', {onlyDeclarations: true}];
12+
const optionsIgnoreDestructuring = ['^[a-z]+$', {ignoreDestructuring: true}];
13+
const error = {
14+
messageId: 'notMatch',
15+
};
16+
17+
test({
18+
valid: [
19+
{
20+
code: 'const foo = 1;',
21+
options,
22+
},
23+
{
24+
code: 'import {foo$ as foo} from "module";',
25+
options,
26+
},
27+
{
28+
code: 'import {foo$} from "module";',
29+
options: optionsDoNotCheckNamedSpecifiers,
30+
},
31+
{
32+
code: 'import {foo as bar$} from "module";',
33+
options: optionsDoNotCheckNamedSpecifiers,
34+
},
35+
{
36+
code: 'import {foo$ as bar$} from "module";',
37+
options: optionsDoNotCheckNamedSpecifiers,
38+
},
39+
{
40+
code: 'export {foo$} from "module";',
41+
options: optionsDoNotCheckNamedSpecifiers,
42+
},
43+
{
44+
code: 'export {foo$ as foo} from "module";',
45+
options: optionsDoNotCheckNamedSpecifiers,
46+
},
47+
{
48+
code: 'export {foo as bar$} from "module";',
49+
options: optionsDoNotCheckNamedSpecifiers,
50+
},
51+
{
52+
code: 'const object = {foo$: 1};',
53+
options,
54+
},
55+
{
56+
code: 'class foo { foo$; }',
57+
options,
58+
},
59+
{
60+
code: 'foo$;',
61+
options: optionsOnlyDeclarations,
62+
},
63+
{
64+
code: 'const {foo$} = object;',
65+
options: optionsIgnoreDestructuring,
66+
},
67+
{
68+
code: 'import type {ContraImageFragment$key} from "@/__generated__/ContraImageFragment.graphql";',
69+
options: optionsDoNotCheckNamedSpecifiers,
70+
languageOptions: {
71+
parser: parsers.typescript,
72+
},
73+
},
74+
],
75+
invalid: [
76+
{
77+
code: 'const foo$ = 1;',
78+
options,
79+
errors: [error],
80+
},
81+
{
82+
code: 'import {foo$} from "module";',
83+
options,
84+
errors: [error],
85+
},
86+
{
87+
code: 'import {foo as bar$} from "module";',
88+
options,
89+
errors: [error],
90+
},
91+
{
92+
code: 'import {foo$} from "module";',
93+
options: optionsCheckNamedSpecifiers,
94+
errors: [error],
95+
},
96+
{
97+
code: 'export {foo$} from "module";',
98+
options,
99+
errors: [error],
100+
},
101+
{
102+
code: 'export {foo$} from "module";',
103+
options: optionsCheckNamedSpecifiers,
104+
errors: [error],
105+
},
106+
{
107+
code: 'import foo$ from "module";',
108+
options: optionsDoNotCheckNamedSpecifiers,
109+
errors: [error],
110+
},
111+
{
112+
code: 'import * as foo$ from "module";',
113+
options: optionsDoNotCheckNamedSpecifiers,
114+
errors: [error],
115+
},
116+
{
117+
code: 'export * as foo$ from "module";',
118+
options: optionsDoNotCheckNamedSpecifiers,
119+
errors: [error],
120+
},
121+
{
122+
code: 'import {foo$} from "module"; const bar = foo$;',
123+
options: optionsDoNotCheckNamedSpecifiers,
124+
errors: [
125+
{
126+
...error,
127+
line: 1,
128+
column: 42,
129+
endColumn: 46,
130+
},
131+
],
132+
},
133+
{
134+
code: 'import {foo as bar$} from "module"; bar$;',
135+
options: optionsDoNotCheckNamedSpecifiers,
136+
errors: [
137+
{
138+
...error,
139+
line: 1,
140+
column: 37,
141+
endColumn: 41,
142+
},
143+
],
144+
},
145+
{
146+
code: 'const foo = 1; export {foo as bar$};',
147+
options: optionsDoNotCheckNamedSpecifiers,
148+
errors: [
149+
{
150+
...error,
151+
line: 1,
152+
column: 31,
153+
endColumn: 35,
154+
},
155+
],
156+
},
157+
{
158+
code: 'const object = {foo$: 1};',
159+
options: optionsCheckProperties,
160+
errors: [error],
161+
},
162+
{
163+
code: 'class foo { foo$; }',
164+
options: optionsCheckClassFields,
165+
errors: [error],
166+
},
167+
{
168+
code: 'const foo$ = 1; foo$;',
169+
options: optionsOnlyDeclarations,
170+
errors: [error],
171+
},
172+
{
173+
code: 'const {foo$} = object;',
174+
options,
175+
errors: [error],
176+
},
177+
],
178+
});

0 commit comments

Comments
 (0)