Skip to content

Commit

Permalink
feat(eslint-plugin): [naming-convention] add modifiers exported, `g…
Browse files Browse the repository at this point in the history
…lobal`, and `destructured` (#2808)

Fixes #2239
Fixes #2512
Fixes #2318
Closes #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}`).
  • Loading branch information
bradzacher committed Nov 24, 2020
1 parent 665b6d4 commit fb254a1
Show file tree
Hide file tree
Showing 3 changed files with 474 additions and 77 deletions.
35 changes: 29 additions & 6 deletions packages/eslint-plugin/docs/rules/naming-convention.md
Expand Up @@ -163,6 +163,10 @@ If these are provided, the identifier must start with one of the provided values
- 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.
- The following `modifiers` are allowed:
- `const` - matches a variable declared as being `const` (`const x = 1`).
- `destructured` - matches a variable declared via an object destructuring pattern (`const {x, z = 2}`).
- Note that this does not match renamed destructured properties (`const {x: y, a: b = 2}`).
- `global` - matches a variable/function declared in the top-level scope.
- `exported` - matches anything that is exported from the module.
- `public` - matches any member that is either explicitly declared as `public`, or has no visibility modifier (i.e. implicitly public).
- `readonly`, `static`, `abstract`, `protected`, `private` - matches any member explicitly declared with the given modifier.
- `types` allows you to specify which types to match. This option supports simple, primitive types only (`boolean`, `string`, `number`, `array`, `function`).
Expand Down Expand Up @@ -200,10 +204,10 @@ There are two types of selectors, individual selectors, and grouped selectors.
Individual Selectors match specific, well-defined sets. There is no overlap between each of the individual selectors.

- `variable` - matches any `var` / `let` / `const` variable name.
- Allowed `modifiers`: `const`.
- Allowed `modifiers`: `const`, `destructured`, `global`, `exported`.
- Allowed `types`: `boolean`, `string`, `number`, `function`, `array`.
- `function` - matches any named function declaration or named function expression.
- Allowed `modifiers`: none.
- Allowed `modifiers`: `global`, `exported`.
- Allowed `types`: none.
- `parameter` - matches any function parameter. Does not match parameter properties.
- Allowed `modifiers`: none.
Expand Down Expand Up @@ -236,16 +240,16 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw
- Allowed `modifiers`: none.
- Allowed `types`: none.
- `class` - matches any class declaration.
- Allowed `modifiers`: `abstract`.
- Allowed `modifiers`: `abstract`, `exported`.
- Allowed `types`: none.
- `interface` - matches any interface declaration.
- Allowed `modifiers`: none.
- Allowed `modifiers`: `exported`.
- Allowed `types`: none.
- `typeAlias` - matches any type alias declaration.
- Allowed `modifiers`: none.
- Allowed `modifiers`: `exported`.
- Allowed `types`: none.
- `enum` - matches any enum declaration.
- Allowed `modifiers`: none.
- Allowed `modifiers`: `exported`.
- Allowed `types`: none.
- `typeParameter` - matches any generic type parameter declaration.
- Allowed `modifiers`: none.
Expand Down Expand Up @@ -447,6 +451,25 @@ You can use the `filter` option to ignore names that require quoting:
}
```

### Ignore destructured names

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

You can use the `destructured` modifier to match these names, and explicitly set `format: null` to apply no formatting:

```jsonc
{
"@typescript-eslint/naming-convention": [
"error",
{
"selector": "variable",
"modifiers": ["destructured"],
"format": null
}
]
}
```

### Enforce the codebase follows ESLint's `camelcase` conventions

```json
Expand Down
204 changes: 133 additions & 71 deletions packages/eslint-plugin/src/rules/naming-convention.ts
Expand Up @@ -4,6 +4,7 @@ import {
TSESLint,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { PatternVisitor } from '@typescript-eslint/scope-manager';
import * as ts from 'typescript';
import * as util from '../util';

Expand Down Expand Up @@ -95,13 +96,23 @@ type MetaSelectorsString = keyof typeof MetaSelectors;
type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString;

enum Modifiers {
// const variable
const = 1 << 0,
// readonly members
readonly = 1 << 1,
// static members
static = 1 << 2,
// member accessibility
public = 1 << 3,
protected = 1 << 4,
private = 1 << 5,
abstract = 1 << 6,
// destructured variable
destructured = 1 << 7,
// variables declared in the top-level scope
global = 1 << 8,
// things that are exported
exported = 1 << 9,
}
type ModifiersString = keyof typeof Modifiers;

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

...selectorSchema('variableLike', false),
...selectorSchema('variable', true, ['const']),
...selectorSchema('function', false),
...selectorSchema('variable', true, [
'const',
'destructured',
'global',
'exported',
]),
...selectorSchema('function', false, ['global', 'exported']),
...selectorSchema('parameter', true),

...selectorSchema('memberLike', false, [
Expand Down Expand Up @@ -412,11 +428,11 @@ const SCHEMA: JSONSchema.JSONSchema4 = {
]),
...selectorSchema('enumMember', false),

...selectorSchema('typeLike', false, ['abstract']),
...selectorSchema('class', false, ['abstract']),
...selectorSchema('interface', false),
...selectorSchema('typeAlias', false),
...selectorSchema('enum', false),
...selectorSchema('typeLike', false, ['abstract', 'exported']),
...selectorSchema('class', false, ['abstract', 'exported']),
...selectorSchema('interface', false, ['exported']),
...selectorSchema('typeAlias', false, ['exported']),
...selectorSchema('enum', false, ['exported']),
...selectorSchema('typeParameter', false),
],
},
Expand Down Expand Up @@ -550,22 +566,40 @@ export default util.createRule<Options, MessageIds>({
if (!validator) {
return;
}
const identifiers = getIdentifiersFromPattern(node.id);

const identifiers: TSESTree.Identifier[] = [];
getIdentifiersFromPattern(node.id, identifiers);

const modifiers = new Set<Modifiers>();
const baseModifiers = new Set<Modifiers>();
const parent = node.parent;
if (
parent &&
parent.type === AST_NODE_TYPES.VariableDeclaration &&
parent.kind === 'const'
) {
modifiers.add(Modifiers.const);
if (parent?.type === AST_NODE_TYPES.VariableDeclaration) {
if (parent.kind === 'const') {
baseModifiers.add(Modifiers.const);
}
if (isGlobal(context.getScope())) {
baseModifiers.add(Modifiers.global);
}
}

identifiers.forEach(i => {
validator(i, modifiers);
identifiers.forEach(id => {
const modifiers = new Set(baseModifiers);
if (
// `const { x }`
// does not match `const { x: y }`
(id.parent?.type === AST_NODE_TYPES.Property &&
id.parent.shorthand) ||
// `const { x = 2 }`
// does not match const `{ x: y = 2 }`
(id.parent?.type === AST_NODE_TYPES.AssignmentPattern &&
id.parent.parent?.type === AST_NODE_TYPES.Property &&
id.parent.parent.shorthand)
) {
modifiers.add(Modifiers.destructured);
}

if (isExported(parent, id.name, context.getScope())) {
modifiers.add(Modifiers.exported);
}

validator(id, modifiers);
});
},

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

validator(node.id);
const modifiers = new Set<Modifiers>();
// functions create their own nested scope
const scope = context.getScope().upper;
if (isGlobal(scope)) {
modifiers.add(Modifiers.global);
}
if (isExported(node, node.id.name, scope)) {
modifiers.add(Modifiers.exported);
}

validator(node.id, modifiers);
},

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

const identifiers: TSESTree.Identifier[] = [];
getIdentifiersFromPattern(param, identifiers);
const identifiers = getIdentifiersFromPattern(param);

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

const modifiers = getMemberModifiers(node);

const identifiers: TSESTree.Identifier[] = [];
getIdentifiersFromPattern(node.parameter, identifiers);
const identifiers = getIdentifiersFromPattern(node.parameter);

identifiers.forEach(i => {
validator(i, modifiers);
Expand Down Expand Up @@ -765,6 +807,11 @@ export default util.createRule<Options, MessageIds>({
modifiers.add(Modifiers.abstract);
}

// classes create their own nested scope
if (isExported(node, id.name, context.getScope().upper)) {
modifiers.add(Modifiers.exported);
}

validator(id, modifiers);
},

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

validator(node.id);
const modifiers = new Set<Modifiers>();
if (isExported(node, node.id.name, context.getScope())) {
modifiers.add(Modifiers.exported);
}

validator(node.id, modifiers);
},

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

validator(node.id);
const modifiers = new Set<Modifiers>();
if (isExported(node, node.id.name, context.getScope())) {
modifiers.add(Modifiers.exported);
}

validator(node.id, modifiers);
},

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

validator(node.id);
const modifiers = new Set<Modifiers>();
// enums create their own nested scope
if (isExported(node, node.id.name, context.getScope().upper)) {
modifiers.add(Modifiers.exported);
}

validator(node.id, modifiers);
},

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

function getIdentifiersFromPattern(
pattern: TSESTree.DestructuringPattern,
identifiers: TSESTree.Identifier[],
): void {
switch (pattern.type) {
case AST_NODE_TYPES.Identifier:
identifiers.push(pattern);
break;

case AST_NODE_TYPES.ArrayPattern:
pattern.elements.forEach(element => {
if (element !== null) {
getIdentifiersFromPattern(element, identifiers);
}
});
break;

case AST_NODE_TYPES.ObjectPattern:
pattern.properties.forEach(property => {
if (property.type === AST_NODE_TYPES.RestElement) {
getIdentifiersFromPattern(property, identifiers);
} else {
// this is a bit weird, but it's because ESTree doesn't have a new node type
// for object destructuring properties - it just reuses Property...
// https://github.com/estree/estree/blob/9ae284b71130d53226e7153b42f01bf819e6e657/es2015.md#L206-L211
// However, the parser guarantees this is safe (and there is error handling)
getIdentifiersFromPattern(
property.value as TSESTree.DestructuringPattern,
identifiers,
);
}
});
break;
): TSESTree.Identifier[] {
const identifiers: TSESTree.Identifier[] = [];
const visitor = new PatternVisitor({}, pattern, id => identifiers.push(id));
visitor.visit(pattern);
return identifiers;
}

case AST_NODE_TYPES.RestElement:
getIdentifiersFromPattern(pattern.argument, identifiers);
break;
function isExported(
node: TSESTree.Node | undefined,
name: string,
scope: TSESLint.Scope.Scope | null,
): boolean {
if (
node?.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ||
node?.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration
) {
return true;
}

case AST_NODE_TYPES.AssignmentPattern:
getIdentifiersFromPattern(pattern.left, identifiers);
break;
if (scope == null) {
return false;
}

case AST_NODE_TYPES.MemberExpression:
// ignore member expressions, as the everything must already be defined
break;
const variable = scope.set.get(name);
if (variable) {
for (const ref of variable.references) {
const refParent = ref.identifier.parent;
if (
refParent?.type === AST_NODE_TYPES.ExportDefaultDeclaration ||
refParent?.type === AST_NODE_TYPES.ExportSpecifier
) {
return true;
}
}
}

return false;
}

default:
// https://github.com/typescript-eslint/typescript-eslint/issues/1282
// eslint-disable-next-line @typescript-eslint/no-unnecessary-type-assertion
throw new Error(`Unexpected pattern type ${pattern!.type}`);
function isGlobal(scope: TSESLint.Scope.Scope | null): boolean {
if (scope == null) {
return false;
}

return (
scope.type === TSESLint.Scope.ScopeType.global ||
scope.type === TSESLint.Scope.ScopeType.module
);
}

type ValidatorFunction = (
Expand Down

0 comments on commit fb254a1

Please sign in to comment.