Skip to content

Commit

Permalink
feat: consume new scope analysis package
Browse files Browse the repository at this point in the history
  • Loading branch information
bradzacher committed Jun 25, 2020
1 parent 6949551 commit d7a35dd
Show file tree
Hide file tree
Showing 42 changed files with 469 additions and 165,649 deletions.
1 change: 0 additions & 1 deletion packages/eslint-plugin/README.md
Expand Up @@ -144,7 +144,6 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int
| [`@typescript-eslint/no-unsafe-call`](./docs/rules/no-unsafe-call.md) | Disallows calling an any type value | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-unsafe-member-access`](./docs/rules/no-unsafe-member-access.md) | Disallows member access on any typed variables | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-unsafe-return`](./docs/rules/no-unsafe-return.md) | Disallows returning any from a function | :heavy_check_mark: | | :thought_balloon: |
| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments | | | |
| [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | |
| [`@typescript-eslint/prefer-as-const`](./docs/rules/prefer-as-const.md) | Prefer usage of `as const` over literal type | :heavy_check_mark: | :wrench: | |
| [`@typescript-eslint/prefer-for-of`](./docs/rules/prefer-for-of.md) | Prefer a ‘for-of’ loop over a standard ‘for’ loop if the index is only used to access the array being iterated | | | |
Expand Down
2 changes: 0 additions & 2 deletions packages/eslint-plugin/docs/rules/no-unused-vars.md
@@ -1,7 +1,5 @@
# Disallow unused variables (`no-unused-vars`)

## PLEASE READ THIS ISSUE BEFORE USING THIS RULE [#1856](https://github.com/typescript-eslint/typescript-eslint/issues/1856)

## Rule Details

This rule extends the base [`eslint/no-unused-vars`](https://eslint.org/docs/rules/no-unused-vars) rule.
Expand Down
51 changes: 31 additions & 20 deletions packages/eslint-plugin/docs/rules/no-use-before-define.md
Expand Up @@ -23,42 +23,34 @@ See [`eslint/no-use-before-define` options](https://eslint.org/docs/rules/no-use
This rule adds the following options:

```ts
interface Options extends BaseNoMagicNumbersOptions {
interface Options extends BaseNoUseBeforeDefineOptions {
enums?: boolean;
typedefs?: boolean;
ignoreTypeReferences?: boolean;
}

const defaultOptions: Options = {
...baseNoMagicNumbersDefaultOptions,
...baseNoUseBeforeDefineDefaultOptions,
enums: true,
typedefs: true,
ignoreTypeReferences: true,
};
```

### `enums`

The flag which shows whether or not this rule checks enum declarations of upper scopes.
If this is `true`, this rule warns every reference to a enum before the enum declaration.
Otherwise, ignores those references.
If this is `false`, this rule will ignore references to enums, when the reference is in a child scope.

Examples of **incorrect** code for the `{ "enums": true }` option:
Examples of **incorrect** code for the `{ "enums": false }` option:

```ts
/*eslint no-use-before-define: ["error", { "enums": true }]*/
/*eslint no-use-before-define: ["error", { "enums": false }]*/

function foo() {
return Foo.FOO;
}

class Test {
foo() {
return Foo.FOO;
}
}
const x = Foo.FOO;

enum Foo {
FOO,
BAR,
}
```

Expand All @@ -78,10 +70,8 @@ enum Foo {

### `typedefs`

The flag which shows whether or not this rule checks type declarations.
If this is `true`, this rule warns every reference to a type before the type declaration.
Otherwise, ignores those references.
Type declarations are hoisted, so it's safe.
If this is `false`, this rule will ignore references to types.

Examples of **correct** code for the `{ "typedefs": false }` option:

Expand All @@ -92,4 +82,25 @@ let myVar: StringOrNumber;
type StringOrNumber = string | number;
```

Copied from [the original ESLint rule docs](https://github.com/eslint/eslint/blob/a113cd3/docs/rules/no-use-before-define.md)
### `ignoreTypeReferences`

If this is `true`, this rule ignores all type references, such as in type annotations and assertions.
If this is `false`, this will will check all type references.

Examples of **correct** code for the `{ "ignoreTypeReferences": true }` option:

```ts
/*eslint no-use-before-define: ["error", { "ignoreTypeReferences": true }]*/

let var1: StringOrNumber;
type StringOrNumber = string | number;

let var2: Enum;
enum Enum {}
```

### Other Options

See [`eslint/no-use-before-define` options](https://eslint.org/docs/rules/no-use-before-define#options).

<sup>Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-use-before-define.md)</sup>
1 change: 1 addition & 0 deletions packages/eslint-plugin/package.json
Expand Up @@ -43,6 +43,7 @@
},
"dependencies": {
"@typescript-eslint/experimental-utils": "3.4.0",
"@typescript-eslint/scope-manager": "3.4.0",
"debug": "^4.1.1",
"functional-red-black-tree": "^1.0.1",
"regexpp": "^3.0.0",
Expand Down
1 change: 0 additions & 1 deletion packages/eslint-plugin/src/configs/all.ts
Expand Up @@ -86,7 +86,6 @@ export = {
'@typescript-eslint/no-unused-expressions': 'error',
'no-unused-vars': 'off',
'@typescript-eslint/no-unused-vars': 'error',
'@typescript-eslint/no-unused-vars-experimental': 'error',
'no-use-before-define': 'off',
'@typescript-eslint/no-use-before-define': 'error',
'no-useless-constructor': 'off',
Expand Down
12 changes: 2 additions & 10 deletions packages/eslint-plugin/src/rules/no-empty-interface.ts
@@ -1,8 +1,5 @@
import * as util from '../util';
import {
AST_NODE_TYPES,
TSESLint,
} from '@typescript-eslint/experimental-utils';
import { TSESLint } from '@typescript-eslint/experimental-utils';

type Options = [
{
Expand Down Expand Up @@ -81,12 +78,7 @@ export default util.createRule<Options, MessageIds>({
let useAutoFix = true;
if (util.isDefinitionFile(filename)) {
const scope = context.getScope();
if (
scope.block.parent &&
scope.block.parent.type ===
AST_NODE_TYPES.TSModuleDeclaration &&
scope.block.parent.declare
) {
if (scope.type === 'tsModule' && scope.block.declare) {
useAutoFix = false;
}
}
Expand Down
Expand Up @@ -30,6 +30,8 @@ export default util.createRule<Options, MessageIds>({
category: 'Best Practices',
recommended: false,
},
deprecated: true,
replacedBy: ['no-unused-vars'],
schema: [
{
type: 'object',
Expand Down
156 changes: 125 additions & 31 deletions packages/eslint-plugin/src/rules/no-unused-vars.ts
@@ -1,7 +1,5 @@
import {
AST_NODE_TYPES,
TSESTree,
} from '@typescript-eslint/experimental-utils';
import { TSESTree, TSESLint } from '@typescript-eslint/experimental-utils';
import { PatternVisitor } from '@typescript-eslint/scope-manager';
import baseRule from 'eslint/lib/rules/no-unused-vars';
import * as util from '../util';

Expand All @@ -26,45 +24,133 @@ export default util.createRule<Options, MessageIds>({
const rules = baseRule.create(context);

/**
* Mark heritage clause as used
* @param node The node currently being traversed
* Gets a list of TS module definitions for a specified variable.
* @param variable eslint-scope variable object.
*/
function markHeritageAsUsed(node: TSESTree.Expression): void {
switch (node.type) {
case AST_NODE_TYPES.Identifier:
context.markVariableAsUsed(node.name);
break;
case AST_NODE_TYPES.MemberExpression:
markHeritageAsUsed(node.object);
break;
case AST_NODE_TYPES.CallExpression:
markHeritageAsUsed(node.callee);
break;
function getModuleDeclarations(
variable: TSESLint.Scope.Variable,
): TSESTree.TSModuleDeclaration[] {
const functionDefinitions: TSESTree.TSModuleDeclaration[] = [];

variable.defs.forEach(def => {
// FunctionDeclarations
if (def.type === 'TSModuleName') {
functionDefinitions.push(def.node);
}
});

return functionDefinitions;
}

/**
* Determine if an identifier is referencing an enclosing module name.
* @param ref The reference to check.
* @param nodes The candidate function nodes.
* @returns True if it's a self-reference, false if not.
*/
function isSelfReference(
ref: TSESLint.Scope.Reference,
nodes: TSESTree.Node[],
): boolean {
let scope: TSESLint.Scope.Scope | null = ref.from;

while (scope) {
if (nodes.indexOf(scope.block) >= 0) {
return true;
}

scope = scope.upper;
}

return false;
}

return Object.assign({}, rules, {
'TSTypeReference Identifier'(node: TSESTree.Identifier) {
context.markVariableAsUsed(node.name);
return {
...rules,
'TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'(
node:
| TSESTree.TSConstructorType
| TSESTree.TSConstructSignatureDeclaration
| TSESTree.TSDeclareFunction
| TSESTree.TSEmptyBodyFunctionExpression
| TSESTree.TSFunctionType
| TSESTree.TSMethodSignature,
): void {
// function type signature params create variables because they can be referenced within the signature,
// but they obviously aren't unused variables for the purposes of this rule.
for (const param of node.params) {
visitPattern(param, name => {
context.markVariableAsUsed(name.name);
});
}
},
TSInterfaceHeritage(node: TSESTree.TSInterfaceHeritage) {
if (node.expression) {
markHeritageAsUsed(node.expression);
TSEnumDeclaration(): void {
// enum members create variables because they can be referenced within the enum,
// but they obviously aren't unused variables for the purposes of this rule.
const scope = context.getScope();
for (const variable of scope.variables) {
context.markVariableAsUsed(variable.name);
}
},
TSClassImplements(node: TSESTree.TSClassImplements) {
if (node.expression) {
markHeritageAsUsed(node.expression);
TSMappedType(node): void {
// mapped types create a variable for their type name, but it's not necessary to reference it,
// so we shouldn't consider it as unused for the purpose of this rule.
context.markVariableAsUsed(node.typeParameter.name.name);
},
TSModuleDeclaration(): void {
const childScope = context.getScope();
const scope = util.nullThrows(
context.getScope().upper,
util.NullThrowsReasons.MissingToken(childScope.type, 'upper scope'),
);
for (const variable of scope.variables) {
// check if the only reference to a module's name is a self-reference in its body
const moduleNodes = getModuleDeclarations(variable);
const isModuleDefinition = moduleNodes.length > 0;

if (
!isModuleDefinition ||
// ignore unreferenced module definitions, as the base rule will report on them
variable.references.length === 0
) {
continue;
}

const isVariableOnlySelfReferenced = variable.references.every(
ref => {
return isSelfReference(ref, moduleNodes);
},
);

if (isVariableOnlySelfReferenced) {
context.report({
node: variable.identifiers[0],
messageId: 'unusedVar',
data: {
varName: variable.name,
action: 'defined',
additional: '',
},
});
}
}
},
'TSParameterProperty Identifier'(node: TSESTree.Identifier) {
// just assume parameter properties are used
[[
'TSParameterProperty > AssignmentPattern > Identifier.left',
'TSParameterProperty > Identifier.parameter',
].join(', ')](node: TSESTree.Identifier): void {
// just assume parameter properties are used as property usage tracking is beyond the scope of this rule
context.markVariableAsUsed(node.name);
},
'TSEnumMember Identifier'(node: TSESTree.Identifier) {
':matches(FunctionDeclaration, FunctionExpression, ArrowFunctionExpression) > Identifier[name="this"].params'(
node: TSESTree.Identifier,
): void {
// this parameters should always be considered used as they're pseudo-parameters
context.markVariableAsUsed(node.name);
},
'*[declare=true] Identifier'(node: TSESTree.Identifier) {

// TODO
'*[declare=true] Identifier'(node: TSESTree.Identifier): void {
context.markVariableAsUsed(node.name);
const scope = context.getScope();
const { variableScope } = scope;
Expand All @@ -75,6 +161,14 @@ export default util.createRule<Options, MessageIds>({
}
}
},
});
};

function visitPattern(
node: TSESTree.Node,
cb: (node: TSESTree.Identifier) => void,
): void {
const visitor = new PatternVisitor({}, node, cb);
visitor.visit(node);
}
},
});

0 comments on commit d7a35dd

Please sign in to comment.