diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index f269414cdd0..d59b2a507ed 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -186,6 +186,7 @@ Then you should add `airbnb` (or `airbnb-base`) to your `extends` section of `.e | [`@typescript-eslint/no-untyped-public-signature`](./docs/rules/no-untyped-public-signature.md) | Requires that all public method arguments and return type will be explicitly typed | | | | | [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | | [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | +| [`@typescript-eslint/no-unused-vars-experimental`](./docs/rules/no-unused-vars-experimental.md) | Disallow unused variables and arguments. | | | :thought_balloon: | | [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | :heavy_check_mark: | | | | [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | | [`@typescript-eslint/no-var-requires`](./docs/rules/no-var-requires.md) | Disallows the use of require statements except in import statements | :heavy_check_mark: | | | diff --git a/packages/eslint-plugin/docs/rules/no-unused-vars-experimental.md b/packages/eslint-plugin/docs/rules/no-unused-vars-experimental.md new file mode 100644 index 00000000000..7ee9a6d8432 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unused-vars-experimental.md @@ -0,0 +1,115 @@ +# Disallow unused variables and arguments (no-unused-vars-experimental) + +Variables that are declared and not used anywhere in the code are most likely an error due to incomplete refactoring. Such variables take up space in the code and can lead to confusion by readers. + +## Rule Details + +This rule leverages the TypeScript compiler's unused variable checks to report. This means that with all rule options set to `false`, it should report the same errors as if you used both the `noUnusedLocals` and `noUnusedParameters` compiler options. + +This rule is vastly different to, and maintains no compatability with the base eslint version of the rule. + +### Limitations + +There are two limitations to this rule when compared with eslint's `no-unused-vars` rule, which are imposed by the fact that it directly uses TypeScript's implementation. + +1. This rule only works on files that TypeScript deems is a module (i.e. it has an `import` or an `export` statement). +2. The rule is significantly less configurable, as it cannot deviate too far from the base implementation. + +## Supported Nodes + +This rule supports checks on the following features: + +- Declarations: + - `var` / `const` / `let` + - `function` + - `class` + - `enum` + - `interface` + - `type` +- Class methods +- Class properties and parameter properties +- Function parameters +- Generic type parameters +- Import statements + +## Options + +```ts +type Options = { + ignoredNamesRegex?: string | boolean; + ignoreArgsIfArgsAfterAreUsed?: boolean; +}; + +const defaultOptions: Options = { + ignoredNamesRegex: '^_', + ignoreArgsIfArgsAfterAreUsed: false, +}; +``` + +### ignoredNamesRegex + +This option accepts a regex string to match names against. +Any matched names will be ignored and have no errors reported. +If you set it to false, it will never ignore any names. + +The default value is `'^_'` (i.e. matches any name prefixed with an underscore). + +Examples of valid code with `{ variables: { ignoredNamesRegex: '^_' } }`. + +```ts +const _unusedVar = 'unused'; +class _Unused { + private _unused = 1; + private _unusedMethod() {} +} +function _unusedFunction() {} +enum _UnusedEnum { + a = 1, +} +interface _UnusedInterface {} +type _UnusedType = {}; +``` + +**_NOTE:_** The TypeScript compiler automatically ignores imports, function arguments, type parameter declarations, and object destructuring variables prefixed with an underscore. +As this is hard-coded into the compiler, we cannot change this. + +Examples of valid code based on the unchangable compiler settings + +```ts +import _UnusedDefault, { _UnusedNamed } from 'foo'; +export function foo(_unusedProp: string) {} +export class Foo<_UnusedGeneric> {} +const { prop: _unusedDesctructure } = foo; +``` + +## ignoreArgsIfArgsAfterAreUsed + +When true, this option will ignore unused function arguments if the arguments proceeding arguments are used. + +Examples of invalid code with `{ ignoreArgsIfArgsAfterAreUsed: false }` + +```ts +function foo(unused: string, used: number) { + console.log(used); +} + +class Foo { + constructor(unused: string, public used: number) { + console.log(used); + } +} +``` + +Examples of valid code with `{ ignoreArgsIfArgsAfterAreUsed: true }` + +```ts +function foo(unused: string, used: number) { + console.log(used); +} + +class Foo { + constructor(unused: string, public used: number) { + console.log(used); + } +} +``` diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 847b6ea4a81..ee8f64f84f5 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -31,8 +31,8 @@ "@typescript-eslint/no-empty-function": "error", "@typescript-eslint/no-empty-interface": "error", "@typescript-eslint/no-explicit-any": "error", - "no-extra-parens": "off", "@typescript-eslint/no-extra-non-null-assertion": "error", + "no-extra-parens": "off", "@typescript-eslint/no-extra-parens": "error", "@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-floating-promises": "error", @@ -57,6 +57,7 @@ "@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", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 82b6e4a0906..8991371f1a3 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -41,6 +41,7 @@ import noUnnecessaryCondition from './no-unnecessary-condition'; import noUnnecessaryQualifier from './no-unnecessary-qualifier'; import noUnnecessaryTypeAssertion from './no-unnecessary-type-assertion'; import noUnusedVars from './no-unused-vars'; +import noUnusedVarsExperimental from './no-unused-vars-experimental'; import noUntypedPublicSignature from './no-untyped-public-signature'; import noUnusedExpressions from './no-unused-expressions'; import noUseBeforeDefine from './no-use-before-define'; @@ -116,6 +117,7 @@ export default { 'no-unnecessary-type-assertion': noUnnecessaryTypeAssertion, 'no-untyped-public-signature': noUntypedPublicSignature, 'no-unused-vars': noUnusedVars, + 'no-unused-vars-experimental': noUnusedVarsExperimental, 'no-unused-expressions': noUnusedExpressions, 'no-use-before-define': noUseBeforeDefine, 'no-useless-constructor': noUselessConstructor, diff --git a/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts new file mode 100644 index 00000000000..1286982895e --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unused-vars-experimental.ts @@ -0,0 +1,364 @@ +/* eslint-disable no-fallthrough */ + +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import ts from 'typescript'; +import * as util from '../util'; + +export type Options = [ + { + ignoredNamesRegex?: string | boolean; + ignoreArgsIfArgsAfterAreUsed?: boolean; + }, +]; +export type MessageIds = + | 'unused' + | 'unusedWithIgnorePattern' + | 'unusedImport' + | 'unusedTypeParameters'; + +type NodeWithTypeParams = ts.Node & { + typeParameters: ts.NodeArray; +}; + +export const DEFAULT_IGNORED_REGEX_STRING = '^_'; +export default util.createRule({ + name: 'no-unused-vars-experimental', + meta: { + type: 'problem', + docs: { + description: 'Disallow unused variables and arguments.', + category: 'Best Practices', + recommended: false, + requiresTypeChecking: true, + }, + schema: [ + { + type: 'object', + properties: { + ignoredNamesRegex: { + oneOf: [ + { + type: 'string', + }, + { + type: 'boolean', + enum: [false], + }, + ], + }, + ignoreArgsIfArgsAfterAreUsed: { + type: 'boolean', + }, + }, + additionalProperties: false, + }, + ], + messages: { + unused: "{{type}} '{{name}}' is declared but its value is never read.", + unusedWithIgnorePattern: + "{{type}} '{{name}}' is declared but its value is never read. Allowed unused names must match {{pattern}}", + unusedImport: 'All imports in import declaration are unused.', + unusedTypeParameters: 'All type parameters are unused.', + }, + }, + defaultOptions: [ + { + ignoredNamesRegex: DEFAULT_IGNORED_REGEX_STRING, + ignoreArgsIfArgsAfterAreUsed: false, + }, + ], + create(context, [userOptions]) { + const parserServices = util.getParserServices(context); + const tsProgram = parserServices.program; + const afterAllDiagnosticsCallbacks: (() => void)[] = []; + + const options = { + ignoredNames: + userOptions && typeof userOptions.ignoredNamesRegex === 'string' + ? new RegExp(userOptions.ignoredNamesRegex) + : null, + ignoreArgsIfArgsAfterAreUsed: + userOptions.ignoreArgsIfArgsAfterAreUsed || false, + }; + + function handleIdentifier(identifier: ts.Identifier): void { + function report(type: string): void { + const node = parserServices.tsNodeToESTreeNodeMap.get(identifier); + const regex = options.ignoredNames; + const name = identifier.getText(); + if (regex) { + if (!regex.test(name)) { + context.report({ + node, + messageId: 'unusedWithIgnorePattern', + data: { + name, + type, + pattern: regex.toString(), + }, + }); + } + } else { + context.report({ + node, + messageId: 'unused', + data: { + name, + type, + }, + }); + } + } + + const parent = identifier.parent; + + // is a single variable diagnostic + switch (parent.kind) { + case ts.SyntaxKind.BindingElement: + case ts.SyntaxKind.ObjectBindingPattern: + report('Destructured Variable'); + break; + + case ts.SyntaxKind.ClassDeclaration: + report('Class'); + break; + + case ts.SyntaxKind.EnumDeclaration: + report('Enum'); + break; + + case ts.SyntaxKind.FunctionDeclaration: + report('Function'); + break; + + // this won't happen because there are specific nodes that wrap up named/default import identifiers + // case ts.SyntaxKind.ImportDeclaration: + // import equals is always treated as a variable + case ts.SyntaxKind.ImportEqualsDeclaration: + // the default import is NOT used, but a named import is used + case ts.SyntaxKind.ImportClause: + // a named import is NOT used, but either another named import, or the default import is used + case ts.SyntaxKind.ImportSpecifier: + // a namespace import is NOT used, but the default import is used + case ts.SyntaxKind.NamespaceImport: + report('Import'); + break; + + case ts.SyntaxKind.InterfaceDeclaration: + report('Interface'); + break; + + case ts.SyntaxKind.MethodDeclaration: + report('Method'); + break; + + case ts.SyntaxKind.Parameter: + handleParameterDeclaration( + identifier, + parent as ts.ParameterDeclaration, + ); + break; + + case ts.SyntaxKind.PropertyDeclaration: + report('Property'); + break; + + case ts.SyntaxKind.TypeAliasDeclaration: + report('Type'); + break; + + case ts.SyntaxKind.TypeParameter: + handleTypeParam(identifier); + break; + + case ts.SyntaxKind.VariableDeclaration: + report('Variable'); + break; + + default: + throw new Error(`Unknown node with kind ${parent.kind}.`); + // TODO - should we just handle this gracefully? + // report('Unknown Node'); + // break; + } + } + + const unusedParameters = new Set(); + function handleParameterDeclaration( + identifier: ts.Identifier, + parent: ts.ParameterDeclaration, + ): void { + const name = identifier.getText(); + // regardless of if the paramter is ignored, track that it had a diagnostic fired on it + unusedParameters.add(identifier); + + /* + NOTE - Typescript will automatically ignore parameters that have a + leading underscore in their name. We cannot do anything about this. + */ + + function report(): void { + const node = parserServices.tsNodeToESTreeNodeMap.get(identifier); + context.report({ + node, + messageId: 'unused', + data: { + name, + type: 'Parameter', + }, + }); + } + + const isLastParameter = + parent.parent.parameters.indexOf(parent) === + parent.parent.parameters.length - 1; + if (!isLastParameter && options.ignoreArgsIfArgsAfterAreUsed) { + // once all diagnostics are processed, we can check if the following args are unused + afterAllDiagnosticsCallbacks.push(() => { + for (const param of parent.parent.parameters) { + if (!unusedParameters.has(param.name)) { + return; + } + } + + // none of the following params were unused, so report + report(); + }); + } else { + report(); + } + } + + function handleImportDeclaration(parent: ts.ImportDeclaration): void { + // the entire import statement is unused + + /* + NOTE - Typescript will automatically ignore imports that have a + leading underscore in their name. We cannot do anything about this. + */ + + context.report({ + messageId: 'unusedImport', + node: parserServices.tsNodeToESTreeNodeMap.get(parent), + }); + } + + function handleDestructure(parent: ts.BindingPattern): void { + // the entire desctructure is unused + // note that this case only ever triggers for simple, single-level destructured objects + // i.e. these will not trigger it: + // - const {a:_a, b, c: {d}} = z; + // - const [a, b] = c; + + parent.elements.forEach(element => { + if (element.kind === ts.SyntaxKind.BindingElement) { + const name = element.name; + if (name.kind === ts.SyntaxKind.Identifier) { + handleIdentifier(name); + } + } + }); + } + + function handleTypeParamList(node: NodeWithTypeParams): void { + // the entire generic decl list is unused + + /* + NOTE - Typescript will automatically ignore generics that have a + leading underscore in their name. We cannot do anything about this. + */ + + const parent = parserServices.tsNodeToESTreeNodeMap.get( + node as never, + ) as { + typeParameters: TSESTree.TSTypeParameterDeclaration; + }; + context.report({ + messageId: 'unusedTypeParameters', + node: parent.typeParameters, + }); + } + function handleTypeParam(identifier: ts.Identifier): void { + context.report({ + node: parserServices.tsNodeToESTreeNodeMap.get(identifier), + messageId: 'unused', + data: { + name: identifier.getText(), + type: 'Type Parameter', + }, + }); + } + + return { + 'Program:exit'(program: TSESTree.Node): void { + const tsNode = parserServices.esTreeNodeToTSNodeMap.get(program); + const sourceFile = util.getSourceFileOfNode(tsNode); + const diagnostics = tsProgram.getSemanticDiagnostics(sourceFile); + + diagnostics.forEach(diag => { + if (isUnusedDiagnostic(diag.code)) { + if (diag.start !== undefined) { + const node = util.getTokenAtPosition(sourceFile, diag.start); + const parent = node.parent; + if (isIdentifier(node)) { + handleIdentifier(node); + } else if (isImport(parent)) { + handleImportDeclaration(parent); + } else if (isDestructure(parent)) { + handleDestructure(parent); + } else if (isGeneric(node, parent)) { + handleTypeParamList(parent); + } + } + } + }); + + // trigger all the checks to be done after all the diagnostics have been evaluated + afterAllDiagnosticsCallbacks.forEach(cb => cb()); + }, + }; + }, +}); + +/** + * Checks if the diagnostic code is one of the expected "unused var" codes + */ +function isUnusedDiagnostic(code: number): boolean { + return [ + 6133, // '{0}' is declared but never used. + 6138, // Property '{0}' is declared but its value is never read. + 6192, // All imports in import declaration are unused. + 6196, // '{0}' is declared but its value is never read. + 6198, // All destructured elements are unused. + 6199, // All variables are unused. + 6205, // All type parameters are unused. + ].includes(code); +} + +/** + * Checks if the given node is a destructuring pattern + */ +function isDestructure(node: ts.Node): node is ts.BindingPattern { + return ( + node.kind === ts.SyntaxKind.ObjectBindingPattern || + node.kind === ts.SyntaxKind.ArrayBindingPattern + ); +} + +function isImport(node: ts.Node): node is ts.ImportDeclaration { + return node.kind === ts.SyntaxKind.ImportDeclaration; +} + +function isIdentifier(node: ts.Node): node is ts.Identifier { + return node.kind === ts.SyntaxKind.Identifier; +} + +function isGeneric( + node: ts.Node, + parent: ts.Node & Partial, +): parent is NodeWithTypeParams { + return ( + node.kind === ts.SyntaxKind.LessThanToken && + parent.typeParameters !== undefined + ); +} diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index 72213da6ebb..2d0abcf963e 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -215,3 +215,42 @@ export function typeIsOrHasBaseType( return false; } + +/** + * Gets the source file for a given node + */ +export function getSourceFileOfNode(node: ts.Node): ts.SourceFile { + while (node && node.kind !== ts.SyntaxKind.SourceFile) { + node = node.parent; + } + return node as ts.SourceFile; +} + +export function getTokenAtPosition( + sourceFile: ts.SourceFile, + position: number, +): ts.Node { + const queue: ts.Node[] = [sourceFile]; + let current: ts.Node; + while (queue.length > 0) { + current = queue.shift()!; + // find the child that contains 'position' + for (const child of current.getChildren(sourceFile)) { + const start = child.getFullStart(); + if (start > position) { + // If this child begins after position, then all subsequent children will as well. + return current; + } + + const end = child.getEnd(); + if ( + position < end || + (position === end && child.kind === ts.SyntaxKind.EndOfFileToken) + ) { + queue.push(child); + break; + } + } + } + return current!; +} diff --git a/packages/eslint-plugin/tests/fixtures/tsconfig.json b/packages/eslint-plugin/tests/fixtures/tsconfig.json index bedda0e3dec..92694993c53 100644 --- a/packages/eslint-plugin/tests/fixtures/tsconfig.json +++ b/packages/eslint-plugin/tests/fixtures/tsconfig.json @@ -4,6 +4,7 @@ "module": "commonjs", "strict": true, "esModuleInterop": true, - "lib": ["es2015", "es2017", "esnext"] + "lib": ["es2015", "es2017", "esnext"], + "experimentalDecorators": true } } diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts new file mode 100644 index 00000000000..6cb2a24e5a3 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-vars-experimental.test.ts @@ -0,0 +1,1361 @@ +import { + InvalidTestCase, + ValidTestCase, +} from '@typescript-eslint/experimental-utils/dist/ts-eslint'; +import rule, { + DEFAULT_IGNORED_REGEX_STRING, + Options, + MessageIds, +} from '../../src/rules/no-unused-vars-experimental'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2018, + sourceType: 'module', + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }, + parser: '@typescript-eslint/parser', +}); + +const hasExport = /^export/m; +// const hasImport = /^import .+? from ['"]/m; +function makeExternalModule< + T extends ValidTestCase | InvalidTestCase +>(tests: T[]): T[] { + tests.forEach(t => { + if (!hasExport.test(t.code)) { + t.code = `${t.code}\nexport const __externalModule = 1;`; + } + }); + return tests; +} + +const DEFAULT_IGNORED_REGEX = new RegExp( + DEFAULT_IGNORED_REGEX_STRING, +).toString(); +ruleTester.run('no-unused-vars-experimental', rule, { + valid: makeExternalModule([ + /////////////////////// + // #region variables // + /////////////////////// + { code: 'const _x = "unused"' }, + { code: 'export const x = "used";' }, + { + code: ` +const x = "used"; +console.log(x); + `, + }, + { + code: ` +function foo() {} +foo(); + `, + }, + { code: 'function _foo() {}' }, + { + // decorators require the tsconfig compiler option + // or else they are marked as unused because it is not a valid usage + code: ` +function decorator(_clazz: any) {} + +@decorator +export class Foo {} + `, + parser: require.resolve('@typescript-eslint/parser'), + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: rootDir, + }, + }, + { + code: ` +type Foo = { a?: string }; +export const foo: Foo = {}; + `, + }, + { + code: ` +interface Foo { a?: string }; +export const foo: Foo = {}; + `, + }, + { code: 'type _Foo = { a?: string };' }, + { code: 'interface _Foo { a?: string };' }, + { + code: ` +class Foo {} +new Foo(); + `, + }, + { code: 'class _Foo {}' }, + { + code: ` +export class Foo { + private foo: string; + bar() { + console.log(this.foo); + } +} + `, + }, + { + code: ` +export class Foo { + private _foo: string; +} + `, + }, + { + code: ` +export class Foo { + private foo() {}; + bar() { + this.foo(); + } +} + `, + }, + { + code: ` +export class Foo { + private _foo() {}; +} + `, + }, + { + code: ` +enum Foo { a = 1 } +console.log(Foo.a); + `, + }, + { code: 'enum _Foo { a = 1 }' }, + { code: 'export const {a, b} = c;' }, + { + code: ` +const {a, b: {c}} = d; +console.log(a, c); + `, + }, + { + code: ` +const {a, b} = c; +console.log(a, b); + `, + }, + { + code: ` +const {a: _a, b} = c; +console.log(b); + `, + }, + { code: `const {a: _a, b: _b} = c;` }, + { code: 'export const [a, b] = c;' }, + { + code: ` +const [a, b] = c; +console.log(a, b); + `, + }, + { + code: ` +const [a, [b]] = c; +console.log(a, b); + `, + }, + { + code: ` +const [_a, b] = c; +console.log(b); + `, + }, + { code: `const [_a, _b] = c;` }, + // #endregion variables // + ////////////////////////// + + //////////////////////// + // #region parameters // + //////////////////////// + { + code: ` +export function foo(a) { + console.log(a); +} + `, + }, + { + code: ` +export function foo(a: string, b: string) { + console.log(b); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(a: string, public b: string) {} +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(private a: string, public b: string) {} +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(private a: string) {} + foo() { console.log(this.a) } +} + `, + }, + { code: 'export function foo({a: _a}) {}' }, + { code: 'export function foo({a: { b: _b }}) {}' }, + { code: 'export function foo([_a]) {}' }, + { code: 'export function foo([[_a]]) {}' }, + { + code: ` +export function foo({a: _a}, used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export function foo({a: { b: _b }}, used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export function foo([_a], used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + { + code: ` +export function foo([[_a]], used) { + console.log(used); +} + `, + options: [ + { + ignoreArgsIfArgsAfterAreUsed: true, + }, + ], + }, + // #endregion parameters // + /////////////////////////// + + //////////////////// + // #region import // + //////////////////// + { + code: ` +import defaultImp from "thing"; +console.log(defaultImp); + `, + }, + { + code: ` +import { named } from "thing"; +console.log(named); + `, + }, + { + code: ` +import defaultImp, { named } from "thing"; +console.log(defaultImp, named); + `, + }, + { + code: ` +import defaultImp = require("thing"); +console.log(defaultImp, named); + `, + }, + { + code: ` +import * as namespace from "thing"; +console.log(namespace); + `, + }, + { + code: ` +import defaultImp, * as namespace from "thing"; +console.log(defaultImp, namespace); + `, + }, + { code: 'import _defaultImp from "thing";' }, + { code: 'import { named as _named } from "thing";' }, + { code: 'import _defaultImp, { named as _named } from "thing";' }, + { code: 'import _defaultImp = require("thing");' }, + { code: 'import * as _namespace from "thing";' }, + { code: 'import _defaultImp, * as _namespace from "thing";' }, + // #endregion import // + /////////////////////// + + ////////////////////// + // #region generics // + ////////////////////// + { code: 'export function foo(): T {}' }, + { code: 'export function foo(): T & T2 {}' }, + { code: 'export function foo(): T {}' }, + { + code: ` +export class foo { + prop: T +} + `, + }, + { + code: ` +export class foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export class foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export interface foo { + prop: T +} + `, + }, + { + code: ` +export interface foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export interface foo { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export type foo = { + prop: T + +} + `, + }, + { + code: ` +export type foo = { + prop: T + prop2: T2 +} + `, + }, + { + code: ` +export type foo = { + prop: T + prop2: T2 +} + `, + }, + // #endregion generics // + ///////////////////////// + ]), + invalid: makeExternalModule([ + /////////////////////// + // #region variables // + /////////////////////// + { + code: 'const x = "unused"', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'x', + type: 'Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 7, + endColumn: 8, + }, + ], + }, + { + code: 'const x: string = "unused"', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'x', + type: 'Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 7, + endColumn: 16, + }, + ], + }, + { + code: 'const x = "unused"', + options: [ + { + ignoredNamesRegex: false, + }, + ], + errors: [ + { + messageId: 'unused', + data: { + name: 'x', + type: 'Variable', + }, + line: 1, + column: 7, + endColumn: 8, + }, + ], + }, + { + code: 'function foo() {}', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Function', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 10, + endColumn: 13, + }, + ], + }, + { + code: 'type Foo = { a?: string };', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Type', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 6, + endColumn: 9, + }, + ], + }, + { + code: 'interface Foo { a?: string };', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Interface', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 11, + endColumn: 14, + }, + ], + }, + { + code: 'class Foo {}', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Class', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 7, + endColumn: 10, + }, + ], + }, + { + code: ` +export class Foo { + private foo: string; +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Property', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 3, + column: 11, + endColumn: 14, + }, + ], + }, + { + code: ` +export class Foo { + private foo() {}; +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Method', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 3, + column: 11, + endColumn: 14, + }, + ], + }, + { + code: 'enum Foo { a = 1 }', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'Foo', + type: 'Enum', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 6, + endColumn: 9, + }, + ], + }, + { + code: ` +const {foo, bar} = baz; +console.log(foo); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: ` +const [foo, bar] = baz; +console.log(foo); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: 'const {foo, bar} = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: 'const {foo, bar: _bar} = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + ], + }, + { + code: 'const [foo, bar] = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 13, + endColumn: 16, + }, + ], + }, + { + code: 'const [foo, _bar] = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + ], + }, + { + code: 'const [foo, [bar]] = baz;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'bar', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 14, + endColumn: 17, + }, + ], + }, + { + code: 'const {foo, bar: {baz}} = bam;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'baz', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 19, + endColumn: 22, + }, + ], + }, + { + code: 'const {foo, bar: [baz]} = bam;', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'baz', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 19, + endColumn: 22, + }, + ], + }, + // #endregion variables // + ////////////////////////// + + //////////////////////// + // #region parameters // + //////////////////////// + { + code: ` +export function foo(a, b) { + console.log(b); +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 2, + column: 21, + endColumn: 22, + }, + ], + }, + { + code: ` +export function foo(a: string, b: string) { + console.log(b); +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 2, + column: 21, + endColumn: 30, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(a: string, b: string) { + console.log(b); + } +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 3, + column: 15, + endColumn: 24, + }, + ], + }, + { + code: ` +export class Clazz { + constructor(a: string, public b: string) {} +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'a', + type: 'Parameter', + }, + line: 3, + column: 15, + endColumn: 24, + }, + ], + }, + { + code: ` +export function foo({a}, used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'a', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: ` +export function foo({a: {b}}, used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'b', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 26, + endColumn: 27, + }, + ], + }, + { + code: ` +export function foo([a], used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'a', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: ` +export function foo([[a]], used) { + console.log(used); +} + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'a', + type: 'Destructured Variable', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 23, + endColumn: 24, + }, + ], + }, + // #endregion parameters // + /////////////////////////// + + //////////////////// + // #region import // + //////////////////// + { + code: 'import foo = require("test")', + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'foo', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 1, + column: 8, + endColumn: 11, + }, + ], + }, + { + code: 'import defaultImp from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 32, + }, + ], + }, + { + code: 'import { named } from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 31, + }, + ], + }, + { + code: 'import * as namespace from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 36, + }, + ], + }, + { + code: 'import defaultImp, { named } from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 43, + }, + ], + }, + { + code: 'import defaultImp, * as namespace from "thing";', + errors: [ + { + messageId: 'unusedImport', + line: 1, + column: 1, + endColumn: 48, + }, + ], + }, + { + code: ` +import defaultImp, { named } from "thing"; +console.log(named); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'defaultImp', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 8, + endColumn: 18, + }, + ], + }, + { + code: ` +import defaultImp, * as named from "thing"; +console.log(named); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'defaultImp', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 8, + endColumn: 18, + }, + ], + }, + { + code: ` +import defaultImp, * as named from "thing"; +console.log(defaultImp); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'named', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 25, + endColumn: 30, + }, + ], + }, + { + code: ` +import defaultImp, { named } from "thing"; +console.log(defaultImp); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'named', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 22, + endColumn: 27, + }, + ], + }, + { + code: ` +import { named1, named2 } from "thing"; +console.log(named1); + `, + errors: [ + { + messageId: 'unusedWithIgnorePattern', + data: { + name: 'named2', + type: 'Import', + pattern: DEFAULT_IGNORED_REGEX, + }, + line: 2, + column: 18, + endColumn: 24, + }, + ], + }, + // #endregion import // + /////////////////////// + + ////////////////////// + // #region generics // + ////////////////////// + { + code: 'export function foo() {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 20, + endColumn: 23, + }, + ], + }, + { + code: 'export function foo() {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 20, + endColumn: 27, + }, + ], + }, + { + code: 'export function foo(): T2 {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 21, + endColumn: 22, + }, + ], + }, + { + code: 'export function foo() {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 21, + endColumn: 22, + }, + ], + }, + { + code: 'export class foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 17, + endColumn: 20, + }, + ], + }, + { + code: 'export class foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 17, + endColumn: 24, + }, + ], + }, + { + code: ` +export class foo { + prop: T2 +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 2, + column: 18, + endColumn: 19, + }, + ], + }, + { + code: 'export class foo {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 18, + endColumn: 19, + }, + ], + }, + { + code: 'export interface foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 21, + endColumn: 24, + }, + ], + }, + { + code: 'export interface foo {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 21, + endColumn: 28, + }, + ], + }, + { + code: ` +export interface foo { + prop: T2 +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 2, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: 'export interface foo {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 22, + endColumn: 23, + }, + ], + }, + { + code: 'export type foo = {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 16, + endColumn: 19, + }, + ], + }, + { + code: 'export type foo = {}', + errors: [ + { + messageId: 'unusedTypeParameters', + line: 1, + column: 16, + endColumn: 23, + }, + ], + }, + { + code: ` +export type foo = { + prop: T2 +} + `, + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 2, + column: 17, + endColumn: 18, + }, + ], + }, + { + code: 'export type foo = {}', + errors: [ + { + messageId: 'unused', + data: { + name: 'T', + type: 'Type Parameter', + }, + line: 1, + column: 17, + endColumn: 18, + }, + ], + }, + // #endregion generics // + ///////////////////////// + ]), +}); diff --git a/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts b/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts index ed6a8131edb..cd5828a8e36 100644 --- a/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts +++ b/packages/eslint-plugin/tools/validate-docs/validate-table-structure.ts @@ -37,8 +37,12 @@ function validateTableStructure( console.error( chalk.bold.red('✗'), chalk.bold('Sorting:'), - 'Incorrect line number for', + 'Incorrect row index for', chalk.bold(rowRuleName), + 'expected', + ruleIndex, + 'got', + rowIndex, ); hasErrors = true; return; diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index a19bef27413..1ba44449867 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -16,6 +16,8 @@ const DEFAULT_COMPILER_OPTIONS: ts.CompilerOptions = { checkJs: true, noEmit: true, // extendedDiagnostics: true, + noUnusedLocals: true, + noUnusedParameters: true, }; // This narrows the type so we can be sure we're passing canonical names in the correct places