Skip to content

Commit fb254a1

Browse files
authored
feat(eslint-plugin): [naming-convention] add modifiers exported, global, and destructured (typescript-eslint#2808)
Fixes typescript-eslint#2239 Fixes typescript-eslint#2512 Fixes typescript-eslint#2318 Closes typescript-eslint#2802 Adds the following modifiers: - `exported` - matches anything that is exported from the module. - `global` - matches a variable/function declared in the top-level scope. - `destructured` - matches a variable declared via an object destructuring pattern (`const {x, z = 2}`).
1 parent 665b6d4 commit fb254a1

File tree

3 files changed

+474
-77
lines changed

3 files changed

+474
-77
lines changed

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

+29-6
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,10 @@ If these are provided, the identifier must start with one of the provided values
163163
- For example, if you provide `{ modifiers: ['private', 'static', 'readonly'] }`, then it will only match something that is `private static readonly`, and something that is just `private` will not match.
164164
- The following `modifiers` are allowed:
165165
- `const` - matches a variable declared as being `const` (`const x = 1`).
166+
- `destructured` - matches a variable declared via an object destructuring pattern (`const {x, z = 2}`).
167+
- Note that this does not match renamed destructured properties (`const {x: y, a: b = 2}`).
168+
- `global` - matches a variable/function declared in the top-level scope.
169+
- `exported` - matches anything that is exported from the module.
166170
- `public` - matches any member that is either explicitly declared as `public`, or has no visibility modifier (i.e. implicitly public).
167171
- `readonly`, `static`, `abstract`, `protected`, `private` - matches any member explicitly declared with the given modifier.
168172
- `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`).
@@ -200,10 +204,10 @@ There are two types of selectors, individual selectors, and grouped selectors.
200204
Individual Selectors match specific, well-defined sets. There is no overlap between each of the individual selectors.
201205

202206
- `variable` - matches any `var` / `let` / `const` variable name.
203-
- Allowed `modifiers`: `const`.
207+
- Allowed `modifiers`: `const`, `destructured`, `global`, `exported`.
204208
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
205209
- `function` - matches any named function declaration or named function expression.
206-
- Allowed `modifiers`: none.
210+
- Allowed `modifiers`: `global`, `exported`.
207211
- Allowed `types`: none.
208212
- `parameter` - matches any function parameter. Does not match parameter properties.
209213
- Allowed `modifiers`: none.
@@ -236,16 +240,16 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw
236240
- Allowed `modifiers`: none.
237241
- Allowed `types`: none.
238242
- `class` - matches any class declaration.
239-
- Allowed `modifiers`: `abstract`.
243+
- Allowed `modifiers`: `abstract`, `exported`.
240244
- Allowed `types`: none.
241245
- `interface` - matches any interface declaration.
242-
- Allowed `modifiers`: none.
246+
- Allowed `modifiers`: `exported`.
243247
- Allowed `types`: none.
244248
- `typeAlias` - matches any type alias declaration.
245-
- Allowed `modifiers`: none.
249+
- Allowed `modifiers`: `exported`.
246250
- Allowed `types`: none.
247251
- `enum` - matches any enum declaration.
248-
- Allowed `modifiers`: none.
252+
- Allowed `modifiers`: `exported`.
249253
- Allowed `types`: none.
250254
- `typeParameter` - matches any generic type parameter declaration.
251255
- Allowed `modifiers`: none.
@@ -447,6 +451,25 @@ You can use the `filter` option to ignore names that require quoting:
447451
}
448452
```
449453

454+
### Ignore destructured names
455+
456+
Sometimes you might want to allow destructured properties to retain their original name, even if it breaks your naming convention.
457+
458+
You can use the `destructured` modifier to match these names, and explicitly set `format: null` to apply no formatting:
459+
460+
```jsonc
461+
{
462+
"@typescript-eslint/naming-convention": [
463+
"error",
464+
{
465+
"selector": "variable",
466+
"modifiers": ["destructured"],
467+
"format": null
468+
}
469+
]
470+
}
471+
```
472+
450473
### Enforce the codebase follows ESLint's `camelcase` conventions
451474

452475
```json

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

+133-71
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import {
44
TSESLint,
55
TSESTree,
66
} from '@typescript-eslint/experimental-utils';
7+
import { PatternVisitor } from '@typescript-eslint/scope-manager';
78
import * as ts from 'typescript';
89
import * as util from '../util';
910

@@ -95,13 +96,23 @@ type MetaSelectorsString = keyof typeof MetaSelectors;
9596
type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString;
9697

9798
enum Modifiers {
99+
// const variable
98100
const = 1 << 0,
101+
// readonly members
99102
readonly = 1 << 1,
103+
// static members
100104
static = 1 << 2,
105+
// member accessibility
101106
public = 1 << 3,
102107
protected = 1 << 4,
103108
private = 1 << 5,
104109
abstract = 1 << 6,
110+
// destructured variable
111+
destructured = 1 << 7,
112+
// variables declared in the top-level scope
113+
global = 1 << 8,
114+
// things that are exported
115+
exported = 1 << 9,
105116
}
106117
type ModifiersString = keyof typeof Modifiers;
107118

@@ -324,8 +335,13 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
324335
...selectorSchema('default', false, util.getEnumNames(Modifiers)),
325336

326337
...selectorSchema('variableLike', false),
327-
...selectorSchema('variable', true, ['const']),
328-
...selectorSchema('function', false),
338+
...selectorSchema('variable', true, [
339+
'const',
340+
'destructured',
341+
'global',
342+
'exported',
343+
]),
344+
...selectorSchema('function', false, ['global', 'exported']),
329345
...selectorSchema('parameter', true),
330346

331347
...selectorSchema('memberLike', false, [
@@ -412,11 +428,11 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
412428
]),
413429
...selectorSchema('enumMember', false),
414430

415-
...selectorSchema('typeLike', false, ['abstract']),
416-
...selectorSchema('class', false, ['abstract']),
417-
...selectorSchema('interface', false),
418-
...selectorSchema('typeAlias', false),
419-
...selectorSchema('enum', false),
431+
...selectorSchema('typeLike', false, ['abstract', 'exported']),
432+
...selectorSchema('class', false, ['abstract', 'exported']),
433+
...selectorSchema('interface', false, ['exported']),
434+
...selectorSchema('typeAlias', false, ['exported']),
435+
...selectorSchema('enum', false, ['exported']),
420436
...selectorSchema('typeParameter', false),
421437
],
422438
},
@@ -550,22 +566,40 @@ export default util.createRule<Options, MessageIds>({
550566
if (!validator) {
551567
return;
552568
}
569+
const identifiers = getIdentifiersFromPattern(node.id);
553570

554-
const identifiers: TSESTree.Identifier[] = [];
555-
getIdentifiersFromPattern(node.id, identifiers);
556-
557-
const modifiers = new Set<Modifiers>();
571+
const baseModifiers = new Set<Modifiers>();
558572
const parent = node.parent;
559-
if (
560-
parent &&
561-
parent.type === AST_NODE_TYPES.VariableDeclaration &&
562-
parent.kind === 'const'
563-
) {
564-
modifiers.add(Modifiers.const);
573+
if (parent?.type === AST_NODE_TYPES.VariableDeclaration) {
574+
if (parent.kind === 'const') {
575+
baseModifiers.add(Modifiers.const);
576+
}
577+
if (isGlobal(context.getScope())) {
578+
baseModifiers.add(Modifiers.global);
579+
}
565580
}
566581

567-
identifiers.forEach(i => {
568-
validator(i, modifiers);
582+
identifiers.forEach(id => {
583+
const modifiers = new Set(baseModifiers);
584+
if (
585+
// `const { x }`
586+
// does not match `const { x: y }`
587+
(id.parent?.type === AST_NODE_TYPES.Property &&
588+
id.parent.shorthand) ||
589+
// `const { x = 2 }`
590+
// does not match const `{ x: y = 2 }`
591+
(id.parent?.type === AST_NODE_TYPES.AssignmentPattern &&
592+
id.parent.parent?.type === AST_NODE_TYPES.Property &&
593+
id.parent.parent.shorthand)
594+
) {
595+
modifiers.add(Modifiers.destructured);
596+
}
597+
598+
if (isExported(parent, id.name, context.getScope())) {
599+
modifiers.add(Modifiers.exported);
600+
}
601+
602+
validator(id, modifiers);
569603
});
570604
},
571605

@@ -584,7 +618,17 @@ export default util.createRule<Options, MessageIds>({
584618
return;
585619
}
586620

587-
validator(node.id);
621+
const modifiers = new Set<Modifiers>();
622+
// functions create their own nested scope
623+
const scope = context.getScope().upper;
624+
if (isGlobal(scope)) {
625+
modifiers.add(Modifiers.global);
626+
}
627+
if (isExported(node, node.id.name, scope)) {
628+
modifiers.add(Modifiers.exported);
629+
}
630+
631+
validator(node.id, modifiers);
588632
},
589633

590634
// #endregion function
@@ -608,8 +652,7 @@ export default util.createRule<Options, MessageIds>({
608652
return;
609653
}
610654

611-
const identifiers: TSESTree.Identifier[] = [];
612-
getIdentifiersFromPattern(param, identifiers);
655+
const identifiers = getIdentifiersFromPattern(param);
613656

614657
identifiers.forEach(i => {
615658
validator(i);
@@ -629,8 +672,7 @@ export default util.createRule<Options, MessageIds>({
629672

630673
const modifiers = getMemberModifiers(node);
631674

632-
const identifiers: TSESTree.Identifier[] = [];
633-
getIdentifiersFromPattern(node.parameter, identifiers);
675+
const identifiers = getIdentifiersFromPattern(node.parameter);
634676

635677
identifiers.forEach(i => {
636678
validator(i, modifiers);
@@ -765,6 +807,11 @@ export default util.createRule<Options, MessageIds>({
765807
modifiers.add(Modifiers.abstract);
766808
}
767809

810+
// classes create their own nested scope
811+
if (isExported(node, id.name, context.getScope().upper)) {
812+
modifiers.add(Modifiers.exported);
813+
}
814+
768815
validator(id, modifiers);
769816
},
770817

@@ -778,7 +825,12 @@ export default util.createRule<Options, MessageIds>({
778825
return;
779826
}
780827

781-
validator(node.id);
828+
const modifiers = new Set<Modifiers>();
829+
if (isExported(node, node.id.name, context.getScope())) {
830+
modifiers.add(Modifiers.exported);
831+
}
832+
833+
validator(node.id, modifiers);
782834
},
783835

784836
// #endregion interface
@@ -791,7 +843,12 @@ export default util.createRule<Options, MessageIds>({
791843
return;
792844
}
793845

794-
validator(node.id);
846+
const modifiers = new Set<Modifiers>();
847+
if (isExported(node, node.id.name, context.getScope())) {
848+
modifiers.add(Modifiers.exported);
849+
}
850+
851+
validator(node.id, modifiers);
795852
},
796853

797854
// #endregion typeAlias
@@ -804,7 +861,13 @@ export default util.createRule<Options, MessageIds>({
804861
return;
805862
}
806863

807-
validator(node.id);
864+
const modifiers = new Set<Modifiers>();
865+
// enums create their own nested scope
866+
if (isExported(node, node.id.name, context.getScope().upper)) {
867+
modifiers.add(Modifiers.exported);
868+
}
869+
870+
validator(node.id, modifiers);
808871
},
809872

810873
// #endregion enum
@@ -829,55 +892,54 @@ export default util.createRule<Options, MessageIds>({
829892

830893
function getIdentifiersFromPattern(
831894
pattern: TSESTree.DestructuringPattern,
832-
identifiers: TSESTree.Identifier[],
833-
): void {
834-
switch (pattern.type) {
835-
case AST_NODE_TYPES.Identifier:
836-
identifiers.push(pattern);
837-
break;
838-
839-
case AST_NODE_TYPES.ArrayPattern:
840-
pattern.elements.forEach(element => {
841-
if (element !== null) {
842-
getIdentifiersFromPattern(element, identifiers);
843-
}
844-
});
845-
break;
846-
847-
case AST_NODE_TYPES.ObjectPattern:
848-
pattern.properties.forEach(property => {
849-
if (property.type === AST_NODE_TYPES.RestElement) {
850-
getIdentifiersFromPattern(property, identifiers);
851-
} else {
852-
// this is a bit weird, but it's because ESTree doesn't have a new node type
853-
// for object destructuring properties - it just reuses Property...
854-
// https://github.com/estree/estree/blob/9ae284b71130d53226e7153b42f01bf819e6e657/es2015.md#L206-L211
855-
// However, the parser guarantees this is safe (and there is error handling)
856-
getIdentifiersFromPattern(
857-
property.value as TSESTree.DestructuringPattern,
858-
identifiers,
859-
);
860-
}
861-
});
862-
break;
895+
): TSESTree.Identifier[] {
896+
const identifiers: TSESTree.Identifier[] = [];
897+
const visitor = new PatternVisitor({}, pattern, id => identifiers.push(id));
898+
visitor.visit(pattern);
899+
return identifiers;
900+
}
863901

864-
case AST_NODE_TYPES.RestElement:
865-
getIdentifiersFromPattern(pattern.argument, identifiers);
866-
break;
902+
function isExported(
903+
node: TSESTree.Node | undefined,
904+
name: string,
905+
scope: TSESLint.Scope.Scope | null,
906+
): boolean {
907+
if (
908+
node?.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ||
909+
node?.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration
910+
) {
911+
return true;
912+
}
867913

868-
case AST_NODE_TYPES.AssignmentPattern:
869-
getIdentifiersFromPattern(pattern.left, identifiers);
870-
break;
914+
if (scope == null) {
915+
return false;
916+
}
871917

872-
case AST_NODE_TYPES.MemberExpression:
873-
// ignore member expressions, as the everything must already be defined
874-
break;
918+
const variable = scope.set.get(name);
919+
if (variable) {
920+
for (const ref of variable.references) {
921+
const refParent = ref.identifier.parent;
922+
if (
923+
refParent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ||
924+
refParent?.type === AST_NODE_TYPES.ExportSpecifier
925+
) {
926+
return true;
927+
}
928+
}
929+
}
930+
931+
return false;
932+
}
875933

876-
default:
877-
// https://github.com/typescript-eslint/typescript-eslint/issues/1282
878-
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
879-
throw new Error(`Unexpected pattern type ${pattern!.type}`);
934+
function isGlobal(scope: TSESLint.Scope.Scope | null): boolean {
935+
if (scope == null) {
936+
return false;
880937
}
938+
939+
return (
940+
scope.type === TSESLint.Scope.ScopeType.global ||
941+
scope.type === TSESLint.Scope.ScopeType.module
942+
);
881943
}
882944

883945
type ValidatorFunction = (

0 commit comments

Comments
 (0)