Skip to content

Commit 6fc8409

Browse files
authored
feat(eslint-plugin): [naming-convention] add requiresQuotes modifier (typescript-eslint#2813)
Fixes typescript-eslint#2761 Fixes typescript-eslint#1483 This modifier simply matches any member name that requires quotes. To clarify: this does not match names that have quotes - only names that ***require*** quotes.
1 parent dd0576a commit 6fc8409

File tree

6 files changed

+259
-38
lines changed

6 files changed

+259
-38
lines changed

packages/eslint-plugin/docs/rules/naming-convention.md

Lines changed: 45 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -183,6 +183,7 @@ If these are provided, the identifier must start with one of the provided values
183183
- `global` - matches a variable/function declared in the top-level scope.
184184
- `exported` - matches anything that is exported from the module.
185185
- `unused` - matches anything that is not used.
186+
- `requiresQuotes` - matches any name that requires quotes as it is not a valid identifier (i.e. has a space, a dash, etc in it).
186187
- `public` - matches any member that is either explicitly declared as `public`, or has no visibility modifier (i.e. implicitly public).
187188
- `readonly`, `static`, `abstract`, `protected`, `private` - matches any member explicitly declared with the given modifier.
188189
- `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`).
@@ -229,31 +230,31 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw
229230
- Allowed `modifiers`: `unused`.
230231
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
231232
- `classProperty` - matches any class property. Does not match properties that have direct function expression or arrow function expression values.
232-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
233+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
233234
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
234235
- `objectLiteralProperty` - matches any object literal property. Does not match properties that have direct function expression or arrow function expression values.
235-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
236+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
236237
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
237238
- `typeProperty` - matches any object type property. Does not match properties that have direct function expression or arrow function expression values.
238-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
239+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
239240
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
240241
- `parameterProperty` - matches any parameter property.
241242
- Allowed `modifiers`: `private`, `protected`, `public`, `readonly`.
242243
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
243244
- `classMethod` - matches any class method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors.
244-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
245+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
245246
- Allowed `types`: none.
246247
- `objectLiteralMethod` - matches any object literal method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors.
247-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
248+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
248249
- Allowed `types`: none.
249250
- `typeMethod` - matches any object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors.
250-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
251+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
251252
- Allowed `types`: none.
252253
- `accessor` - matches any accessor.
253-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
254+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
254255
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
255256
- `enumMember` - matches any enum member.
256-
- Allowed `modifiers`: none.
257+
- Allowed `modifiers`: `requiresQuotes`.
257258
- Allowed `types`: none.
258259
- `class` - matches any class declaration.
259260
- Allowed `modifiers`: `abstract`, `exported`, `unused`.
@@ -276,22 +277,22 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw
276277
Group Selectors are provided for convenience, and essentially bundle up sets of individual selectors.
277278

278279
- `default` - matches everything.
279-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
280+
- Allowed `modifiers`: all modifiers.
280281
- Allowed `types`: none.
281282
- `variableLike` - matches the same as `variable`, `function` and `parameter`.
282283
- Allowed `modifiers`: `unused`.
283284
- Allowed `types`: none.
284285
- `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`.
285-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
286+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
286287
- Allowed `types`: none.
287288
- `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`.
288289
- Allowed `modifiers`: `abstract`, `unused`.
289290
- Allowed `types`: none.
290291
- `property` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`.
291-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
292+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
292293
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
293294
- `method` - matches the same as `classMethod`, `objectLiteralMethod`, `typeMethod`.
294-
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`.
295+
- Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`.
295296
- Allowed `types`: none.
296297

297298
## Examples
@@ -424,12 +425,36 @@ This allows you to lint multiple type with same pattern.
424425
}
425426
```
426427

427-
### Ignore properties that require quotes
428+
### Ignore properties that **_require_** quotes
428429

429430
Sometimes you have to use a quoted name that breaks the convention (for example, HTTP headers).
430-
If this is a common thing in your codebase, then you can use the `filter` option in one of two ways:
431+
If this is a common thing in your codebase, then you have a few options.
431432

432-
You can use the `filter` option to ignore specific names only:
433+
If you simply want to allow all property names that require quotes, you can use the `requiresQuotes` modifier to match any property name that _requires_ quoting, and use `format: null` to ignore the name.
434+
435+
```jsonc
436+
{
437+
"@typescript-eslint/naming-convention": [
438+
"error",
439+
{
440+
"selector": [
441+
"classProperty",
442+
"objectLiteralProperty",
443+
"typeProperty",
444+
"classMethod",
445+
"objectLiteralMethod",
446+
"typeMethod",
447+
"accessor",
448+
"enumMember"
449+
],
450+
"format": null,
451+
"modifiers": ["requiresQuotes"]
452+
}
453+
]
454+
}
455+
```
456+
457+
If you have a small and known list of exceptions, you can use the `filter` option to ignore these specific names only:
433458

434459
```jsonc
435460
{
@@ -448,7 +473,7 @@ You can use the `filter` option to ignore specific names only:
448473
}
449474
```
450475

451-
You can use the `filter` option to ignore names that require quoting:
476+
You can use the `filter` option to ignore names with specific characters:
452477

453478
```jsonc
454479
{
@@ -467,6 +492,10 @@ You can use the `filter` option to ignore names that require quoting:
467492
}
468493
```
469494

495+
Note that there is no way to ignore any name that is quoted - only names that are required to be quoted.
496+
This is intentional - adding quotes around a name is not an escape hatch for proper naming.
497+
If you want an escape hatch for a specific name - you should can use an [`eslint-disable` comment](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments).
498+
470499
### Ignore destructured names
471500

472501
Sometimes you might want to allow destructured properties to retain their original name, even if it breaks your naming convention.

packages/eslint-plugin/src/rules/naming-convention.ts

Lines changed: 37 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -120,6 +120,8 @@ enum Modifiers {
120120
exported = 1 << 9,
121121
// things that are unused
122122
unused = 1 << 10,
123+
// properties that require quoting
124+
requiresQuotes = 1 << 11,
123125
}
124126
type ModifiersString = keyof typeof Modifiers;
125127

@@ -359,6 +361,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
359361
'static',
360362
'readonly',
361363
'abstract',
364+
'requiresQuotes',
362365
]),
363366
...selectorSchema('classProperty', true, [
364367
'private',
@@ -367,6 +370,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
367370
'static',
368371
'readonly',
369372
'abstract',
373+
'requiresQuotes',
370374
]),
371375
...selectorSchema('objectLiteralProperty', true, [
372376
'private',
@@ -375,6 +379,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
375379
'static',
376380
'readonly',
377381
'abstract',
382+
'requiresQuotes',
378383
]),
379384
...selectorSchema('typeProperty', true, [
380385
'private',
@@ -383,6 +388,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
383388
'static',
384389
'readonly',
385390
'abstract',
391+
'requiresQuotes',
386392
]),
387393
...selectorSchema('parameterProperty', true, [
388394
'private',
@@ -397,6 +403,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
397403
'static',
398404
'readonly',
399405
'abstract',
406+
'requiresQuotes',
400407
]),
401408

402409
...selectorSchema('classMethod', false, [
@@ -405,36 +412,41 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
405412
'public',
406413
'static',
407414
'abstract',
415+
'requiresQuotes',
408416
]),
409417
...selectorSchema('objectLiteralMethod', false, [
410418
'private',
411419
'protected',
412420
'public',
413421
'static',
414422
'abstract',
423+
'requiresQuotes',
415424
]),
416425
...selectorSchema('typeMethod', false, [
417426
'private',
418427
'protected',
419428
'public',
420429
'static',
421430
'abstract',
431+
'requiresQuotes',
422432
]),
423433
...selectorSchema('method', false, [
424434
'private',
425435
'protected',
426436
'public',
427437
'static',
428438
'abstract',
439+
'requiresQuotes',
429440
]),
430441
...selectorSchema('accessor', true, [
431442
'private',
432443
'protected',
433444
'public',
434445
'static',
435446
'abstract',
447+
'requiresQuotes',
436448
]),
437-
...selectorSchema('enumMember', false),
449+
...selectorSchema('enumMember', false, ['requiresQuotes']),
438450

439451
...selectorSchema('typeLike', false, ['abstract', 'exported', 'unused']),
440452
...selectorSchema('class', false, ['abstract', 'exported', 'unused']),
@@ -516,6 +528,9 @@ export default util.createRule<Options, MessageIds>({
516528

517529
const validators = parseOptions(context);
518530

531+
const compilerOptions = util
532+
.getParserServices(context, true)
533+
.program.getCompilerOptions();
519534
function handleMember(
520535
validator: ValidatorFunction | null,
521536
node:
@@ -533,6 +548,10 @@ export default util.createRule<Options, MessageIds>({
533548
}
534549

535550
const key = node.key;
551+
if (requiresQuoting(key, compilerOptions.target)) {
552+
modifiers.add(Modifiers.requiresQuotes);
553+
}
554+
536555
validator(key, modifiers);
537556
}
538557

@@ -829,7 +848,13 @@ export default util.createRule<Options, MessageIds>({
829848
}
830849

831850
const id = node.id;
832-
validator(id);
851+
const modifiers = new Set<Modifiers>();
852+
853+
if (requiresQuoting(id, compilerOptions.target)) {
854+
modifiers.add(Modifiers.requiresQuotes);
855+
}
856+
857+
validator(id, modifiers);
833858
},
834859

835860
// #endregion enumMember
@@ -1020,8 +1045,17 @@ function isGlobal(scope: TSESLint.Scope.Scope | null): boolean {
10201045
);
10211046
}
10221047

1023-
type ValidatorFunction = (
1048+
function requiresQuoting(
10241049
node: TSESTree.Identifier | TSESTree.Literal,
1050+
target: ts.ScriptTarget | undefined,
1051+
): boolean {
1052+
const name =
1053+
node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`;
1054+
return util.requiresQuoting(name, target);
1055+
}
1056+
1057+
type ValidatorFunction = (
1058+
node: TSESTree.Identifier | TSESTree.StringLiteral | TSESTree.NumberLiteral,
10251059
modifiers?: Set<Modifiers>,
10261060
) => void;
10271061
type ParsedOptions = Record<SelectorsString, null | ValidatorFunction>;

packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts

Lines changed: 2 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import {
66
getParserServices,
77
isClosingBraceToken,
88
isOpeningBraceToken,
9+
requiresQuoting,
910
} from '../util';
1011
import { isTypeFlagSet, unionTypeParts } from 'tsutils';
1112

@@ -34,24 +35,6 @@ export default createRule({
3435
const checker = service.program.getTypeChecker();
3536
const compilerOptions = service.program.getCompilerOptions();
3637

37-
function requiresQuoting(name: string): boolean {
38-
if (name.length === 0) {
39-
return true;
40-
}
41-
42-
if (!ts.isIdentifierStart(name.charCodeAt(0), compilerOptions.target)) {
43-
return true;
44-
}
45-
46-
for (let i = 1; i < name.length; i += 1) {
47-
if (!ts.isIdentifierPart(name.charCodeAt(i), compilerOptions.target)) {
48-
return true;
49-
}
50-
}
51-
52-
return false;
53-
}
54-
5538
function getNodeType(node: TSESTree.Node): ts.Type {
5639
const tsNode = service.esTreeNodeToTSNodeMap.get(node);
5740
return getConstrainedTypeAtLocation(checker, tsNode);
@@ -93,7 +76,7 @@ export default createRule({
9376
if (
9477
symbolName &&
9578
(missingBranchName || missingBranchName === '') &&
96-
requiresQuoting(missingBranchName.toString())
79+
requiresQuoting(missingBranchName.toString(), compilerOptions.target)
9780
) {
9881
caseTest = `${symbolName}['${missingBranchName}']`;
9982
}

packages/eslint-plugin/src/util/index.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@ export * from './misc';
88
export * from './nullThrows';
99
export * from './objectIterators';
1010
export * from './propertyTypes';
11+
export * from './requiresQuoting';
1112
export * from './types';
1213

1314
// this is done for convenience - saves migrating all of the old rules
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
import * as ts from 'typescript';
2+
3+
function requiresQuoting(
4+
name: string,
5+
target: ts.ScriptTarget = ts.ScriptTarget.ESNext,
6+
): boolean {
7+
if (name.length === 0) {
8+
return true;
9+
}
10+
11+
if (!ts.isIdentifierStart(name.charCodeAt(0), target)) {
12+
return true;
13+
}
14+
15+
for (let i = 1; i < name.length; i += 1) {
16+
if (!ts.isIdentifierPart(name.charCodeAt(i), target)) {
17+
return true;
18+
}
19+
}
20+
21+
return false;
22+
}
23+
24+
export { requiresQuoting };

0 commit comments

Comments
 (0)