Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat(eslint-plugin): [naming-convention] add modifiers exported, global, and destructured #2808

Merged
merged 1 commit into from Nov 24, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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