From a8227a6185dd24de4bfc7d766931643871155021 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Mon, 23 Nov 2020 16:45:50 -0800 Subject: [PATCH 01/19] feat(eslint-plugin): [no-unused-vars] fork the base rule (#2768) Fixes #2782 Fixes #2714 Fixes #2648 --- .../eslint-plugin/src/rules/no-unused-vars.ts | 499 ++-- .../src/util/collectUnusedVariables.ts | 751 +++++ packages/eslint-plugin/src/util/index.ts | 1 + .../no-unused-vars-eslint.test.ts | 2457 +++++++++++++++++ .../no-unused-vars.test.ts | 68 +- .../eslint-plugin/typings/eslint-rules.d.ts | 14 + .../src/ast-utils/predicates.ts | 22 + .../src/eslint-utils/InferTypesFromRule.ts | 21 +- .../experimental-utils/src/ts-eslint/Rule.ts | 9 +- .../src/ts-eslint/RuleTester.ts | 16 +- .../experimental-utils/src/ts-eslint/Scope.ts | 75 +- .../src/referencer/VisitorBase.ts | 13 +- packages/types/src/ts-estree.ts | 6 + 13 files changed, 3719 insertions(+), 233 deletions(-) create mode 100644 packages/eslint-plugin/src/util/collectUnusedVariables.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts rename packages/eslint-plugin/tests/rules/{ => no-unused-vars}/no-unused-vars.test.ts (95%) diff --git a/packages/eslint-plugin/src/rules/no-unused-vars.ts b/packages/eslint-plugin/src/rules/no-unused-vars.ts index 8753452dd097..aac30b87a9e2 100644 --- a/packages/eslint-plugin/src/rules/no-unused-vars.ts +++ b/packages/eslint-plugin/src/rules/no-unused-vars.ts @@ -4,11 +4,33 @@ import { TSESTree, } from '@typescript-eslint/experimental-utils'; import { PatternVisitor } from '@typescript-eslint/scope-manager'; -import baseRule from 'eslint/lib/rules/no-unused-vars'; +import { getNameLocationInGlobalDirectiveComment } from 'eslint/lib/rules/utils/ast-utils'; import * as util from '../util'; -type MessageIds = util.InferMessageIdsTypeFromRule; -type Options = util.InferOptionsTypeFromRule; +export type MessageIds = 'unusedVar'; +export type Options = [ + | 'all' + | 'local' + | { + vars?: 'all' | 'local'; + varsIgnorePattern?: string; + args?: 'all' | 'after-used' | 'none'; + ignoreRestSiblings?: boolean; + argsIgnorePattern?: string; + caughtErrors?: 'all' | 'none'; + caughtErrorsIgnorePattern?: string; + }, +]; + +interface TranslatedOptions { + vars: 'all' | 'local'; + varsIgnorePattern?: RegExp; + args: 'all' | 'after-used' | 'none'; + ignoreRestSiblings: boolean; + argsIgnorePattern?: RegExp; + caughtErrors: 'all' | 'none'; + caughtErrorsIgnorePattern?: RegExp; +} export default util.createRule({ name: 'no-unused-vars', @@ -20,195 +42,211 @@ export default util.createRule({ recommended: 'warn', extendsBaseRule: true, }, - schema: baseRule.meta.schema, - messages: baseRule.meta.messages ?? { + schema: [ + { + oneOf: [ + { + enum: ['all', 'local'], + }, + { + type: 'object', + properties: { + vars: { + enum: ['all', 'local'], + }, + varsIgnorePattern: { + type: 'string', + }, + args: { + enum: ['all', 'after-used', 'none'], + }, + ignoreRestSiblings: { + type: 'boolean', + }, + argsIgnorePattern: { + type: 'string', + }, + caughtErrors: { + enum: ['all', 'none'], + }, + caughtErrorsIgnorePattern: { + type: 'string', + }, + }, + additionalProperties: false, + }, + ], + }, + ], + messages: { unusedVar: "'{{varName}}' is {{action}} but never used{{additional}}.", }, }, defaultOptions: [{}], create(context) { - const rules = baseRule.create(context); const filename = context.getFilename(); + const sourceCode = context.getSourceCode(); const MODULE_DECL_CACHE = new Map(); - /** - * Gets a list of TS module definitions for a specified variable. - * @param variable eslint-scope variable object. - */ - function getModuleNameDeclarations( - variable: TSESLint.Scope.Variable, - ): TSESTree.TSModuleDeclaration[] { - const moduleDeclarations: TSESTree.TSModuleDeclaration[] = []; - - variable.defs.forEach(def => { - if (def.type === 'TSModuleName') { - moduleDeclarations.push(def.node); - } - }); - - return moduleDeclarations; - } + const options = ((): TranslatedOptions => { + const options: TranslatedOptions = { + vars: 'all', + args: 'after-used', + ignoreRestSiblings: false, + caughtErrors: 'none', + }; + + const firstOption = context.options[0]; + + if (firstOption) { + if (typeof firstOption === 'string') { + options.vars = firstOption; + } else { + options.vars = firstOption.vars ?? options.vars; + options.args = firstOption.args ?? options.args; + options.ignoreRestSiblings = + firstOption.ignoreRestSiblings ?? options.ignoreRestSiblings; + options.caughtErrors = + firstOption.caughtErrors ?? options.caughtErrors; + + if (firstOption.varsIgnorePattern) { + options.varsIgnorePattern = new RegExp( + firstOption.varsIgnorePattern, + 'u', + ); + } - /** - * Determine if an identifier is referencing an enclosing name. - * This only applies to declarations that create their own scope (modules, functions, classes) - * @param ref The reference to check. - * @param nodes The candidate function nodes. - * @returns True if it's a self-reference, false if not. - */ - function isBlockSelfReference( - ref: TSESLint.Scope.Reference, - nodes: TSESTree.Node[], - ): boolean { - let scope: TSESLint.Scope.Scope | null = ref.from; + if (firstOption.argsIgnorePattern) { + options.argsIgnorePattern = new RegExp( + firstOption.argsIgnorePattern, + 'u', + ); + } - while (scope) { - if (nodes.indexOf(scope.block) >= 0) { - return true; + if (firstOption.caughtErrorsIgnorePattern) { + options.caughtErrorsIgnorePattern = new RegExp( + firstOption.caughtErrorsIgnorePattern, + 'u', + ); + } } - - scope = scope.upper; } + return options; + })(); + + function collectUnusedVariables(): TSESLint.Scope.Variable[] { + /** + * Determines if a variable has a sibling rest property + * @param variable eslint-scope variable object. + * @returns True if the variable is exported, false if not. + */ + function hasRestSpreadSibling( + variable: TSESLint.Scope.Variable, + ): boolean { + if (options.ignoreRestSiblings) { + return variable.defs.some(def => { + const propertyNode = def.name.parent!; + const patternNode = propertyNode.parent!; + + return ( + propertyNode.type === AST_NODE_TYPES.Property && + patternNode.type === AST_NODE_TYPES.ObjectPattern && + patternNode.properties[patternNode.properties.length - 1].type === + AST_NODE_TYPES.RestElement + ); + }); + } - return false; - } + return false; + } - function isExported( - variable: TSESLint.Scope.Variable, - target: AST_NODE_TYPES, - ): boolean { - // TS will require that all merged namespaces/interfaces are exported, so we only need to find one - return variable.defs.some( - def => - def.node.type === target && - (def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration || - def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration), - ); - } + /** + * Checks whether the given variable is after the last used parameter. + * @param variable The variable to check. + * @returns `true` if the variable is defined after the last used parameter. + */ + function isAfterLastUsedArg(variable: TSESLint.Scope.Variable): boolean { + const def = variable.defs[0]; + const params = context.getDeclaredVariables(def.node); + const posteriorParams = params.slice(params.indexOf(variable) + 1); + + // If any used parameters occur after this parameter, do not report. + return !posteriorParams.some( + v => v.references.length > 0 || v.eslintUsed, + ); + } - return { - ...rules, - 'TSCallSignatureDeclaration, TSConstructorType, TSConstructSignatureDeclaration, TSDeclareFunction, TSEmptyBodyFunctionExpression, TSFunctionType, TSMethodSignature'( - node: - | TSESTree.TSCallSignatureDeclaration - | 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); - }); + const unusedVariablesOriginal = util.collectUnusedVariables(context); + const unusedVariablesReturn: TSESLint.Scope.Variable[] = []; + for (const variable of unusedVariablesOriginal) { + // explicit global variables don't have definitions. + if (variable.defs.length === 0) { + unusedVariablesReturn.push(variable); + continue; } - }, - 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); + const def = variable.defs[0]; + + if ( + variable.scope.type === TSESLint.Scope.ScopeType.global && + options.vars === 'local' + ) { + // skip variables in the global scope if configured to + continue; } - }, - 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) { - const moduleNodes = getModuleNameDeclarations(variable); + // skip catch variables + if (def.type === TSESLint.Scope.DefinitionType.CatchClause) { + if (options.caughtErrors === 'none') { + continue; + } + // skip ignored parameters if ( - moduleNodes.length === 0 || - // ignore unreferenced module definitions, as the base rule will report on them - variable.references.length === 0 || - // ignore exported nodes - isExported(variable, AST_NODE_TYPES.TSModuleDeclaration) + 'name' in def.name && + options.caughtErrorsIgnorePattern?.test(def.name.name) ) { continue; } - - // check if the only reference to a module's name is a self-reference in its body - // this won't be caught by the base rule because it doesn't understand TS modules - const isOnlySelfReferenced = variable.references.every(ref => { - return isBlockSelfReference(ref, moduleNodes); - }); - - if (isOnlySelfReferenced) { - context.report({ - node: variable.identifiers[0], - messageId: 'unusedVar', - data: { - varName: variable.name, - action: 'defined', - additional: '', - }, - }); - } - } - }, - [[ - '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); - }, - ':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); - }, - 'TSInterfaceDeclaration, TSTypeAliasDeclaration'( - node: TSESTree.TSInterfaceDeclaration | TSESTree.TSTypeAliasDeclaration, - ): void { - const variable = context.getScope().set.get(node.id.name); - if (!variable) { - return; - } - if ( - variable.references.length === 0 || - // ignore exported nodes - isExported(variable, node.type) - ) { - return; } - // check if the type is only self-referenced - // this won't be caught by the base rule because it doesn't understand self-referencing types - const isOnlySelfReferenced = variable.references.every(ref => { + if (def.type === TSESLint.Scope.DefinitionType.Parameter) { + // if "args" option is "none", skip any parameter + if (options.args === 'none') { + continue; + } + // skip ignored parameters if ( - ref.identifier.range[0] >= node.range[0] && - ref.identifier.range[1] <= node.range[1] + 'name' in def.name && + options.argsIgnorePattern?.test(def.name.name) ) { - return true; + continue; + } + // if "args" option is "after-used", skip used variables + if ( + options.args === 'after-used' && + util.isFunction(def.name.parent) && + !isAfterLastUsedArg(variable) + ) { + continue; + } + } else { + // skip ignored variables + if ( + 'name' in def.name && + options.varsIgnorePattern?.test(def.name.name) + ) { + continue; } - return false; - }); - if (isOnlySelfReferenced) { - context.report({ - node: variable.identifiers[0], - messageId: 'unusedVar', - data: { - varName: variable.name, - action: 'defined', - additional: '', - }, - }); } - }, + if (!hasRestSpreadSibling(variable)) { + unusedVariablesReturn.push(variable); + } + } + + return unusedVariablesReturn; + } + + return { // declaration file handling [ambientDeclarationSelector(AST_NODE_TYPES.Program, true)]( node: DeclarationSelectorNode, @@ -219,11 +257,6 @@ export default util.createRule({ markDeclarationChildAsUsed(node); }, - // global augmentation can be in any file, and they do not need exports - 'TSModuleDeclaration[declare = true][global = true]'(): void { - context.markVariableAsUsed('global'); - }, - // children of a namespace that is a child of a declared namespace are auto-exported [ambientDeclarationSelector( 'TSModuleDeclaration[declare = true] > TSModuleBlock TSModuleDeclaration > TSModuleBlock', @@ -253,6 +286,111 @@ export default util.createRule({ markDeclarationChildAsUsed(node); }, + + // collect + 'Program:exit'(programNode): void { + /** + * Generates the message data about the variable being defined and unused, + * including the ignore pattern if configured. + * @param unusedVar eslint-scope variable object. + * @returns The message data to be used with this unused variable. + */ + function getDefinedMessageData( + unusedVar: TSESLint.Scope.Variable, + ): Record { + const defType = unusedVar?.defs[0]?.type; + let type; + let pattern; + + if ( + defType === TSESLint.Scope.DefinitionType.CatchClause && + options.caughtErrorsIgnorePattern + ) { + type = 'args'; + pattern = options.caughtErrorsIgnorePattern.toString(); + } else if ( + defType === TSESLint.Scope.DefinitionType.Parameter && + options.argsIgnorePattern + ) { + type = 'args'; + pattern = options.argsIgnorePattern.toString(); + } else if ( + defType !== TSESLint.Scope.DefinitionType.Parameter && + options.varsIgnorePattern + ) { + type = 'vars'; + pattern = options.varsIgnorePattern.toString(); + } + + const additional = type + ? `. Allowed unused ${type} must match ${pattern}` + : ''; + + return { + varName: unusedVar.name, + action: 'defined', + additional, + }; + } + + /** + * Generate the warning message about the variable being + * assigned and unused, including the ignore pattern if configured. + * @param unusedVar eslint-scope variable object. + * @returns The message data to be used with this unused variable. + */ + function getAssignedMessageData( + unusedVar: TSESLint.Scope.Variable, + ): Record { + const additional = options.varsIgnorePattern + ? `. Allowed unused vars must match ${options.varsIgnorePattern.toString()}` + : ''; + + return { + varName: unusedVar.name, + action: 'assigned a value', + additional, + }; + } + + const unusedVars = collectUnusedVariables(); + + for (let i = 0, l = unusedVars.length; i < l; ++i) { + const unusedVar = unusedVars[i]; + + // Report the first declaration. + if (unusedVar.defs.length > 0) { + context.report({ + node: unusedVar.references.length + ? unusedVar.references[unusedVar.references.length - 1] + .identifier + : unusedVar.identifiers[0], + messageId: 'unusedVar', + data: unusedVar.references.some(ref => ref.isWrite()) + ? getAssignedMessageData(unusedVar) + : getDefinedMessageData(unusedVar), + }); + + // If there are no regular declaration, report the first `/*globals*/` comment directive. + } else if ( + 'eslintExplicitGlobalComments' in unusedVar && + unusedVar.eslintExplicitGlobalComments + ) { + const directiveComment = unusedVar.eslintExplicitGlobalComments[0]; + + context.report({ + node: programNode, + loc: getNameLocationInGlobalDirectiveComment( + sourceCode, + directiveComment, + unusedVar.name, + ), + messageId: 'unusedVar', + data: getDefinedMessageData(unusedVar), + }); + } + } + }, }; function checkModuleDeclForExportEquals( @@ -391,6 +529,31 @@ function bar( // bar should be unused _arg: typeof bar ) {} + +--- if an interface is merged into a namespace --- +--- NOTE - TS gets these cases wrong + +namespace Test { + interface Foo { // Foo should be unused here + a: string; + } + export namespace Foo { + export type T = 'b'; + } +} +type T = Test.Foo; // Error: Namespace 'Test' has no exported member 'Foo'. + + +namespace Test { + export interface Foo { + a: string; + } + namespace Foo { // Foo should be unused here + export type T = 'b'; + } +} +type T = Test.Foo.T; // Error: Namespace 'Test' has no exported member 'Foo'. + */ /* diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts new file mode 100644 index 000000000000..bd8ed859b6d5 --- /dev/null +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -0,0 +1,751 @@ +import { + AST_NODE_TYPES, + TSESLint, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import { ImplicitLibVariable } from '@typescript-eslint/scope-manager'; +import { Visitor } from '@typescript-eslint/scope-manager/dist/referencer/Visitor'; +import * as util from '.'; + +class UnusedVarsVisitor< + TMessageIds extends string, + TOptions extends readonly unknown[] +> extends Visitor { + private static readonly RESULTS_CACHE = new WeakMap< + TSESTree.Program, + ReadonlySet + >(); + + readonly #scopeManager: TSESLint.Scope.ScopeManager; + // readonly #unusedVariables = new Set(); + + private constructor(context: TSESLint.RuleContext) { + super({ + visitChildrenEvenIfSelectorExists: true, + }); + + this.#scopeManager = util.nullThrows( + context.getSourceCode().scopeManager, + 'Missing required scope manager', + ); + } + + public static collectUnusedVariables< + TMessageIds extends string, + TOptions extends readonly unknown[] + >( + context: TSESLint.RuleContext, + ): ReadonlySet { + const program = context.getSourceCode().ast; + const cached = this.RESULTS_CACHE.get(program); + if (cached) { + return cached; + } + + const visitor = new this(context); + visitor.visit(program); + + const unusedVars = visitor.collectUnusedVariables( + visitor.getScope(program), + ); + this.RESULTS_CACHE.set(program, unusedVars); + return unusedVars; + } + + private collectUnusedVariables( + scope: TSESLint.Scope.Scope, + unusedVariables = new Set(), + ): ReadonlySet { + for (const variable of scope.variables) { + if ( + // skip function expression names, + scope.functionExpressionScope || + // variables marked with markVariableAsUsed(), + variable.eslintUsed || + // implicit lib variables (from @typescript-eslint/scope-manager), + variable instanceof ImplicitLibVariable || + // basic exported variables + isExported(variable) || + // variables implicitly exported via a merged declaration + isMergableExported(variable) || + // used variables + isUsedVariable(variable) + ) { + continue; + } + + unusedVariables.add(variable); + } + + for (const childScope of scope.childScopes) { + this.collectUnusedVariables(childScope, unusedVariables); + } + + return unusedVariables; + } + + //#region HELPERS + + private getScope( + currentNode: TSESTree.Node, + ): T { + // On Program node, get the outermost scope to avoid return Node.js special function scope or ES modules scope. + const inner = currentNode.type !== AST_NODE_TYPES.Program; + + let node: TSESTree.Node | undefined = currentNode; + while (node) { + const scope = this.#scopeManager.acquire(node, inner); + + if (scope) { + if (scope.type === 'function-expression-name') { + return scope.childScopes[0] as T; + } + return scope as T; + } + + node = node.parent; + } + + return this.#scopeManager.scopes[0] as T; + } + + private markVariableAsUsed( + variableOrIdentifier: TSESLint.Scope.Variable | TSESTree.Identifier, + ): void; + private markVariableAsUsed(name: string, parent: TSESTree.Node): void; + private markVariableAsUsed( + variableOrIdentifierOrName: + | TSESLint.Scope.Variable + | TSESTree.Identifier + | string, + parent?: TSESTree.Node, + ): void { + if ( + typeof variableOrIdentifierOrName !== 'string' && + !('type' in variableOrIdentifierOrName) + ) { + variableOrIdentifierOrName.eslintUsed = true; + return; + } + + let name: string; + let node: TSESTree.Node; + if (typeof variableOrIdentifierOrName === 'string') { + name = variableOrIdentifierOrName; + node = parent!; + } else { + name = variableOrIdentifierOrName.name; + node = variableOrIdentifierOrName; + } + + let currentScope: TSESLint.Scope.Scope | null = this.getScope(node); + while (currentScope) { + const variable = currentScope.variables.find( + scopeVar => scopeVar.name === name, + ); + + if (variable) { + variable.eslintUsed = true; + return; + } + + currentScope = currentScope.upper; + } + } + + private visitFunction( + node: TSESTree.FunctionDeclaration | TSESTree.FunctionExpression, + ): void { + const scope = this.getScope(node); + // skip implicit "arguments" variable + const variable = scope.set.get('arguments'); + if (variable?.defs.length === 0) { + this.markVariableAsUsed(variable); + } + } + + private visitFunctionTypeSignature( + node: + | TSESTree.TSCallSignatureDeclaration + | 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) { + this.visitPattern(param, name => { + this.markVariableAsUsed(name); + }); + } + } + + private visitSetter( + node: TSESTree.MethodDefinition | TSESTree.Property, + ): void { + if (node.kind === 'set') { + // ignore setter parameters because they're syntactically required to exist + for (const param of (node.value as TSESTree.FunctionLike).params) { + this.visitPattern(param, id => { + this.markVariableAsUsed(id); + }); + } + } + } + + //#endregion HELPERS + + //#region VISITORS + // NOTE - This is a simple visitor - meaning it does not support selectors + + protected ClassDeclaration(node: TSESTree.ClassDeclaration): void { + // skip a variable of class itself name in the class scope + const scope = this.getScope(node); + for (const variable of scope.variables) { + if (variable.identifiers[0] === scope.block.id) { + this.markVariableAsUsed(variable); + return; + } + } + } + + protected FunctionDeclaration = this.visitFunction; + + protected FunctionExpression = this.visitFunction; + + protected ForInStatement(node: TSESTree.ForInStatement): void { + /** + * (Brad Zacher): I hate that this has to exist. + * But it is required for compat with the base ESLint rule. + * + * In 2015, ESLint decided to add an exception for these two specific cases + * ``` + * for (var key in object) return; + * + * var key; + * for (key in object) return; + * ``` + * + * I disagree with it, but what are you going to do... + * + * https://github.com/eslint/eslint/issues/2342 + */ + + let idOrVariable; + if (node.left.type === AST_NODE_TYPES.VariableDeclaration) { + const variable = this.#scopeManager.getDeclaredVariables(node.left)[0]; + if (!variable) { + return; + } + idOrVariable = variable; + } + if (node.left.type === AST_NODE_TYPES.Identifier) { + idOrVariable = node.left; + } + + if (idOrVariable == null) { + return; + } + + let body = node.body; + if (node.body.type === AST_NODE_TYPES.BlockStatement) { + if (node.body.body.length !== 1) { + return; + } + body = node.body.body[0]; + } + + if (body.type !== AST_NODE_TYPES.ReturnStatement) { + return; + } + + this.markVariableAsUsed(idOrVariable); + } + + protected Identifier(node: TSESTree.Identifier): void { + const scope = this.getScope(node); + if ( + scope.type === TSESLint.Scope.ScopeType.function && + node.name === 'this' + ) { + // this parameters should always be considered used as they're pseudo-parameters + if ('params' in scope.block && scope.block.params.includes(node)) { + this.markVariableAsUsed(node); + } + } + } + + protected MethodDefinition = this.visitSetter; + + protected Property = this.visitSetter; + + protected TSCallSignatureDeclaration = this.visitFunctionTypeSignature; + + protected TSConstructorType = this.visitFunctionTypeSignature; + + protected TSConstructSignatureDeclaration = this.visitFunctionTypeSignature; + + protected TSDeclareFunction = this.visitFunctionTypeSignature; + + protected TSEmptyBodyFunctionExpression = this.visitFunctionTypeSignature; + + protected TSEnumDeclaration(node: TSESTree.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 = this.getScope(node); + for (const variable of scope.variables) { + this.markVariableAsUsed(variable); + } + } + + protected TSFunctionType = this.visitFunctionTypeSignature; + + protected TSMappedType(node: TSESTree.TSMappedType): 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. + this.markVariableAsUsed(node.typeParameter.name); + } + + protected TSMethodSignature = this.visitFunctionTypeSignature; + + protected TSModuleDeclaration(node: TSESTree.TSModuleDeclaration): void { + // global augmentation can be in any file, and they do not need exports + if (node.global === true) { + this.markVariableAsUsed('global', node.parent!); + } + } + + protected TSParameterProperty(node: TSESTree.TSParameterProperty): void { + let identifier: TSESTree.Identifier | null = null; + switch (node.parameter.type) { + case AST_NODE_TYPES.AssignmentPattern: + if (node.parameter.left.type === AST_NODE_TYPES.Identifier) { + identifier = node.parameter.left; + } + break; + + case AST_NODE_TYPES.Identifier: + identifier = node.parameter; + break; + } + + if (identifier) { + this.markVariableAsUsed(identifier); + } + } + + //#endregion VISITORS +} + +//#region private helpers + +/** + * Checks the position of given nodes. + * @param inner A node which is expected as inside. + * @param outer A node which is expected as outside. + * @returns `true` if the `inner` node exists in the `outer` node. + */ +function isInside(inner: TSESTree.Node, outer: TSESTree.Node): boolean { + return inner.range[0] >= outer.range[0] && inner.range[1] <= outer.range[1]; +} + +/** + * Determine if an identifier is referencing an enclosing name. + * This only applies to declarations that create their own scope (modules, functions, classes) + * @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: Set, +): boolean { + let scope: TSESLint.Scope.Scope | null = ref.from; + + while (scope) { + if (nodes.has(scope.block)) { + return true; + } + + scope = scope.upper; + } + + return false; +} + +const MERGABLE_TYPES = new Set([ + AST_NODE_TYPES.TSInterfaceDeclaration, + AST_NODE_TYPES.TSTypeAliasDeclaration, + AST_NODE_TYPES.TSModuleDeclaration, + AST_NODE_TYPES.ClassDeclaration, + AST_NODE_TYPES.FunctionDeclaration, +]); +/** + * Determine if the variable is directly exported + * @param variable the variable to check + * @param target the type of node that is expected to be exported + */ +function isMergableExported(variable: TSESLint.Scope.Variable): boolean { + // If all of the merged things are of the same type, TS will error if not all of them are exported - so we only need to find one + for (const def of variable.defs) { + // parameters can never be exported. + // their `node` prop points to the function decl, which can be exported + // so we need to special case them + if (def.type === TSESLint.Scope.DefinitionType.Parameter) { + continue; + } + + if ( + (MERGABLE_TYPES.has(def.node.type) && + def.node.parent?.type === AST_NODE_TYPES.ExportNamedDeclaration) || + def.node.parent?.type === AST_NODE_TYPES.ExportDefaultDeclaration + ) { + return true; + } + } + + return false; +} + +/** + * Determines if a given variable is being exported from a module. + * @param variable eslint-scope variable object. + * @returns True if the variable is exported, false if not. + */ +function isExported(variable: TSESLint.Scope.Variable): boolean { + const definition = variable.defs[0]; + + if (definition) { + let node = definition.node; + + if (node.type === AST_NODE_TYPES.VariableDeclarator) { + node = node.parent!; + } else if (definition.type === TSESLint.Scope.DefinitionType.Parameter) { + return false; + } + + return node.parent!.type.indexOf('Export') === 0; + } + return false; +} + +/** + * Determines if the variable is used. + * @param variable The variable to check. + * @returns True if the variable is used + */ +function isUsedVariable(variable: TSESLint.Scope.Variable): boolean { + /** + * Gets a list of function definitions for a specified variable. + * @param variable eslint-scope variable object. + * @returns Function nodes. + */ + function getFunctionDefinitions( + variable: TSESLint.Scope.Variable, + ): Set { + const functionDefinitions = new Set(); + + variable.defs.forEach(def => { + // FunctionDeclarations + if (def.type === TSESLint.Scope.DefinitionType.FunctionName) { + functionDefinitions.add(def.node); + } + + // FunctionExpressions + if ( + def.type === TSESLint.Scope.DefinitionType.Variable && + (def.node.init?.type === AST_NODE_TYPES.FunctionExpression || + def.node.init?.type === AST_NODE_TYPES.ArrowFunctionExpression) + ) { + functionDefinitions.add(def.node.init); + } + }); + return functionDefinitions; + } + + function getTypeDeclarations( + variable: TSESLint.Scope.Variable, + ): Set { + const nodes = new Set(); + + variable.defs.forEach(def => { + if ( + def.node.type === AST_NODE_TYPES.TSInterfaceDeclaration || + def.node.type === AST_NODE_TYPES.TSTypeAliasDeclaration + ) { + nodes.add(def.node); + } + }); + + return nodes; + } + + function getModuleDeclarations( + variable: TSESLint.Scope.Variable, + ): Set { + const nodes = new Set(); + + variable.defs.forEach(def => { + if (def.node.type === AST_NODE_TYPES.TSModuleDeclaration) { + nodes.add(def.node); + } + }); + + return nodes; + } + + /** + * Checks if the ref is contained within one of the given nodes + */ + function isInsideOneOf( + ref: TSESLint.Scope.Reference, + nodes: Set, + ): boolean { + for (const node of nodes) { + if (isInside(ref.identifier, node)) { + return true; + } + } + + return false; + } + + /** + * If a given reference is left-hand side of an assignment, this gets + * the right-hand side node of the assignment. + * + * In the following cases, this returns null. + * + * - The reference is not the LHS of an assignment expression. + * - The reference is inside of a loop. + * - The reference is inside of a function scope which is different from + * the declaration. + * @param ref A reference to check. + * @param prevRhsNode The previous RHS node. + * @returns The RHS node or null. + */ + function getRhsNode( + ref: TSESLint.Scope.Reference, + prevRhsNode: TSESTree.Node | null, + ): TSESTree.Node | null { + /** + * Checks whether the given node is in a loop or not. + * @param node The node to check. + * @returns `true` if the node is in a loop. + */ + function isInLoop(node: TSESTree.Node): boolean { + let currentNode: TSESTree.Node | undefined = node; + while (currentNode) { + if (util.isFunction(currentNode)) { + break; + } + + if (util.isLoop(currentNode)) { + return true; + } + + currentNode = currentNode.parent; + } + + return false; + } + + const id = ref.identifier; + const parent = id.parent!; + const grandparent = parent.parent!; + const refScope = ref.from.variableScope; + const varScope = ref.resolved!.scope.variableScope; + const canBeUsedLater = refScope !== varScope || isInLoop(id); + + /* + * Inherits the previous node if this reference is in the node. + * This is for `a = a + a`-like code. + */ + if (prevRhsNode && isInside(id, prevRhsNode)) { + return prevRhsNode; + } + + if ( + parent.type === AST_NODE_TYPES.AssignmentExpression && + grandparent.type === AST_NODE_TYPES.ExpressionStatement && + id === parent.left && + !canBeUsedLater + ) { + return parent.right; + } + return null; + } + + /** + * Checks whether a given reference is a read to update itself or not. + * @param ref A reference to check. + * @param rhsNode The RHS node of the previous assignment. + * @returns The reference is a read to update itself. + */ + function isReadForItself( + ref: TSESLint.Scope.Reference, + rhsNode: TSESTree.Node | null, + ): boolean { + /** + * Checks whether a given Identifier node exists inside of a function node which can be used later. + * + * "can be used later" means: + * - the function is assigned to a variable. + * - the function is bound to a property and the object can be used later. + * - the function is bound as an argument of a function call. + * + * If a reference exists in a function which can be used later, the reference is read when the function is called. + * @param id An Identifier node to check. + * @param rhsNode The RHS node of the previous assignment. + * @returns `true` if the `id` node exists inside of a function node which can be used later. + */ + function isInsideOfStorableFunction( + id: TSESTree.Node, + rhsNode: TSESTree.Node, + ): boolean { + /** + * Finds a function node from ancestors of a node. + * @param node A start node to find. + * @returns A found function node. + */ + function getUpperFunction(node: TSESTree.Node): TSESTree.Node | null { + let currentNode: TSESTree.Node | undefined = node; + while (currentNode) { + if (util.isFunction(currentNode)) { + return currentNode; + } + currentNode = currentNode.parent; + } + + return null; + } + + /** + * Checks whether a given function node is stored to somewhere or not. + * If the function node is stored, the function can be used later. + * @param funcNode A function node to check. + * @param rhsNode The RHS node of the previous assignment. + * @returns `true` if under the following conditions: + * - the funcNode is assigned to a variable. + * - the funcNode is bound as an argument of a function call. + * - the function is bound to a property and the object satisfies above conditions. + */ + function isStorableFunction( + funcNode: TSESTree.Node, + rhsNode: TSESTree.Node, + ): boolean { + let node = funcNode; + let parent = funcNode.parent; + + while (parent && isInside(parent, rhsNode)) { + switch (parent.type) { + case AST_NODE_TYPES.SequenceExpression: + if (parent.expressions[parent.expressions.length - 1] !== node) { + return false; + } + break; + + case AST_NODE_TYPES.CallExpression: + case AST_NODE_TYPES.NewExpression: + return parent.callee !== node; + + case AST_NODE_TYPES.AssignmentExpression: + case AST_NODE_TYPES.TaggedTemplateExpression: + case AST_NODE_TYPES.YieldExpression: + return true; + + default: + if ( + parent.type.endsWith('Statement') || + parent.type.endsWith('Declaration') + ) { + /* + * If it encountered statements, this is a complex pattern. + * Since analyzing complex patterns is hard, this returns `true` to avoid false positive. + */ + return true; + } + } + + node = parent; + parent = parent.parent; + } + + return false; + } + + const funcNode = getUpperFunction(id); + + return ( + !!funcNode && + isInside(funcNode, rhsNode) && + isStorableFunction(funcNode, rhsNode) + ); + } + + const id = ref.identifier; + const parent = id.parent!; + const grandparent = parent.parent!; + + return ( + ref.isRead() && // in RHS of an assignment for itself. e.g. `a = a + 1` + // self update. e.g. `a += 1`, `a++` + ((parent.type === AST_NODE_TYPES.AssignmentExpression && + grandparent.type === AST_NODE_TYPES.ExpressionStatement && + parent.left === id) || + (parent.type === AST_NODE_TYPES.UpdateExpression && + grandparent.type === AST_NODE_TYPES.ExpressionStatement) || + (!!rhsNode && + isInside(id, rhsNode) && + !isInsideOfStorableFunction(id, rhsNode))) + ); + } + + const functionNodes = getFunctionDefinitions(variable); + const isFunctionDefinition = functionNodes.size > 0; + + const typeDeclNodes = getTypeDeclarations(variable); + const isTypeDecl = typeDeclNodes.size > 0; + + const moduleDeclNodes = getModuleDeclarations(variable); + const isModuleDecl = moduleDeclNodes.size > 0; + + let rhsNode: TSESTree.Node | null = null; + + return variable.references.some(ref => { + const forItself = isReadForItself(ref, rhsNode); + + rhsNode = getRhsNode(ref, rhsNode); + + return ( + ref.isRead() && + !forItself && + !(isFunctionDefinition && isSelfReference(ref, functionNodes)) && + !(isTypeDecl && isInsideOneOf(ref, typeDeclNodes)) && + !(isModuleDecl && isSelfReference(ref, moduleDeclNodes)) + ); + }); +} + +//#endregion private helpers + +/** + * Collects the set of unused variables for a given context. + * + * Due to complexity, this does not take into consideration: + * - variables within declaration files + * - variables within ambient module declarations + */ +function collectUnusedVariables< + TMessageIds extends string, + TOptions extends readonly unknown[] +>( + context: Readonly>, +): ReadonlySet { + return UnusedVarsVisitor.collectUnusedVariables(context); +} + +export { collectUnusedVariables }; diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index 672f50dc4fff..af0a64eddbfc 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -1,6 +1,7 @@ import { ESLintUtils } from '@typescript-eslint/experimental-utils'; export * from './astUtils'; +export * from './collectUnusedVariables'; export * from './createRule'; export * from './isTypeReadonly'; export * from './misc'; diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts new file mode 100644 index 000000000000..8ef68ae781ce --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars-eslint.test.ts @@ -0,0 +1,2457 @@ +// The following tests are adapted from the tests in eslint. +// Original Code: https://github.com/eslint/eslint/blob/0cb81a9b90dd6b92bac383022f886e501bd2cb31/tests/lib/rules/no-unused-vars.js +// Licence : https://github.com/eslint/eslint/blob/0cb81a9b90dd6b92bac383022f886e501bd2cb31/LICENSE + +'use strict'; + +import { + AST_NODE_TYPES, + TSESLint, +} from '@typescript-eslint/experimental-utils'; +import rule, { MessageIds } from '../../../src/rules/no-unused-vars'; +import { RuleTester } from '../../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + // espree defaults to `script`, so we need to mirror it + sourceType: 'script', + }, +}); + +ruleTester.defineRule('use-every-a', context => { + /** + * Mark a variable as used + */ + function useA(): void { + context.markVariableAsUsed('a'); + } + return { + VariableDeclaration: useA, + ReturnStatement: useA, + }; +}); + +/** + * Returns an expected error for defined-but-not-used variables. + * @param varName The name of the variable + * @param [additional] The additional text for the message data + * @param [type] The node type (defaults to "Identifier") + * @returns An expected error object + */ +function definedError( + varName: string, + additional = '', + type = AST_NODE_TYPES.Identifier, +): TSESLint.TestCaseError { + return { + messageId: 'unusedVar', + data: { + varName, + action: 'defined', + additional, + }, + type, + }; +} + +/** + * Returns an expected error for assigned-but-not-used variables. + * @param varName The name of the variable + * @param [additional] The additional text for the message data + * @param [type] The node type (defaults to "Identifier") + * @returns An expected error object + */ +function assignedError( + varName: string, + additional = '', + type = AST_NODE_TYPES.Identifier, +): TSESLint.TestCaseError { + return { + messageId: 'unusedVar', + data: { + varName, + action: 'assigned a value', + additional, + }, + type, + }; +} + +ruleTester.run('no-unused-vars', rule, { + valid: [ + 'var foo = 5;\n\nlabel: while (true) {\n console.log(foo);\n break label;\n}', + 'var foo = 5;\n\nwhile (true) {\n console.log(foo);\n break;\n}', + { + code: ` +for (let prop in box) { + box[prop] = parseInt(box[prop]); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +var box = { a: 2 }; +for (var prop in box) { + box[prop] = parseInt(box[prop]); +} + `, + ` +f({ + set foo(a) { + return; + }, +}); + `, + { + code: ` +a; +var a; + `, + options: ['all'], + }, + { + code: ` +var a = 10; +alert(a); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +(function () { + alert(a); +})(); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +(function () { + setTimeout(function () { + alert(a); + }, 0); +})(); + `, + options: ['all'], + }, + { + code: ` +var a = 10; +d[a] = 0; + `, + options: ['all'], + }, + { + code: ` +(function () { + var a = 10; + return a; +})(); + `, + options: ['all'], + }, + { + code: '(function g() {})();', + options: ['all'], + }, + { + code: ` +function f(a) { + alert(a); +} +f(); + `, + options: ['all'], + }, + { + code: ` +var c = 0; +function f(a) { + var b = a; + return b; +} +f(c); + `, + options: ['all'], + }, + { + code: ` +function a(x, y) { + return y; +} +a(); + `, + options: ['all'], + }, + { + code: ` +var arr1 = [1, 2]; +var arr2 = [3, 4]; +for (var i in arr1) { + arr1[i] = 5; +} +for (var i in arr2) { + arr2[i] = 10; +} + `, + options: ['all'], + }, + { + code: 'var a = 10;', + options: ['local'], + }, + { + code: ` +var min = 'min'; +Math[min]; + `, + options: ['all'], + }, + { + code: ` +Foo.bar = function (baz) { + return baz; +}; + `, + options: ['all'], + }, + 'myFunc(function foo() {}.bind(this));', + 'myFunc(function foo() {}.toString());', + ` +function foo(first, second) { + doStuff(function () { + console.log(second); + }); +} +foo(); + `, + ` +(function () { + var doSomething = function doSomething() {}; + doSomething(); +})(); + `, + ` +try { +} catch (e) {} + `, + '/*global a */ a;', + { + code: ` +var a = 10; +(function () { + alert(a); +})(); + `, + options: [{ vars: 'all' }], + }, + { + code: ` +function g(bar, baz) { + return baz; +} +g(); + `, + options: [{ vars: 'all' }], + }, + { + code: ` +function g(bar, baz) { + return baz; +} +g(); + `, + options: [{ vars: 'all', args: 'after-used' }], + }, + { + code: ` +function g(bar, baz) { + return bar; +} +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + { + code: ` +function g(bar, baz) { + return 2; +} +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + { + code: ` +function g(bar, baz) { + return bar + baz; +} +g(); + `, + options: [{ vars: 'local', args: 'all' }], + }, + { + code: ` +var g = function (bar, baz) { + return 2; +}; +g(); + `, + options: [{ vars: 'all', args: 'none' }], + }, + ` +(function z() { + z(); +})(); + `, + { + code: ' ', + globals: { a: true }, + }, + { + code: ` +var who = 'Paul'; +module.exports = \`Hello \${who}!\`; + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: 'export var foo = 123;', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'export function foo() {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: ` +let toUpper = partial => partial.toUpperCase; +export { toUpper }; + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: 'export class foo {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + }, + { + code: ` +class Foo {} +var x = new Foo(); +x.foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const foo = 'hello!'; +function bar(foobar = foo) { + foobar.replace(/!$/, ' world!'); +} +bar(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +function Foo() {} +var x = new Foo(); +x.foo(); + `, + ` +function foo() { + var foo = 1; + return foo; +} +foo(); + `, + ` +function foo(foo) { + return foo; +} +foo(1); + `, + ` +function foo() { + function foo() { + return 1; + } + return foo(); +} +foo(); + `, + { + code: ` +function foo() { + var foo = 1; + return foo; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo(foo) { + return foo; +} +foo(1); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo() { + function foo() { + return 1; + } + return foo(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const [y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const { y = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +const { + z: [y = x], +} = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = []; +const { z: [y] = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +let y; +[y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +let y; +({ + z: [y = x], +} = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = []; +let y; +({ z: [y] = x } = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo(y = x) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo({ y = x } = {}) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo( + y = function (z = x) { + bar(z); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +const x = 1; +function foo( + y = function () { + bar(x); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var [y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var { y = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +var { + z: [y = x], +} = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = []; +var { z: [y] = x } = {}; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1, + y; +[y = x] = []; +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1, + y; +({ + z: [y = x], +} = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = [], + y; +({ z: [y] = x } = {}); +foo(y); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo(y = x) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo({ y = x } = {}) { + bar(y); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo( + y = function (z = x) { + bar(z); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var x = 1; +function foo( + y = function () { + bar(x); + }, +) { + y(); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // exported variables should work + "/*exported toaster*/ var toaster = 'great';", + ` +/*exported toaster, poster*/ var toaster = 1; +poster = 0; + `, + { + code: '/*exported x*/ var { x } = y;', + parserOptions: { ecmaVersion: 6 }, + }, + { + code: '/*exported x, y*/ var { x, y } = z;', + parserOptions: { ecmaVersion: 6 }, + }, + + // Can mark variables as used via context.markVariableAsUsed() + '/*eslint use-every-a:1*/ var a;', + ` +/*eslint use-every-a:1*/ !function (a) { + return 1; +}; + `, + ` +/*eslint use-every-a:1*/ !function () { + var a; + return 1; +}; + `, + + // ignore pattern + { + code: 'var _a;', + options: [{ vars: 'all', varsIgnorePattern: '^_' }], + }, + { + code: ` +var a; +function foo() { + var _b; +} +foo(); + `, + options: [{ vars: 'local', varsIgnorePattern: '^_' }], + }, + { + code: ` +function foo(_a) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '^_' }], + }, + { + code: ` +function foo(a, _b) { + return a; +} +foo(); + `, + options: [{ args: 'after-used', argsIgnorePattern: '^_' }], + }, + { + code: ` +var [firstItemIgnored, secondItem] = items; +console.log(secondItem); + `, + options: [{ vars: 'all', varsIgnorePattern: '[iI]gnored' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // for-in loops (see #2342) + ` +(function (obj) { + var name; + for (name in obj) return; +})({}); + `, + ` +(function (obj) { + var name; + for (name in obj) { + return; + } +})({}); + `, + ` +(function (obj) { + for (var name in obj) { + return true; + } +})({}); + `, + ` +(function (obj) { + for (var name in obj) return true; +})({}); + `, + + { + code: ` +(function (obj) { + let name; + for (name in obj) return; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + let name; + for (name in obj) { + return; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (let name in obj) { + return true; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (let name in obj) return true; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + { + code: ` +(function (obj) { + for (const name in obj) { + return true; + } +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (obj) { + for (const name in obj) return true; +})({}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // caughtErrors + { + code: ` +try { +} catch (err) { + console.error(err); +} + `, + options: [{ caughtErrors: 'all' }], + }, + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'none' }], + }, + { + code: ` +try { +} catch (ignoreErr) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + }, + + // caughtErrors with other combinations + { + code: ` +try { +} catch (err) {} + `, + options: [{ vars: 'all', args: 'all' }], + }, + + // Using object rest for variable omission + { + code: ` +const data = { type: 'coords', x: 1, y: 2 }; +const { type, ...coords } = data; +console.log(coords); + `, + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + }, + + // https://github.com/eslint/eslint/issues/6348 + ` +var a = 0, + b; +b = a = a + 1; +foo(b); + `, + ` +var a = 0, + b; +b = a += a + 1; +foo(b); + `, + ` +var a = 0, + b; +b = a++; +foo(b); + `, + ` +function foo(a) { + var b = (a = a + 1); + bar(b); +} +foo(); + `, + ` +function foo(a) { + var b = (a += a + 1); + bar(b); +} +foo(); + `, + ` +function foo(a) { + var b = a++; + bar(b); +} +foo(); + `, + + // https://github.com/eslint/eslint/issues/6576 + [ + 'var unregisterFooWatcher;', + '// ...', + 'unregisterFooWatcher = $scope.$watch( "foo", function() {', + ' // ...some code..', + ' unregisterFooWatcher();', + '});', + ].join('\n'), + [ + 'var ref;', + 'ref = setInterval(', + ' function(){', + ' clearInterval(ref);', + ' }, 10);', + ].join('\n'), + [ + 'var _timer;', + 'function f() {', + ' _timer = setTimeout(function () {}, _timer ? 100 : 0);', + '}', + 'f();', + ].join('\n'), + ` +function foo(cb) { + cb = (function () { + function something(a) { + cb(1 + a); + } + register(something); + })(); +} +foo(); + `, + { + code: ` +function* foo(cb) { + cb = yield function (a) { + cb(1 + a); + }; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +function foo(cb) { + cb = tag\`hello\${function (a) { + cb(1 + a); + }}\`; +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + }, + ` +function foo(cb) { + var b; + cb = b = function (a) { + cb(1 + a); + }; + b(); +} +foo(); + `, + + // https://github.com/eslint/eslint/issues/6646 + [ + 'function someFunction() {', + ' var a = 0, i;', + ' for (i = 0; i < 2; i++) {', + ' a = myFunction(a);', + ' }', + '}', + 'someFunction();', + ].join('\n'), + + // https://github.com/eslint/eslint/issues/7124 + { + code: ` +(function (a, b, { c, d }) { + d; +}); + `, + options: [{ argsIgnorePattern: 'c' }], + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +(function (a, b, { c, d }) { + c; +}); + `, + options: [{ argsIgnorePattern: 'd' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/7250 + { + code: ` +(function (a, b, c) { + c; +}); + `, + options: [{ argsIgnorePattern: 'c' }], + }, + { + code: ` +(function (a, b, { c, d }) { + c; +}); + `, + options: [{ argsIgnorePattern: '[cd]' }], + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/7351 + { + code: ` +(class { + set foo(UNUSED) {} +}); + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +class Foo { + set bar(UNUSED) {} +} +console.log(Foo); + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // https://github.com/eslint/eslint/issues/8119 + { + code: '({ a, ...rest }) => rest;', + options: [{ args: 'all', ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + }, + + // https://github.com/eslint/eslint/issues/10952 + ` +/*eslint use-every-a:1*/ !function (b, a) { + return 1; +}; + `, + + // https://github.com/eslint/eslint/issues/10982 + ` +var a = function () { + a(); +}; +a(); + `, + ` +var a = function () { + return function () { + a(); + }; +}; +a(); + `, + { + code: ` +const a = () => { + a(); +}; +a(); + `, + parserOptions: { ecmaVersion: 2015 }, + }, + { + code: ` +const a = () => () => { + a(); +}; +a(); + `, + parserOptions: { ecmaVersion: 2015 }, + }, + + // export * as ns from "source" + { + code: "export * as ns from 'source';", + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + + // import.meta + { + code: 'import.meta;', + parserOptions: { ecmaVersion: 2020, sourceType: 'module' }, + }, + ], + invalid: [ + { + code: ` +function foox() { + return foox(); +} + `, + errors: [definedError('foox')], + }, + { + code: ` +(function () { + function foox() { + if (true) { + return foox(); + } + } +})(); + `, + errors: [definedError('foox')], + }, + { + code: 'var a = 10;', + errors: [assignedError('a')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f((a *= 2)); + }; +} + `, + errors: [definedError('f')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f(++a); + }; +} + `, + errors: [definedError('f')], + }, + { + code: '/*global a */', + errors: [definedError('a', '', AST_NODE_TYPES.Program)], + }, + { + code: ` +function foo(first, second) { + doStuff(function () { + console.log(second); + }); +} + `, + errors: [definedError('foo')], + }, + { + code: 'var a = 10;', + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10; +a = 20; + `, + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10; +(function () { + var a = 1; + alert(a); +})(); + `, + options: ['all'], + errors: [assignedError('a')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +alert(a + b); + `, + options: ['all'], + errors: [assignedError('c')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +setTimeout(function () { + var b = 2; + alert(a + b + c); +}, 0); + `, + options: ['all'], + errors: [assignedError('b')], + }, + { + code: ` +var a = 10, + b = 0, + c = null; +setTimeout(function () { + var b = 2; + var c = 2; + alert(a + b + c); +}, 0); + `, + options: ['all'], + errors: [assignedError('b'), assignedError('c')], + }, + { + code: ` +function f() { + var a = []; + return a.map(function () {}); +} + `, + options: ['all'], + errors: [definedError('f')], + }, + { + code: ` +function f() { + var a = []; + return a.map(function g() {}); +} + `, + options: ['all'], + errors: [definedError('f')], + }, + { + code: ` +function foo() { + function foo(x) { + return x; + } + return function () { + return foo; + }; +} + `, + errors: [ + { + messageId: 'unusedVar', + data: { varName: 'foo', action: 'defined', additional: '' }, + line: 2, + type: AST_NODE_TYPES.Identifier, + }, + ], + }, + { + code: ` +function f() { + var x; + function a() { + x = 42; + } + function b() { + alert(x); + } +} + `, + options: ['all'], + errors: [definedError('f'), definedError('a'), definedError('b')], + }, + { + code: ` +function f(a) {} +f(); + `, + options: ['all'], + errors: [definedError('a')], + }, + { + code: ` +function a(x, y, z) { + return y; +} +a(); + `, + options: ['all'], + errors: [definedError('z')], + }, + { + code: 'var min = Math.min;', + options: ['all'], + errors: [assignedError('min')], + }, + { + code: 'var min = { min: 1 };', + options: ['all'], + errors: [assignedError('min')], + }, + { + code: ` +Foo.bar = function (baz) { + return 1; +}; + `, + options: ['all'], + errors: [definedError('baz')], + }, + { + code: 'var min = { min: 1 };', + options: [{ vars: 'all' }], + errors: [assignedError('min')], + }, + { + code: ` +function gg(baz, bar) { + return baz; +} +gg(); + `, + options: [{ vars: 'all' }], + errors: [definedError('bar')], + }, + { + code: ` +(function (foo, baz, bar) { + return baz; +})(); + `, + options: [{ vars: 'all', args: 'after-used' }], + errors: [definedError('bar')], + }, + { + code: ` +(function (foo, baz, bar) { + return baz; +})(); + `, + options: [{ vars: 'all', args: 'all' }], + errors: [definedError('foo'), definedError('bar')], + }, + { + code: ` +(function z(foo) { + var bar = 33; +})(); + `, + options: [{ vars: 'all', args: 'all' }], + errors: [definedError('foo'), assignedError('bar')], + }, + { + code: ` +(function z(foo) { + z(); +})(); + `, + options: [{}], + errors: [definedError('foo')], + }, + { + code: ` +function f() { + var a = 1; + return function () { + f((a = 2)); + }; +} + `, + options: [{}], + errors: [definedError('f'), assignedError('a')], + }, + { + code: "import x from 'y';", + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('x')], + }, + { + code: ` +export function fn2({ x, y }) { + console.log(x); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('y')], + }, + { + code: ` +export function fn2(x, y) { + console.log(x); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('y')], + }, + + // exported + { + code: ` +/*exported max*/ var max = 1, + min = { min: 1 }; + `, + errors: [assignedError('min')], + }, + { + code: '/*exported x*/ var { x, y } = z;', + parserOptions: { ecmaVersion: 6 }, + errors: [assignedError('y')], + }, + + // ignore pattern + { + code: ` +var _a; +var b; + `, + options: [{ vars: 'all', varsIgnorePattern: '^_' }], + errors: [ + { + line: 3, + column: 5, + messageId: 'unusedVar', + data: { + varName: 'b', + action: 'defined', + additional: '. Allowed unused vars must match /^_/u', + }, + }, + ], + }, + { + code: ` +var a; +function foo() { + var _b; + var c_; +} +foo(); + `, + options: [{ vars: 'local', varsIgnorePattern: '^_' }], + errors: [ + { + line: 5, + column: 7, + messageId: 'unusedVar', + data: { + varName: 'c_', + action: 'defined', + additional: '. Allowed unused vars must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(a, _b) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '^_' }], + errors: [ + { + line: 2, + column: 14, + messageId: 'unusedVar', + data: { + varName: 'a', + action: 'defined', + additional: '. Allowed unused args must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(a, _b, c) { + return a; +} +foo(); + `, + options: [{ args: 'after-used', argsIgnorePattern: '^_' }], + errors: [ + { + line: 2, + column: 21, + messageId: 'unusedVar', + data: { + varName: 'c', + action: 'defined', + additional: '. Allowed unused args must match /^_/u', + }, + }, + ], + }, + { + code: ` +function foo(_a) {} +foo(); + `, + options: [{ args: 'all', argsIgnorePattern: '[iI]gnored' }], + errors: [ + { + line: 2, + column: 14, + messageId: 'unusedVar', + data: { + varName: '_a', + action: 'defined', + additional: '. Allowed unused args must match /[iI]gnored/u', + }, + }, + ], + }, + { + code: 'var [firstItemIgnored, secondItem] = items;', + options: [{ vars: 'all', varsIgnorePattern: '[iI]gnored' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + line: 1, + column: 24, + messageId: 'unusedVar', + data: { + varName: 'secondItem', + action: 'assigned a value', + additional: '. Allowed unused vars must match /[iI]gnored/u', + }, + }, + ], + }, + + // for-in loops (see #2342) + { + code: ` +(function (obj) { + var name; + for (name in obj) { + i(); + return; + } +})({}); + `, + errors: [ + { + line: 4, + column: 8, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + { + code: ` +(function (obj) { + var name; + for (name in obj) { + } +})({}); + `, + errors: [ + { + line: 4, + column: 8, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + { + code: ` +(function (obj) { + for (var name in obj) { + } +})({}); + `, + errors: [ + { + line: 3, + column: 12, + messageId: 'unusedVar', + data: { + varName: 'name', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/3617 + { + code: ` +/* global foobar, foo, bar */ +foobar; + `, + errors: [ + { + line: 2, + endLine: 2, + column: 19, + endColumn: 22, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + { + line: 2, + endLine: 2, + column: 24, + endColumn: 27, + messageId: 'unusedVar', + data: { + varName: 'bar', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: ` +/* global foobar, + foo, + bar + */ +foobar; + `, + errors: [ + { + line: 3, + column: 4, + endLine: 3, + endColumn: 7, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + { + line: 4, + column: 4, + endLine: 4, + endColumn: 7, + messageId: 'unusedVar', + data: { + varName: 'bar', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // Rest property sibling without ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 1, y: 2 }; +const { type, ...coords } = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 9, + messageId: 'unusedVar', + data: { + varName: 'type', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Unused rest property with ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 2, y: 2 }; +const { type, ...coords } = data; +console.log(type); + `, + options: [{ ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 18, + messageId: 'unusedVar', + data: { + varName: 'coords', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Unused rest property without ignoreRestSiblings + { + code: ` +const data = { type: 'coords', x: 3, y: 2 }; +const { type, ...coords } = data; +console.log(type); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 3, + column: 18, + messageId: 'unusedVar', + data: { + varName: 'coords', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Nested array destructuring with rest property + { + code: ` +const data = { vars: ['x', 'y'], x: 1, y: 2 }; +const { + vars: [x], + ...coords +} = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 4, + column: 10, + messageId: 'unusedVar', + data: { + varName: 'x', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // Nested object destructuring with rest property + { + code: ` +const data = { defaults: { x: 0 }, x: 1, y: 2 }; +const { + defaults: { x }, + ...coords +} = data; +console.log(coords); + `, + parserOptions: { ecmaVersion: 2018 }, + errors: [ + { + line: 4, + column: 15, + messageId: 'unusedVar', + data: { + varName: 'x', + action: 'assigned a value', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/8119 + { + code: '({ a, ...rest }) => {};', + options: [{ args: 'all', ignoreRestSiblings: true }], + parserOptions: { ecmaVersion: 2018 }, + errors: [definedError('rest')], + }, + + // https://github.com/eslint/eslint/issues/3714 + { + // cspell:disable-next-line + code: '/* global a$fooz,$foo */\na$fooz;', + errors: [ + { + line: 1, + column: 18, + endLine: 1, + endColumn: 22, + messageId: 'unusedVar', + data: { + varName: '$foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + // cspell:disable-next-line + code: '/* globals a$fooz, $ */\na$fooz;', + errors: [ + { + line: 1, + column: 20, + endLine: 1, + endColumn: 21, + messageId: 'unusedVar', + data: { + varName: '$', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/*globals $foo*/', + errors: [ + { + line: 1, + column: 11, + endLine: 1, + endColumn: 15, + messageId: 'unusedVar', + data: { + varName: '$foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/* global global*/', + errors: [ + { + line: 1, + column: 11, + endLine: 1, + endColumn: 17, + messageId: 'unusedVar', + data: { + varName: 'global', + action: 'defined', + additional: '', + }, + }, + ], + }, + { + code: '/*global foo:true*/', + errors: [ + { + line: 1, + column: 10, + endLine: 1, + endColumn: 13, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // non ascii. + { + code: '/*global 変数, 数*/\n変数;', + errors: [ + { + line: 1, + column: 14, + endLine: 1, + endColumn: 15, + messageId: 'unusedVar', + data: { + varName: '数', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // surrogate pair. + { + code: ` +/*global 𠮷𩸽, 𠮷*/ +𠮷𩸽; + `, + env: { es6: true }, + errors: [ + { + line: 2, + column: 16, + endLine: 2, + endColumn: 18, + messageId: 'unusedVar', + data: { + varName: '𠮷', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/4047 + { + code: 'export default function (a) {}', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default function (a, b) { + console.log(a); +} + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + { + code: 'export default (function (a) {});', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default (function (a, b) { + console.log(a); +}); + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + { + code: 'export default a => {};', + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('a')], + }, + { + code: ` +export default (a, b) => { + console.log(a); +}; + `, + parserOptions: { ecmaVersion: 6, sourceType: 'module' }, + errors: [definedError('b')], + }, + + // caughtErrors + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all' }], + errors: [definedError('err')], + }, + { + code: ` +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // multiple try catch with one success + { + code: ` +try { +} catch (ignoreErr) {} +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // multiple try catch both fail + { + code: ` +try { +} catch (error) {} +try { +} catch (err) {} + `, + options: [{ caughtErrors: 'all', caughtErrorsIgnorePattern: '^ignore' }], + errors: [ + definedError('error', '. Allowed unused args must match /^ignore/u'), + definedError('err', '. Allowed unused args must match /^ignore/u'), + ], + }, + + // caughtErrors with other configs + { + code: ` +try { +} catch (err) {} + `, + options: [{ vars: 'all', args: 'all', caughtErrors: 'all' }], + errors: [definedError('err')], + }, + + // no conflict in ignore patterns + { + code: ` +try { +} catch (err) {} + `, + options: [ + { + vars: 'all', + args: 'all', + caughtErrors: 'all', + argsIgnorePattern: '^er', + }, + ], + errors: [definedError('err')], + }, + + // Ignore reads for modifications to itself: https://github.com/eslint/eslint/issues/6348 + { + code: ` +var a = 0; +a = a + 1; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a = a + a; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a += a + 1; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 0; +a++; + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a = a + 1; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a += a + 1; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +function foo(a) { + a++; +} +foo(); + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 3; +a = a * 5 + 6; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = 2, + b = 4; +a = a * 2 + b; + `, + errors: [assignedError('a')], + }, + + // https://github.com/eslint/eslint/issues/6576 (For coverage) + { + code: ` +function foo(cb) { + cb = function (a) { + cb(1 + a); + }; + bar(not_cb); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = (function (a) { + return cb(1 + a); + })(); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = + (function (a) { + cb(1 + a); + }, + cb); +} +foo(); + `, + errors: [assignedError('cb')], + }, + { + code: ` +function foo(cb) { + cb = + (0, + function (a) { + cb(1 + a); + }); +} +foo(); + `, + errors: [assignedError('cb')], + }, + + // https://github.com/eslint/eslint/issues/6646 + { + code: [ + 'while (a) {', + ' function foo(b) {', + ' b = b + 1;', + ' }', + ' foo()', + '}', + ].join('\n'), + errors: [assignedError('b')], + }, + + // https://github.com/eslint/eslint/issues/7124 + { + code: '(function (a, b, c) {});', + options: [{ argsIgnorePattern: 'c' }], + errors: [ + definedError('a', '. Allowed unused args must match /c/u'), + definedError('b', '. Allowed unused args must match /c/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: '[cd]' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /[cd]/u'), + definedError('b', '. Allowed unused args must match /[cd]/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: 'c' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /c/u'), + definedError('b', '. Allowed unused args must match /c/u'), + definedError('d', '. Allowed unused args must match /c/u'), + ], + }, + { + code: '(function (a, b, { c, d }) {});', + options: [{ argsIgnorePattern: 'd' }], + parserOptions: { ecmaVersion: 6 }, + errors: [ + definedError('a', '. Allowed unused args must match /d/u'), + definedError('b', '. Allowed unused args must match /d/u'), + definedError('c', '. Allowed unused args must match /d/u'), + ], + }, + { + code: ` +/*global +foo*/ + `, + errors: [ + { + line: 3, + column: 1, + endLine: 3, + endColumn: 4, + messageId: 'unusedVar', + data: { + varName: 'foo', + action: 'defined', + additional: '', + }, + }, + ], + }, + + // https://github.com/eslint/eslint/issues/8442 + { + code: ` +(function ({ a }, b) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a')], + }, + { + code: ` +(function ({ a }, { b, c }) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ({ a, b }, { c }) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ([a], b) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a')], + }, + { + code: ` +(function ([a], [b, c]) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + { + code: ` +(function ([a, b], [c]) { + return b; +})(); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [definedError('a'), definedError('c')], + }, + + // https://github.com/eslint/eslint/issues/9774 + { + code: '(function (_a) {})();', + options: [{ args: 'all', varsIgnorePattern: '^_' }], + errors: [definedError('_a')], + }, + { + code: '(function (_a) {})();', + options: [{ args: 'all', caughtErrorsIgnorePattern: '^_' }], + errors: [definedError('_a')], + }, + + // https://github.com/eslint/eslint/issues/10982 + { + code: ` +var a = function () { + a(); +}; + `, + errors: [assignedError('a')], + }, + { + code: ` +var a = function () { + return function () { + a(); + }; +}; + `, + errors: [assignedError('a')], + }, + { + code: ` +const a = () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [assignedError('a')], + }, + { + code: ` +const a = () => () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [assignedError('a')], + }, + { + code: ` +let myArray = [1, 2, 3, 4].filter(x => x == 0); +myArray = myArray.filter(x => x == 1); + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('myArray'), + line: 3, + column: 11, + }, + ], + }, + { + code: ` +const a = 1; +a += 1; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 1, + }, + ], + }, + { + code: ` +var a = function () { + a(); +}; + `, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +var a = function () { + return function () { + a(); + }; +}; + `, + errors: [ + { + ...assignedError('a'), + line: 4, + column: 5, + }, + ], + }, + { + code: ` +const a = () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +const a = () => () => { + a(); +}; + `, + parserOptions: { ecmaVersion: 2015 }, + errors: [ + { + ...assignedError('a'), + line: 3, + column: 3, + }, + ], + }, + { + code: ` +let a = 'a'; +a = 10; +function foo() { + a = 11; + a = () => { + a = 13; + }; +} + `, + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...definedError('foo'), + line: 4, + column: 10, + }, + { + ...assignedError('a'), + line: 7, + column: 5, + }, + ], + }, + { + code: ` +let c = 'c'; +c = 10; +function foo1() { + c = 11; + c = () => { + c = 13; + }; +} +c = foo1; + `, + parserOptions: { ecmaVersion: 2020 }, + errors: [ + { + ...assignedError('c'), + line: 10, + column: 1, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts similarity index 95% rename from packages/eslint-plugin/tests/rules/no-unused-vars.test.ts rename to packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts index 9bc46bc75ade..9e06ea0dc484 100644 --- a/packages/eslint-plugin/tests/rules/no-unused-vars.test.ts +++ b/packages/eslint-plugin/tests/rules/no-unused-vars/no-unused-vars.test.ts @@ -1,5 +1,5 @@ -import rule from '../../src/rules/no-unused-vars'; -import { noFormat, RuleTester } from '../RuleTester'; +import rule from '../../../src/rules/no-unused-vars'; +import { noFormat, RuleTester } from '../../RuleTester'; const ruleTester = new RuleTester({ parserOptions: { @@ -927,6 +927,39 @@ export declare function setAlignment(value: \`\${VerticalAlignment}-\${Horizonta type EnthusiasticGreeting = \`\${Uppercase} - \${Lowercase} - \${Capitalize} - \${Uncapitalize}\`; export type HELLO = EnthusiasticGreeting<"heLLo">; `, + // https://github.com/typescript-eslint/typescript-eslint/issues/2714 + { + code: ` +interface IItem { + title: string; + url: string; + children?: IItem[]; +} + `, + // unreported because it's in a decl file, even though it's only self-referenced + filename: 'foo.d.ts', + }, + // https://github.com/typescript-eslint/typescript-eslint/issues/2648 + { + code: ` +namespace _Foo { + export const bar = 1; + export const baz = Foo.bar; +} + `, + // ignored by pattern, even though it's only self-referenced + options: [{ varsIgnorePattern: '^_' }], + }, + { + code: ` +interface _Foo { + a: string; + b: Foo; +} + `, + // ignored by pattern, even though it's only self-referenced + options: [{ varsIgnorePattern: '^_' }], + }, ], invalid: [ @@ -1376,8 +1409,8 @@ namespace Foo { action: 'defined', additional: '', }, - line: 2, - column: 11, + line: 4, + column: 15, }, ], }, @@ -1408,8 +1441,8 @@ namespace Foo { action: 'defined', additional: '', }, - line: 3, - column: 13, + line: 5, + column: 17, }, ], }, @@ -1424,7 +1457,7 @@ interface Foo { errors: [ { messageId: 'unusedVar', - line: 2, + line: 4, data: { varName: 'Foo', action: 'defined', @@ -1575,5 +1608,26 @@ export namespace Foo { }, ], }, + { + code: ` +interface Foo { + a: string; +} +interface Foo { + b: Foo; +} + `, + errors: [ + { + messageId: 'unusedVar', + line: 6, + data: { + varName: 'Foo', + action: 'defined', + additional: '', + }, + }, + ], + }, ], }); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 4e10fe235786..93b75e3a956d 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -811,3 +811,17 @@ declare module 'eslint/lib/rules/space-infix-ops' { >; export = rule; } + +declare module 'eslint/lib/rules/utils/ast-utils' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const utils: { + getNameLocationInGlobalDirectiveComment( + sourceCode: TSESLint.SourceCode, + comment: TSESTree.Comment, + name: string, + ): TSESTree.SourceLocation; + }; + + export = utils; +} diff --git a/packages/experimental-utils/src/ast-utils/predicates.ts b/packages/experimental-utils/src/ast-utils/predicates.ts index c23f30f17077..bf62a6f77cb2 100644 --- a/packages/experimental-utils/src/ast-utils/predicates.ts +++ b/packages/experimental-utils/src/ast-utils/predicates.ts @@ -214,6 +214,27 @@ function isAwaitKeyword( return node?.type === AST_TOKEN_TYPES.Identifier && node.value === 'await'; } +function isLoop( + node: TSESTree.Node | undefined | null, +): node is + | TSESTree.DoWhileStatement + | TSESTree.ForStatement + | TSESTree.ForInStatement + | TSESTree.ForOfStatement + | TSESTree.WhileStatement { + if (!node) { + return false; + } + + return ( + node.type === AST_NODE_TYPES.DoWhileStatement || + node.type === AST_NODE_TYPES.ForStatement || + node.type === AST_NODE_TYPES.ForInStatement || + node.type === AST_NODE_TYPES.ForOfStatement || + node.type === AST_NODE_TYPES.WhileStatement + ); +} + export { isAwaitExpression, isAwaitKeyword, @@ -223,6 +244,7 @@ export { isFunctionOrFunctionType, isFunctionType, isIdentifier, + isLoop, isLogicalOrOperator, isNonNullAssertionPunctuator, isNotNonNullAssertionPunctuator, diff --git a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts index 66e5b1153c3a..1fd2e752baa4 100644 --- a/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts +++ b/packages/experimental-utils/src/eslint-utils/InferTypesFromRule.ts @@ -1,25 +1,26 @@ -import { RuleModule } from '../ts-eslint'; +import { RuleCreateFunction, RuleModule } from '../ts-eslint'; -type InferOptionsTypeFromRuleNever = T extends RuleModule< - never, - infer TOptions -> - ? TOptions - : unknown; /** * Uses type inference to fetch the TOptions type from the given RuleModule */ -type InferOptionsTypeFromRule = T extends RuleModule +type InferOptionsTypeFromRule = T extends RuleModule< + infer _TMessageIds, + infer TOptions +> + ? TOptions + : T extends RuleCreateFunction ? TOptions - : InferOptionsTypeFromRuleNever; + : unknown; /** * Uses type inference to fetch the TMessageIds type from the given RuleModule */ type InferMessageIdsTypeFromRule = T extends RuleModule< infer TMessageIds, - unknown[] + infer _TOptions > + ? TMessageIds + : T extends RuleCreateFunction ? TMessageIds : unknown; diff --git a/packages/experimental-utils/src/ts-eslint/Rule.ts b/packages/experimental-utils/src/ts-eslint/Rule.ts index d305b29125b9..8ced374dd700 100644 --- a/packages/experimental-utils/src/ts-eslint/Rule.ts +++ b/packages/experimental-utils/src/ts-eslint/Rule.ts @@ -440,9 +440,12 @@ interface RuleModule< create(context: Readonly>): TRuleListener; } -type RuleCreateFunction = ( - context: Readonly>, -) => RuleListener; +type RuleCreateFunction< + TMessageIds extends string = never, + TOptions extends readonly unknown[] = unknown[], + // for extending base rules + TRuleListener extends RuleListener = RuleListener +> = (context: Readonly>) => TRuleListener; export { ReportDescriptor, diff --git a/packages/experimental-utils/src/ts-eslint/RuleTester.ts b/packages/experimental-utils/src/ts-eslint/RuleTester.ts index a1fa2104a91e..652567f6b9fd 100644 --- a/packages/experimental-utils/src/ts-eslint/RuleTester.ts +++ b/packages/experimental-utils/src/ts-eslint/RuleTester.ts @@ -1,7 +1,7 @@ import { RuleTester as ESLintRuleTester } from 'eslint'; import { AST_NODE_TYPES, AST_TOKEN_TYPES } from '../ts-estree'; import { ParserOptions } from './ParserOptions'; -import { RuleModule } from './Rule'; +import { RuleCreateFunction, RuleModule } from './Rule'; interface ValidTestCase> { /** @@ -19,7 +19,7 @@ interface ValidTestCase> { /** * The additional global variables. */ - readonly globals?: Record; + readonly globals?: Record; /** * Options for the test case. */ @@ -157,6 +157,18 @@ declare class RuleTesterBase { * @param callback the test callback */ static it?: (text: string, callback: () => void) => void; + + /** + * Define a rule for one particular run of tests. + * @param name The name of the rule to define. + * @param rule The rule definition. + */ + defineRule>( + name: string, + rule: + | RuleModule + | RuleCreateFunction, + ): void; } class RuleTester extends (ESLintRuleTester as typeof RuleTesterBase) {} diff --git a/packages/experimental-utils/src/ts-eslint/Scope.ts b/packages/experimental-utils/src/ts-eslint/Scope.ts index 2934a4d27561..6907e2290fd9 100644 --- a/packages/experimental-utils/src/ts-eslint/Scope.ts +++ b/packages/experimental-utils/src/ts-eslint/Scope.ts @@ -1,56 +1,51 @@ /* eslint-disable @typescript-eslint/no-namespace */ import * as scopeManager from '@typescript-eslint/scope-manager'; -import { TSESTree } from '@typescript-eslint/types'; namespace Scope { - // ESLint defines global variables using the eslint-scope Variable class - // So a variable in the scope may be either of these - declare class ESLintScopeVariable { - public readonly defs: Definition[]; - public readonly identifiers: TSESTree.Identifier[]; - public readonly name: string; - public readonly references: Reference[]; - public readonly scope: Scope; - - /** - * Written to by ESLint. - * If this key exists, this variable is a global variable added by ESLint. - * If this is `true`, this variable can be assigned arbitrary values. - * If this is `false`, this variable is readonly. - */ - public writeable?: boolean; // note that this isn't a typo - ESlint uses this spelling here - - /** - * Written to by ESLint. - * This property is undefined if there are no globals directive comments. - * The array of globals directive comments which defined this global variable in the source code file. - */ - public eslintExplicitGlobal?: boolean; - - /** - * Written to by ESLint. - * The configured value in config files. This can be different from `variable.writeable` if there are globals directive comments. - */ - public eslintImplicitGlobalSetting?: 'readonly' | 'writable'; - - /** - * Written to by ESLint. - * If this key exists, it is a global variable added by ESLint. - * If `true`, this global variable was defined by a globals directive comment in the source code file. - */ - public eslintExplicitGlobalComments?: TSESTree.Comment[]; - } - export type ScopeManager = scopeManager.ScopeManager; export type Reference = scopeManager.Reference; - export type Variable = scopeManager.Variable | ESLintScopeVariable; + export type Variable = + | scopeManager.Variable + | scopeManager.ESLintScopeVariable; export type Scope = scopeManager.Scope; export const ScopeType = scopeManager.ScopeType; // TODO - in the next major, clean this up with a breaking change export type DefinitionType = scopeManager.Definition; export type Definition = scopeManager.Definition; export const DefinitionType = scopeManager.DefinitionType; + + export namespace Definitions { + export type CatchClauseDefinition = scopeManager.CatchClauseDefinition; + export type ClassNameDefinition = scopeManager.ClassNameDefinition; + export type FunctionNameDefinition = scopeManager.FunctionNameDefinition; + export type ImplicitGlobalVariableDefinition = scopeManager.ImplicitGlobalVariableDefinition; + export type ImportBindingDefinition = scopeManager.ImportBindingDefinition; + export type ParameterDefinition = scopeManager.ParameterDefinition; + export type TSEnumMemberDefinition = scopeManager.TSEnumMemberDefinition; + export type TSEnumNameDefinition = scopeManager.TSEnumNameDefinition; + export type TSModuleNameDefinition = scopeManager.TSModuleNameDefinition; + export type TypeDefinition = scopeManager.TypeDefinition; + export type VariableDefinition = scopeManager.VariableDefinition; + } + export namespace Scopes { + export type BlockScope = scopeManager.BlockScope; + export type CatchScope = scopeManager.CatchScope; + export type ClassScope = scopeManager.ClassScope; + export type ConditionalTypeScope = scopeManager.ConditionalTypeScope; + export type ForScope = scopeManager.ForScope; + export type FunctionExpressionNameScope = scopeManager.FunctionExpressionNameScope; + export type FunctionScope = scopeManager.FunctionScope; + export type FunctionTypeScope = scopeManager.FunctionTypeScope; + export type GlobalScope = scopeManager.GlobalScope; + export type MappedTypeScope = scopeManager.MappedTypeScope; + export type ModuleScope = scopeManager.ModuleScope; + export type SwitchScope = scopeManager.SwitchScope; + export type TSEnumScope = scopeManager.TSEnumScope; + export type TSModuleScope = scopeManager.TSModuleScope; + export type TypeScope = scopeManager.TypeScope; + export type WithScope = scopeManager.WithScope; + } } export { Scope }; diff --git a/packages/scope-manager/src/referencer/VisitorBase.ts b/packages/scope-manager/src/referencer/VisitorBase.ts index 0d37ac31e352..8e06863a229d 100644 --- a/packages/scope-manager/src/referencer/VisitorBase.ts +++ b/packages/scope-manager/src/referencer/VisitorBase.ts @@ -3,6 +3,7 @@ import { visitorKeys, VisitorKeys } from '@typescript-eslint/visitor-keys'; interface VisitorOptions { childVisitorKeys?: VisitorKeys | null; + visitChildrenEvenIfSelectorExists?: boolean; } function isObject(obj: unknown): obj is Record { @@ -18,8 +19,11 @@ type NodeVisitor = { abstract class VisitorBase { readonly #childVisitorKeys: VisitorKeys; + readonly #visitChildrenEvenIfSelectorExists: boolean; constructor(options: VisitorOptions) { this.#childVisitorKeys = options.childVisitorKeys ?? visitorKeys; + this.#visitChildrenEvenIfSelectorExists = + options.visitChildrenEvenIfSelectorExists ?? false; } /** @@ -29,13 +33,13 @@ abstract class VisitorBase { */ visitChildren( node: T | null | undefined, - excludeArr?: (keyof T)[], + excludeArr: (keyof T)[] = [], ): void { if (node == null || node.type == null) { return; } - const exclude = new Set(excludeArr) as Set; + const exclude = new Set(excludeArr.concat(['parent'])) as Set; const children = this.#childVisitorKeys[node.type] ?? Object.keys(node); for (const key of children) { if (exclude.has(key)) { @@ -69,7 +73,10 @@ abstract class VisitorBase { const visitor = (this as NodeVisitor)[node.type]; if (visitor) { - return visitor.call(this, node); + visitor.call(this, node); + if (!this.#visitChildrenEvenIfSelectorExists) { + return; + } } this.visitChildren(node); diff --git a/packages/types/src/ts-estree.ts b/packages/types/src/ts-estree.ts index 003b0fd2e736..4dd89c962bef 100644 --- a/packages/types/src/ts-estree.ts +++ b/packages/types/src/ts-estree.ts @@ -376,6 +376,12 @@ export type Expression = | TSUnaryExpression | YieldExpression; export type ForInitialiser = Expression | VariableDeclaration; +export type FunctionLike = + | ArrowFunctionExpression + | FunctionDeclaration + | FunctionExpression + | TSDeclareFunction + | TSEmptyBodyFunctionExpression; export type ImportClause = | ImportDefaultSpecifier | ImportNamespaceSpecifier From 665b6d4023fb9d821f348c39aefff0d7571a98bf Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 11:37:52 -0800 Subject: [PATCH 02/19] feat(eslint-plugin): [naming-convention] split `property` and `method` selectors into more granular `classXXX`, `objectLiteralXXX`, `typeXXX` (#2807) Fixes #1477 Closes #2802 This allows users to target different types of properties differently. Adds the following selectors (self explanatory - just breaking the selectors up): - `classProperty` - `objectLiteralProperty` - `typeProperty` - `classMethod` - `objectLiteralMethod` - `typeMethod` For backwards compatibility, also converts - `property` to a meta selector for `classProperty`, `objectLiteralProperty`, `typeProperty` - `method` to a meta selector for `classMethod`, `objectLiteralMethod`, `typeMethod` --- .../docs/rules/naming-convention.md | 40 ++- .../src/rules/naming-convention.ts | 149 +++++++--- .../tests/rules/naming-convention.test.ts | 262 +++++++++++++++--- 3 files changed, 361 insertions(+), 90 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index d57bc1c69daf..3c0f83256315 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -154,21 +154,27 @@ If these are provided, the identifier must start with one of the provided values ### Selector Options -- `selector` (see "Allowed Selectors, Modifiers and Types" below). +- `selector` allows you to specify what types of identifiers to target. - Accepts one or array of selectors to define an option block that applies to one or multiple selectors. - For example, if you provide `{ selector: ['variable', 'function'] }`, then it will apply the same option to variable and function nodes. + - See [Allowed Selectors, Modifiers and Types](#allowed-selectors-modifiers-and-types) below for the complete list of allowed selectors. - `modifiers` allows you to specify which modifiers to granularly apply to, such as the accessibility (`private`/`public`/`protected`), or if the thing is `static`, etc. - The name must match _all_ of the modifiers. - 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`). + - `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`). - The name must match _one_ of the types. - **_NOTE - Using this option will require that you lint with type information._** - For example, this lets you do things like enforce that `boolean` variables are prefixed with a verb. - - `boolean` matches any type assignable to `boolean | null | undefined` - - `string` matches any type assignable to `string | null | undefined` - - `number` matches any type assignable to `number | null | undefined` - - `array` matches any type assignable to `Array | null | undefined` - - `function` matches any type assignable to `Function | null | undefined` + - The following `types` are allowed: + - `boolean` matches any type assignable to `boolean | null | undefined` + - `string` matches any type assignable to `string | null | undefined` + - `number` matches any type assignable to `number | null | undefined` + - `array` matches any type assignable to `Array | null | undefined` + - `function` matches any type assignable to `Function | null | undefined` The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. @@ -202,13 +208,25 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - `parameter` - matches any function parameter. Does not match parameter properties. - Allowed `modifiers`: none. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. -- `property` - matches any object, class, or object type property. Does not match properties that have direct function expression or arrow function expression values. +- `classProperty` - matches any class property. Does not match properties that have direct function expression or arrow function expression values. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `objectLiteralProperty` - matches any object literal property. Does not match properties that have direct function expression or arrow function expression values. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `typeProperty` - matches any object type property. Does not match properties that have direct function expression or arrow function expression values. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `parameterProperty` - matches any parameter property. - Allowed `modifiers`: `private`, `protected`, `public`, `readonly`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. -- `method` - matches any object, class, or object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. +- `classMethod` - matches any class method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `objectLiteralMethod` - matches any object literal method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. +- `typeMethod` - matches any object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. - Allowed `types`: none. - `accessor` - matches any accessor. @@ -249,6 +267,12 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. - Allowed `modifiers`: `abstract`. - Allowed `types`: none. +- `property` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. +- `method` - matches the same as `classMethod`, `objectLiteralMethod`, `typeMethod`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `types`: none. ## Examples diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 295e8209e89b..9cc12967c9d2 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -41,21 +41,24 @@ enum Selectors { parameter = 1 << 2, // memberLike - property = 1 << 3, - parameterProperty = 1 << 4, - method = 1 << 5, - accessor = 1 << 6, - enumMember = 1 << 7, + parameterProperty = 1 << 3, + accessor = 1 << 4, + enumMember = 1 << 5, + classMethod = 1 << 6, + objectLiteralMethod = 1 << 7, + typeMethod = 1 << 8, + classProperty = 1 << 9, + objectLiteralProperty = 1 << 10, + typeProperty = 1 << 11, // typeLike - class = 1 << 8, - interface = 1 << 9, - typeAlias = 1 << 10, - enum = 1 << 11, - typeParameter = 1 << 12, + class = 1 << 12, + interface = 1 << 13, + typeAlias = 1 << 14, + enum = 1 << 15, + typeParameter = 1 << 17, } type SelectorsString = keyof typeof Selectors; -const SELECTOR_COUNT = util.getEnumNames(Selectors).length; enum MetaSelectors { default = -1, @@ -64,10 +67,14 @@ enum MetaSelectors { Selectors.function | Selectors.parameter, memberLike = 0 | - Selectors.property | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeProperty | Selectors.parameterProperty | Selectors.enumMember | - Selectors.method | + Selectors.classMethod | + Selectors.objectLiteralMethod | + Selectors.typeMethod | Selectors.accessor, typeLike = 0 | Selectors.class | @@ -75,6 +82,14 @@ enum MetaSelectors { Selectors.typeAlias | Selectors.enum | Selectors.typeParameter, + method = 0 | + Selectors.classMethod | + Selectors.objectLiteralMethod | + Selectors.typeProperty, + property = 0 | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeMethod, } type MetaSelectorsString = keyof typeof MetaSelectors; type IndividualAndMetaSelectorsString = SelectorsString | MetaSelectorsString; @@ -321,7 +336,23 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', 'abstract', ]), - ...selectorSchema('property', true, [ + ...selectorSchema('classProperty', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + ...selectorSchema('objectLiteralProperty', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + ...selectorSchema('typeProperty', true, [ 'private', 'protected', 'public', @@ -335,6 +366,36 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'readonly', ]), + ...selectorSchema('property', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + ]), + + ...selectorSchema('classMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('objectLiteralMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), + ...selectorSchema('typeMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + ]), ...selectorSchema('method', false, [ 'private', 'protected', @@ -584,7 +645,7 @@ export default util.createRule({ node: TSESTree.PropertyNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); - handleMember(validators.property, node, modifiers); + handleMember(validators.objectLiteralProperty, node, modifiers); }, ':matches(ClassProperty, TSAbstractClassProperty)[computed = false][value.type != "ArrowFunctionExpression"][value.type != "FunctionExpression"][value.type != "TSEmptyBodyFunctionExpression"]'( @@ -593,7 +654,7 @@ export default util.createRule({ | TSESTree.TSAbstractClassPropertyNonComputedName, ): void { const modifiers = getMemberModifiers(node); - handleMember(validators.property, node, modifiers); + handleMember(validators.classProperty, node, modifiers); }, 'TSPropertySignature[computed = false]'( @@ -604,7 +665,7 @@ export default util.createRule({ modifiers.add(Modifiers.readonly); } - handleMember(validators.property, node, modifiers); + handleMember(validators.typeProperty, node, modifiers); }, // #endregion property @@ -615,14 +676,13 @@ export default util.createRule({ 'Property[computed = false][kind = "init"][value.type = "ArrowFunctionExpression"]', 'Property[computed = false][kind = "init"][value.type = "FunctionExpression"]', 'Property[computed = false][kind = "init"][value.type = "TSEmptyBodyFunctionExpression"]', - 'TSMethodSignature[computed = false]', ].join(', ')]( node: | TSESTree.PropertyNonComputedName | TSESTree.TSMethodSignatureNonComputedName, ): void { const modifiers = new Set([Modifiers.public]); - handleMember(validators.method, node, modifiers); + handleMember(validators.objectLiteralMethod, node, modifiers); }, [[ @@ -638,7 +698,14 @@ export default util.createRule({ | TSESTree.TSAbstractMethodDefinitionNonComputedName, ): void { const modifiers = getMemberModifiers(node); - handleMember(validators.method, node, modifiers); + handleMember(validators.classMethod, node, modifiers); + }, + + 'TSMethodSignature[computed = false]'( + node: TSESTree.TSMethodSignatureNonComputedName, + ): void { + const modifiers = new Set([Modifiers.public]); + handleMember(validators.typeMethod, node, modifiers); }, // #endregion method @@ -851,21 +918,20 @@ function createValidator( return b.modifierWeight - a.modifierWeight; } - /* - meta selectors will always be larger numbers than the normal selectors they contain, as they are the sum of all - of the selectors that they contain. - to give normal selectors a higher priority, shift them all SELECTOR_COUNT bits to the left before comparison, so - they are instead always guaranteed to be larger than the meta selectors. - */ - const aSelector = isMetaSelector(a.selector) - ? a.selector - : a.selector << SELECTOR_COUNT; - const bSelector = isMetaSelector(b.selector) - ? b.selector - : b.selector << SELECTOR_COUNT; + const aIsMeta = isMetaSelector(a.selector); + const bIsMeta = isMetaSelector(b.selector); + // non-meta selectors should go ahead of meta selectors + if (aIsMeta && !bIsMeta) { + return 1; + } + if (!aIsMeta && bIsMeta) { + return -1; + } + + // both aren't meta selectors // sort descending - the meta selectors are "least important" - return bSelector - aSelector; + return b.selector - a.selector; }); return ( @@ -1314,13 +1380,14 @@ function normalizeOption(option: Selector): NormalizedSelector[] { ? option.selector : [option.selector]; - const selectorsAllowedToHaveTypes: (Selectors | MetaSelectors)[] = [ - Selectors.variable, - Selectors.parameter, - Selectors.property, - Selectors.parameterProperty, - Selectors.accessor, - ]; + const selectorsAllowedToHaveTypes = + Selectors.variable | + Selectors.parameter | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeProperty | + Selectors.parameterProperty | + Selectors.accessor; const config: NormalizedSelector[] = []; selectors @@ -1328,7 +1395,7 @@ function normalizeOption(option: Selector): NormalizedSelector[] { isMetaSelector(selector) ? MetaSelectors[selector] : Selectors[selector], ) .forEach(selector => - selectorsAllowedToHaveTypes.includes(selector) + (selectorsAllowedToHaveTypes & selector) !== 0 ? config.push({ selector: selector, ...normalizedOption }) : config.push({ selector: selector, diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index c57dfc984d47..adf93fa14905 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -214,7 +214,9 @@ function createInvalidTestCases( ...(selector !== 'default' && selector !== 'variableLike' && selector !== 'memberLike' && - selector !== 'typeLike' + selector !== 'typeLike' && + selector !== 'property' && + selector !== 'method' ? { data: { type: selectorTypeToMessageString(selector), @@ -444,12 +446,6 @@ const cases: Cases = [ // #region property { code: [ - 'const ignored = { % };', - 'const ignored = { "%": 1 };', - 'interface Ignored { % }', - 'interface Ignored { "%": string }', - 'type Ignored = { % }', - 'type Ignored = { "%": string }', 'class Ignored { private % }', 'class Ignored { private "%" = 1 }', 'class Ignored { private readonly % = 1 }', @@ -459,16 +455,24 @@ const cases: Cases = [ 'class Ignored { declare % }', ], options: { - selector: 'property', + selector: 'classProperty', + }, + }, + { + code: ['const ignored = { % };', 'const ignored = { "%": 1 };'], + options: { + selector: 'objectLiteralProperty', }, }, { code: [ - 'class Ignored { abstract private static readonly % = 1; ignoredDueToModifiers = 1; }', + 'interface Ignored { % }', + 'interface Ignored { "%": string }', + 'type Ignored = { % }', + 'type Ignored = { "%": string }', ], options: { - selector: 'property', - modifiers: ['static', 'readonly'], + selector: 'typeProperty', }, }, // #endregion property @@ -496,13 +500,6 @@ const cases: Cases = [ // #region method { code: [ - 'const ignored = { %() {} };', - 'const ignored = { "%"() {} };', - 'const ignored = { %: () => {} };', - 'interface Ignored { %(): string }', - 'interface Ignored { "%"(): string }', - 'type Ignored = { %(): string }', - 'type Ignored = { "%"(): string }', 'class Ignored { private %() {} }', 'class Ignored { private "%"() {} }', 'class Ignored { private readonly %() {} }', @@ -513,16 +510,28 @@ const cases: Cases = [ 'class Ignored { declare %() }', ], options: { - selector: 'method', + selector: 'classMethod', + }, + }, + { + code: [ + 'const ignored = { %() {} };', + 'const ignored = { "%"() {} };', + 'const ignored = { %: () => {} };', + ], + options: { + selector: 'objectLiteralMethod', }, }, { code: [ - 'class Ignored { abstract private static %() {}; ignoredDueToModifiers() {}; }', + 'interface Ignored { %(): string }', + 'interface Ignored { "%"(): string }', + 'type Ignored = { %(): string }', + 'type Ignored = { "%"(): string }', ], options: { - selector: 'method', - modifiers: ['abstract', 'static'], + selector: 'typeMethod', }, }, // #endregion method @@ -540,15 +549,6 @@ const cases: Cases = [ selector: 'accessor', }, }, - { - code: [ - 'class Ignored { private static get %() {}; get ignoredDueToModifiers() {}; }', - ], - options: { - selector: 'accessor', - modifiers: ['private', 'static'], - }, - }, // #endregion accessor // #region enumMember @@ -567,13 +567,6 @@ const cases: Cases = [ selector: 'class', }, }, - { - code: ['abstract class % {}; class ignoredDueToModifier {}'], - options: { - selector: 'class', - modifiers: ['abstract'], - }, - }, // #endregion class // #region interface @@ -914,12 +907,103 @@ ruleTester.run('naming-convention', rule, { export const { OtherConstant: otherConstant } = SomeClass; `, - parserOptions, options: [ { selector: 'property', format: ['PascalCase'] }, { selector: 'variable', format: ['camelCase'] }, ], }, + { + code: ` + class Ignored { + private static abstract readonly some_name = 1; + IgnoredDueToModifiers = 1; + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classProperty', + format: ['snake_case'], + modifiers: ['static', 'readonly'], + }, + ], + }, + { + code: ` + class Ignored { + constructor(private readonly some_name, IgnoredDueToModifiers) {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'parameterProperty', + format: ['snake_case'], + modifiers: ['readonly'], + }, + ], + }, + { + code: ` + class Ignored { + private static abstract some_name() {} + IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classMethod', + format: ['snake_case'], + modifiers: ['abstract', 'static'], + }, + ], + }, + { + code: ` + class Ignored { + private static get some_name() {} + get IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'accessor', + format: ['snake_case'], + modifiers: ['private', 'static'], + }, + ], + }, + { + code: ` + abstract class some_name {} + class IgnoredDueToModifier {} + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'class', + format: ['snake_case'], + modifiers: ['abstract'], + }, + ], + }, ], invalid: [ { @@ -1239,8 +1323,7 @@ ruleTester.run('naming-convention', rule, { line: 3, messageId: 'doesNotMatchFormat', data: { - // eslint-disable-next-line @typescript-eslint/internal/prefer-ast-types-enum - type: 'Property', + type: 'Object Literal Property', name: 'Property Name', formats: 'strictCamelCase', }, @@ -1331,5 +1414,102 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + class Ignored { + private static abstract readonly some_name = 1; + IgnoredDueToModifiers = 1; + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classProperty', + format: ['UPPER_CASE'], + modifiers: ['static', 'readonly'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + class Ignored { + constructor(private readonly some_name, IgnoredDueToModifiers) {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'parameterProperty', + format: ['UPPER_CASE'], + modifiers: ['readonly'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + class Ignored { + private static abstract some_name() {} + IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'classMethod', + format: ['UPPER_CASE'], + modifiers: ['abstract', 'static'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + class Ignored { + private static get some_name() {} + get IgnoredDueToModifiers() {} + } + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'accessor', + format: ['UPPER_CASE'], + modifiers: ['private', 'static'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, + { + code: ` + abstract class some_name {} + class IgnoredDueToModifier {} + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'class', + format: ['UPPER_CASE'], + modifiers: ['abstract'], + }, + ], + errors: [{ messageId: 'doesNotMatchFormat' }], + }, ], }); From fb254a1036b89f9b78f927d607358e65e81a2250 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 11:58:16 -0800 Subject: [PATCH 03/19] feat(eslint-plugin): [naming-convention] add modifiers `exported`, `global`, 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}`). --- .../docs/rules/naming-convention.md | 35 +- .../src/rules/naming-convention.ts | 204 ++++++++---- .../tests/rules/naming-convention.test.ts | 312 ++++++++++++++++++ 3 files changed, 474 insertions(+), 77 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 3c0f83256315..089bf6d3f816 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -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`). @@ -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. @@ -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. @@ -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 diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 9cc12967c9d2..597a7492428a 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -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'; @@ -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; @@ -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, [ @@ -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), ], }, @@ -550,22 +566,40 @@ export default util.createRule({ if (!validator) { return; } + const identifiers = getIdentifiersFromPattern(node.id); - const identifiers: TSESTree.Identifier[] = []; - getIdentifiersFromPattern(node.id, identifiers); - - const modifiers = new Set(); + const baseModifiers = new Set(); 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); }); }, @@ -584,7 +618,17 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + // 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 @@ -608,8 +652,7 @@ export default util.createRule({ return; } - const identifiers: TSESTree.Identifier[] = []; - getIdentifiersFromPattern(param, identifiers); + const identifiers = getIdentifiersFromPattern(param); identifiers.forEach(i => { validator(i); @@ -629,8 +672,7 @@ export default util.createRule({ const modifiers = getMemberModifiers(node); - const identifiers: TSESTree.Identifier[] = []; - getIdentifiersFromPattern(node.parameter, identifiers); + const identifiers = getIdentifiersFromPattern(node.parameter); identifiers.forEach(i => { validator(i, modifiers); @@ -765,6 +807,11 @@ export default util.createRule({ 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); }, @@ -778,7 +825,12 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + if (isExported(node, node.id.name, context.getScope())) { + modifiers.add(Modifiers.exported); + } + + validator(node.id, modifiers); }, // #endregion interface @@ -791,7 +843,12 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + if (isExported(node, node.id.name, context.getScope())) { + modifiers.add(Modifiers.exported); + } + + validator(node.id, modifiers); }, // #endregion typeAlias @@ -804,7 +861,13 @@ export default util.createRule({ return; } - validator(node.id); + const modifiers = new Set(); + // 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 @@ -829,55 +892,54 @@ export default util.createRule({ 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 = ( diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index adf93fa14905..aed9bf9c47d1 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -912,6 +912,177 @@ ruleTester.run('naming-convention', rule, { { selector: 'variable', format: ['camelCase'] }, ], }, + { + code: ` + const camelCaseVar = 1; + enum camelCaseEnum {} + class camelCaseClass {} + function camelCaseFunction() {} + interface camelCaseInterface {} + type camelCaseType = {}; + export const PascalCaseVar = 1; + export enum PascalCaseEnum {} + export class PascalCaseClass {} + export function PascalCaseFunction() {} + export interface PascalCaseInterface {} + export type PascalCaseType = {}; + `, + options: [ + { selector: 'default', format: ['camelCase'] }, + { + selector: 'variable', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['PascalCase'], + modifiers: ['exported'], + }, + ], + }, + { + code: ` + const camelCaseVar = 1; + enum camelCaseEnum {} + class camelCaseClass {} + function camelCaseFunction() {} + interface camelCaseInterface {} + type camelCaseType = {}; + const PascalCaseVar = 1; + enum PascalCaseEnum {} + class PascalCaseClass {} + function PascalCaseFunction() {} + interface PascalCaseInterface {} + type PascalCaseType = {}; + export { + PascalCaseVar, + PascalCaseEnum, + PascalCaseClass, + PascalCaseFunction, + PascalCaseInterface, + PascalCaseType, + }; + `, + options: [ + { selector: 'default', format: ['camelCase'] }, + { + selector: 'variable', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['PascalCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['PascalCase'], + modifiers: ['exported'], + }, + ], + }, + { + code: ` + { + const camelCaseVar = 1; + function camelCaseFunction() {} + declare function camelCaseDeclaredFunction() { + }; + } + const PascalCaseVar = 1; + function PascalCaseFunction() {} + declare function PascalCaseDeclaredFunction() { + }; + `, + options: [ + { selector: 'default', format: ['camelCase'] }, + { + selector: 'variable', + format: ['PascalCase'], + modifiers: ['global'], + }, + { + selector: 'function', + format: ['PascalCase'], + modifiers: ['global'], + }, + ], + }, + { + code: ` + const { some_name1 } = {}; + const { ignore: IgnoredDueToModifiers1 } = {}; + const { some_name2 = 2 } = {}; + const IgnoredDueToModifiers2 = 1; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'variable', + format: ['snake_case'], + modifiers: ['destructured'], + }, + ], + }, + { + code: ` + const { some_name1 } = {}; + const { ignore: IgnoredDueToModifiers1 } = {}; + const { some_name2 = 2 } = {}; + const IgnoredDueToModifiers2 = 1; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'variable', + format: null, + modifiers: ['destructured'], + }, + ], + }, { code: ` class Ignored { @@ -1414,6 +1585,147 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + export const PascalCaseVar = 1; + export enum PascalCaseEnum {} + export class PascalCaseClass {} + export function PascalCaseFunction() {} + export interface PascalCaseInterface {} + export type PascalCaseType = {}; + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'variable', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['camelCase'], + modifiers: ['exported'], + }, + ], + errors: Array(6).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + const PascalCaseVar = 1; + enum PascalCaseEnum {} + class PascalCaseClass {} + function PascalCaseFunction() {} + interface PascalCaseInterface {} + type PascalCaseType = {}; + export { + PascalCaseVar, + PascalCaseEnum, + PascalCaseClass, + PascalCaseFunction, + PascalCaseInterface, + PascalCaseType, + }; + `, + options: [ + { selector: 'default', format: ['snake_case'] }, + { + selector: 'variable', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'function', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'class', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'interface', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'typeAlias', + format: ['camelCase'], + modifiers: ['exported'], + }, + { + selector: 'enum', + format: ['camelCase'], + modifiers: ['exported'], + }, + ], + errors: Array(6).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + const PascalCaseVar = 1; + function PascalCaseFunction() {} + declare function PascalCaseDeclaredFunction() { + }; + `, + options: [ + { selector: 'default', format: ['snake_case'] }, + { + selector: 'variable', + format: ['camelCase'], + modifiers: ['global'], + }, + { + selector: 'function', + format: ['camelCase'], + modifiers: ['global'], + }, + ], + errors: Array(3).fill({ messageId: 'doesNotMatchFormat' }), + }, + { + code: ` + const { some_name1 } = {}; + const { ignore: IgnoredDueToModifiers1 } = {}; + const { some_name2 = 2 } = {}; + const IgnoredDueToModifiers2 = 1; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'variable', + format: ['UPPER_CASE'], + modifiers: ['destructured'], + }, + ], + errors: Array(2).fill({ messageId: 'doesNotMatchFormat' }), + }, { code: ` class Ignored { From 14bdc2ee02636cf89464ee32ebbb0ed929eee902 Mon Sep 17 00:00:00 2001 From: Trivikram Kamat <16024985+trivikr@users.noreply.github.com> Date: Tue, 24 Nov 2020 12:05:42 -0800 Subject: [PATCH 04/19] docs: update supported TS versions to include 4.1 (#2806) The support for TS 4.1 features was added in #2748 --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index e46c5eb441c2..93cacd404622 100644 --- a/README.md +++ b/README.md @@ -233,7 +233,7 @@ The latest version under the `canary` tag **(latest commit to master)** is: ## Supported TypeScript Version -**The version range of TypeScript currently supported by this parser is `>=3.3.1 <4.1.0`.** +**The version range of TypeScript currently supported by this parser is `>=3.3.1 <4.2.0`.** These versions are what we test against. From fa6849245ca55ca407dc031afbad456f2925a8e9 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 12:26:24 -0800 Subject: [PATCH 05/19] fix(scope-manager): fix assertion assignments not being marked as write references (#2809) Fixes #2804 --- .../tests/eslint-rules/prefer-const.test.ts | 24 +++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 21 ++++++- .../src/referencer/Referencer.ts | 22 +++++-- .../assignment/angle-bracket-assignment.ts | 2 + .../angle-bracket-assignment.ts.shot | 62 +++++++++++++++++++ .../assignment/as-assignment.ts | 2 + .../assignment/as-assignment.ts.shot | 62 +++++++++++++++++++ .../assignment/non-null-assignment.ts | 2 + .../assignment/non-null-assignment.ts.shot | 62 +++++++++++++++++++ 9 files changed, 253 insertions(+), 6 deletions(-) create mode 100644 packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts create mode 100644 packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot diff --git a/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts b/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts new file mode 100644 index 000000000000..a051ebb68610 --- /dev/null +++ b/packages/eslint-plugin/tests/eslint-rules/prefer-const.test.ts @@ -0,0 +1,24 @@ +import rule from 'eslint/lib/rules/prefer-const'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('prefer-const', rule, { + valid: [ + ` +let x: number | undefined = 1; +x! += 1; + `, + ` +let x: number | undefined = 1; +(x) += 1; + `, + ` +let x: number | undefined = 1; +(x as number) += 1; + `, + ], + invalid: [], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 93b75e3a956d..e25ce02f88bd 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -797,7 +797,7 @@ declare module 'eslint/lib/rules/space-infix-ops' { 'missingSpace', [ { - int32Hint: boolean; + int32Hint?: boolean; }, ], { @@ -812,6 +812,25 @@ declare module 'eslint/lib/rules/space-infix-ops' { export = rule; } +declare module 'eslint/lib/rules/prefer-const' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'useConst', + [ + { + destructuring?: 'any' | 'all'; + ignoreReadBeforeAssign?: boolean; + }, + ], + { + 'Program:exit'(node: TSESTree.Program): void; + VariableDeclaration(node: TSESTree.VariableDeclaration): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/utils/ast-utils' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; diff --git a/packages/scope-manager/src/referencer/Referencer.ts b/packages/scope-manager/src/referencer/Referencer.ts index 56c974055bce..a36dcfc9c1a0 100644 --- a/packages/scope-manager/src/referencer/Referencer.ts +++ b/packages/scope-manager/src/referencer/Referencer.ts @@ -386,10 +386,22 @@ class Referencer extends Visitor { } protected AssignmentExpression(node: TSESTree.AssignmentExpression): void { - if (PatternVisitor.isPattern(node.left)) { + let left = node.left; + switch (left.type) { + case AST_NODE_TYPES.TSAsExpression: + case AST_NODE_TYPES.TSTypeAssertion: + // explicitly visit the type annotation + this.visit(left.typeAnnotation); + // intentional fallthrough + case AST_NODE_TYPES.TSNonNullExpression: + // unwrap the expression + left = left.expression; + } + + if (PatternVisitor.isPattern(left)) { if (node.operator === '=') { this.visitPattern( - node.left, + left, (pattern, info) => { const maybeImplicitGlobal = !this.currentScope().isStrict ? { @@ -413,15 +425,15 @@ class Referencer extends Visitor { }, { processRightHandNodes: true }, ); - } else if (node.left.type === AST_NODE_TYPES.Identifier) { + } else if (left.type === AST_NODE_TYPES.Identifier) { this.currentScope().referenceValue( - node.left, + left, ReferenceFlag.ReadWrite, node.right, ); } } else { - this.visit(node.left); + this.visit(left); } this.visit(node.right); } diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts new file mode 100644 index 000000000000..af751891afe4 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts @@ -0,0 +1,2 @@ +let x: number | undefined = 1; +(x) += 1; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot new file mode 100644 index 000000000000..b5ee6ad87b09 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/angle-bracket-assignment.ts.shot @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-assertion assignment angle-bracket-assignment 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$2, + }, + Reference$2 { + identifier: Identifier<"x">, + init: false, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$3, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "x" => Variable$2, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts new file mode 100644 index 000000000000..ed2cc7ef0f5a --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts @@ -0,0 +1,2 @@ +let x: number | undefined = 1; +(x as number) += 1; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot new file mode 100644 index 000000000000..3baa60792938 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/as-assignment.ts.shot @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-assertion assignment as-assignment 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$2, + }, + Reference$2 { + identifier: Identifier<"x">, + init: false, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$3, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "x" => Variable$2, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + ], + }, + ], +} +`; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts new file mode 100644 index 000000000000..a64362f7b620 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts @@ -0,0 +1,2 @@ +let x: number | undefined = 1; +x! += 1; diff --git a/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot new file mode 100644 index 000000000000..6aa2a23aecb7 --- /dev/null +++ b/packages/scope-manager/tests/fixtures/type-assertion/assignment/non-null-assignment.ts.shot @@ -0,0 +1,62 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`type-assertion assignment non-null-assignment 1`] = ` +ScopeManager { + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2 { + defs: Array [ + VariableDefinition$1 { + name: Identifier<"x">, + node: VariableDeclarator$1, + }, + ], + name: "x", + references: Array [ + Reference$1 { + identifier: Identifier<"x">, + init: true, + isRead: false, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$2, + }, + Reference$2 { + identifier: Identifier<"x">, + init: false, + isRead: true, + isTypeReference: false, + isValueReference: true, + isWrite: true, + resolved: Variable$2, + writeExpr: Literal$3, + }, + ], + isValueVariable: true, + isTypeVariable: false, + }, + ], + scopes: Array [ + GlobalScope$1 { + block: Program$4, + isStrict: false, + references: Array [ + Reference$1, + Reference$2, + ], + set: Map { + "const" => ImplicitGlobalConstTypeVariable, + "x" => Variable$2, + }, + type: "global", + upper: null, + variables: Array [ + ImplicitGlobalConstTypeVariable, + Variable$2, + ], + }, + ], +} +`; From 6a06944e60677a402e7ab432e6ac1209737a7027 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 13:27:08 -0800 Subject: [PATCH 06/19] feat(eslint-plugin): [naming-convention] add modifier `unused` (#2810) --- .../docs/rules/naming-convention.md | 21 ++-- .../src/rules/naming-convention.ts | 105 +++++++++++++++--- .../tests/rules/naming-convention.test.ts | 67 +++++++++++ 3 files changed, 167 insertions(+), 26 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 089bf6d3f816..70722707b872 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -167,6 +167,7 @@ If these are provided, the identifier must start with one of the provided values - 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. + - `unused` - matches anything that is not used. - `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`). @@ -204,13 +205,13 @@ 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`, `destructured`, `global`, `exported`. + - Allowed `modifiers`: `const`, `destructured`, `global`, `exported`, `unused`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `function` - matches any named function declaration or named function expression. - - Allowed `modifiers`: `global`, `exported`. + - Allowed `modifiers`: `global`, `exported`, `unused`. - Allowed `types`: none. - `parameter` - matches any function parameter. Does not match parameter properties. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `unused`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classProperty` - matches any class property. Does not match properties that have direct function expression or arrow function expression values. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. @@ -240,19 +241,19 @@ 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`, `exported`. + - Allowed `modifiers`: `abstract`, `exported`, `unused`. - Allowed `types`: none. - `interface` - matches any interface declaration. - - Allowed `modifiers`: `exported`. + - Allowed `modifiers`: `exported`, `unused`. - Allowed `types`: none. - `typeAlias` - matches any type alias declaration. - - Allowed `modifiers`: `exported`. + - Allowed `modifiers`: `exported`, `unused`. - Allowed `types`: none. - `enum` - matches any enum declaration. - - Allowed `modifiers`: `exported`. + - Allowed `modifiers`: `exported`, `unused`. - Allowed `types`: none. - `typeParameter` - matches any generic type parameter declaration. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `unused`. - Allowed `types`: none. ##### Group Selectors @@ -263,13 +264,13 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. - Allowed `types`: none. - `variableLike` - matches the same as `variable`, `function` and `parameter`. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `unused`. - Allowed `types`: none. - `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. - Allowed `types`: none. - `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. - - Allowed `modifiers`: `abstract`. + - Allowed `modifiers`: `abstract`, `unused`. - Allowed `types`: none. - `property` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`. - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 597a7492428a..2b5dac564e11 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -113,6 +113,8 @@ enum Modifiers { global = 1 << 8, // things that are exported exported = 1 << 9, + // things that are unused + unused = 1 << 10, } type ModifiersString = keyof typeof Modifiers; @@ -334,15 +336,16 @@ const SCHEMA: JSONSchema.JSONSchema4 = { selectorsSchema(), ...selectorSchema('default', false, util.getEnumNames(Modifiers)), - ...selectorSchema('variableLike', false), + ...selectorSchema('variableLike', false, ['unused']), ...selectorSchema('variable', true, [ 'const', 'destructured', 'global', 'exported', + 'unused', ]), - ...selectorSchema('function', false, ['global', 'exported']), - ...selectorSchema('parameter', true), + ...selectorSchema('function', false, ['global', 'exported', 'unused']), + ...selectorSchema('parameter', true, ['unused']), ...selectorSchema('memberLike', false, [ 'private', @@ -428,12 +431,12 @@ const SCHEMA: JSONSchema.JSONSchema4 = { ]), ...selectorSchema('enumMember', 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), + ...selectorSchema('typeLike', false, ['abstract', 'exported', 'unused']), + ...selectorSchema('class', false, ['abstract', 'exported', 'unused']), + ...selectorSchema('interface', false, ['exported', 'unused']), + ...selectorSchema('typeAlias', false, ['exported', 'unused']), + ...selectorSchema('enum', false, ['exported', 'unused']), + ...selectorSchema('typeParameter', false, ['unused']), ], }, additionalItems: false, @@ -558,6 +561,27 @@ export default util.createRule({ return modifiers; } + const unusedVariables = util.collectUnusedVariables(context); + function isUnused( + name: string, + initialScope: TSESLint.Scope.Scope | null = context.getScope(), + ): boolean { + let variable: TSESLint.Scope.Variable | null = null; + let scope: TSESLint.Scope.Scope | null = initialScope; + while (scope) { + variable = scope.set.get(name) ?? null; + if (variable) { + break; + } + scope = scope.upper; + } + if (!variable) { + return false; + } + + return unusedVariables.has(variable); + } + return { // #region variable @@ -574,6 +598,7 @@ export default util.createRule({ if (parent.kind === 'const') { baseModifiers.add(Modifiers.const); } + if (isGlobal(context.getScope())) { baseModifiers.add(Modifiers.global); } @@ -581,6 +606,7 @@ export default util.createRule({ identifiers.forEach(id => { const modifiers = new Set(baseModifiers); + if ( // `const { x }` // does not match `const { x: y }` @@ -599,6 +625,10 @@ export default util.createRule({ modifiers.add(Modifiers.exported); } + if (isUnused(id.name)) { + modifiers.add(Modifiers.unused); + } + validator(id, modifiers); }); }, @@ -621,13 +651,19 @@ export default util.createRule({ const modifiers = new Set(); // 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); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -655,7 +691,13 @@ export default util.createRule({ const identifiers = getIdentifiersFromPattern(param); identifiers.forEach(i => { - validator(i); + const modifiers = new Set(); + + if (isUnused(i.name)) { + modifiers.add(Modifiers.unused); + } + + validator(i, modifiers); }); }); }, @@ -803,15 +845,21 @@ export default util.createRule({ } const modifiers = new Set(); + // classes create their own nested scope + const scope = context.getScope().upper; + if (node.abstract) { modifiers.add(Modifiers.abstract); } - // classes create their own nested scope - if (isExported(node, id.name, context.getScope().upper)) { + if (isExported(node, id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(id, modifiers); }, @@ -826,10 +874,16 @@ export default util.createRule({ } const modifiers = new Set(); - if (isExported(node, node.id.name, context.getScope())) { + const scope = context.getScope(); + + if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -844,10 +898,16 @@ export default util.createRule({ } const modifiers = new Set(); - if (isExported(node, node.id.name, context.getScope())) { + const scope = context.getScope(); + + if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -863,10 +923,16 @@ export default util.createRule({ const modifiers = new Set(); // enums create their own nested scope - if (isExported(node, node.id.name, context.getScope().upper)) { + const scope = context.getScope().upper; + + if (isExported(node, node.id.name, scope)) { modifiers.add(Modifiers.exported); } + if (isUnused(node.id.name, scope)) { + modifiers.add(Modifiers.unused); + } + validator(node.id, modifiers); }, @@ -882,7 +948,14 @@ export default util.createRule({ return; } - validator(node.name); + const modifiers = new Set(); + const scope = context.getScope(); + + if (isUnused(node.name.name, scope)) { + modifiers.add(Modifiers.unused); + } + + validator(node.name, modifiers); }, // #endregion typeParameter diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index aed9bf9c47d1..28fdc4c060b6 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -1175,6 +1175,46 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + const UnusedVar = 1; + function UnusedFunc( + // this line is intentionally broken out + UnusedParam: string, + ) {} + class UnusedClass {} + interface UnusedInterface {} + type UnusedType< + // this line is intentionally broken out + UnusedTypeParam + > = {}; + + export const used_var = 1; + export function used_func( + // this line is intentionally broken out + used_param: string, + ) { + return used_param; + } + export class used_class {} + export interface used_interface {} + export type used_type< + // this line is intentionally broken out + used_typeparam + > = used_typeparam; + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'default', + modifiers: ['unused'], + format: ['PascalCase'], + }, + ], + }, ], invalid: [ { @@ -1823,5 +1863,32 @@ ruleTester.run('naming-convention', rule, { ], errors: [{ messageId: 'doesNotMatchFormat' }], }, + { + code: ` + const UnusedVar = 1; + function UnusedFunc( + // this line is intentionally broken out + UnusedParam: string, + ) {} + class UnusedClass {} + interface UnusedInterface {} + type UnusedType< + // this line is intentionally broken out + UnusedTypeParam + > = {}; + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'default', + modifiers: ['unused'], + format: ['snake_case'], + }, + ], + errors: Array(7).fill({ messageId: 'doesNotMatchFormat' }), + }, ], }); From dd0576a66c34810bc60e0958948c9a8104a3f1a3 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 17:20:54 -0800 Subject: [PATCH 07/19] feat(eslint-plugin): [naming-convention] add `requireDouble`, `allowDouble`, `allowSingleOrDouble` options for underscores (#2812) --- .../docs/rules/naming-convention.md | 23 +++- .../src/rules/naming-convention.ts | 114 +++++++++++++---- .../tests/rules/naming-convention.test.ts | 116 +++++++++++++++++- 3 files changed, 223 insertions(+), 30 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 70722707b872..0d180bbf7ae7 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -33,8 +33,20 @@ type Options = { regex: string; match: boolean; }; - leadingUnderscore?: 'forbid' | 'allow' | 'require'; - trailingUnderscore?: 'forbid' | 'allow' | 'require'; + leadingUnderscore?: + | 'forbid' + | 'require' + | 'requireDouble' + | 'allow' + | 'allowDouble' + | 'allowSingleOrDouble'; + trailingUnderscore?: + | 'forbid' + | 'require' + | 'requireDouble' + | 'allow' + | 'allowDouble' + | 'allowSingleOrDouble'; prefix?: string[]; suffix?: string[]; @@ -141,8 +153,11 @@ Alternatively, `filter` accepts a regular expression (anything accepted into `ne The `leadingUnderscore` / `trailingUnderscore` options control whether leading/trailing underscores are considered valid. Accepts one of the following values: - `forbid` - a leading/trailing underscore is not allowed at all. -- `allow` - existence of a leading/trailing underscore is not explicitly enforced. -- `require` - a leading/trailing underscore must be included. +- `require` - a single leading/trailing underscore must be included. +- `requireDouble` - two leading/trailing underscores must be included. +- `allow` - existence of a single leading/trailing underscore is not explicitly enforced. +- `allowDouble` - existence of a double leading/trailing underscore is not explicitly enforced. +- `allowSingleOrDouble` - existence of a single or a double leading/trailing underscore is not explicitly enforced. #### `prefix` / `suffix` diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 2b5dac564e11..fe025389af9c 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -19,19 +19,24 @@ type MessageIds = // #region Options Type Config enum PredefinedFormats { - camelCase = 1 << 0, - strictCamelCase = 1 << 1, - PascalCase = 1 << 2, - StrictPascalCase = 1 << 3, - snake_case = 1 << 4, - UPPER_CASE = 1 << 5, + camelCase = 1, + strictCamelCase, + PascalCase, + StrictPascalCase, + snake_case, + UPPER_CASE, } type PredefinedFormatsString = keyof typeof PredefinedFormats; enum UnderscoreOptions { - forbid = 1 << 0, - allow = 1 << 1, - require = 1 << 2, + forbid = 1, + allow, + require, + + // special cases as it's common practice to use double underscore + requireDouble, + allowDouble, + allowSingleOrDouble, } type UnderscoreOptionsString = keyof typeof UnderscoreOptions; @@ -483,7 +488,7 @@ export default util.createRule({ unexpectedUnderscore: '{{type}} name `{{name}}` must not have a {{position}} underscore.', missingUnderscore: - '{{type}} name `{{name}}` must have a {{position}} underscore.', + '{{type}} name `{{name}}` must have {{count}} {{position}} underscore(s).', missingAffix: '{{type}} name `{{name}}` must have one of the following {{position}}es: {{affixes}}', satisfyCustom: @@ -1143,6 +1148,7 @@ function createValidator( processedName, position, custom, + count, }: { affixes?: string[]; formats?: PredefinedFormats[]; @@ -1150,12 +1156,14 @@ function createValidator( processedName?: string; position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; custom?: NonNullable; + count?: 'one' | 'two'; }): Record { return { type: selectorTypeToMessageString(type), name: originalName, processedName, position, + count, affixes: affixes?.join(', '), formats: formats?.map(f => PredefinedFormats[f]).join(', '), regex: custom?.regex?.toString(), @@ -1186,47 +1194,107 @@ function createValidator( return name; } - const hasUnderscore = - position === 'leading' ? name.startsWith('_') : name.endsWith('_'); - const trimUnderscore = + const hasSingleUnderscore = + position === 'leading' + ? (): boolean => name.startsWith('_') + : (): boolean => name.endsWith('_'); + const trimSingleUnderscore = position === 'leading' ? (): string => name.slice(1) : (): string => name.slice(0, -1); + const hasDoubleUnderscore = + position === 'leading' + ? (): boolean => name.startsWith('__') + : (): boolean => name.endsWith('__'); + const trimDoubleUnderscore = + position === 'leading' + ? (): string => name.slice(2) + : (): string => name.slice(0, -2); + switch (option) { - case UnderscoreOptions.allow: - // no check - the user doesn't care if it's there or not - break; + // ALLOW - no conditions as the user doesn't care if it's there or not + case UnderscoreOptions.allow: { + if (hasSingleUnderscore()) { + return trimSingleUnderscore(); + } + + return name; + } + + case UnderscoreOptions.allowDouble: { + if (hasDoubleUnderscore()) { + return trimDoubleUnderscore(); + } - case UnderscoreOptions.forbid: - if (hasUnderscore) { + return name; + } + + case UnderscoreOptions.allowSingleOrDouble: { + if (hasDoubleUnderscore()) { + return trimDoubleUnderscore(); + } + + if (hasSingleUnderscore()) { + return trimSingleUnderscore(); + } + + return name; + } + + // FORBID + case UnderscoreOptions.forbid: { + if (hasSingleUnderscore()) { context.report({ node, messageId: 'unexpectedUnderscore', data: formatReportData({ originalName, position, + count: 'one', }), }); return null; } - break; - case UnderscoreOptions.require: - if (!hasUnderscore) { + return name; + } + + // REQUIRE + case UnderscoreOptions.require: { + if (!hasSingleUnderscore()) { context.report({ node, messageId: 'missingUnderscore', data: formatReportData({ originalName, position, + count: 'one', + }), + }); + return null; + } + + return trimSingleUnderscore(); + } + + case UnderscoreOptions.requireDouble: { + if (!hasDoubleUnderscore()) { + context.report({ + node, + messageId: 'missingUnderscore', + data: formatReportData({ + originalName, + position, + count: 'two', }), }); return null; } - } - return hasUnderscore ? trimUnderscore() : name; + return trimDoubleUnderscore(); + } + } } /** diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 28fdc4c060b6..87123331153c 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -128,6 +128,11 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { format, leadingUnderscore: 'require', }), + createCase(`__${name}`, { + ...test.options, + format, + leadingUnderscore: 'requireDouble', + }), createCase(`_${name}`, { ...test.options, format, @@ -138,6 +143,36 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { format, leadingUnderscore: 'allow', }), + createCase(`__${name}`, { + ...test.options, + format, + leadingUnderscore: 'allowDouble', + }), + createCase(name, { + ...test.options, + format, + leadingUnderscore: 'allowDouble', + }), + createCase(`_${name}`, { + ...test.options, + format, + leadingUnderscore: 'allowSingleOrDouble', + }), + createCase(name, { + ...test.options, + format, + leadingUnderscore: 'allowSingleOrDouble', + }), + createCase(`__${name}`, { + ...test.options, + format, + leadingUnderscore: 'allowSingleOrDouble', + }), + createCase(name, { + ...test.options, + format, + leadingUnderscore: 'allowSingleOrDouble', + }), // trailingUnderscore createCase(name, { @@ -150,6 +185,11 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { format, trailingUnderscore: 'require', }), + createCase(`${name}__`, { + ...test.options, + format, + trailingUnderscore: 'requireDouble', + }), createCase(`${name}_`, { ...test.options, format, @@ -160,6 +200,36 @@ function createValidTestCases(cases: Cases): TSESLint.ValidTestCase[] { format, trailingUnderscore: 'allow', }), + createCase(`${name}__`, { + ...test.options, + format, + trailingUnderscore: 'allowDouble', + }), + createCase(name, { + ...test.options, + format, + trailingUnderscore: 'allowDouble', + }), + createCase(`${name}_`, { + ...test.options, + format, + trailingUnderscore: 'allowSingleOrDouble', + }), + createCase(name, { + ...test.options, + format, + trailingUnderscore: 'allowSingleOrDouble', + }), + createCase(`${name}__`, { + ...test.options, + format, + trailingUnderscore: 'allowSingleOrDouble', + }), + createCase(name, { + ...test.options, + format, + trailingUnderscore: 'allowSingleOrDouble', + }), // prefix createCase(`MyPrefix${name}`, { @@ -283,7 +353,27 @@ function createInvalidTestCases( leadingUnderscore: 'require', }, 'missingUnderscore', - { position: 'leading' }, + { position: 'leading', count: 'one' }, + ), + createCase( + name, + { + ...test.options, + format, + leadingUnderscore: 'requireDouble', + }, + 'missingUnderscore', + { position: 'leading', count: 'two' }, + ), + createCase( + `_${name}`, + { + ...test.options, + format, + leadingUnderscore: 'requireDouble', + }, + 'missingUnderscore', + { position: 'leading', count: 'two' }, ), // trailingUnderscore @@ -305,7 +395,27 @@ function createInvalidTestCases( trailingUnderscore: 'require', }, 'missingUnderscore', - { position: 'trailing' }, + { position: 'trailing', count: 'one' }, + ), + createCase( + name, + { + ...test.options, + format, + trailingUnderscore: 'requireDouble', + }, + 'missingUnderscore', + { position: 'trailing', count: 'two' }, + ), + createCase( + `${name}_`, + { + ...test.options, + format, + trailingUnderscore: 'requireDouble', + }, + 'missingUnderscore', + { position: 'trailing', count: 'two' }, ), // prefix @@ -1188,7 +1298,7 @@ ruleTester.run('naming-convention', rule, { // this line is intentionally broken out UnusedTypeParam > = {}; - + export const used_var = 1; export function used_func( // this line is intentionally broken out From 6fc84094928c3645a0e04c31bd4d759fdfbdcb74 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Tue, 24 Nov 2020 22:09:42 -0800 Subject: [PATCH 08/19] feat(eslint-plugin): [naming-convention] add `requiresQuotes` modifier (#2813) Fixes #2761 Fixes #1483 This modifier simply matches any member name that requires quotes. To clarify: this does not match names that have quotes - only names that ***require*** quotes. --- .../docs/rules/naming-convention.md | 61 +++++-- .../src/rules/naming-convention.ts | 40 ++++- .../src/rules/switch-exhaustiveness-check.ts | 21 +-- packages/eslint-plugin/src/util/index.ts | 1 + .../eslint-plugin/src/util/requiresQuoting.ts | 24 +++ .../tests/rules/naming-convention.test.ts | 150 ++++++++++++++++++ 6 files changed, 259 insertions(+), 38 deletions(-) create mode 100644 packages/eslint-plugin/src/util/requiresQuoting.ts diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 0d180bbf7ae7..6a116ebece66 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -183,6 +183,7 @@ If these are provided, the identifier must start with one of the provided values - `global` - matches a variable/function declared in the top-level scope. - `exported` - matches anything that is exported from the module. - `unused` - matches anything that is not used. + - `requiresQuotes` - matches any name that requires quotes as it is not a valid identifier (i.e. has a space, a dash, etc in it). - `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`). @@ -229,31 +230,31 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - Allowed `modifiers`: `unused`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classProperty` - matches any class property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `objectLiteralProperty` - matches any object literal property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `typeProperty` - matches any object type property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `parameterProperty` - matches any parameter property. - Allowed `modifiers`: `private`, `protected`, `public`, `readonly`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classMethod` - matches any class method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `objectLiteralMethod` - matches any object literal method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `typeMethod` - matches any object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `accessor` - matches any accessor. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `enumMember` - matches any enum member. - - Allowed `modifiers`: none. + - Allowed `modifiers`: `requiresQuotes`. - Allowed `types`: none. - `class` - matches any class declaration. - Allowed `modifiers`: `abstract`, `exported`, `unused`. @@ -276,22 +277,22 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw Group Selectors are provided for convenience, and essentially bundle up sets of individual selectors. - `default` - matches everything. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: all modifiers. - Allowed `types`: none. - `variableLike` - matches the same as `variable`, `function` and `parameter`. - Allowed `modifiers`: `unused`. - Allowed `types`: none. - `memberLike` - matches the same as `property`, `parameterProperty`, `method`, `accessor`, `enumMember`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. - `typeLike` - matches the same as `class`, `interface`, `typeAlias`, `enum`, `typeParameter`. - Allowed `modifiers`: `abstract`, `unused`. - Allowed `types`: none. - `property` - matches the same as `classProperty`, `objectLiteralProperty`, `typeProperty`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `method` - matches the same as `classMethod`, `objectLiteralMethod`, `typeMethod`. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`. + - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. ## Examples @@ -424,12 +425,36 @@ This allows you to lint multiple type with same pattern. } ``` -### Ignore properties that require quotes +### Ignore properties that **_require_** quotes Sometimes you have to use a quoted name that breaks the convention (for example, HTTP headers). -If this is a common thing in your codebase, then you can use the `filter` option in one of two ways: +If this is a common thing in your codebase, then you have a few options. -You can use the `filter` option to ignore specific names only: +If you simply want to allow all property names that require quotes, you can use the `requiresQuotes` modifier to match any property name that _requires_ quoting, and use `format: null` to ignore the name. + +```jsonc +{ + "@typescript-eslint/naming-convention": [ + "error", + { + "selector": [ + "classProperty", + "objectLiteralProperty", + "typeProperty", + "classMethod", + "objectLiteralMethod", + "typeMethod", + "accessor", + "enumMember" + ], + "format": null, + "modifiers": ["requiresQuotes"] + } + ] +} +``` + +If you have a small and known list of exceptions, you can use the `filter` option to ignore these specific names only: ```jsonc { @@ -448,7 +473,7 @@ You can use the `filter` option to ignore specific names only: } ``` -You can use the `filter` option to ignore names that require quoting: +You can use the `filter` option to ignore names with specific characters: ```jsonc { @@ -467,6 +492,10 @@ You can use the `filter` option to ignore names that require quoting: } ``` +Note that there is no way to ignore any name that is quoted - only names that are required to be quoted. +This is intentional - adding quotes around a name is not an escape hatch for proper naming. +If you want an escape hatch for a specific name - you should can use an [`eslint-disable` comment](https://eslint.org/docs/user-guide/configuring#disabling-rules-with-inline-comments). + ### Ignore destructured names Sometimes you might want to allow destructured properties to retain their original name, even if it breaks your naming convention. diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index fe025389af9c..c0211bb64ef2 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -120,6 +120,8 @@ enum Modifiers { exported = 1 << 9, // things that are unused unused = 1 << 10, + // properties that require quoting + requiresQuotes = 1 << 11, } type ModifiersString = keyof typeof Modifiers; @@ -359,6 +361,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('classProperty', true, [ 'private', @@ -367,6 +370,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('objectLiteralProperty', true, [ 'private', @@ -375,6 +379,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('typeProperty', true, [ 'private', @@ -383,6 +388,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('parameterProperty', true, [ 'private', @@ -397,6 +403,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'static', 'readonly', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('classMethod', false, [ @@ -405,6 +412,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('objectLiteralMethod', false, [ 'private', @@ -412,6 +420,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('typeMethod', false, [ 'private', @@ -419,6 +428,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('method', false, [ 'private', @@ -426,6 +436,7 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), ...selectorSchema('accessor', true, [ 'private', @@ -433,8 +444,9 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'public', 'static', 'abstract', + 'requiresQuotes', ]), - ...selectorSchema('enumMember', false), + ...selectorSchema('enumMember', false, ['requiresQuotes']), ...selectorSchema('typeLike', false, ['abstract', 'exported', 'unused']), ...selectorSchema('class', false, ['abstract', 'exported', 'unused']), @@ -516,6 +528,9 @@ export default util.createRule({ const validators = parseOptions(context); + const compilerOptions = util + .getParserServices(context, true) + .program.getCompilerOptions(); function handleMember( validator: ValidatorFunction | null, node: @@ -533,6 +548,10 @@ export default util.createRule({ } const key = node.key; + if (requiresQuoting(key, compilerOptions.target)) { + modifiers.add(Modifiers.requiresQuotes); + } + validator(key, modifiers); } @@ -829,7 +848,13 @@ export default util.createRule({ } const id = node.id; - validator(id); + const modifiers = new Set(); + + if (requiresQuoting(id, compilerOptions.target)) { + modifiers.add(Modifiers.requiresQuotes); + } + + validator(id, modifiers); }, // #endregion enumMember @@ -1020,8 +1045,17 @@ function isGlobal(scope: TSESLint.Scope.Scope | null): boolean { ); } -type ValidatorFunction = ( +function requiresQuoting( node: TSESTree.Identifier | TSESTree.Literal, + target: ts.ScriptTarget | undefined, +): boolean { + const name = + node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; + return util.requiresQuoting(name, target); +} + +type ValidatorFunction = ( + node: TSESTree.Identifier | TSESTree.StringLiteral | TSESTree.NumberLiteral, modifiers?: Set, ) => void; type ParsedOptions = Record; diff --git a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts index d8a7750efbaa..8881473da051 100644 --- a/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts +++ b/packages/eslint-plugin/src/rules/switch-exhaustiveness-check.ts @@ -6,6 +6,7 @@ import { getParserServices, isClosingBraceToken, isOpeningBraceToken, + requiresQuoting, } from '../util'; import { isTypeFlagSet, unionTypeParts } from 'tsutils'; @@ -34,24 +35,6 @@ export default createRule({ const checker = service.program.getTypeChecker(); const compilerOptions = service.program.getCompilerOptions(); - function requiresQuoting(name: string): boolean { - if (name.length === 0) { - return true; - } - - if (!ts.isIdentifierStart(name.charCodeAt(0), compilerOptions.target)) { - return true; - } - - for (let i = 1; i < name.length; i += 1) { - if (!ts.isIdentifierPart(name.charCodeAt(i), compilerOptions.target)) { - return true; - } - } - - return false; - } - function getNodeType(node: TSESTree.Node): ts.Type { const tsNode = service.esTreeNodeToTSNodeMap.get(node); return getConstrainedTypeAtLocation(checker, tsNode); @@ -93,7 +76,7 @@ export default createRule({ if ( symbolName && (missingBranchName || missingBranchName === '') && - requiresQuoting(missingBranchName.toString()) + requiresQuoting(missingBranchName.toString(), compilerOptions.target) ) { caseTest = `${symbolName}['${missingBranchName}']`; } diff --git a/packages/eslint-plugin/src/util/index.ts b/packages/eslint-plugin/src/util/index.ts index af0a64eddbfc..86e0ec233fd4 100644 --- a/packages/eslint-plugin/src/util/index.ts +++ b/packages/eslint-plugin/src/util/index.ts @@ -8,6 +8,7 @@ export * from './misc'; export * from './nullThrows'; export * from './objectIterators'; export * from './propertyTypes'; +export * from './requiresQuoting'; export * from './types'; // this is done for convenience - saves migrating all of the old rules diff --git a/packages/eslint-plugin/src/util/requiresQuoting.ts b/packages/eslint-plugin/src/util/requiresQuoting.ts new file mode 100644 index 000000000000..27c9a2ff77cf --- /dev/null +++ b/packages/eslint-plugin/src/util/requiresQuoting.ts @@ -0,0 +1,24 @@ +import * as ts from 'typescript'; + +function requiresQuoting( + name: string, + target: ts.ScriptTarget = ts.ScriptTarget.ESNext, +): boolean { + if (name.length === 0) { + return true; + } + + if (!ts.isIdentifierStart(name.charCodeAt(0), target)) { + return true; + } + + for (let i = 1; i < name.length; i += 1) { + if (!ts.isIdentifierPart(name.charCodeAt(i), target)) { + return true; + } + } + + return false; +} + +export { requiresQuoting }; diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index 87123331153c..b53d7b9304cc 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -1325,6 +1325,113 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + const ignored1 = { + 'a a': 1, + 'b b'() {}, + get 'c c'() { + return 1; + }, + set 'd d'(value: string) {}, + }; + class ignored2 { + 'a a' = 1; + 'b b'() {} + get 'c c'() { + return 1; + } + set 'd d'(value: string) {} + } + interface ignored3 { + 'a a': 1; + 'b b'(): void; + } + type ignored4 = { + 'a a': 1; + 'b b'(): void; + }; + enum ignored5 { + 'a a', + } + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'default', + format: null, + modifiers: ['requiresQuotes'], + }, + ], + }, + { + code: ` + const ignored1 = { + 'a a': 1, + 'b b'() {}, + get 'c c'() { + return 1; + }, + set 'd d'(value: string) {}, + }; + class ignored2 { + 'a a' = 1; + 'b b'() {} + get 'c c'() { + return 1; + } + set 'd d'(value: string) {} + } + interface ignored3 { + 'a a': 1; + 'b b'(): void; + } + type ignored4 = { + 'a a': 1; + 'b b'(): void; + }; + enum ignored5 { + 'a a', + } + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: [ + 'classProperty', + 'objectLiteralProperty', + 'typeProperty', + 'classMethod', + 'objectLiteralMethod', + 'typeMethod', + 'accessor', + 'enumMember', + ], + format: null, + modifiers: ['requiresQuotes'], + }, + // making sure the `requoresQuotes` modifier appropriately overrides this + { + selector: [ + 'classProperty', + 'objectLiteralProperty', + 'typeProperty', + 'classMethod', + 'objectLiteralMethod', + 'typeMethod', + 'accessor', + 'enumMember', + ], + format: ['PascalCase'], + }, + ], + }, ], invalid: [ { @@ -2000,5 +2107,48 @@ ruleTester.run('naming-convention', rule, { ], errors: Array(7).fill({ messageId: 'doesNotMatchFormat' }), }, + { + code: ` + const ignored1 = { + 'a a': 1, + 'b b'() {}, + get 'c c'() { + return 1; + }, + set 'd d'(value: string) {}, + }; + class ignored2 { + 'a a' = 1; + 'b b'() {} + get 'c c'() { + return 1; + } + set 'd d'(value: string) {} + } + interface ignored3 { + 'a a': 1; + 'b b'(): void; + } + type ignored4 = { + 'a a': 1; + 'b b'(): void; + }; + enum ignored5 { + 'a a', + } + `, + options: [ + { + selector: 'default', + format: ['snake_case'], + }, + { + selector: 'default', + format: ['PascalCase'], + modifiers: ['requiresQuotes'], + }, + ], + errors: Array(13).fill({ messageId: 'doesNotMatchFormat' }), + }, ], }); From 05c9bed83a110e39254dda999050a61dd29cdf3c Mon Sep 17 00:00:00 2001 From: Frezc <504021398@qq.com> Date: Thu, 26 Nov 2020 04:18:58 +0800 Subject: [PATCH 09/19] test: fix jest config on windows (#2765) --- packages/scope-manager/jest.config.js | 6 +++--- packages/types/jest.config.js | 4 ++-- packages/typescript-estree/jest.config.js | 8 ++++---- packages/visitor-keys/jest.config.js | 4 ++-- 4 files changed, 11 insertions(+), 11 deletions(-) diff --git a/packages/scope-manager/jest.config.js b/packages/scope-manager/jest.config.js index 3e3bcb726edc..629629e0cf5e 100644 --- a/packages/scope-manager/jest.config.js +++ b/packages/scope-manager/jest.config.js @@ -10,11 +10,11 @@ module.exports = { }, testEnvironment: 'node', transform: { - [/^.+\.tsx?$/.source]: 'ts-jest', + ['^.+\\.tsx?$']: 'ts-jest', }, testRegex: [ - /.\/tests\/.+\.test\.ts$/.source, - /.\/tests\/eslint-scope\/[^/]+\.test\.ts$/.source, + './tests/.+\\.test\\.ts$', + './tests/eslint-scope/[^/]+\\.test\\.ts$', ], collectCoverage: false, collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], diff --git a/packages/types/jest.config.js b/packages/types/jest.config.js index dd66fc204b38..c23ca67fbc68 100644 --- a/packages/types/jest.config.js +++ b/packages/types/jest.config.js @@ -10,9 +10,9 @@ module.exports = { }, testEnvironment: 'node', transform: { - [/^.+\.tsx?$/.source]: 'ts-jest', + ['^.+\\.tsx?$']: 'ts-jest', }, - testRegex: [/.\/tests\/.+\.test\.ts$/.source], + testRegex: ['./tests/.+\\.test\\.ts$'], collectCoverage: false, collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], diff --git a/packages/typescript-estree/jest.config.js b/packages/typescript-estree/jest.config.js index 6f03f91a534d..4b54bcf55cc7 100644 --- a/packages/typescript-estree/jest.config.js +++ b/packages/typescript-estree/jest.config.js @@ -3,12 +3,12 @@ module.exports = { testEnvironment: 'node', transform: { - [/^.+\.tsx?$/.source]: 'ts-jest', + ['^.+\\.tsx?$']: 'ts-jest', }, testRegex: [ - /.\/tests\/lib\/.*\.ts$/.source, - /.\/tests\/ast-alignment\/spec\.ts$/.source, - /.\/tests\/[^\/]+\.test\.ts$/.source, + './tests/lib/.*\\.ts$', + './tests/ast-alignment/spec\\.ts$', + './tests/[^/]+\\.test\\.ts$', ], collectCoverage: false, collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], diff --git a/packages/visitor-keys/jest.config.js b/packages/visitor-keys/jest.config.js index dd66fc204b38..c23ca67fbc68 100644 --- a/packages/visitor-keys/jest.config.js +++ b/packages/visitor-keys/jest.config.js @@ -10,9 +10,9 @@ module.exports = { }, testEnvironment: 'node', transform: { - [/^.+\.tsx?$/.source]: 'ts-jest', + ['^.+\\.tsx?$']: 'ts-jest', }, - testRegex: [/.\/tests\/.+\.test\.ts$/.source], + testRegex: ['./tests/.+\\.test\\.ts$'], collectCoverage: false, collectCoverageFrom: ['src/**/*.{js,jsx,ts,tsx}'], moduleFileExtensions: ['ts', 'tsx', 'js', 'jsx', 'json', 'node'], From c816b84814214f7504a0d89a5cd3b08c595bfb50 Mon Sep 17 00:00:00 2001 From: Yosuke Ota Date: Thu, 26 Nov 2020 05:21:11 +0900 Subject: [PATCH 10/19] fix(eslint-plugin): [consistent-type-imports] crash when using both default and namespace in one import (#2778) --- .../src/rules/consistent-type-imports.ts | 86 +++- .../rules/consistent-type-imports.test.ts | 408 ++++++++++++------ 2 files changed, 341 insertions(+), 153 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-type-imports.ts b/packages/eslint-plugin/src/rules/consistent-type-imports.ts index 830c7b6669a6..c4cb6586a00f 100644 --- a/packages/eslint-plugin/src/rules/consistent-type-imports.ts +++ b/packages/eslint-plugin/src/rules/consistent-type-imports.ts @@ -252,15 +252,16 @@ export default util.createRule({ ? node.specifiers[0] : null; const namespaceSpecifier: TSESTree.ImportNamespaceSpecifier | null = - node.specifiers[0].type === AST_NODE_TYPES.ImportNamespaceSpecifier - ? node.specifiers[0] - : null; + node.specifiers.find( + (specifier): specifier is TSESTree.ImportNamespaceSpecifier => + specifier.type === AST_NODE_TYPES.ImportNamespaceSpecifier, + ) ?? null; const namedSpecifiers: TSESTree.ImportSpecifier[] = node.specifiers.filter( (specifier): specifier is TSESTree.ImportSpecifier => specifier.type === AST_NODE_TYPES.ImportSpecifier, ); - if (namespaceSpecifier) { + if (namespaceSpecifier && !defaultSpecifier) { // e.g. // import * as types from 'foo' yield* fixToTypeImportByInsertType(fixer, node, false); @@ -268,7 +269,8 @@ export default util.createRule({ } else if (defaultSpecifier) { if ( report.typeSpecifiers.includes(defaultSpecifier) && - namedSpecifiers.length === 0 + namedSpecifiers.length === 0 && + !namespaceSpecifier ) { // e.g. // import Type from 'foo' @@ -279,7 +281,8 @@ export default util.createRule({ if ( namedSpecifiers.every(specifier => report.typeSpecifiers.includes(specifier), - ) + ) && + !namespaceSpecifier ) { // e.g. // import {Type1, Type2} from 'foo' @@ -336,11 +339,40 @@ export default util.createRule({ } } + const fixesRemoveTypeNamespaceSpecifier: TSESLint.RuleFix[] = []; + if ( + namespaceSpecifier && + report.typeSpecifiers.includes(namespaceSpecifier) + ) { + // e.g. + // import Foo, * as Type from 'foo' + // import DefType, * as Type from 'foo' + // import DefType, * as Type from 'foo' + const commaToken = util.nullThrows( + sourceCode.getTokenBefore(namespaceSpecifier, util.isCommaToken), + util.NullThrowsReasons.MissingToken(',', node.type), + ); + + // import Def, * as Ns from 'foo' + // ^^^^^^^^^ remove + fixesRemoveTypeNamespaceSpecifier.push( + fixer.removeRange([commaToken.range[0], namespaceSpecifier.range[1]]), + ); + + // import type * as Ns from 'foo' + // ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ insert + yield fixer.insertTextBefore( + node, + `import type ${sourceCode.getText( + namespaceSpecifier, + )} from ${sourceCode.getText(node.source)};\n`, + ); + } if ( defaultSpecifier && report.typeSpecifiers.includes(defaultSpecifier) ) { - if (typeNamedSpecifiers.length === namedSpecifiers.length) { + if (report.typeSpecifiers.length === node.specifiers.length) { const importToken = util.nullThrows( sourceCode.getFirstToken(node, isImportToken), util.NullThrowsReasons.MissingToken('import', node.type), @@ -349,20 +381,36 @@ export default util.createRule({ // ^^^^ insert yield fixer.insertTextAfter(importToken, ' type'); } else { + const commaToken = util.nullThrows( + sourceCode.getTokenAfter(defaultSpecifier, util.isCommaToken), + util.NullThrowsReasons.MissingToken(',', defaultSpecifier.type), + ); + // import Type , {...} from 'foo' + // ^^^^^ pick + const defaultText = sourceCode.text + .slice(defaultSpecifier.range[0], commaToken.range[0]) + .trim(); yield fixer.insertTextBefore( node, - `import type ${sourceCode.getText( - defaultSpecifier, - )} from ${sourceCode.getText(node.source)};\n`, + `import type ${defaultText} from ${sourceCode.getText( + node.source, + )};\n`, + ); + const afterToken = util.nullThrows( + sourceCode.getTokenAfter(commaToken, { includeComments: true }), + util.NullThrowsReasons.MissingToken('any token', node.type), ); // import Type , {...} from 'foo' - // ^^^^^^ remove - yield fixer.remove(defaultSpecifier); - yield fixer.remove(sourceCode.getTokenAfter(defaultSpecifier)!); + // ^^^^^^^ remove + yield fixer.removeRange([ + defaultSpecifier.range[0], + afterToken.range[0], + ]); } } yield* fixesNamedSpecifiers.removeTypeNamedSpecifiers; + yield* fixesRemoveTypeNamespaceSpecifier; yield* afterFixes; @@ -376,6 +424,12 @@ export default util.createRule({ typeNamedSpecifiersText: string; removeTypeNamedSpecifiers: TSESLint.RuleFix[]; } { + if (allNamedSpecifiers.length === 0) { + return { + typeNamedSpecifiersText: '', + removeTypeNamedSpecifiers: [], + }; + } const typeNamedSpecifiersTexts: string[] = []; const removeTypeNamedSpecifiers: TSESLint.RuleFix[] = []; if (typeNamedSpecifiers.length === allNamedSpecifiers.length) { @@ -564,7 +618,11 @@ export default util.createRule({ ), util.NullThrowsReasons.MissingToken('type', node.type), ); - return fixer.remove(typeToken); + const afterToken = util.nullThrows( + sourceCode.getTokenAfter(typeToken, { includeComments: true }), + util.NullThrowsReasons.MissingToken('any token', node.type), + ); + return fixer.removeRange([typeToken.range[0], afterToken.range[0]]); } }, }); diff --git a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts index 88d6db374a04..8818225a024a 100644 --- a/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-type-imports.test.ts @@ -225,6 +225,11 @@ ruleTester.run('consistent-type-imports', rule, { jsxFragmentName: 'Fragment', }, }, + ` + import Default, * as Rest from 'module'; + const a: typeof Default = Default; + const b: typeof Rest = Rest; + `, ], invalid: [ { @@ -362,11 +367,11 @@ ruleTester.run('consistent-type-imports', rule, { ], }, { - code: noFormat` + code: ` import * as A from 'foo'; let foo: A.Foo; `, - output: noFormat` + output: ` import type * as A from 'foo'; let foo: A.Foo; `, @@ -381,22 +386,21 @@ ruleTester.run('consistent-type-imports', rule, { { // default and named code: ` - import A, { B } from 'foo'; - let foo: A; - let bar: B; +import A, { B } from 'foo'; +let foo: A; +let bar: B; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting - output: noFormat` - import type { B } from 'foo'; + output: ` +import type { B } from 'foo'; import type A from 'foo'; - let foo: A; - let bar: B; +let foo: A; +let bar: B; `, errors: [ { messageId: 'typeOverValue', line: 2, - column: 9, + column: 1, }, ], }, @@ -405,7 +409,7 @@ import type A from 'foo'; import A, {} from 'foo'; let foo: A; `, - output: noFormat` + output: ` import type A from 'foo'; let foo: A; `, @@ -419,110 +423,105 @@ import type A from 'foo'; }, { code: ` - import { A, B } from 'foo'; - const foo: A = B(); +import { A, B } from 'foo'; +const foo: A = B(); `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting output: noFormat` - import type { A} from 'foo'; +import type { A} from 'foo'; import { B } from 'foo'; - const foo: A = B(); +const foo: A = B(); `, errors: [ { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"A"' }, line: 2, - column: 9, + column: 1, }, ], }, { code: ` - import { A, B, C } from 'foo'; - const foo: A = B(); - let bar: C; +import { A, B, C } from 'foo'; +const foo: A = B(); +let bar: C; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting - output: noFormat` - import type { A, C } from 'foo'; + output: ` +import type { A, C } from 'foo'; import { B } from 'foo'; - const foo: A = B(); - let bar: C; +const foo: A = B(); +let bar: C; `, errors: [ { messageId: 'someImportsAreOnlyTypes', data: { typeImports: '"A" and "C"' }, line: 2, - column: 9, + column: 1, }, ], }, { code: ` - import { A, B, C, D } from 'foo'; - const foo: A = B(); - type T = { bar: C; baz: D }; +import { A, B, C, D } from 'foo'; +const foo: A = B(); +type T = { bar: C; baz: D }; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting - output: noFormat` - import type { A, C, D } from 'foo'; + output: ` +import type { A, C, D } from 'foo'; import { B } from 'foo'; - const foo: A = B(); - type T = { bar: C; baz: D }; +const foo: A = B(); +type T = { bar: C; baz: D }; `, errors: [ { messageId: 'someImportsAreOnlyTypes', data: { typeImports: '"A", "C" and "D"' }, line: 2, - column: 9, + column: 1, }, ], }, { code: ` - import A, { B, C, D } from 'foo'; - B(); - type T = { foo: A; bar: C; baz: D }; +import A, { B, C, D } from 'foo'; +B(); +type T = { foo: A; bar: C; baz: D }; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting - output: noFormat` - import type { C, D } from 'foo'; + output: ` +import type { C, D } from 'foo'; import type A from 'foo'; -import { B } from 'foo'; - B(); - type T = { foo: A; bar: C; baz: D }; +import { B } from 'foo'; +B(); +type T = { foo: A; bar: C; baz: D }; `, errors: [ { messageId: 'someImportsAreOnlyTypes', data: { typeImports: '"A", "C" and "D"' }, line: 2, - column: 9, + column: 1, }, ], }, { code: ` - import A, { B } from 'foo'; - B(); - type T = A; +import A, { B } from 'foo'; +B(); +type T = A; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting - output: noFormat` - import type A from 'foo'; -import { B } from 'foo'; - B(); - type T = A; + output: ` +import type A from 'foo'; +import { B } from 'foo'; +B(); +type T = A; `, errors: [ { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"A"' }, line: 2, - column: 9, + column: 1, }, ], }, @@ -560,198 +559,192 @@ import { B } from 'foo'; }, { code: ` - import A, { /* comment */ B } from 'foo'; - type T = B; +import A, { /* comment */ B } from 'foo'; +type T = B; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting - output: noFormat` - import type { /* comment */ B } from 'foo'; + output: ` +import type { /* comment */ B } from 'foo'; import A from 'foo'; - type T = B; +type T = B; `, errors: [ { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"B"' }, line: 2, - column: 9, + column: 1, }, ], }, { code: noFormat` - import { A, B, C } from 'foo'; - import { D, E, F, } from 'bar'; - type T = A | D; +import { A, B, C } from 'foo'; +import { D, E, F, } from 'bar'; +type T = A | D; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting output: noFormat` - import type { A} from 'foo'; +import type { A} from 'foo'; import { B, C } from 'foo'; - import type { D} from 'bar'; +import type { D} from 'bar'; import { E, F, } from 'bar'; - type T = A | D; +type T = A | D; `, errors: [ { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"A"' }, line: 2, - column: 9, + column: 1, }, { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"D"' }, line: 3, - column: 9, + column: 1, }, ], }, { code: noFormat` - import { A, B, C } from 'foo'; - import { D, E, F, } from 'bar'; - type T = B | E; +import { A, B, C } from 'foo'; +import { D, E, F, } from 'bar'; +type T = B | E; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting output: noFormat` - import type { B} from 'foo'; +import type { B} from 'foo'; import { A, C } from 'foo'; - import type { E} from 'bar'; +import type { E} from 'bar'; import { D, F, } from 'bar'; - type T = B | E; +type T = B | E; `, errors: [ { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"B"' }, line: 2, - column: 9, + column: 1, }, { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"E"' }, line: 3, - column: 9, + column: 1, }, ], }, { code: noFormat` - import { A, B, C } from 'foo'; - import { D, E, F, } from 'bar'; - type T = C | F; +import { A, B, C } from 'foo'; +import { D, E, F, } from 'bar'; +type T = C | F; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting output: noFormat` - import type { C } from 'foo'; +import type { C } from 'foo'; import { A, B } from 'foo'; - import type { F} from 'bar'; +import type { F} from 'bar'; import { D, E } from 'bar'; - type T = C | F; +type T = C | F; `, errors: [ { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"C"' }, line: 2, - column: 9, + column: 1, }, { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"F"' }, line: 3, - column: 9, + column: 1, }, ], }, { // all type fix cases code: ` - import { Type1, Type2 } from 'named_types'; - import Type from 'default_type'; - import * as Types from 'namespace_type'; - import Default, { Named } from 'default_and_named_type'; - type T = Type1 | Type2 | Type | Types.A | Default | Named; +import { Type1, Type2 } from 'named_types'; +import Type from 'default_type'; +import * as Types from 'namespace_type'; +import Default, { Named } from 'default_and_named_type'; +type T = Type1 | Type2 | Type | Types.A | Default | Named; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting - output: noFormat` - import type { Type1, Type2 } from 'named_types'; - import type Type from 'default_type'; - import type * as Types from 'namespace_type'; - import type { Named } from 'default_and_named_type'; + output: ` +import type { Type1, Type2 } from 'named_types'; +import type Type from 'default_type'; +import type * as Types from 'namespace_type'; +import type { Named } from 'default_and_named_type'; import type Default from 'default_and_named_type'; - type T = Type1 | Type2 | Type | Types.A | Default | Named; +type T = Type1 | Type2 | Type | Types.A | Default | Named; `, errors: [ { messageId: 'typeOverValue', line: 2, - column: 9, + column: 1, }, { messageId: 'typeOverValue', line: 3, - column: 9, + column: 1, }, { messageId: 'typeOverValue', line: 4, - column: 9, + column: 1, }, { messageId: 'typeOverValue', line: 5, - column: 9, + column: 1, }, ], }, { // some type fix cases code: ` - import { Value1, Type1 } from 'named_import'; - import Type2, { Value2 } from 'default_import'; - import Value3, { Type3 } from 'default_import2'; - import Type4, { Type5, Value4 } from 'default_and_named_import'; - type T = Type1 | Type2 | Type3 | Type4 | Type5; +import { Value1, Type1 } from 'named_import'; +import Type2, { Value2 } from 'default_import'; +import Value3, { Type3 } from 'default_import2'; +import Type4, { Type5, Value4 } from 'default_and_named_import'; +type T = Type1 | Type2 | Type3 | Type4 | Type5; `, - // eslint-disable-next-line @typescript-eslint/internal/plugin-test-formatting output: noFormat` - import type { Type1 } from 'named_import'; +import type { Type1 } from 'named_import'; import { Value1 } from 'named_import'; - import type Type2 from 'default_import'; -import { Value2 } from 'default_import'; - import type { Type3 } from 'default_import2'; +import type Type2 from 'default_import'; +import { Value2 } from 'default_import'; +import type { Type3 } from 'default_import2'; import Value3 from 'default_import2'; - import type { Type5} from 'default_and_named_import'; +import type { Type5} from 'default_and_named_import'; import type Type4 from 'default_and_named_import'; -import { Value4 } from 'default_and_named_import'; - type T = Type1 | Type2 | Type3 | Type4 | Type5; +import { Value4 } from 'default_and_named_import'; +type T = Type1 | Type2 | Type3 | Type4 | Type5; `, errors: [ { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"Type1"' }, line: 2, - column: 9, + column: 1, }, { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"Type2"' }, line: 3, - column: 9, + column: 1, }, { messageId: 'aImportIsOnlyTypes', data: { typeImports: '"Type3"' }, line: 4, - column: 9, + column: 1, }, { messageId: 'someImportsAreOnlyTypes', data: { typeImports: '"Type4" and "Type5"' }, line: 5, - column: 9, + column: 1, }, ], }, @@ -795,8 +788,8 @@ import { Value4 } from 'default_and_named_import'; let foo: Foo; `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import Foo from 'foo'; + output: ` + import Foo from 'foo'; let foo: Foo; `, errors: [ @@ -813,8 +806,8 @@ import { Value4 } from 'default_and_named_import'; let foo: Foo; `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import { Foo } from 'foo'; + output: ` + import { Foo } from 'foo'; let foo: Foo; `, errors: [ @@ -897,8 +890,8 @@ import { Value4 } from 'default_and_named_import'; type T = typeof Type.foo; `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import Type from 'foo'; + output: ` + import Type from 'foo'; type T = typeof Type; type T = typeof Type.foo; @@ -919,8 +912,8 @@ import { Value4 } from 'default_and_named_import'; type T = typeof Type.foo; `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import { Type } from 'foo'; + output: ` + import { Type } from 'foo'; type T = typeof Type; type T = typeof Type.foo; @@ -941,8 +934,8 @@ import { Value4 } from 'default_and_named_import'; type T = typeof Type.foo; `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import * as Type from 'foo'; + output: ` + import * as Type from 'foo'; type T = typeof Type; type T = typeof Type.foo; @@ -1022,8 +1015,8 @@ import { Value4 } from 'default_and_named_import'; export type { Type }; // is a type-only export `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import Type from 'foo'; + output: ` + import Type from 'foo'; export { Type }; // is a type-only export export default Type; // is a type-only export @@ -1046,8 +1039,8 @@ import { Value4 } from 'default_and_named_import'; export type { Type }; // is a type-only export `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import { Type } from 'foo'; + output: ` + import { Type } from 'foo'; export { Type }; // is a type-only export export default Type; // is a type-only export @@ -1070,8 +1063,8 @@ import { Value4 } from 'default_and_named_import'; export type { Type }; // is a type-only export `, options: [{ prefer: 'no-type-imports' }], - output: noFormat` - import * as Type from 'foo'; + output: ` + import * as Type from 'foo'; export { Type }; // is a type-only export export default Type; // is a type-only export @@ -1085,5 +1078,142 @@ import { Value4 } from 'default_and_named_import'; }, ], }, + { + // type with comments + code: noFormat` +import type /*comment*/ * as AllType from 'foo'; +import type // comment +DefType from 'foo'; +import type /*comment*/ { Type } from 'foo'; + +type T = { a: AllType; b: DefType; c: Type }; + `, + options: [{ prefer: 'no-type-imports' }], + output: noFormat` +import /*comment*/ * as AllType from 'foo'; +import // comment +DefType from 'foo'; +import /*comment*/ { Type } from 'foo'; + +type T = { a: AllType; b: DefType; c: Type }; + `, + errors: [ + { + messageId: 'valueOverType', + line: 2, + column: 1, + }, + { + messageId: 'valueOverType', + line: 3, + column: 1, + }, + { + messageId: 'valueOverType', + line: 5, + column: 1, + }, + ], + }, + { + // https://github.com/typescript-eslint/typescript-eslint/issues/2775 + code: ` +import Default, * as Rest from 'module'; +const a: Rest.A = ''; + `, + options: [{ prefer: 'type-imports' }], + output: ` +import type * as Rest from 'module'; +import Default from 'module'; +const a: Rest.A = ''; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + line: 2, + column: 1, + }, + ], + }, + { + code: ` +import Default, * as Rest from 'module'; +const a: Default = ''; + `, + options: [{ prefer: 'type-imports' }], + output: ` +import type Default from 'module'; +import * as Rest from 'module'; +const a: Default = ''; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + line: 2, + column: 1, + }, + ], + }, + { + code: ` +import Default, * as Rest from 'module'; +const a: Default = ''; +const b: Rest.A = ''; + `, + options: [{ prefer: 'type-imports' }], + output: ` +import type * as Rest from 'module'; +import type Default from 'module'; +const a: Default = ''; +const b: Rest.A = ''; + `, + errors: [ + { + messageId: 'typeOverValue', + line: 2, + column: 1, + }, + ], + }, + { + // type with comments + code: ` +import Default, /*comment*/ * as Rest from 'module'; +const a: Default = ''; + `, + options: [{ prefer: 'type-imports' }], + output: ` +import type Default from 'module'; +import /*comment*/ * as Rest from 'module'; +const a: Default = ''; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + line: 2, + column: 1, + }, + ], + }, + { + // type with comments + code: noFormat` +import Default /*comment1*/, /*comment2*/ { Data } from 'module'; +const a: Default = ''; + `, + options: [{ prefer: 'type-imports' }], + output: noFormat` +import type Default /*comment1*/ from 'module'; +import /*comment2*/ { Data } from 'module'; +const a: Default = ''; + `, + errors: [ + { + messageId: 'aImportIsOnlyTypes', + line: 2, + column: 1, + }, + ], + }, ], }); From 32b1b6839fb32d93b7faa8fec74c9cb68ea587bb Mon Sep 17 00:00:00 2001 From: Daniil Dubrava Date: Wed, 25 Nov 2020 23:22:26 +0300 Subject: [PATCH 11/19] fix(eslint-plugin): [triple-slash-reference] fix crash with external module reference (#2788) --- .../src/rules/triple-slash-reference.ts | 9 +++++--- .../rules/triple-slash-reference.test.ts | 22 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/packages/eslint-plugin/src/rules/triple-slash-reference.ts b/packages/eslint-plugin/src/rules/triple-slash-reference.ts index 1bb16c229a11..08c3ad6291b2 100644 --- a/packages/eslint-plugin/src/rules/triple-slash-reference.ts +++ b/packages/eslint-plugin/src/rules/triple-slash-reference.ts @@ -1,4 +1,5 @@ import { + AST_NODE_TYPES, AST_TOKEN_TYPES, TSESTree, } from '@typescript-eslint/experimental-utils'; @@ -81,9 +82,11 @@ export default util.createRule({ }, TSImportEqualsDeclaration(node): void { if (programNode) { - const source = (node.moduleReference as TSESTree.TSExternalModuleReference) - .expression as TSESTree.Literal; - hasMatchingReference(source); + const reference = node.moduleReference; + + if (reference.type === AST_NODE_TYPES.TSExternalModuleReference) { + hasMatchingReference(reference.expression as TSESTree.Literal); + } } }, Program(node): void { diff --git a/packages/eslint-plugin/tests/rules/triple-slash-reference.test.ts b/packages/eslint-plugin/tests/rules/triple-slash-reference.test.ts index ab10ba5f6d97..8dd71212fbb0 100644 --- a/packages/eslint-plugin/tests/rules/triple-slash-reference.test.ts +++ b/packages/eslint-plugin/tests/rules/triple-slash-reference.test.ts @@ -54,6 +54,28 @@ ruleTester.run('triple-slash-reference', rule, { `, options: [{ path: 'always', types: 'always', lib: 'always' }], }, + { + code: ` + /// + /// + /// + import foo = foo; + import bar = bar; + import baz = baz; + `, + options: [{ path: 'always', types: 'always', lib: 'always' }], + }, + { + code: ` + /// + /// + /// + import foo = foo.foo; + import bar = bar.bar.bar.bar; + import baz = baz.baz; + `, + options: [{ path: 'always', types: 'always', lib: 'always' }], + }, { code: "import * as foo from 'foo';", options: [{ path: 'never' }], From 73a63ee9ea00b2db0a29f148d7863c3778e4a483 Mon Sep 17 00:00:00 2001 From: pvanagtmaal Date: Wed, 25 Nov 2020 21:24:51 +0100 Subject: [PATCH 12/19] fix(eslint-plugin): [explicit-module-boundary-types] ignore functions exported within typed object/array literals (#2805) --- .../rules/explicit-module-boundary-types.ts | 4 ++++ .../explicit-module-boundary-types.test.ts | 20 +++++++++++++++++++ 2 files changed, 24 insertions(+) diff --git a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts index f4c73ef0d9b9..2214f8fa3435 100644 --- a/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts +++ b/packages/eslint-plugin/src/rules/explicit-module-boundary-types.ts @@ -390,6 +390,10 @@ export default util.createRule({ function ancestorHasReturnType(node: FunctionNode): boolean { let ancestor = node.parent; + if (ancestor?.type === AST_NODE_TYPES.Property) { + ancestor = ancestor.value; + } + // if the ancestor is not a return, then this function was not returned at all, so we can exit early const isReturnStatement = ancestor?.type === AST_NODE_TYPES.ReturnStatement; diff --git a/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts b/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts index 25b70794843c..b2906d17a98e 100644 --- a/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts +++ b/packages/eslint-plugin/tests/rules/explicit-module-boundary-types.test.ts @@ -661,6 +661,26 @@ export class A { b = A; } `, + ` +interface Foo { + f: (x: boolean) => boolean; +} + +export const a: Foo[] = [ + { + f: (x: boolean) => x, + }, +]; + `, + ` +interface Foo { + f: (x: boolean) => boolean; +} + +export const a: Foo = { + f: (x: boolean) => x, +}; + `, ], invalid: [ { From 29428a4dbef133563f2ee54b22908a01ab9a9472 Mon Sep 17 00:00:00 2001 From: Ryutaro Yamada Date: Thu, 26 Nov 2020 05:26:32 +0900 Subject: [PATCH 13/19] fix(eslint-plugin): [consistent-indexed-object-style] convert readonly index signature to readonly record (#2798) --- .../rules/consistent-indexed-object-style.ts | 8 +-- .../consistent-indexed-object-style.test.ts | 65 +++++++++++++++++++ 2 files changed, 69 insertions(+), 4 deletions(-) diff --git a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts index c1b86bf8c61f..bd48718c68b5 100644 --- a/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts +++ b/packages/eslint-plugin/src/rules/consistent-indexed-object-style.ts @@ -101,10 +101,10 @@ export default createRule({ fix(fixer) { const key = sourceCode.getText(keyType.typeAnnotation); const value = sourceCode.getText(valueType.typeAnnotation); - return fixer.replaceText( - node, - `${prefix}Record<${key}, ${value}>${postfix}`, - ); + const record = member.readonly + ? `Readonly>` + : `Record<${key}, ${value}>`; + return fixer.replaceText(node, `${prefix}${record}${postfix}`); }, }); } diff --git a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts index ebedccf424a7..31077fcf6206 100644 --- a/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts +++ b/packages/eslint-plugin/tests/rules/consistent-indexed-object-style.test.ts @@ -140,6 +140,19 @@ type Foo = Record; errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, + // Readonly interface + { + code: ` +interface Foo { + readonly [key: string]: any; +} + `, + output: ` +type Foo = Readonly>; + `, + errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], + }, + // Interface with generic parameter { code: ` @@ -153,6 +166,19 @@ type Foo = Record; errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, + // Readonly interface with generic parameter + { + code: ` +interface Foo { + readonly [key: string]: A; +} + `, + output: ` +type Foo = Readonly>; + `, + errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], + }, + // Interface with multiple generic parameters { code: ` @@ -166,6 +192,19 @@ type Foo = Record; errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], }, + // Readonly interface with multiple generic parameters + { + code: ` +interface Foo { + readonly [key: A]: B; +} + `, + output: ` +type Foo = Readonly>; + `, + errors: [{ messageId: 'preferRecord', line: 2, column: 1 }], + }, + // Type literal { code: 'type Foo = { [key: string]: any };', @@ -173,6 +212,13 @@ type Foo = Record; errors: [{ messageId: 'preferRecord', line: 1, column: 12 }], }, + // Readonly type literal + { + code: 'type Foo = { readonly [key: string]: any };', + output: 'type Foo = Readonly>;', + errors: [{ messageId: 'preferRecord', line: 1, column: 12 }], + }, + // Generic { code: 'type Foo = Generic<{ [key: string]: any }>;', @@ -180,6 +226,13 @@ type Foo = Record; errors: [{ messageId: 'preferRecord', line: 1, column: 20 }], }, + // Readonly Generic + { + code: 'type Foo = Generic<{ readonly [key: string]: any }>;', + output: 'type Foo = Generic>>;', + errors: [{ messageId: 'preferRecord', line: 1, column: 20 }], + }, + // Function types { code: 'function foo(arg: { [key: string]: any }) {}', @@ -192,6 +245,18 @@ type Foo = Record; errors: [{ messageId: 'preferRecord', line: 1, column: 17 }], }, + // Readonly function types + { + code: 'function foo(arg: { readonly [key: string]: any }) {}', + output: 'function foo(arg: Readonly>) {}', + errors: [{ messageId: 'preferRecord', line: 1, column: 19 }], + }, + { + code: 'function foo(): { readonly [key: string]: any } {}', + output: 'function foo(): Readonly> {}', + errors: [{ messageId: 'preferRecord', line: 1, column: 17 }], + }, + // Never // Type literal { From 878dd4ae8c408f1eb42790a8fac37f85040b7f3c Mon Sep 17 00:00:00 2001 From: Ryota Kameoka Date: Thu, 26 Nov 2020 05:28:44 +0900 Subject: [PATCH 14/19] feat(eslint-plugin): [unbound-method] add support for methods with a `this: void` parameter (#2796) --- .../docs/rules/unbound-method.md | 16 ++++++++++++ .../eslint-plugin/src/rules/unbound-method.ts | 25 ++++++++++++++----- .../tests/rules/unbound-method.test.ts | 16 +++++++++--- 3 files changed, 47 insertions(+), 10 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/unbound-method.md b/packages/eslint-plugin/docs/rules/unbound-method.md index 0de7b17abcf5..a9a4fe396a24 100644 --- a/packages/eslint-plugin/docs/rules/unbound-method.md +++ b/packages/eslint-plugin/docs/rules/unbound-method.md @@ -23,6 +23,14 @@ myLog(); // This log might later be called with an incorrect scope const { log } = instance; + +// arith.double may refer to `this` internally +const arith = { + double(x: number): number { + return x * 2; + }, +}; +const { double } = arith; ``` Examples of **correct** code for this rule @@ -45,6 +53,14 @@ logBound(); // .bind and lambdas will also add a correct scope const dotBindLog = instance.logBound.bind(instance); const innerLog = () => instance.logBound(); + +// arith.double explicitly declares that it does not refer to `this` internally +const arith = { + double(this: void, x: number): number { + return x * 2; + }, +}; +const { double } = arith; ``` ## Options diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 246023fce104..407d04c11ece 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -248,14 +248,27 @@ function isDangerousMethod(symbol: ts.Symbol, ignoreStatic: boolean): boolean { ts.SyntaxKind.FunctionExpression ); case ts.SyntaxKind.MethodDeclaration: - case ts.SyntaxKind.MethodSignature: - return !( - ignoreStatic && - tsutils.hasModifier( - valueDeclaration.modifiers, - ts.SyntaxKind.StaticKeyword, + case ts.SyntaxKind.MethodSignature: { + const decl = valueDeclaration as + | ts.MethodDeclaration + | ts.MethodSignature; + const firstParam = decl.parameters[0]; + const thisArgIsVoid = + firstParam?.name.kind === ts.SyntaxKind.Identifier && + firstParam?.name.escapedText === 'this' && + firstParam?.type?.kind === ts.SyntaxKind.VoidKeyword; + + return ( + !thisArgIsVoid && + !( + ignoreStatic && + tsutils.hasModifier( + valueDeclaration.modifiers, + ts.SyntaxKind.StaticKeyword, + ) ) ); + } } return false; diff --git a/packages/eslint-plugin/tests/rules/unbound-method.test.ts b/packages/eslint-plugin/tests/rules/unbound-method.test.ts index 4c0cb8f58d36..83f3b93524ce 100644 --- a/packages/eslint-plugin/tests/rules/unbound-method.test.ts +++ b/packages/eslint-plugin/tests/rules/unbound-method.test.ts @@ -25,6 +25,12 @@ class ContainsMethods { let instance = new ContainsMethods(); +const arith = { + double(this: void, x: number): number { + return x * 2; + } +}; + ${code} `; } @@ -35,7 +41,7 @@ function addContainsMethodsClassInvalid( code: addContainsMethodsClass(c), errors: [ { - line: 12, + line: 18, messageId: 'unbound', }, ], @@ -153,6 +159,8 @@ ruleTester.run('unbound-method', rule, { 'if (!!instance.unbound) {}', 'void instance.unbound', 'delete instance.unbound', + + 'const { double } = arith;', ].map(addContainsMethodsClass), ` interface RecordA { @@ -300,15 +308,15 @@ function foo(arg: ContainsMethods | null) { `), errors: [ { - line: 14, + line: 20, messageId: 'unbound', }, { - line: 15, + line: 21, messageId: 'unbound', }, { - line: 16, + line: 22, messageId: 'unbound', }, ], From 14758d2df6339f011514fd034e09a17a6345b667 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Wed, 25 Nov 2020 12:51:27 -0800 Subject: [PATCH 15/19] chore(eslint-plugin): [naming-convention] refactor rule to split it up (#2816) The rule file was up to 1700 LOC. It was a pain in the butt to scroll around it and find pieces. I'm pretty sure ESLint / TypeScript gets a bit choked up on a file that large as well (I've been running into all sorts of slowness with it). So simply isolate each part into a module to better separate things and reduce the total LOC per file. Also moved some stuff around in the docs to try and focus each section, and add an FAQ section for the misc stuff --- .../docs/rules/naming-convention.md | 213 +++- .../rules/naming-convention-utils/enums.ts | 133 ++ .../rules/naming-convention-utils/format.ts | 111 ++ .../rules/naming-convention-utils/index.ts | 6 + .../naming-convention-utils/parse-options.ts | 92 ++ .../rules/naming-convention-utils/schema.ts | 286 +++++ .../rules/naming-convention-utils/shared.ts | 20 + .../rules/naming-convention-utils/types.ts | 75 ++ .../naming-convention-utils/validator.ts | 474 +++++++ .../src/rules/naming-convention.ts | 1121 +---------------- .../tests/rules/naming-convention.test.ts | 9 +- 11 files changed, 1389 insertions(+), 1151 deletions(-) create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/format.ts create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/index.ts create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/types.ts create mode 100644 packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index 6a116ebece66..e5bbfb018dfa 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -89,29 +89,7 @@ const defaultOptions: Options = [ ### Format Options Every single selector can have the same set of format options. -When the format of an identifier is checked, it is checked in the following order: - -1. validate leading underscore -1. validate trailing underscore -1. validate prefix -1. validate suffix -1. validate custom -1. validate format - -For steps 1-4, if the identifier matches the option, the matching part will be removed. -For example, if you provide the following formatting option: `{ leadingUnderscore: 'allow', prefix: ['I'], format: ['StrictPascalCase'] }`, for the identifier `_IMyInterface`, then the following checks will occur: - -1. `name = _IMyInterface` -1. validate leading underscore - pass - - Trim leading underscore - `name = IMyInterface` -1. validate trailing underscore - no check -1. validate prefix - pass - - Trim prefix - `name = MyInterface` -1. validate suffix - no check -1. validate custom - no check -1. validate format - pass - -One final note is that if the name were to become empty via this trimming process, it is considered to match all `format`s. An example of where this might be useful is for generic type parameters, where you want all names to be prefixed with `T`, but also want to allow for the single character `T` name. +For information about how each selector is applied, see ["How does the rule evaluate a name's format?"](#how-does-the-rule-evaluate-a-names-format). #### `format` @@ -197,20 +175,7 @@ If these are provided, the identifier must start with one of the provided values - `array` matches any type assignable to `Array | null | undefined` - `function` matches any type assignable to `Function | null | undefined` -The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. - -For example, if you provide the following config: - -```ts -[ - /* 1 */ { selector: 'default', format: ['camelCase'] }, - /* 2 */ { selector: 'variable', format: ['snake_case'] }, - /* 3 */ { selector: 'variable', types: ['boolean'], format: ['UPPER_CASE'] }, - /* 4 */ { selector: 'variableLike', format: ['PascalCase'] }, -]; -``` - -Then for the code `const x = 1`, the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`. +The ordering of selectors does not matter. The implementation will automatically sort the selectors to ensure they match from most-specific to least specific. It will keep checking selectors in that order until it finds one that matches the name. See ["How does the rule automatically order selectors?"](#how-does-the-rule-automatically-order-selectors) #### Allowed Selectors, Modifiers and Types @@ -295,6 +260,180 @@ Group Selectors are provided for convenience, and essentially bundle up sets of - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. - Allowed `types`: none. +## FAQ + +This is a big rule, and there's a lot of docs. Here are a few clarifications that people often ask about or figure out via trial-and-error. + +### How does the rule evaluate a selector? + +Each selector is checked in the following way: + +1. check the `selector` + 1. if `selector` is one individual selector → the name's type must be of that type. + 1. if `selector` is a group selector → the name's type must be one of the grouped types. + 1. if `selector` is an array of selectors → apply the above for each selector in the array. +1. check the `filter` + 1. if `filter` is omitted → skip this step. + 1. if the name matches the `filter` → continue evaluating this selector. + 1. if the name does not match the `filter` → skip this selector and continue to the next selector. +1. check the `types` + 1. if `types` is omitted → skip this step. + 1. if the name has a type in `types` → continue evaluating this selector. + 1. if the name does not have a type in `types` → skip this selector and continue to the next selector. + +A name is considered to pass the config if it: + +1. Matches one selector and passes all of that selector's format checks. +2. Matches no selectors. + +A name is considered to fail the config if it matches one selector and fails one that selector's format checks. + +### How does the rule automatically order selectors? + +Each identifier should match exactly one selector. It may match multiple group selectors - but only ever one selector. +With that in mind - the base sort order works out to be: + +1. Individual Selectors +2. Grouped Selectors +3. Default Selector + +Within each of these categories, some further sorting occurs based on what selector options are supplied: + +1. `filter` is given the highest priority above all else. +2. `types` +3. `modifiers` +4. everything else + +For example, if you provide the following config: + +```ts +[ + /* 1 */ { selector: 'default', format: ['camelCase'] }, + /* 2 */ { selector: 'variable', format: ['snake_case'] }, + /* 3 */ { selector: 'variable', types: ['boolean'], format: ['UPPER_CASE'] }, + /* 4 */ { selector: 'variableLike', format: ['PascalCase'] }, +]; +``` + +Then for the code `const x = 1`, the rule will validate the selectors in the following order: `3`, `2`, `4`, `1`. +To clearly spell it out: + +- (3) is tested first because it has `types` and is an individual selector. +- (2) is tested next because it is an individual selector. +- (1) is tested next as it is a grouped selector. +- (4) is tested last as it is the base default selector. + +Its worth noting that whilst this order is applied, all selectors may not run on a name. +This is explained in ["How does the rule evaluate a name's format?"](#how-does-the-rule-evaluate-a-names-format) + +### How does the rule evaluate a name's format? + +When the format of an identifier is checked, it is checked in the following order: + +1. validate leading underscore +1. validate trailing underscore +1. validate prefix +1. validate suffix +1. validate custom +1. validate format + +For steps 1-4, if the identifier matches the option, the matching part will be removed. +This is done so that you can apply formats like PascalCase without worrying about prefixes or underscores causing it to not match. + +One final note is that if the name were to become empty via this trimming process, it is considered to match all `format`s. An example of where this might be useful is for generic type parameters, where you want all names to be prefixed with `T`, but also want to allow for the single character `T` name. + +Here are some examples to help illustrate + +Name: `_IMyInterface` +Selector: + +```json +{ + "leadingUnderscore": "require", + "prefix": ["I"], + "format": ["UPPER_CASE", "StrictPascalCase"] +} +``` + +1. `name = _IMyInterface` +1. validate leading underscore + 1. config is provided + 1. check name → pass + 1. Trim underscore → `name = IMyInterface` +1. validate trailing underscore + 1. config is not provided → skip +1. validate prefix + 1. config is provided + 1. check name → pass + 1. Trim prefix → `name = MyInterface` +1. validate suffix + 1. config is not provided → skip +1. validate custom + 1. config is not provided → skip +1. validate format + 1. for each format... + 1. `format = 'UPPER_CASE'` + 1. check format → fail. + - Important to note that if you supply multiple formats - the name only needs to match _one_ of them! + 1. `format = 'StrictPascalCase'` + 1. check format → success. +1. **_success_** + +Name: `IMyInterface` +Selector: + +```json +{ + "format": ["StrictPascalCase"], + "trailingUnderscore": "allow", + "custom": { + "regex": "^I[A-Z]", + "match": false + } +} +``` + +1. `name = IMyInterface` +1. validate leading underscore + 1. config is not provided → skip +1. validate trailing underscore + 1. config is provided + 1. check name → pass + 1. Trim underscore → `name = IMyInterface` +1. validate prefix + 1. config is not provided → skip +1. validate suffix + 1. config is not provided → skip +1. validate custom + 1. config is provided + 1. `regex = new RegExp("^I[A-Z]")` + 1. `regex.test(name) === custom.match` + 1. **_fail_** → report and exit + +### What happens if I provide a `modifiers` to a Group Selector? + +Some group selectors accept `modifiers`. For the most part these will work exactly the same as with individual selectors. +There is one exception to this in that a modifier might not apply to all individual selectors covered by a group selector. + +For example - `memberLike` includes the `enumMember` selector, and it allows the `protected` modifier. +An `enumMember` can never ever be `protected`, which means that the following config will never match any `enumMember`: + +```json +{ + "selector": "memberLike", + "modifiers": ["protected"] +} +``` + +To help with matching, members that cannot specify an accessibility will always have the `public` modifier. This means that the following config will always match any `enumMember`: + +```json +{ + "selector": "memberLike", + "modifiers": ["public"] +} +``` + ## Examples ### Enforce that all variables, functions and properties follow are camelCase diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts new file mode 100644 index 000000000000..4dff9611dcfd --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/enums.ts @@ -0,0 +1,133 @@ +enum PredefinedFormats { + camelCase = 1, + strictCamelCase, + PascalCase, + StrictPascalCase, + snake_case, + UPPER_CASE, +} +type PredefinedFormatsString = keyof typeof PredefinedFormats; + +enum UnderscoreOptions { + forbid = 1, + allow, + require, + + // special cases as it's common practice to use double underscore + requireDouble, + allowDouble, + allowSingleOrDouble, +} +type UnderscoreOptionsString = keyof typeof UnderscoreOptions; + +enum Selectors { + // variableLike + variable = 1 << 0, + function = 1 << 1, + parameter = 1 << 2, + + // memberLike + parameterProperty = 1 << 3, + accessor = 1 << 4, + enumMember = 1 << 5, + classMethod = 1 << 6, + objectLiteralMethod = 1 << 7, + typeMethod = 1 << 8, + classProperty = 1 << 9, + objectLiteralProperty = 1 << 10, + typeProperty = 1 << 11, + + // typeLike + class = 1 << 12, + interface = 1 << 13, + typeAlias = 1 << 14, + enum = 1 << 15, + typeParameter = 1 << 17, +} +type SelectorsString = keyof typeof Selectors; + +enum MetaSelectors { + default = -1, + variableLike = 0 | + Selectors.variable | + Selectors.function | + Selectors.parameter, + memberLike = 0 | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeProperty | + Selectors.parameterProperty | + Selectors.enumMember | + Selectors.classMethod | + Selectors.objectLiteralMethod | + Selectors.typeMethod | + Selectors.accessor, + typeLike = 0 | + Selectors.class | + Selectors.interface | + Selectors.typeAlias | + Selectors.enum | + Selectors.typeParameter, + method = 0 | + Selectors.classMethod | + Selectors.objectLiteralMethod | + Selectors.typeProperty, + property = 0 | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeMethod, +} +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, + // things that are unused + unused = 1 << 10, + // properties that require quoting + requiresQuotes = 1 << 11, + + // make sure TypeModifiers starts at Modifiers + 1 or else sorting won't work +} +type ModifiersString = keyof typeof Modifiers; + +enum TypeModifiers { + boolean = 1 << 12, + string = 1 << 13, + number = 1 << 14, + function = 1 << 15, + array = 1 << 16, +} +type TypeModifiersString = keyof typeof TypeModifiers; + +export { + IndividualAndMetaSelectorsString, + MetaSelectors, + MetaSelectorsString, + Modifiers, + ModifiersString, + PredefinedFormats, + PredefinedFormatsString, + Selectors, + SelectorsString, + TypeModifiers, + TypeModifiersString, + UnderscoreOptions, + UnderscoreOptionsString, +}; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/format.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/format.ts new file mode 100644 index 000000000000..2640aee6ecd4 --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/format.ts @@ -0,0 +1,111 @@ +import { PredefinedFormats } from './enums'; + +/* +These format functions are taken from `tslint-consistent-codestyle/naming-convention`: +https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/rules/namingConventionRule.ts#L603-L645 + +The licence for the code can be viewed here: +https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/LICENSE +*/ + +/* +Why not regex here? Because it's actually really, really difficult to create a regex to handle +all of the unicode cases, and we have many non-english users that use non-english characters. +https://gist.github.com/mathiasbynens/6334847 +*/ + +function isPascalCase(name: string): boolean { + return ( + name.length === 0 || + (name[0] === name[0].toUpperCase() && !name.includes('_')) + ); +} +function isStrictPascalCase(name: string): boolean { + return ( + name.length === 0 || + (name[0] === name[0].toUpperCase() && hasStrictCamelHumps(name, true)) + ); +} + +function isCamelCase(name: string): boolean { + return ( + name.length === 0 || + (name[0] === name[0].toLowerCase() && !name.includes('_')) + ); +} +function isStrictCamelCase(name: string): boolean { + return ( + name.length === 0 || + (name[0] === name[0].toLowerCase() && hasStrictCamelHumps(name, false)) + ); +} + +function hasStrictCamelHumps(name: string, isUpper: boolean): boolean { + function isUppercaseChar(char: string): boolean { + return char === char.toUpperCase() && char !== char.toLowerCase(); + } + + if (name.startsWith('_')) { + return false; + } + for (let i = 1; i < name.length; ++i) { + if (name[i] === '_') { + return false; + } + if (isUpper === isUppercaseChar(name[i])) { + if (isUpper) { + return false; + } + } else { + isUpper = !isUpper; + } + } + return true; +} + +function isSnakeCase(name: string): boolean { + return ( + name.length === 0 || + (name === name.toLowerCase() && validateUnderscores(name)) + ); +} + +function isUpperCase(name: string): boolean { + return ( + name.length === 0 || + (name === name.toUpperCase() && validateUnderscores(name)) + ); +} + +/** Check for leading trailing and adjacent underscores */ +function validateUnderscores(name: string): boolean { + if (name.startsWith('_')) { + return false; + } + let wasUnderscore = false; + for (let i = 1; i < name.length; ++i) { + if (name[i] === '_') { + if (wasUnderscore) { + return false; + } + wasUnderscore = true; + } else { + wasUnderscore = false; + } + } + return !wasUnderscore; +} + +const PredefinedFormatToCheckFunction: Readonly boolean +>> = { + [PredefinedFormats.PascalCase]: isPascalCase, + [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, + [PredefinedFormats.camelCase]: isCamelCase, + [PredefinedFormats.strictCamelCase]: isStrictCamelCase, + [PredefinedFormats.UPPER_CASE]: isUpperCase, + [PredefinedFormats.snake_case]: isSnakeCase, +}; + +export { PredefinedFormatToCheckFunction }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/index.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/index.ts new file mode 100644 index 000000000000..56297213b66c --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/index.ts @@ -0,0 +1,6 @@ +export { Modifiers } from './enums'; +export type { PredefinedFormatsString } from './enums'; +export type { Context, Selector, ValidatorFunction } from './types'; +export { SCHEMA } from './schema'; +export { selectorTypeToMessageString } from './shared'; +export { parseOptions } from './parse-options'; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts new file mode 100644 index 000000000000..c4e6e36b3039 --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/parse-options.ts @@ -0,0 +1,92 @@ +import * as util from '../../util'; +import { + MetaSelectors, + Modifiers, + PredefinedFormats, + Selectors, + TypeModifiers, + UnderscoreOptions, +} from './enums'; +import { isMetaSelector } from './shared'; +import type { + Context, + NormalizedSelector, + ParsedOptions, + Selector, +} from './types'; +import { createValidator } from './validator'; + +function normalizeOption(option: Selector): NormalizedSelector[] { + let weight = 0; + option.modifiers?.forEach(mod => { + weight |= Modifiers[mod]; + }); + option.types?.forEach(mod => { + weight |= TypeModifiers[mod]; + }); + + // give selectors with a filter the _highest_ priority + if (option.filter) { + weight |= 1 << 30; + } + + const normalizedOption = { + // format options + format: option.format ? option.format.map(f => PredefinedFormats[f]) : null, + custom: option.custom + ? { + regex: new RegExp(option.custom.regex, 'u'), + match: option.custom.match, + } + : null, + leadingUnderscore: + option.leadingUnderscore !== undefined + ? UnderscoreOptions[option.leadingUnderscore] + : null, + trailingUnderscore: + option.trailingUnderscore !== undefined + ? UnderscoreOptions[option.trailingUnderscore] + : null, + prefix: option.prefix && option.prefix.length > 0 ? option.prefix : null, + suffix: option.suffix && option.suffix.length > 0 ? option.suffix : null, + modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, + types: option.types?.map(m => TypeModifiers[m]) ?? null, + filter: + option.filter !== undefined + ? typeof option.filter === 'string' + ? { + regex: new RegExp(option.filter, 'u'), + match: true, + } + : { + regex: new RegExp(option.filter.regex, 'u'), + match: option.filter.match, + } + : null, + // calculated ordering weight based on modifiers + modifierWeight: weight, + }; + + const selectors = Array.isArray(option.selector) + ? option.selector + : [option.selector]; + + return selectors.map(selector => ({ + selector: isMetaSelector(selector) + ? MetaSelectors[selector] + : Selectors[selector], + ...normalizedOption, + })); +} + +function parseOptions(context: Context): ParsedOptions { + const normalizedOptions = context.options + .map(opt => normalizeOption(opt)) + .reduce((acc, val) => acc.concat(val), []); + return util.getEnumNames(Selectors).reduce((acc, k) => { + acc[k] = createValidator(k, context, normalizedOptions); + return acc; + }, {} as ParsedOptions); +} + +export { parseOptions }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts new file mode 100644 index 000000000000..990017db7c95 --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts @@ -0,0 +1,286 @@ +import { JSONSchema } from '@typescript-eslint/experimental-utils'; +import { + IndividualAndMetaSelectorsString, + MetaSelectors, + Modifiers, + ModifiersString, + PredefinedFormats, + Selectors, + TypeModifiers, + UnderscoreOptions, +} from './enums'; +import * as util from '../../util'; + +const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'string', + enum: util.getEnumNames(UnderscoreOptions), +}; +const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'array', + items: { + type: 'string', + minLength: 1, + }, + additionalItems: false, +}; +const MATCH_REGEX_SCHEMA: JSONSchema.JSONSchema4 = { + type: 'object', + properties: { + match: { type: 'boolean' }, + regex: { type: 'string' }, + }, + required: ['match', 'regex'], +}; +type JSONSchemaProperties = Record; +const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { + format: { + oneOf: [ + { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(PredefinedFormats), + }, + additionalItems: false, + }, + { + type: 'null', + }, + ], + }, + custom: MATCH_REGEX_SCHEMA, + leadingUnderscore: UNDERSCORE_SCHEMA, + trailingUnderscore: UNDERSCORE_SCHEMA, + prefix: PREFIX_SUFFIX_SCHEMA, + suffix: PREFIX_SUFFIX_SCHEMA, + failureMessage: { + type: 'string', + }, +}; +function selectorSchema( + selectorString: IndividualAndMetaSelectorsString, + allowType: boolean, + modifiers?: ModifiersString[], +): JSONSchema.JSONSchema4[] { + const selector: JSONSchemaProperties = { + filter: { + oneOf: [ + { + type: 'string', + minLength: 1, + }, + MATCH_REGEX_SCHEMA, + ], + }, + selector: { + type: 'string', + enum: [selectorString], + }, + }; + if (modifiers && modifiers.length > 0) { + selector.modifiers = { + type: 'array', + items: { + type: 'string', + enum: modifiers, + }, + additionalItems: false, + }; + } + if (allowType) { + selector.types = { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(TypeModifiers), + }, + additionalItems: false, + }; + } + + return [ + { + type: 'object', + properties: { + ...FORMAT_OPTIONS_PROPERTIES, + ...selector, + }, + required: ['selector', 'format'], + additionalProperties: false, + }, + ]; +} + +function selectorsSchema(): JSONSchema.JSONSchema4 { + return { + type: 'object', + properties: { + ...FORMAT_OPTIONS_PROPERTIES, + ...{ + filter: { + oneOf: [ + { + type: 'string', + minLength: 1, + }, + MATCH_REGEX_SCHEMA, + ], + }, + selector: { + type: 'array', + items: { + type: 'string', + enum: [ + ...util.getEnumNames(MetaSelectors), + ...util.getEnumNames(Selectors), + ], + }, + additionalItems: false, + }, + modifiers: { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(Modifiers), + }, + additionalItems: false, + }, + types: { + type: 'array', + items: { + type: 'string', + enum: util.getEnumNames(TypeModifiers), + }, + additionalItems: false, + }, + }, + }, + required: ['selector', 'format'], + additionalProperties: false, + }; +} + +const SCHEMA: JSONSchema.JSONSchema4 = { + type: 'array', + items: { + oneOf: [ + selectorsSchema(), + ...selectorSchema('default', false, util.getEnumNames(Modifiers)), + + ...selectorSchema('variableLike', false, ['unused']), + ...selectorSchema('variable', true, [ + 'const', + 'destructured', + 'global', + 'exported', + 'unused', + ]), + ...selectorSchema('function', false, ['global', 'exported', 'unused']), + ...selectorSchema('parameter', true, ['unused']), + + ...selectorSchema('memberLike', false, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('classProperty', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('objectLiteralProperty', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('typeProperty', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('parameterProperty', true, [ + 'private', + 'protected', + 'public', + 'readonly', + ]), + ...selectorSchema('property', true, [ + 'private', + 'protected', + 'public', + 'static', + 'readonly', + 'abstract', + 'requiresQuotes', + ]), + + ...selectorSchema('classMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('objectLiteralMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('typeMethod', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('method', false, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('accessor', true, [ + 'private', + 'protected', + 'public', + 'static', + 'abstract', + 'requiresQuotes', + ]), + ...selectorSchema('enumMember', false, ['requiresQuotes']), + + ...selectorSchema('typeLike', false, ['abstract', 'exported', 'unused']), + ...selectorSchema('class', false, ['abstract', 'exported', 'unused']), + ...selectorSchema('interface', false, ['exported', 'unused']), + ...selectorSchema('typeAlias', false, ['exported', 'unused']), + ...selectorSchema('enum', false, ['exported', 'unused']), + ...selectorSchema('typeParameter', false, ['unused']), + ], + }, + additionalItems: false, +}; + +export { SCHEMA }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts new file mode 100644 index 000000000000..71a6a4b99b7b --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/shared.ts @@ -0,0 +1,20 @@ +import { + IndividualAndMetaSelectorsString, + MetaSelectors, + MetaSelectorsString, + Selectors, + SelectorsString, +} from './enums'; + +function selectorTypeToMessageString(selectorType: SelectorsString): string { + const notCamelCase = selectorType.replace(/([A-Z])/g, ' $1'); + return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1); +} + +function isMetaSelector( + selector: IndividualAndMetaSelectorsString | Selectors | MetaSelectors, +): selector is MetaSelectorsString { + return selector in MetaSelectors; +} + +export { selectorTypeToMessageString, isMetaSelector }; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/types.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/types.ts new file mode 100644 index 000000000000..d66f9ad13d4f --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/types.ts @@ -0,0 +1,75 @@ +import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; +import { + IndividualAndMetaSelectorsString, + MetaSelectors, + Modifiers, + ModifiersString, + PredefinedFormats, + PredefinedFormatsString, + Selectors, + SelectorsString, + TypeModifiers, + TypeModifiersString, + UnderscoreOptions, + UnderscoreOptionsString, +} from './enums'; +import { MessageIds, Options } from '../naming-convention'; + +interface MatchRegex { + regex: string; + match: boolean; +} + +interface Selector { + // format options + format: PredefinedFormatsString[] | null; + custom?: MatchRegex; + leadingUnderscore?: UnderscoreOptionsString; + trailingUnderscore?: UnderscoreOptionsString; + prefix?: string[]; + suffix?: string[]; + // selector options + selector: + | IndividualAndMetaSelectorsString + | IndividualAndMetaSelectorsString[]; + modifiers?: ModifiersString[]; + types?: TypeModifiersString[]; + filter?: string | MatchRegex; +} + +interface NormalizedMatchRegex { + regex: RegExp; + match: boolean; +} + +interface NormalizedSelector { + // format options + format: PredefinedFormats[] | null; + custom: NormalizedMatchRegex | null; + leadingUnderscore: UnderscoreOptions | null; + trailingUnderscore: UnderscoreOptions | null; + prefix: string[] | null; + suffix: string[] | null; + // selector options + selector: Selectors | MetaSelectors; + modifiers: Modifiers[] | null; + types: TypeModifiers[] | null; + filter: NormalizedMatchRegex | null; + // calculated ordering weight based on modifiers + modifierWeight: number; +} + +type ValidatorFunction = ( + node: TSESTree.Identifier | TSESTree.Literal, + modifiers?: Set, +) => void; +type ParsedOptions = Record; +type Context = Readonly>; + +export type { + Context, + NormalizedSelector, + ParsedOptions, + Selector, + ValidatorFunction, +}; diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts new file mode 100644 index 000000000000..8945c726368e --- /dev/null +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/validator.ts @@ -0,0 +1,474 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import * as ts from 'typescript'; +import { + MetaSelectors, + Modifiers, + PredefinedFormats, + Selectors, + SelectorsString, + TypeModifiers, + UnderscoreOptions, +} from './enums'; +import { PredefinedFormatToCheckFunction } from './format'; +import { isMetaSelector, selectorTypeToMessageString } from './shared'; +import type { Context, NormalizedSelector } from './types'; +import * as util from '../../util'; + +function createValidator( + type: SelectorsString, + context: Context, + allConfigs: NormalizedSelector[], +): (node: TSESTree.Identifier | TSESTree.Literal) => void { + // make sure the "highest priority" configs are checked first + const selectorType = Selectors[type]; + const configs = allConfigs + // gather all of the applicable selectors + .filter( + c => + (c.selector & selectorType) !== 0 || + c.selector === MetaSelectors.default, + ) + .sort((a, b) => { + if (a.selector === b.selector) { + // in the event of the same selector, order by modifier weight + // sort descending - the type modifiers are "more important" + return b.modifierWeight - a.modifierWeight; + } + + const aIsMeta = isMetaSelector(a.selector); + const bIsMeta = isMetaSelector(b.selector); + + // non-meta selectors should go ahead of meta selectors + if (aIsMeta && !bIsMeta) { + return 1; + } + if (!aIsMeta && bIsMeta) { + return -1; + } + + // both aren't meta selectors + // sort descending - the meta selectors are "least important" + return b.selector - a.selector; + }); + + return ( + node: TSESTree.Identifier | TSESTree.Literal, + modifiers: Set = new Set(), + ): void => { + const originalName = + node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; + + // return will break the loop and stop checking configs + // it is only used when the name is known to have failed or succeeded a config. + for (const config of configs) { + if (config.filter?.regex.test(originalName) !== config.filter?.match) { + // name does not match the filter + continue; + } + + if (config.modifiers?.some(modifier => !modifiers.has(modifier))) { + // does not have the required modifiers + continue; + } + + if (!isCorrectType(node, config, context, selectorType)) { + // is not the correct type + continue; + } + + let name: string | null = originalName; + + name = validateUnderscore('leading', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + name = validateUnderscore('trailing', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + name = validateAffix('prefix', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + name = validateAffix('suffix', config, name, node, originalName); + if (name === null) { + // fail + return; + } + + if (!validateCustom(config, name, node, originalName)) { + // fail + return; + } + + if (!validatePredefinedFormat(config, name, node, originalName)) { + // fail + return; + } + + // it's valid for this config, so we don't need to check any more configs + return; + } + }; + + // centralizes the logic for formatting the report data + function formatReportData({ + affixes, + formats, + originalName, + processedName, + position, + custom, + count, + }: { + affixes?: string[]; + formats?: PredefinedFormats[]; + originalName: string; + processedName?: string; + position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; + custom?: NonNullable; + count?: 'one' | 'two'; + }): Record { + return { + type: selectorTypeToMessageString(type), + name: originalName, + processedName, + position, + count, + affixes: affixes?.join(', '), + formats: formats?.map(f => PredefinedFormats[f]).join(', '), + regex: custom?.regex?.toString(), + regexMatch: + custom?.match === true + ? 'match' + : custom?.match === false + ? 'not match' + : null, + }; + } + + /** + * @returns the name with the underscore removed, if it is valid according to the specified underscore option, null otherwise + */ + function validateUnderscore( + position: 'leading' | 'trailing', + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): string | null { + const option = + position === 'leading' + ? config.leadingUnderscore + : config.trailingUnderscore; + if (!option) { + return name; + } + + const hasSingleUnderscore = + position === 'leading' + ? (): boolean => name.startsWith('_') + : (): boolean => name.endsWith('_'); + const trimSingleUnderscore = + position === 'leading' + ? (): string => name.slice(1) + : (): string => name.slice(0, -1); + + const hasDoubleUnderscore = + position === 'leading' + ? (): boolean => name.startsWith('__') + : (): boolean => name.endsWith('__'); + const trimDoubleUnderscore = + position === 'leading' + ? (): string => name.slice(2) + : (): string => name.slice(0, -2); + + switch (option) { + // ALLOW - no conditions as the user doesn't care if it's there or not + case UnderscoreOptions.allow: { + if (hasSingleUnderscore()) { + return trimSingleUnderscore(); + } + + return name; + } + + case UnderscoreOptions.allowDouble: { + if (hasDoubleUnderscore()) { + return trimDoubleUnderscore(); + } + + return name; + } + + case UnderscoreOptions.allowSingleOrDouble: { + if (hasDoubleUnderscore()) { + return trimDoubleUnderscore(); + } + + if (hasSingleUnderscore()) { + return trimSingleUnderscore(); + } + + return name; + } + + // FORBID + case UnderscoreOptions.forbid: { + if (hasSingleUnderscore()) { + context.report({ + node, + messageId: 'unexpectedUnderscore', + data: formatReportData({ + originalName, + position, + count: 'one', + }), + }); + return null; + } + + return name; + } + + // REQUIRE + case UnderscoreOptions.require: { + if (!hasSingleUnderscore()) { + context.report({ + node, + messageId: 'missingUnderscore', + data: formatReportData({ + originalName, + position, + count: 'one', + }), + }); + return null; + } + + return trimSingleUnderscore(); + } + + case UnderscoreOptions.requireDouble: { + if (!hasDoubleUnderscore()) { + context.report({ + node, + messageId: 'missingUnderscore', + data: formatReportData({ + originalName, + position, + count: 'two', + }), + }); + return null; + } + + return trimDoubleUnderscore(); + } + } + } + + /** + * @returns the name with the affix removed, if it is valid according to the specified affix option, null otherwise + */ + function validateAffix( + position: 'prefix' | 'suffix', + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): string | null { + const affixes = config[position]; + if (!affixes || affixes.length === 0) { + return name; + } + + for (const affix of affixes) { + const hasAffix = + position === 'prefix' ? name.startsWith(affix) : name.endsWith(affix); + const trimAffix = + position === 'prefix' + ? (): string => name.slice(affix.length) + : (): string => name.slice(0, -affix.length); + + if (hasAffix) { + // matches, so trim it and return + return trimAffix(); + } + } + + context.report({ + node, + messageId: 'missingAffix', + data: formatReportData({ + originalName, + position, + affixes, + }), + }); + return null; + } + + /** + * @returns true if the name is valid according to the `regex` option, false otherwise + */ + function validateCustom( + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): boolean { + const custom = config.custom; + if (!custom) { + return true; + } + + const result = custom.regex.test(name); + if (custom.match && result) { + return true; + } + if (!custom.match && !result) { + return true; + } + + context.report({ + node, + messageId: 'satisfyCustom', + data: formatReportData({ + originalName, + custom, + }), + }); + return false; + } + + /** + * @returns true if the name is valid according to the `format` option, false otherwise + */ + function validatePredefinedFormat( + config: NormalizedSelector, + name: string, + node: TSESTree.Identifier | TSESTree.Literal, + originalName: string, + ): boolean { + const formats = config.format; + if (formats === null || formats.length === 0) { + return true; + } + + for (const format of formats) { + const checker = PredefinedFormatToCheckFunction[format]; + if (checker(name)) { + return true; + } + } + + context.report({ + node, + messageId: + originalName === name + ? 'doesNotMatchFormat' + : 'doesNotMatchFormatTrimmed', + data: formatReportData({ + originalName, + processedName: name, + formats, + }), + }); + return false; + } +} + +const SelectorsAllowedToHaveTypes = + Selectors.variable | + Selectors.parameter | + Selectors.classProperty | + Selectors.objectLiteralProperty | + Selectors.typeProperty | + Selectors.parameterProperty | + Selectors.accessor; + +function isCorrectType( + node: TSESTree.Node, + config: NormalizedSelector, + context: Context, + selector: Selectors, +): boolean { + if (config.types === null) { + return true; + } + + if ((SelectorsAllowedToHaveTypes & selector) === 0) { + return true; + } + + const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); + const checker = program.getTypeChecker(); + const tsNode = esTreeNodeToTSNodeMap.get(node); + const type = checker + .getTypeAtLocation(tsNode) + // remove null and undefined from the type, as we don't care about it here + .getNonNullableType(); + + for (const allowedType of config.types) { + switch (allowedType) { + case TypeModifiers.array: + if ( + isAllTypesMatch( + type, + t => checker.isArrayType(t) || checker.isTupleType(t), + ) + ) { + return true; + } + break; + + case TypeModifiers.function: + if (isAllTypesMatch(type, t => t.getCallSignatures().length > 0)) { + return true; + } + break; + + case TypeModifiers.boolean: + case TypeModifiers.number: + case TypeModifiers.string: { + const typeString = checker.typeToString( + // this will resolve things like true => boolean, 'a' => string and 1 => number + checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), + ); + const allowedTypeString = TypeModifiers[allowedType]; + if (typeString === allowedTypeString) { + return true; + } + break; + } + } + } + + return false; +} + +/** + * @returns `true` if the type (or all union types) in the given type return true for the callback + */ +function isAllTypesMatch( + type: ts.Type, + cb: (type: ts.Type) => boolean, +): boolean { + if (type.isUnion()) { + return type.types.every(t => cb(t)); + } + + return cb(type); +} + +export { createValidator }; diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index c0211bb64ef2..338ebfd866b2 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -1,12 +1,19 @@ import { AST_NODE_TYPES, - JSONSchema, TSESLint, TSESTree, } from '@typescript-eslint/experimental-utils'; import { PatternVisitor } from '@typescript-eslint/scope-manager'; -import * as ts from 'typescript'; +import type { ScriptTarget } from 'typescript'; import * as util from '../util'; +import { + Context, + Modifiers, + parseOptions, + SCHEMA, + Selector, + ValidatorFunction, +} from './naming-convention-utils'; type MessageIds = | 'unexpectedUnderscore' @@ -16,451 +23,11 @@ type MessageIds = | 'doesNotMatchFormat' | 'doesNotMatchFormatTrimmed'; -// #region Options Type Config - -enum PredefinedFormats { - camelCase = 1, - strictCamelCase, - PascalCase, - StrictPascalCase, - snake_case, - UPPER_CASE, -} -type PredefinedFormatsString = keyof typeof PredefinedFormats; - -enum UnderscoreOptions { - forbid = 1, - allow, - require, - - // special cases as it's common practice to use double underscore - requireDouble, - allowDouble, - allowSingleOrDouble, -} -type UnderscoreOptionsString = keyof typeof UnderscoreOptions; - -enum Selectors { - // variableLike - variable = 1 << 0, - function = 1 << 1, - parameter = 1 << 2, - - // memberLike - parameterProperty = 1 << 3, - accessor = 1 << 4, - enumMember = 1 << 5, - classMethod = 1 << 6, - objectLiteralMethod = 1 << 7, - typeMethod = 1 << 8, - classProperty = 1 << 9, - objectLiteralProperty = 1 << 10, - typeProperty = 1 << 11, - - // typeLike - class = 1 << 12, - interface = 1 << 13, - typeAlias = 1 << 14, - enum = 1 << 15, - typeParameter = 1 << 17, -} -type SelectorsString = keyof typeof Selectors; - -enum MetaSelectors { - default = -1, - variableLike = 0 | - Selectors.variable | - Selectors.function | - Selectors.parameter, - memberLike = 0 | - Selectors.classProperty | - Selectors.objectLiteralProperty | - Selectors.typeProperty | - Selectors.parameterProperty | - Selectors.enumMember | - Selectors.classMethod | - Selectors.objectLiteralMethod | - Selectors.typeMethod | - Selectors.accessor, - typeLike = 0 | - Selectors.class | - Selectors.interface | - Selectors.typeAlias | - Selectors.enum | - Selectors.typeParameter, - method = 0 | - Selectors.classMethod | - Selectors.objectLiteralMethod | - Selectors.typeProperty, - property = 0 | - Selectors.classProperty | - Selectors.objectLiteralProperty | - Selectors.typeMethod, -} -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, - // things that are unused - unused = 1 << 10, - // properties that require quoting - requiresQuotes = 1 << 11, -} -type ModifiersString = keyof typeof Modifiers; - -enum TypeModifiers { - boolean = 1 << 10, - string = 1 << 11, - number = 1 << 12, - function = 1 << 13, - array = 1 << 14, -} -type TypeModifiersString = keyof typeof TypeModifiers; - -interface Selector { - // format options - format: PredefinedFormatsString[] | null; - custom?: { - regex: string; - match: boolean; - }; - leadingUnderscore?: UnderscoreOptionsString; - trailingUnderscore?: UnderscoreOptionsString; - prefix?: string[]; - suffix?: string[]; - // selector options - selector: - | IndividualAndMetaSelectorsString - | IndividualAndMetaSelectorsString[]; - modifiers?: ModifiersString[]; - types?: TypeModifiersString[]; - filter?: - | string - | { - regex: string; - match: boolean; - }; -} -interface NormalizedSelector { - // format options - format: PredefinedFormats[] | null; - custom: { - regex: RegExp; - match: boolean; - } | null; - leadingUnderscore: UnderscoreOptions | null; - trailingUnderscore: UnderscoreOptions | null; - prefix: string[] | null; - suffix: string[] | null; - // selector options - selector: Selectors | MetaSelectors; - modifiers: Modifiers[] | null; - types: TypeModifiers[] | null; - filter: { - regex: RegExp; - match: boolean; - } | null; - // calculated ordering weight based on modifiers - modifierWeight: number; -} - // Note that this intentionally does not strictly type the modifiers/types properties. // This is because doing so creates a huge headache, as the rule's code doesn't need to care. // The JSON Schema strictly types these properties, so we know the user won't input invalid config. type Options = Selector[]; -// #endregion Options Type Config - -// #region Schema Config - -const UNDERSCORE_SCHEMA: JSONSchema.JSONSchema4 = { - type: 'string', - enum: util.getEnumNames(UnderscoreOptions), -}; -const PREFIX_SUFFIX_SCHEMA: JSONSchema.JSONSchema4 = { - type: 'array', - items: { - type: 'string', - minLength: 1, - }, - additionalItems: false, -}; -const MATCH_REGEX_SCHEMA: JSONSchema.JSONSchema4 = { - type: 'object', - properties: { - match: { type: 'boolean' }, - regex: { type: 'string' }, - }, - required: ['match', 'regex'], -}; -type JSONSchemaProperties = Record; -const FORMAT_OPTIONS_PROPERTIES: JSONSchemaProperties = { - format: { - oneOf: [ - { - type: 'array', - items: { - type: 'string', - enum: util.getEnumNames(PredefinedFormats), - }, - additionalItems: false, - }, - { - type: 'null', - }, - ], - }, - custom: MATCH_REGEX_SCHEMA, - leadingUnderscore: UNDERSCORE_SCHEMA, - trailingUnderscore: UNDERSCORE_SCHEMA, - prefix: PREFIX_SUFFIX_SCHEMA, - suffix: PREFIX_SUFFIX_SCHEMA, -}; -function selectorSchema( - selectorString: IndividualAndMetaSelectorsString, - allowType: boolean, - modifiers?: ModifiersString[], -): JSONSchema.JSONSchema4[] { - const selector: JSONSchemaProperties = { - filter: { - oneOf: [ - { - type: 'string', - minLength: 1, - }, - MATCH_REGEX_SCHEMA, - ], - }, - selector: { - type: 'string', - enum: [selectorString], - }, - }; - if (modifiers && modifiers.length > 0) { - selector.modifiers = { - type: 'array', - items: { - type: 'string', - enum: modifiers, - }, - additionalItems: false, - }; - } - if (allowType) { - selector.types = { - type: 'array', - items: { - type: 'string', - enum: util.getEnumNames(TypeModifiers), - }, - additionalItems: false, - }; - } - - return [ - { - type: 'object', - properties: { - ...FORMAT_OPTIONS_PROPERTIES, - ...selector, - }, - required: ['selector', 'format'], - additionalProperties: false, - }, - ]; -} - -function selectorsSchema(): JSONSchema.JSONSchema4 { - return { - type: 'object', - properties: { - ...FORMAT_OPTIONS_PROPERTIES, - ...{ - filter: { - oneOf: [ - { - type: 'string', - minLength: 1, - }, - MATCH_REGEX_SCHEMA, - ], - }, - selector: { - type: 'array', - items: { - type: 'string', - enum: [ - ...util.getEnumNames(MetaSelectors), - ...util.getEnumNames(Selectors), - ], - }, - additionalItems: false, - }, - modifiers: { - type: 'array', - items: { - type: 'string', - enum: util.getEnumNames(Modifiers), - }, - additionalItems: false, - }, - types: { - type: 'array', - items: { - type: 'string', - enum: util.getEnumNames(TypeModifiers), - }, - additionalItems: false, - }, - }, - }, - required: ['selector', 'format'], - additionalProperties: false, - }; -} - -const SCHEMA: JSONSchema.JSONSchema4 = { - type: 'array', - items: { - oneOf: [ - selectorsSchema(), - ...selectorSchema('default', false, util.getEnumNames(Modifiers)), - - ...selectorSchema('variableLike', false, ['unused']), - ...selectorSchema('variable', true, [ - 'const', - 'destructured', - 'global', - 'exported', - 'unused', - ]), - ...selectorSchema('function', false, ['global', 'exported', 'unused']), - ...selectorSchema('parameter', true, ['unused']), - - ...selectorSchema('memberLike', false, [ - 'private', - 'protected', - 'public', - 'static', - 'readonly', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('classProperty', true, [ - 'private', - 'protected', - 'public', - 'static', - 'readonly', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('objectLiteralProperty', true, [ - 'private', - 'protected', - 'public', - 'static', - 'readonly', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('typeProperty', true, [ - 'private', - 'protected', - 'public', - 'static', - 'readonly', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('parameterProperty', true, [ - 'private', - 'protected', - 'public', - 'readonly', - ]), - ...selectorSchema('property', true, [ - 'private', - 'protected', - 'public', - 'static', - 'readonly', - 'abstract', - 'requiresQuotes', - ]), - - ...selectorSchema('classMethod', false, [ - 'private', - 'protected', - 'public', - 'static', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('objectLiteralMethod', false, [ - 'private', - 'protected', - 'public', - 'static', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('typeMethod', false, [ - 'private', - 'protected', - 'public', - 'static', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('method', false, [ - 'private', - 'protected', - 'public', - 'static', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('accessor', true, [ - 'private', - 'protected', - 'public', - 'static', - 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('enumMember', false, ['requiresQuotes']), - - ...selectorSchema('typeLike', false, ['abstract', 'exported', 'unused']), - ...selectorSchema('class', false, ['abstract', 'exported', 'unused']), - ...selectorSchema('interface', false, ['exported', 'unused']), - ...selectorSchema('typeAlias', false, ['exported', 'unused']), - ...selectorSchema('enum', false, ['exported', 'unused']), - ...selectorSchema('typeParameter', false, ['unused']), - ], - }, - additionalItems: false, -}; - -// #endregion Schema Config - // This essentially mirrors ESLint's `camelcase` rule // note that that rule ignores leading and trailing underscores and only checks those in the middle of a variable name const defaultCamelCaseAllTheThingsConfig: Options = [ @@ -528,6 +95,7 @@ export default util.createRule({ const validators = parseOptions(context); + // getParserServices(context, false) -- dirty hack to work around the docs checker test... const compilerOptions = util .getParserServices(context, true) .program.getCompilerOptions(); @@ -1047,676 +615,11 @@ function isGlobal(scope: TSESLint.Scope.Scope | null): boolean { function requiresQuoting( node: TSESTree.Identifier | TSESTree.Literal, - target: ts.ScriptTarget | undefined, + target: ScriptTarget | undefined, ): boolean { const name = node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; return util.requiresQuoting(name, target); } -type ValidatorFunction = ( - node: TSESTree.Identifier | TSESTree.StringLiteral | TSESTree.NumberLiteral, - modifiers?: Set, -) => void; -type ParsedOptions = Record; -type Context = Readonly>; - -function parseOptions(context: Context): ParsedOptions { - const normalizedOptions = context.options - .map(opt => normalizeOption(opt)) - .reduce((acc, val) => acc.concat(val), []); - return util.getEnumNames(Selectors).reduce((acc, k) => { - acc[k] = createValidator(k, context, normalizedOptions); - return acc; - }, {} as ParsedOptions); -} - -function createValidator( - type: SelectorsString, - context: Context, - allConfigs: NormalizedSelector[], -): (node: TSESTree.Identifier | TSESTree.Literal) => void { - // make sure the "highest priority" configs are checked first - const selectorType = Selectors[type]; - const configs = allConfigs - // gather all of the applicable selectors - .filter( - c => - (c.selector & selectorType) !== 0 || - c.selector === MetaSelectors.default, - ) - .sort((a, b) => { - if (a.selector === b.selector) { - // in the event of the same selector, order by modifier weight - // sort descending - the type modifiers are "more important" - return b.modifierWeight - a.modifierWeight; - } - - const aIsMeta = isMetaSelector(a.selector); - const bIsMeta = isMetaSelector(b.selector); - - // non-meta selectors should go ahead of meta selectors - if (aIsMeta && !bIsMeta) { - return 1; - } - if (!aIsMeta && bIsMeta) { - return -1; - } - - // both aren't meta selectors - // sort descending - the meta selectors are "least important" - return b.selector - a.selector; - }); - - return ( - node: TSESTree.Identifier | TSESTree.Literal, - modifiers: Set = new Set(), - ): void => { - const originalName = - node.type === AST_NODE_TYPES.Identifier ? node.name : `${node.value}`; - - // return will break the loop and stop checking configs - // it is only used when the name is known to have failed or succeeded a config. - for (const config of configs) { - if (config.filter?.regex.test(originalName) !== config.filter?.match) { - // name does not match the filter - continue; - } - - if (config.modifiers?.some(modifier => !modifiers.has(modifier))) { - // does not have the required modifiers - continue; - } - - if (!isCorrectType(node, config, context)) { - // is not the correct type - continue; - } - - let name: string | null = originalName; - - name = validateUnderscore('leading', config, name, node, originalName); - if (name === null) { - // fail - return; - } - - name = validateUnderscore('trailing', config, name, node, originalName); - if (name === null) { - // fail - return; - } - - name = validateAffix('prefix', config, name, node, originalName); - if (name === null) { - // fail - return; - } - - name = validateAffix('suffix', config, name, node, originalName); - if (name === null) { - // fail - return; - } - - if (!validateCustom(config, name, node, originalName)) { - // fail - return; - } - - if (!validatePredefinedFormat(config, name, node, originalName)) { - // fail - return; - } - - // it's valid for this config, so we don't need to check any more configs - return; - } - }; - - // centralizes the logic for formatting the report data - function formatReportData({ - affixes, - formats, - originalName, - processedName, - position, - custom, - count, - }: { - affixes?: string[]; - formats?: PredefinedFormats[]; - originalName: string; - processedName?: string; - position?: 'leading' | 'trailing' | 'prefix' | 'suffix'; - custom?: NonNullable; - count?: 'one' | 'two'; - }): Record { - return { - type: selectorTypeToMessageString(type), - name: originalName, - processedName, - position, - count, - affixes: affixes?.join(', '), - formats: formats?.map(f => PredefinedFormats[f]).join(', '), - regex: custom?.regex?.toString(), - regexMatch: - custom?.match === true - ? 'match' - : custom?.match === false - ? 'not match' - : null, - }; - } - - /** - * @returns the name with the underscore removed, if it is valid according to the specified underscore option, null otherwise - */ - function validateUnderscore( - position: 'leading' | 'trailing', - config: NormalizedSelector, - name: string, - node: TSESTree.Identifier | TSESTree.Literal, - originalName: string, - ): string | null { - const option = - position === 'leading' - ? config.leadingUnderscore - : config.trailingUnderscore; - if (!option) { - return name; - } - - const hasSingleUnderscore = - position === 'leading' - ? (): boolean => name.startsWith('_') - : (): boolean => name.endsWith('_'); - const trimSingleUnderscore = - position === 'leading' - ? (): string => name.slice(1) - : (): string => name.slice(0, -1); - - const hasDoubleUnderscore = - position === 'leading' - ? (): boolean => name.startsWith('__') - : (): boolean => name.endsWith('__'); - const trimDoubleUnderscore = - position === 'leading' - ? (): string => name.slice(2) - : (): string => name.slice(0, -2); - - switch (option) { - // ALLOW - no conditions as the user doesn't care if it's there or not - case UnderscoreOptions.allow: { - if (hasSingleUnderscore()) { - return trimSingleUnderscore(); - } - - return name; - } - - case UnderscoreOptions.allowDouble: { - if (hasDoubleUnderscore()) { - return trimDoubleUnderscore(); - } - - return name; - } - - case UnderscoreOptions.allowSingleOrDouble: { - if (hasDoubleUnderscore()) { - return trimDoubleUnderscore(); - } - - if (hasSingleUnderscore()) { - return trimSingleUnderscore(); - } - - return name; - } - - // FORBID - case UnderscoreOptions.forbid: { - if (hasSingleUnderscore()) { - context.report({ - node, - messageId: 'unexpectedUnderscore', - data: formatReportData({ - originalName, - position, - count: 'one', - }), - }); - return null; - } - - return name; - } - - // REQUIRE - case UnderscoreOptions.require: { - if (!hasSingleUnderscore()) { - context.report({ - node, - messageId: 'missingUnderscore', - data: formatReportData({ - originalName, - position, - count: 'one', - }), - }); - return null; - } - - return trimSingleUnderscore(); - } - - case UnderscoreOptions.requireDouble: { - if (!hasDoubleUnderscore()) { - context.report({ - node, - messageId: 'missingUnderscore', - data: formatReportData({ - originalName, - position, - count: 'two', - }), - }); - return null; - } - - return trimDoubleUnderscore(); - } - } - } - - /** - * @returns the name with the affix removed, if it is valid according to the specified affix option, null otherwise - */ - function validateAffix( - position: 'prefix' | 'suffix', - config: NormalizedSelector, - name: string, - node: TSESTree.Identifier | TSESTree.Literal, - originalName: string, - ): string | null { - const affixes = config[position]; - if (!affixes || affixes.length === 0) { - return name; - } - - for (const affix of affixes) { - const hasAffix = - position === 'prefix' ? name.startsWith(affix) : name.endsWith(affix); - const trimAffix = - position === 'prefix' - ? (): string => name.slice(affix.length) - : (): string => name.slice(0, -affix.length); - - if (hasAffix) { - // matches, so trim it and return - return trimAffix(); - } - } - - context.report({ - node, - messageId: 'missingAffix', - data: formatReportData({ - originalName, - position, - affixes, - }), - }); - return null; - } - - /** - * @returns true if the name is valid according to the `regex` option, false otherwise - */ - function validateCustom( - config: NormalizedSelector, - name: string, - node: TSESTree.Identifier | TSESTree.Literal, - originalName: string, - ): boolean { - const custom = config.custom; - if (!custom) { - return true; - } - - const result = custom.regex.test(name); - if (custom.match && result) { - return true; - } - if (!custom.match && !result) { - return true; - } - - context.report({ - node, - messageId: 'satisfyCustom', - data: formatReportData({ - originalName, - custom, - }), - }); - return false; - } - - /** - * @returns true if the name is valid according to the `format` option, false otherwise - */ - function validatePredefinedFormat( - config: NormalizedSelector, - name: string, - node: TSESTree.Identifier | TSESTree.Literal, - originalName: string, - ): boolean { - const formats = config.format; - if (formats === null || formats.length === 0) { - return true; - } - - for (const format of formats) { - const checker = PredefinedFormatToCheckFunction[format]; - if (checker(name)) { - return true; - } - } - - context.report({ - node, - messageId: - originalName === name - ? 'doesNotMatchFormat' - : 'doesNotMatchFormatTrimmed', - data: formatReportData({ - originalName, - processedName: name, - formats, - }), - }); - return false; - } -} - -// #region Predefined Format Functions - -/* -These format functions are taken from `tslint-consistent-codestyle/naming-convention`: -https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/rules/namingConventionRule.ts#L603-L645 - -The licence for the code can be viewed here: -https://github.com/ajafff/tslint-consistent-codestyle/blob/ab156cc8881bcc401236d999f4ce034b59039e81/LICENSE -*/ - -/* -Why not regex here? Because it's actually really, really difficult to create a regex to handle -all of the unicode cases, and we have many non-english users that use non-english characters. -https://gist.github.com/mathiasbynens/6334847 -*/ - -function isPascalCase(name: string): boolean { - return ( - name.length === 0 || - (name[0] === name[0].toUpperCase() && !name.includes('_')) - ); -} -function isStrictPascalCase(name: string): boolean { - return ( - name.length === 0 || - (name[0] === name[0].toUpperCase() && hasStrictCamelHumps(name, true)) - ); -} - -function isCamelCase(name: string): boolean { - return ( - name.length === 0 || - (name[0] === name[0].toLowerCase() && !name.includes('_')) - ); -} -function isStrictCamelCase(name: string): boolean { - return ( - name.length === 0 || - (name[0] === name[0].toLowerCase() && hasStrictCamelHumps(name, false)) - ); -} - -function hasStrictCamelHumps(name: string, isUpper: boolean): boolean { - function isUppercaseChar(char: string): boolean { - return char === char.toUpperCase() && char !== char.toLowerCase(); - } - - if (name.startsWith('_')) { - return false; - } - for (let i = 1; i < name.length; ++i) { - if (name[i] === '_') { - return false; - } - if (isUpper === isUppercaseChar(name[i])) { - if (isUpper) { - return false; - } - } else { - isUpper = !isUpper; - } - } - return true; -} - -function isSnakeCase(name: string): boolean { - return ( - name.length === 0 || - (name === name.toLowerCase() && validateUnderscores(name)) - ); -} - -function isUpperCase(name: string): boolean { - return ( - name.length === 0 || - (name === name.toUpperCase() && validateUnderscores(name)) - ); -} - -/** Check for leading trailing and adjacent underscores */ -function validateUnderscores(name: string): boolean { - if (name.startsWith('_')) { - return false; - } - let wasUnderscore = false; - for (let i = 1; i < name.length; ++i) { - if (name[i] === '_') { - if (wasUnderscore) { - return false; - } - wasUnderscore = true; - } else { - wasUnderscore = false; - } - } - return !wasUnderscore; -} - -const PredefinedFormatToCheckFunction: Readonly boolean ->> = { - [PredefinedFormats.PascalCase]: isPascalCase, - [PredefinedFormats.StrictPascalCase]: isStrictPascalCase, - [PredefinedFormats.camelCase]: isCamelCase, - [PredefinedFormats.strictCamelCase]: isStrictCamelCase, - [PredefinedFormats.UPPER_CASE]: isUpperCase, - [PredefinedFormats.snake_case]: isSnakeCase, -}; - -// #endregion Predefined Format Functions - -function selectorTypeToMessageString(selectorType: SelectorsString): string { - const notCamelCase = selectorType.replace(/([A-Z])/g, ' $1'); - return notCamelCase.charAt(0).toUpperCase() + notCamelCase.slice(1); -} - -function isMetaSelector( - selector: IndividualAndMetaSelectorsString | Selectors | MetaSelectors, -): selector is MetaSelectorsString { - return selector in MetaSelectors; -} - -function normalizeOption(option: Selector): NormalizedSelector[] { - let weight = 0; - option.modifiers?.forEach(mod => { - weight |= Modifiers[mod]; - }); - option.types?.forEach(mod => { - weight |= TypeModifiers[mod]; - }); - - // give selectors with a filter the _highest_ priority - if (option.filter) { - weight |= 1 << 30; - } - - const normalizedOption = { - // format options - format: option.format ? option.format.map(f => PredefinedFormats[f]) : null, - custom: option.custom - ? { - regex: new RegExp(option.custom.regex, 'u'), - match: option.custom.match, - } - : null, - leadingUnderscore: - option.leadingUnderscore !== undefined - ? UnderscoreOptions[option.leadingUnderscore] - : null, - trailingUnderscore: - option.trailingUnderscore !== undefined - ? UnderscoreOptions[option.trailingUnderscore] - : null, - prefix: option.prefix && option.prefix.length > 0 ? option.prefix : null, - suffix: option.suffix && option.suffix.length > 0 ? option.suffix : null, - modifiers: option.modifiers?.map(m => Modifiers[m]) ?? null, - types: option.types?.map(m => TypeModifiers[m]) ?? null, - filter: - option.filter !== undefined - ? typeof option.filter === 'string' - ? { regex: new RegExp(option.filter, 'u'), match: true } - : { - regex: new RegExp(option.filter.regex, 'u'), - match: option.filter.match, - } - : null, - // calculated ordering weight based on modifiers - modifierWeight: weight, - }; - - const selectors = Array.isArray(option.selector) - ? option.selector - : [option.selector]; - - const selectorsAllowedToHaveTypes = - Selectors.variable | - Selectors.parameter | - Selectors.classProperty | - Selectors.objectLiteralProperty | - Selectors.typeProperty | - Selectors.parameterProperty | - Selectors.accessor; - - const config: NormalizedSelector[] = []; - selectors - .map(selector => - isMetaSelector(selector) ? MetaSelectors[selector] : Selectors[selector], - ) - .forEach(selector => - (selectorsAllowedToHaveTypes & selector) !== 0 - ? config.push({ selector: selector, ...normalizedOption }) - : config.push({ - selector: selector, - ...normalizedOption, - types: null, - }), - ); - - return config; -} - -function isCorrectType( - node: TSESTree.Node, - config: NormalizedSelector, - context: Context, -): boolean { - if (config.types === null) { - return true; - } - - const { esTreeNodeToTSNodeMap, program } = util.getParserServices(context); - const checker = program.getTypeChecker(); - const tsNode = esTreeNodeToTSNodeMap.get(node); - const type = checker - .getTypeAtLocation(tsNode) - // remove null and undefined from the type, as we don't care about it here - .getNonNullableType(); - - for (const allowedType of config.types) { - switch (allowedType) { - case TypeModifiers.array: - if ( - isAllTypesMatch( - type, - t => checker.isArrayType(t) || checker.isTupleType(t), - ) - ) { - return true; - } - break; - - case TypeModifiers.function: - if (isAllTypesMatch(type, t => t.getCallSignatures().length > 0)) { - return true; - } - break; - - case TypeModifiers.boolean: - case TypeModifiers.number: - case TypeModifiers.string: { - const typeString = checker.typeToString( - // this will resolve things like true => boolean, 'a' => string and 1 => number - checker.getWidenedType(checker.getBaseTypeOfLiteralType(type)), - ); - const allowedTypeString = TypeModifiers[allowedType]; - if (typeString === allowedTypeString) { - return true; - } - break; - } - } - } - - return false; -} - -/** - * @returns `true` if the type (or all union types) in the given type return true for the callback - */ -function isAllTypesMatch( - type: ts.Type, - cb: (type: ts.Type) => boolean, -): boolean { - if (type.isUnion()) { - return type.types.every(t => cb(t)); - } - - return cb(type); -} - -export { - MessageIds, - Options, - PredefinedFormatsString, - Selector, - selectorTypeToMessageString, -}; +export { MessageIds, Options }; diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index b53d7b9304cc..f78b061a9696 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -1,11 +1,10 @@ import { TSESLint } from '@typescript-eslint/experimental-utils'; -import rule, { - MessageIds, - Options, +import rule, { MessageIds, Options } from '../../src/rules/naming-convention'; +import { PredefinedFormatsString, - Selector, selectorTypeToMessageString, -} from '../../src/rules/naming-convention'; + Selector, +} from '../../src/rules/naming-convention-utils'; import { RuleTester, getFixturesRootDir } from '../RuleTester'; const ruleTester = new RuleTester({ From bf904ec72db57174fec531f61e9427230662553e Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 26 Nov 2020 13:32:40 -0800 Subject: [PATCH 16/19] fix(typescript-estree): add default value for `parserOptions.projectFolderIgnoreList` and deduplicate resolved projects (#2819) In #2418 I introduced a regression - I forgot to add in the default value for `projectFolderIgnoreList`. This means that globs have been matching `node_modules` since the v4.0 release! Oops :( This PR fixes that. It also hoists the tsconfig path canonicalisation up so that we can deduplicate early. Previously if you provided a config like `projects: ['./tsconfig.json', './**/tsconfig.json']`, then we would resolve this to `./tsconfig.json` and `tsconfig.json`, then later canonicalise them. This meant we'd check that same tsconfig twice! By hoisting the canonicalisation, we can deduplicate this early. Fixes #2814 --- .../create-program/createDefaultProgram.ts | 4 +- .../src/create-program/createWatchProgram.ts | 5 +- .../src/create-program/shared.ts | 5 -- .../typescript-estree/src/parser-options.ts | 3 +- packages/typescript-estree/src/parser.ts | 54 ++++++++++++------- 5 files changed, 39 insertions(+), 32 deletions(-) diff --git a/packages/typescript-estree/src/create-program/createDefaultProgram.ts b/packages/typescript-estree/src/create-program/createDefaultProgram.ts index e0aaf6253d7e..c912a44418a0 100644 --- a/packages/typescript-estree/src/create-program/createDefaultProgram.ts +++ b/packages/typescript-estree/src/create-program/createDefaultProgram.ts @@ -4,7 +4,7 @@ import * as ts from 'typescript'; import { Extra } from '../parser-options'; import { ASTAndProgram, - getTsconfigPath, + CanonicalPath, createDefaultCompilerOptionsFromExtra, } from './shared'; @@ -27,7 +27,7 @@ function createDefaultProgram( return undefined; } - const tsconfigPath = getTsconfigPath(extra.projects[0], extra); + const tsconfigPath: CanonicalPath = extra.projects[0]; const commandLine = ts.getParsedCommandLineOfConfigFile( tsconfigPath, diff --git a/packages/typescript-estree/src/create-program/createWatchProgram.ts b/packages/typescript-estree/src/create-program/createWatchProgram.ts index da9ff8384b73..e00663a114ed 100644 --- a/packages/typescript-estree/src/create-program/createWatchProgram.ts +++ b/packages/typescript-estree/src/create-program/createWatchProgram.ts @@ -9,7 +9,6 @@ import { CanonicalPath, createDefaultCompilerOptionsFromExtra, getCanonicalFileName, - getTsconfigPath, } from './shared'; const log = debug('typescript-eslint:typescript-estree:createWatchProgram'); @@ -197,9 +196,7 @@ function getProgramsForProjects( * - the required program hasn't been created yet, or * - the file is new/renamed, and the program hasn't been updated. */ - for (const rawTsconfigPath of extra.projects) { - const tsconfigPath = getTsconfigPath(rawTsconfigPath, extra); - + for (const tsconfigPath of extra.projects) { const existingWatch = knownWatchProgramMap.get(tsconfigPath); if (existingWatch) { diff --git a/packages/typescript-estree/src/create-program/shared.ts b/packages/typescript-estree/src/create-program/shared.ts index 702e7884ccb5..ae2962528040 100644 --- a/packages/typescript-estree/src/create-program/shared.ts +++ b/packages/typescript-estree/src/create-program/shared.ts @@ -60,10 +60,6 @@ function ensureAbsolutePath(p: string, extra: Extra): string { : path.join(extra.tsconfigRootDir || process.cwd(), p); } -function getTsconfigPath(tsconfigPath: string, extra: Extra): CanonicalPath { - return getCanonicalFileName(ensureAbsolutePath(tsconfigPath, extra)); -} - function canonicalDirname(p: CanonicalPath): CanonicalPath { return path.dirname(p) as CanonicalPath; } @@ -105,5 +101,4 @@ export { ensureAbsolutePath, getCanonicalFileName, getScriptKind, - getTsconfigPath, }; diff --git a/packages/typescript-estree/src/parser-options.ts b/packages/typescript-estree/src/parser-options.ts index 33195b0c8d65..36ca3f80cca3 100644 --- a/packages/typescript-estree/src/parser-options.ts +++ b/packages/typescript-estree/src/parser-options.ts @@ -1,6 +1,7 @@ import { DebugLevel } from '@typescript-eslint/types'; import { Program } from 'typescript'; import { TSESTree, TSNode, TSESTreeToTSNode, TSToken } from './ts-estree'; +import { CanonicalPath } from './create-program/shared'; type DebugModule = 'typescript-eslint' | 'eslint' | 'typescript'; @@ -19,7 +20,7 @@ export interface Extra { loc: boolean; log: (message: string) => void; preserveNodeMaps?: boolean; - projects: string[]; + projects: CanonicalPath[]; range: boolean; strict: boolean; tokens: null | TSESTree.Token[]; diff --git a/packages/typescript-estree/src/parser.ts b/packages/typescript-estree/src/parser.ts index 3dc22e262c8e..dd765eb55aaf 100644 --- a/packages/typescript-estree/src/parser.ts +++ b/packages/typescript-estree/src/parser.ts @@ -12,7 +12,12 @@ import { createSourceFile } from './create-program/createSourceFile'; import { Extra, TSESTreeOptions, ParserServices } from './parser-options'; import { getFirstSemanticOrSyntacticError } from './semantic-or-syntactic-errors'; import { TSESTree } from './ts-estree'; -import { ASTAndProgram, ensureAbsolutePath } from './create-program/shared'; +import { + ASTAndProgram, + CanonicalPath, + ensureAbsolutePath, + getCanonicalFileName, +} from './create-program/shared'; const log = debug('typescript-eslint:typescript-estree:parser'); @@ -109,46 +114,53 @@ function resetExtra(): void { }; } +function getTsconfigPath(tsconfigPath: string, extra: Extra): CanonicalPath { + return getCanonicalFileName(ensureAbsolutePath(tsconfigPath, extra)); +} + /** - * Normalizes, sanitizes, resolves and filters the provided + * Normalizes, sanitizes, resolves and filters the provided project paths */ function prepareAndTransformProjects( projectsInput: string | string[] | undefined, ignoreListInput: string[], -): string[] { - let projects: string[] = []; +): CanonicalPath[] { + const sanitizedProjects: string[] = []; // Normalize and sanitize the project paths if (typeof projectsInput === 'string') { - projects.push(projectsInput); + sanitizedProjects.push(projectsInput); } else if (Array.isArray(projectsInput)) { for (const project of projectsInput) { if (typeof project === 'string') { - projects.push(project); + sanitizedProjects.push(project); } } } - if (projects.length === 0) { - return projects; + if (sanitizedProjects.length === 0) { + return []; } // Transform glob patterns into paths - const globbedProjects = projects.filter(project => isGlob(project)); - projects = projects - .filter(project => !isGlob(project)) - .concat( - globSync([...globbedProjects, ...ignoreListInput], { - cwd: extra.tsconfigRootDir, - }), - ); + const nonGlobProjects = sanitizedProjects.filter(project => !isGlob(project)); + const globProjects = sanitizedProjects.filter(project => isGlob(project)); + const uniqueCanonicalProjectPaths = new Set( + nonGlobProjects + .concat( + globSync([...globProjects, ...ignoreListInput], { + cwd: extra.tsconfigRootDir, + }), + ) + .map(project => getTsconfigPath(project, extra)), + ); log( 'parserOptions.project (excluding ignored) matched projects: %s', - projects, + uniqueCanonicalProjectPaths, ); - return projects; + return Array.from(uniqueCanonicalProjectPaths); } function applyParserOptionsToExtra(options: TSESTreeOptions): void { @@ -252,8 +264,9 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { // NOTE - ensureAbsolutePath relies upon having the correct tsconfigRootDir in extra extra.filePath = ensureAbsolutePath(extra.filePath, extra); - // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra - const projectFolderIgnoreList = (options.projectFolderIgnoreList ?? []) + const projectFolderIgnoreList = ( + options.projectFolderIgnoreList ?? ['**/node_modules/**'] + ) .reduce((acc, folder) => { if (typeof folder === 'string') { acc.push(folder); @@ -262,6 +275,7 @@ function applyParserOptionsToExtra(options: TSESTreeOptions): void { }, []) // prefix with a ! for not match glob .map(folder => (folder.startsWith('!') ? folder : `!${folder}`)); + // NOTE - prepareAndTransformProjects relies upon having the correct tsconfigRootDir in extra extra.projects = prepareAndTransformProjects( options.project, projectFolderIgnoreList, From 050023aa7bd791d0be7b5788a9dcd8e61a00ce79 Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sun, 29 Nov 2020 13:39:05 -0800 Subject: [PATCH 17/19] fix(eslint-plugin): [no-use-before-define] allow class references if they're within a class decorator (#2827) Fixes #2842 --- .cspell.json | 3 +- .../src/rules/no-use-before-define.ts | 32 +++++++++++++++++ .../tests/rules/no-use-before-define.test.ts | 34 +++++++++++++++++++ 3 files changed, 68 insertions(+), 1 deletion(-) diff --git a/.cspell.json b/.cspell.json index 36beb3bb328e..c74d69186b91 100644 --- a/.cspell.json +++ b/.cspell.json @@ -89,9 +89,10 @@ "rulesets", "serializers", "superset", - "transpiling", "thenables", + "transpiled", "transpiles", + "transpiling", "tsconfigs", "tsutils", "typedef", diff --git a/packages/eslint-plugin/src/rules/no-use-before-define.ts b/packages/eslint-plugin/src/rules/no-use-before-define.ts index b67f5ef09c33..2b0bb32ccb57 100644 --- a/packages/eslint-plugin/src/rules/no-use-before-define.ts +++ b/packages/eslint-plugin/src/rules/no-use-before-define.ts @@ -133,6 +133,37 @@ function isInRange( return !!node && node.range[0] <= location && location <= node.range[1]; } +/** + * Decorators are transpiled such that the decorator is placed after the class declaration + * So it is considered safe + */ +function isClassRefInClassDecorator( + variable: TSESLint.Scope.Variable, + reference: TSESLint.Scope.Reference, +): boolean { + if (variable.defs[0].type !== 'ClassName') { + return false; + } + + if ( + !variable.defs[0].node.decorators || + variable.defs[0].node.decorators.length === 0 + ) { + return false; + } + + for (const deco of variable.defs[0].node.decorators) { + if ( + reference.identifier.range[0] >= deco.range[0] && + reference.identifier.range[1] <= deco.range[1] + ) { + return true; + } + } + + return false; +} + /** * Checks whether or not a given reference is inside of the initializers of a given variable. * @@ -292,6 +323,7 @@ export default util.createRule({ (variable.identifiers[0].range[1] <= reference.identifier.range[1] && !isInInitializer(variable, reference)) || !isForbidden(variable, reference) || + isClassRefInClassDecorator(variable, reference) || reference.from.type === TSESLint.Scope.ScopeType.functionType ) { return; diff --git a/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts b/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts index 77e434d33821..8fb4d076602c 100644 --- a/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts +++ b/packages/eslint-plugin/tests/rules/no-use-before-define.test.ts @@ -394,6 +394,40 @@ declare global { } } `, + // https://github.com/typescript-eslint/typescript-eslint/issues/2824 + ` +@Directive({ + selector: '[rcCidrIpPattern]', + providers: [ + { + provide: NG_VALIDATORS, + useExisting: CidrIpPatternDirective, + multi: true, + }, + ], +}) +export class CidrIpPatternDirective implements Validator {} + `, + { + code: ` +@Directive({ + selector: '[rcCidrIpPattern]', + providers: [ + { + provide: NG_VALIDATORS, + useExisting: CidrIpPatternDirective, + multi: true, + }, + ], +}) +export class CidrIpPatternDirective implements Validator {} + `, + options: [ + { + classes: false, + }, + ], + }, ], invalid: [ { From 525d2ff9292d89e1445b273b5378159bca323a1e Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sun, 29 Nov 2020 16:32:42 -0800 Subject: [PATCH 18/19] feat(eslint-plugin): [naming-convention] allow `destructured` modifier for `parameter` selector (#2829) Fixes #2828 --- .../docs/rules/naming-convention.md | 16 +++--- .../rules/naming-convention-utils/schema.ts | 52 ++++++------------- .../src/rules/naming-convention.ts | 29 +++++++---- .../tests/rules/naming-convention.test.ts | 50 +++++++++++++++++- 4 files changed, 90 insertions(+), 57 deletions(-) diff --git a/packages/eslint-plugin/docs/rules/naming-convention.md b/packages/eslint-plugin/docs/rules/naming-convention.md index e5bbfb018dfa..fc4d914e449f 100644 --- a/packages/eslint-plugin/docs/rules/naming-convention.md +++ b/packages/eslint-plugin/docs/rules/naming-convention.md @@ -192,31 +192,31 @@ Individual Selectors match specific, well-defined sets. There is no overlap betw - Allowed `modifiers`: `global`, `exported`, `unused`. - Allowed `types`: none. - `parameter` - matches any function parameter. Does not match parameter properties. - - Allowed `modifiers`: `unused`. + - Allowed `modifiers`: `destructured`, `unused`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classProperty` - matches any class property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `abstract`, `private`, `protected`, `public`, `readonly`, `requiresQuotes`, `static`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `objectLiteralProperty` - matches any object literal property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `public`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `typeProperty` - matches any object type property. Does not match properties that have direct function expression or arrow function expression values. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `public`, `readonly`, `requiresQuotes`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `parameterProperty` - matches any parameter property. - Allowed `modifiers`: `private`, `protected`, `public`, `readonly`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `classMethod` - matches any class method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `abstract`, `private`, `protected`, `public`, `requiresQuotes`, `static`. - Allowed `types`: none. - `objectLiteralMethod` - matches any object literal method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `public`, `requiresQuotes`. - Allowed `types`: none. - `typeMethod` - matches any object type method. Also matches properties that have direct function expression or arrow function expression values. Does not match accessors. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `public`, `requiresQuotes`. - Allowed `types`: none. - `accessor` - matches any accessor. - - Allowed `modifiers`: `private`, `protected`, `public`, `static`, `readonly`, `abstract`, `requiresQuotes`. + - Allowed `modifiers`: `abstract`, `private`, `protected`, `public`, `requiresQuotes`, `static`. - Allowed `types`: `boolean`, `string`, `number`, `function`, `array`. - `enumMember` - matches any enum member. - Allowed `modifiers`: `requiresQuotes`. diff --git a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts index 990017db7c95..4d16617f122e 100644 --- a/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts +++ b/packages/eslint-plugin/src/rules/naming-convention-utils/schema.ts @@ -171,47 +171,38 @@ const SCHEMA: JSONSchema.JSONSchema4 = { ...selectorSchema('variable', true, [ 'const', 'destructured', - 'global', 'exported', + 'global', 'unused', ]), - ...selectorSchema('function', false, ['global', 'exported', 'unused']), - ...selectorSchema('parameter', true, ['unused']), + ...selectorSchema('function', false, ['exported', 'global', 'unused']), + ...selectorSchema('parameter', true, ['destructured', 'unused']), ...selectorSchema('memberLike', false, [ + 'abstract', 'private', 'protected', 'public', - 'static', 'readonly', - 'abstract', 'requiresQuotes', + 'static', ]), ...selectorSchema('classProperty', true, [ + 'abstract', 'private', 'protected', 'public', - 'static', 'readonly', - 'abstract', 'requiresQuotes', + 'static', ]), ...selectorSchema('objectLiteralProperty', true, [ - 'private', - 'protected', 'public', - 'static', - 'readonly', - 'abstract', 'requiresQuotes', ]), ...selectorSchema('typeProperty', true, [ - 'private', - 'protected', 'public', - 'static', 'readonly', - 'abstract', 'requiresQuotes', ]), ...selectorSchema('parameterProperty', true, [ @@ -221,54 +212,43 @@ const SCHEMA: JSONSchema.JSONSchema4 = { 'readonly', ]), ...selectorSchema('property', true, [ + 'abstract', 'private', 'protected', 'public', - 'static', 'readonly', - 'abstract', 'requiresQuotes', + 'static', ]), ...selectorSchema('classMethod', false, [ - 'private', - 'protected', - 'public', - 'static', 'abstract', - 'requiresQuotes', - ]), - ...selectorSchema('objectLiteralMethod', false, [ 'private', 'protected', 'public', - 'static', - 'abstract', 'requiresQuotes', + 'static', ]), - ...selectorSchema('typeMethod', false, [ - 'private', - 'protected', + ...selectorSchema('objectLiteralMethod', false, [ 'public', - 'static', - 'abstract', 'requiresQuotes', ]), + ...selectorSchema('typeMethod', false, ['public', 'requiresQuotes']), ...selectorSchema('method', false, [ + 'abstract', 'private', 'protected', 'public', - 'static', - 'abstract', 'requiresQuotes', + 'static', ]), ...selectorSchema('accessor', true, [ + 'abstract', 'private', 'protected', 'public', - 'static', - 'abstract', 'requiresQuotes', + 'static', ]), ...selectorSchema('enumMember', false, ['requiresQuotes']), diff --git a/packages/eslint-plugin/src/rules/naming-convention.ts b/packages/eslint-plugin/src/rules/naming-convention.ts index 338ebfd866b2..16e31b62ef8a 100644 --- a/packages/eslint-plugin/src/rules/naming-convention.ts +++ b/packages/eslint-plugin/src/rules/naming-convention.ts @@ -174,6 +174,19 @@ export default util.createRule({ return unusedVariables.has(variable); } + function isDestructured(id: TSESTree.Identifier): boolean { + return ( + // `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) + ); + } + return { // #region variable @@ -199,17 +212,7 @@ export default util.createRule({ 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) - ) { + if (isDestructured(id)) { modifiers.add(Modifiers.destructured); } @@ -285,6 +288,10 @@ export default util.createRule({ identifiers.forEach(i => { const modifiers = new Set(); + if (isDestructured(i)) { + modifiers.add(Modifiers.destructured); + } + if (isUnused(i.name)) { modifiers.add(Modifiers.unused); } diff --git a/packages/eslint-plugin/tests/rules/naming-convention.test.ts b/packages/eslint-plugin/tests/rules/naming-convention.test.ts index f78b061a9696..2a1f1c177cd6 100644 --- a/packages/eslint-plugin/tests/rules/naming-convention.test.ts +++ b/packages/eslint-plugin/tests/rules/naming-convention.test.ts @@ -1192,6 +1192,28 @@ ruleTester.run('naming-convention', rule, { }, ], }, + { + code: ` + export function Foo( + { aName }, + { anotherName = 1 }, + { ignored: IgnoredDueToModifiers1 }, + { ignored: IgnoredDueToModifiers1 = 2 }, + IgnoredDueToModifiers2, + ) {} + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'parameter', + modifiers: ['destructured'], + format: ['camelCase'], + }, + ], + }, { code: ` class Ignored { @@ -1965,9 +1987,10 @@ ruleTester.run('naming-convention', rule, { { code: ` const { some_name1 } = {}; - const { ignore: IgnoredDueToModifiers1 } = {}; const { some_name2 = 2 } = {}; - const IgnoredDueToModifiers2 = 1; + const { ignored: IgnoredDueToModifiers1 } = {}; + const { ignored: IgnoredDueToModifiers2 = 3 } = {}; + const IgnoredDueToModifiers3 = 1; `, options: [ { @@ -1982,6 +2005,29 @@ ruleTester.run('naming-convention', rule, { ], errors: Array(2).fill({ messageId: 'doesNotMatchFormat' }), }, + { + code: ` + export function Foo( + { aName }, + { anotherName = 1 }, + { ignored: IgnoredDueToModifiers1 }, + { ignored: IgnoredDueToModifiers1 = 2 }, + IgnoredDueToModifiers2, + ) {} + `, + options: [ + { + selector: 'default', + format: ['PascalCase'], + }, + { + selector: 'parameter', + modifiers: ['destructured'], + format: ['UPPER_CASE'], + }, + ], + errors: Array(2).fill({ messageId: 'doesNotMatchFormat' }), + }, { code: ` class Ignored { From f714911944b95dd4495166b35ff7784aedac1451 Mon Sep 17 00:00:00 2001 From: James Henry Date: Mon, 30 Nov 2020 18:02:29 +0000 Subject: [PATCH 19/19] chore: publish v4.9.0 --- CHANGELOG.md | 29 ++++++++++++++++++++ lerna.json | 2 +- packages/eslint-plugin-internal/CHANGELOG.md | 8 ++++++ packages/eslint-plugin-internal/package.json | 4 +-- packages/eslint-plugin-tslint/CHANGELOG.md | 8 ++++++ packages/eslint-plugin-tslint/package.json | 6 ++-- packages/eslint-plugin/CHANGELOG.md | 28 +++++++++++++++++++ packages/eslint-plugin/package.json | 6 ++-- packages/experimental-utils/CHANGELOG.md | 11 ++++++++ packages/experimental-utils/package.json | 8 +++--- packages/parser/CHANGELOG.md | 8 ++++++ packages/parser/package.json | 12 ++++---- packages/scope-manager/CHANGELOG.md | 16 +++++++++++ packages/scope-manager/package.json | 8 +++--- packages/shared-fixtures/CHANGELOG.md | 8 ++++++ packages/shared-fixtures/package.json | 2 +- packages/types/CHANGELOG.md | 11 ++++++++ packages/types/package.json | 2 +- packages/typescript-estree/CHANGELOG.md | 11 ++++++++ packages/typescript-estree/package.json | 8 +++--- packages/visitor-keys/CHANGELOG.md | 8 ++++++ packages/visitor-keys/package.json | 4 +-- 22 files changed, 177 insertions(+), 31 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 55dd93fd2934..8dbd69097f92 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,35 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + + +### Bug Fixes + +* **eslint-plugin:** [consistent-indexed-object-style] convert readonly index signature to readonly record ([#2798](https://github.com/typescript-eslint/typescript-eslint/issues/2798)) ([29428a4](https://github.com/typescript-eslint/typescript-eslint/commit/29428a4dbef133563f2ee54b22908a01ab9a9472)) +* **eslint-plugin:** [consistent-type-imports] crash when using both default and namespace in one import ([#2778](https://github.com/typescript-eslint/typescript-eslint/issues/2778)) ([c816b84](https://github.com/typescript-eslint/typescript-eslint/commit/c816b84814214f7504a0d89a5cd3b08c595bfb50)) +* **eslint-plugin:** [explicit-module-boundary-types] ignore functions exported within typed object/array literals ([#2805](https://github.com/typescript-eslint/typescript-eslint/issues/2805)) ([73a63ee](https://github.com/typescript-eslint/typescript-eslint/commit/73a63ee9ea00b2db0a29f148d7863c3778e4a483)) +* **eslint-plugin:** [no-use-before-define] allow class references if they're within a class decorator ([#2827](https://github.com/typescript-eslint/typescript-eslint/issues/2827)) ([050023a](https://github.com/typescript-eslint/typescript-eslint/commit/050023aa7bd791d0be7b5788a9dcd8e61a00ce79)), closes [#2842](https://github.com/typescript-eslint/typescript-eslint/issues/2842) +* **eslint-plugin:** [triple-slash-reference] fix crash with external module reference ([#2788](https://github.com/typescript-eslint/typescript-eslint/issues/2788)) ([32b1b68](https://github.com/typescript-eslint/typescript-eslint/commit/32b1b6839fb32d93b7faa8fec74c9cb68ea587bb)) +* **scope-manager:** fix assertion assignments not being marked as write references ([#2809](https://github.com/typescript-eslint/typescript-eslint/issues/2809)) ([fa68492](https://github.com/typescript-eslint/typescript-eslint/commit/fa6849245ca55ca407dc031afbad456f2925a8e9)), closes [#2804](https://github.com/typescript-eslint/typescript-eslint/issues/2804) +* **typescript-estree:** add default value for `parserOptions.projectFolderIgnoreList` and deduplicate resolved projects ([#2819](https://github.com/typescript-eslint/typescript-eslint/issues/2819)) ([bf904ec](https://github.com/typescript-eslint/typescript-eslint/commit/bf904ec72db57174fec531f61e9427230662553e)), closes [#2418](https://github.com/typescript-eslint/typescript-eslint/issues/2418) [#2814](https://github.com/typescript-eslint/typescript-eslint/issues/2814) + + +### Features + +* **eslint-plugin:** [naming-convention] add `requireDouble`, `allowDouble`, `allowSingleOrDouble` options for underscores ([#2812](https://github.com/typescript-eslint/typescript-eslint/issues/2812)) ([dd0576a](https://github.com/typescript-eslint/typescript-eslint/commit/dd0576a66c34810bc60e0958948c9a8104a3f1a3)) +* **eslint-plugin:** [naming-convention] add `requiresQuotes` modifier ([#2813](https://github.com/typescript-eslint/typescript-eslint/issues/2813)) ([6fc8409](https://github.com/typescript-eslint/typescript-eslint/commit/6fc84094928c3645a0e04c31bd4d759fdfbdcb74)), closes [#2761](https://github.com/typescript-eslint/typescript-eslint/issues/2761) [#1483](https://github.com/typescript-eslint/typescript-eslint/issues/1483) +* **eslint-plugin:** [naming-convention] add modifier `unused` ([#2810](https://github.com/typescript-eslint/typescript-eslint/issues/2810)) ([6a06944](https://github.com/typescript-eslint/typescript-eslint/commit/6a06944e60677a402e7ab432e6ac1209737a7027)) +* **eslint-plugin:** [naming-convention] add modifiers `exported`, `global`, and `destructured` ([#2808](https://github.com/typescript-eslint/typescript-eslint/issues/2808)) ([fb254a1](https://github.com/typescript-eslint/typescript-eslint/commit/fb254a1036b89f9b78f927d607358e65e81a2250)), closes [#2239](https://github.com/typescript-eslint/typescript-eslint/issues/2239) [#2512](https://github.com/typescript-eslint/typescript-eslint/issues/2512) [#2318](https://github.com/typescript-eslint/typescript-eslint/issues/2318) [#2802](https://github.com/typescript-eslint/typescript-eslint/issues/2802) +* **eslint-plugin:** [naming-convention] allow `destructured` modifier for `parameter` selector ([#2829](https://github.com/typescript-eslint/typescript-eslint/issues/2829)) ([525d2ff](https://github.com/typescript-eslint/typescript-eslint/commit/525d2ff9292d89e1445b273b5378159bca323a1e)), closes [#2828](https://github.com/typescript-eslint/typescript-eslint/issues/2828) +* **eslint-plugin:** [naming-convention] split `property` and `method` selectors into more granular `classXXX`, `objectLiteralXXX`, `typeXXX` ([#2807](https://github.com/typescript-eslint/typescript-eslint/issues/2807)) ([665b6d4](https://github.com/typescript-eslint/typescript-eslint/commit/665b6d4023fb9d821f348c39aefff0d7571a98bf)), closes [#1477](https://github.com/typescript-eslint/typescript-eslint/issues/1477) [#2802](https://github.com/typescript-eslint/typescript-eslint/issues/2802) +* **eslint-plugin:** [no-unused-vars] fork the base rule ([#2768](https://github.com/typescript-eslint/typescript-eslint/issues/2768)) ([a8227a6](https://github.com/typescript-eslint/typescript-eslint/commit/a8227a6185dd24de4bfc7d766931643871155021)), closes [#2782](https://github.com/typescript-eslint/typescript-eslint/issues/2782) [#2714](https://github.com/typescript-eslint/typescript-eslint/issues/2714) [#2648](https://github.com/typescript-eslint/typescript-eslint/issues/2648) +* **eslint-plugin:** [unbound-method] add support for methods with a `this: void` parameter ([#2796](https://github.com/typescript-eslint/typescript-eslint/issues/2796)) ([878dd4a](https://github.com/typescript-eslint/typescript-eslint/commit/878dd4ae8c408f1eb42790a8fac37f85040b7f3c)) + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) diff --git a/lerna.json b/lerna.json index 1ecbe4b7412e..296f11fc4b1d 100644 --- a/lerna.json +++ b/lerna.json @@ -1,5 +1,5 @@ { - "version": "4.8.2", + "version": "4.9.0", "npmClient": "yarn", "useWorkspaces": true, "stream": true diff --git a/packages/eslint-plugin-internal/CHANGELOG.md b/packages/eslint-plugin-internal/CHANGELOG.md index 71bcd5a71f26..4594319e40c4 100644 --- a/packages/eslint-plugin-internal/CHANGELOG.md +++ b/packages/eslint-plugin-internal/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-internal diff --git a/packages/eslint-plugin-internal/package.json b/packages/eslint-plugin-internal/package.json index 466f44c9e491..6c212757bd1e 100644 --- a/packages/eslint-plugin-internal/package.json +++ b/packages/eslint-plugin-internal/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-internal", - "version": "4.8.2", + "version": "4.9.0", "private": true, "main": "dist/index.js", "scripts": { @@ -14,7 +14,7 @@ }, "dependencies": { "@types/prettier": "*", - "@typescript-eslint/experimental-utils": "4.8.2", + "@typescript-eslint/experimental-utils": "4.9.0", "prettier": "*" } } diff --git a/packages/eslint-plugin-tslint/CHANGELOG.md b/packages/eslint-plugin-tslint/CHANGELOG.md index 9533cd036e96..49f4dd68d716 100644 --- a/packages/eslint-plugin-tslint/CHANGELOG.md +++ b/packages/eslint-plugin-tslint/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + +**Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/eslint-plugin-tslint diff --git a/packages/eslint-plugin-tslint/package.json b/packages/eslint-plugin-tslint/package.json index 3ceedce9bd1c..7373a6630d6f 100644 --- a/packages/eslint-plugin-tslint/package.json +++ b/packages/eslint-plugin-tslint/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin-tslint", - "version": "4.8.2", + "version": "4.9.0", "main": "dist/index.js", "typings": "src/index.ts", "description": "TSLint wrapper plugin for ESLint", @@ -38,7 +38,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "4.8.2", + "@typescript-eslint/experimental-utils": "4.9.0", "lodash": "^4.17.15" }, "peerDependencies": { @@ -48,6 +48,6 @@ }, "devDependencies": { "@types/lodash": "*", - "@typescript-eslint/parser": "4.8.2" + "@typescript-eslint/parser": "4.9.0" } } diff --git a/packages/eslint-plugin/CHANGELOG.md b/packages/eslint-plugin/CHANGELOG.md index e5744f19403b..1e3706cddb52 100644 --- a/packages/eslint-plugin/CHANGELOG.md +++ b/packages/eslint-plugin/CHANGELOG.md @@ -3,6 +3,34 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + + +### Bug Fixes + +* **eslint-plugin:** [consistent-indexed-object-style] convert readonly index signature to readonly record ([#2798](https://github.com/typescript-eslint/typescript-eslint/issues/2798)) ([29428a4](https://github.com/typescript-eslint/typescript-eslint/commit/29428a4dbef133563f2ee54b22908a01ab9a9472)) +* **eslint-plugin:** [consistent-type-imports] crash when using both default and namespace in one import ([#2778](https://github.com/typescript-eslint/typescript-eslint/issues/2778)) ([c816b84](https://github.com/typescript-eslint/typescript-eslint/commit/c816b84814214f7504a0d89a5cd3b08c595bfb50)) +* **eslint-plugin:** [explicit-module-boundary-types] ignore functions exported within typed object/array literals ([#2805](https://github.com/typescript-eslint/typescript-eslint/issues/2805)) ([73a63ee](https://github.com/typescript-eslint/typescript-eslint/commit/73a63ee9ea00b2db0a29f148d7863c3778e4a483)) +* **eslint-plugin:** [no-use-before-define] allow class references if they're within a class decorator ([#2827](https://github.com/typescript-eslint/typescript-eslint/issues/2827)) ([050023a](https://github.com/typescript-eslint/typescript-eslint/commit/050023aa7bd791d0be7b5788a9dcd8e61a00ce79)), closes [#2842](https://github.com/typescript-eslint/typescript-eslint/issues/2842) +* **eslint-plugin:** [triple-slash-reference] fix crash with external module reference ([#2788](https://github.com/typescript-eslint/typescript-eslint/issues/2788)) ([32b1b68](https://github.com/typescript-eslint/typescript-eslint/commit/32b1b6839fb32d93b7faa8fec74c9cb68ea587bb)) +* **scope-manager:** fix assertion assignments not being marked as write references ([#2809](https://github.com/typescript-eslint/typescript-eslint/issues/2809)) ([fa68492](https://github.com/typescript-eslint/typescript-eslint/commit/fa6849245ca55ca407dc031afbad456f2925a8e9)), closes [#2804](https://github.com/typescript-eslint/typescript-eslint/issues/2804) + + +### Features + +* **eslint-plugin:** [naming-convention] add `requireDouble`, `allowDouble`, `allowSingleOrDouble` options for underscores ([#2812](https://github.com/typescript-eslint/typescript-eslint/issues/2812)) ([dd0576a](https://github.com/typescript-eslint/typescript-eslint/commit/dd0576a66c34810bc60e0958948c9a8104a3f1a3)) +* **eslint-plugin:** [naming-convention] add `requiresQuotes` modifier ([#2813](https://github.com/typescript-eslint/typescript-eslint/issues/2813)) ([6fc8409](https://github.com/typescript-eslint/typescript-eslint/commit/6fc84094928c3645a0e04c31bd4d759fdfbdcb74)), closes [#2761](https://github.com/typescript-eslint/typescript-eslint/issues/2761) [#1483](https://github.com/typescript-eslint/typescript-eslint/issues/1483) +* **eslint-plugin:** [naming-convention] add modifier `unused` ([#2810](https://github.com/typescript-eslint/typescript-eslint/issues/2810)) ([6a06944](https://github.com/typescript-eslint/typescript-eslint/commit/6a06944e60677a402e7ab432e6ac1209737a7027)) +* **eslint-plugin:** [naming-convention] add modifiers `exported`, `global`, and `destructured` ([#2808](https://github.com/typescript-eslint/typescript-eslint/issues/2808)) ([fb254a1](https://github.com/typescript-eslint/typescript-eslint/commit/fb254a1036b89f9b78f927d607358e65e81a2250)), closes [#2239](https://github.com/typescript-eslint/typescript-eslint/issues/2239) [#2512](https://github.com/typescript-eslint/typescript-eslint/issues/2512) [#2318](https://github.com/typescript-eslint/typescript-eslint/issues/2318) [#2802](https://github.com/typescript-eslint/typescript-eslint/issues/2802) +* **eslint-plugin:** [naming-convention] allow `destructured` modifier for `parameter` selector ([#2829](https://github.com/typescript-eslint/typescript-eslint/issues/2829)) ([525d2ff](https://github.com/typescript-eslint/typescript-eslint/commit/525d2ff9292d89e1445b273b5378159bca323a1e)), closes [#2828](https://github.com/typescript-eslint/typescript-eslint/issues/2828) +* **eslint-plugin:** [naming-convention] split `property` and `method` selectors into more granular `classXXX`, `objectLiteralXXX`, `typeXXX` ([#2807](https://github.com/typescript-eslint/typescript-eslint/issues/2807)) ([665b6d4](https://github.com/typescript-eslint/typescript-eslint/commit/665b6d4023fb9d821f348c39aefff0d7571a98bf)), closes [#1477](https://github.com/typescript-eslint/typescript-eslint/issues/1477) [#2802](https://github.com/typescript-eslint/typescript-eslint/issues/2802) +* **eslint-plugin:** [no-unused-vars] fork the base rule ([#2768](https://github.com/typescript-eslint/typescript-eslint/issues/2768)) ([a8227a6](https://github.com/typescript-eslint/typescript-eslint/commit/a8227a6185dd24de4bfc7d766931643871155021)), closes [#2782](https://github.com/typescript-eslint/typescript-eslint/issues/2782) [#2714](https://github.com/typescript-eslint/typescript-eslint/issues/2714) [#2648](https://github.com/typescript-eslint/typescript-eslint/issues/2648) +* **eslint-plugin:** [unbound-method] add support for methods with a `this: void` parameter ([#2796](https://github.com/typescript-eslint/typescript-eslint/issues/2796)) ([878dd4a](https://github.com/typescript-eslint/typescript-eslint/commit/878dd4ae8c408f1eb42790a8fac37f85040b7f3c)) + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) diff --git a/packages/eslint-plugin/package.json b/packages/eslint-plugin/package.json index c82629887370..be43e684afed 100644 --- a/packages/eslint-plugin/package.json +++ b/packages/eslint-plugin/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/eslint-plugin", - "version": "4.8.2", + "version": "4.9.0", "description": "TypeScript plugin for ESLint", "keywords": [ "eslint", @@ -42,8 +42,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/experimental-utils": "4.8.2", - "@typescript-eslint/scope-manager": "4.8.2", + "@typescript-eslint/experimental-utils": "4.9.0", + "@typescript-eslint/scope-manager": "4.9.0", "debug": "^4.1.1", "functional-red-black-tree": "^1.0.1", "regexpp": "^3.0.0", diff --git a/packages/experimental-utils/CHANGELOG.md b/packages/experimental-utils/CHANGELOG.md index 9e5705c2a740..ffb1047efee7 100644 --- a/packages/experimental-utils/CHANGELOG.md +++ b/packages/experimental-utils/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + + +### Features + +* **eslint-plugin:** [no-unused-vars] fork the base rule ([#2768](https://github.com/typescript-eslint/typescript-eslint/issues/2768)) ([a8227a6](https://github.com/typescript-eslint/typescript-eslint/commit/a8227a6185dd24de4bfc7d766931643871155021)), closes [#2782](https://github.com/typescript-eslint/typescript-eslint/issues/2782) [#2714](https://github.com/typescript-eslint/typescript-eslint/issues/2714) [#2648](https://github.com/typescript-eslint/typescript-eslint/issues/2648) + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/experimental-utils diff --git a/packages/experimental-utils/package.json b/packages/experimental-utils/package.json index 09625454762e..de34d459cd73 100644 --- a/packages/experimental-utils/package.json +++ b/packages/experimental-utils/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/experimental-utils", - "version": "4.8.2", + "version": "4.9.0", "description": "(Experimental) Utilities for working with TypeScript + ESLint together", "keywords": [ "eslint", @@ -40,9 +40,9 @@ }, "dependencies": { "@types/json-schema": "^7.0.3", - "@typescript-eslint/scope-manager": "4.8.2", - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/typescript-estree": "4.8.2", + "@typescript-eslint/scope-manager": "4.9.0", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/typescript-estree": "4.9.0", "eslint-scope": "^5.0.0", "eslint-utils": "^2.0.0" }, diff --git a/packages/parser/CHANGELOG.md b/packages/parser/CHANGELOG.md index 9703627fc57d..0941e5c7423f 100644 --- a/packages/parser/CHANGELOG.md +++ b/packages/parser/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + +**Note:** Version bump only for package @typescript-eslint/parser + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/parser diff --git a/packages/parser/package.json b/packages/parser/package.json index a8d6aff9e173..2d8f03c732ed 100644 --- a/packages/parser/package.json +++ b/packages/parser/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/parser", - "version": "4.8.2", + "version": "4.9.0", "description": "An ESLint custom parser which leverages TypeScript ESTree", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -44,15 +44,15 @@ "eslint": "^5.0.0 || ^6.0.0 || ^7.0.0" }, "dependencies": { - "@typescript-eslint/scope-manager": "4.8.2", - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/typescript-estree": "4.8.2", + "@typescript-eslint/scope-manager": "4.9.0", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/typescript-estree": "4.9.0", "debug": "^4.1.1" }, "devDependencies": { "@types/glob": "*", - "@typescript-eslint/experimental-utils": "4.8.2", - "@typescript-eslint/shared-fixtures": "4.8.2", + "@typescript-eslint/experimental-utils": "4.9.0", + "@typescript-eslint/shared-fixtures": "4.9.0", "glob": "*", "typescript": "*" }, diff --git a/packages/scope-manager/CHANGELOG.md b/packages/scope-manager/CHANGELOG.md index 210d6ee328cd..6866a37cec13 100644 --- a/packages/scope-manager/CHANGELOG.md +++ b/packages/scope-manager/CHANGELOG.md @@ -3,6 +3,22 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + + +### Bug Fixes + +* **scope-manager:** fix assertion assignments not being marked as write references ([#2809](https://github.com/typescript-eslint/typescript-eslint/issues/2809)) ([fa68492](https://github.com/typescript-eslint/typescript-eslint/commit/fa6849245ca55ca407dc031afbad456f2925a8e9)), closes [#2804](https://github.com/typescript-eslint/typescript-eslint/issues/2804) + + +### Features + +* **eslint-plugin:** [no-unused-vars] fork the base rule ([#2768](https://github.com/typescript-eslint/typescript-eslint/issues/2768)) ([a8227a6](https://github.com/typescript-eslint/typescript-eslint/commit/a8227a6185dd24de4bfc7d766931643871155021)), closes [#2782](https://github.com/typescript-eslint/typescript-eslint/issues/2782) [#2714](https://github.com/typescript-eslint/typescript-eslint/issues/2714) [#2648](https://github.com/typescript-eslint/typescript-eslint/issues/2648) + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/scope-manager diff --git a/packages/scope-manager/package.json b/packages/scope-manager/package.json index 82342b47b531..cac3ee93ce10 100644 --- a/packages/scope-manager/package.json +++ b/packages/scope-manager/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/scope-manager", - "version": "4.8.2", + "version": "4.9.0", "description": "TypeScript scope analyser for ESLint", "keywords": [ "eslint", @@ -39,12 +39,12 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/visitor-keys": "4.8.2" + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0" }, "devDependencies": { "@types/glob": "*", - "@typescript-eslint/typescript-estree": "4.8.2", + "@typescript-eslint/typescript-estree": "4.9.0", "glob": "*", "jest-specific-snapshot": "*", "make-dir": "*", diff --git a/packages/shared-fixtures/CHANGELOG.md b/packages/shared-fixtures/CHANGELOG.md index b2f521739667..ef1cc574bd40 100644 --- a/packages/shared-fixtures/CHANGELOG.md +++ b/packages/shared-fixtures/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + +**Note:** Version bump only for package @typescript-eslint/shared-fixtures + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/shared-fixtures diff --git a/packages/shared-fixtures/package.json b/packages/shared-fixtures/package.json index bae0e8dc9c55..81dcc9fcf0f3 100644 --- a/packages/shared-fixtures/package.json +++ b/packages/shared-fixtures/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/shared-fixtures", - "version": "4.8.2", + "version": "4.9.0", "private": true, "scripts": { "build": "tsc -b tsconfig.build.json", diff --git a/packages/types/CHANGELOG.md b/packages/types/CHANGELOG.md index b9e010522804..0c40832e74e0 100644 --- a/packages/types/CHANGELOG.md +++ b/packages/types/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + + +### Features + +* **eslint-plugin:** [no-unused-vars] fork the base rule ([#2768](https://github.com/typescript-eslint/typescript-eslint/issues/2768)) ([a8227a6](https://github.com/typescript-eslint/typescript-eslint/commit/a8227a6185dd24de4bfc7d766931643871155021)), closes [#2782](https://github.com/typescript-eslint/typescript-eslint/issues/2782) [#2714](https://github.com/typescript-eslint/typescript-eslint/issues/2714) [#2648](https://github.com/typescript-eslint/typescript-eslint/issues/2648) + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/types diff --git a/packages/types/package.json b/packages/types/package.json index bf724342366a..38f956fc1a57 100644 --- a/packages/types/package.json +++ b/packages/types/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/types", - "version": "4.8.2", + "version": "4.9.0", "description": "Types for the TypeScript-ESTree AST spec", "keywords": [ "eslint", diff --git a/packages/typescript-estree/CHANGELOG.md b/packages/typescript-estree/CHANGELOG.md index a954a99c85b6..1e3f80e360e9 100644 --- a/packages/typescript-estree/CHANGELOG.md +++ b/packages/typescript-estree/CHANGELOG.md @@ -3,6 +3,17 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + + +### Bug Fixes + +* **typescript-estree:** add default value for `parserOptions.projectFolderIgnoreList` and deduplicate resolved projects ([#2819](https://github.com/typescript-eslint/typescript-eslint/issues/2819)) ([bf904ec](https://github.com/typescript-eslint/typescript-eslint/commit/bf904ec72db57174fec531f61e9427230662553e)), closes [#2418](https://github.com/typescript-eslint/typescript-eslint/issues/2418) [#2814](https://github.com/typescript-eslint/typescript-eslint/issues/2814) + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) diff --git a/packages/typescript-estree/package.json b/packages/typescript-estree/package.json index 8c9898faf317..44db9b67a5ba 100644 --- a/packages/typescript-estree/package.json +++ b/packages/typescript-estree/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/typescript-estree", - "version": "4.8.2", + "version": "4.9.0", "description": "A parser that converts TypeScript source code into an ESTree compatible form", "main": "dist/index.js", "types": "dist/index.d.ts", @@ -41,8 +41,8 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "4.8.2", - "@typescript-eslint/visitor-keys": "4.8.2", + "@typescript-eslint/types": "4.9.0", + "@typescript-eslint/visitor-keys": "4.9.0", "debug": "^4.1.1", "globby": "^11.0.1", "is-glob": "^4.0.1", @@ -61,7 +61,7 @@ "@types/lodash": "*", "@types/semver": "^7.1.0", "@types/tmp": "^0.2.0", - "@typescript-eslint/shared-fixtures": "4.8.2", + "@typescript-eslint/shared-fixtures": "4.9.0", "glob": "*", "jest-specific-snapshot": "*", "make-dir": "*", diff --git a/packages/visitor-keys/CHANGELOG.md b/packages/visitor-keys/CHANGELOG.md index 43b4b54ae950..cac3a7bce90c 100644 --- a/packages/visitor-keys/CHANGELOG.md +++ b/packages/visitor-keys/CHANGELOG.md @@ -3,6 +3,14 @@ All notable changes to this project will be documented in this file. See [Conventional Commits](https://conventionalcommits.org) for commit guidelines. +# [4.9.0](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.2...v4.9.0) (2020-11-30) + +**Note:** Version bump only for package @typescript-eslint/visitor-keys + + + + + ## [4.8.2](https://github.com/typescript-eslint/typescript-eslint/compare/v4.8.1...v4.8.2) (2020-11-23) **Note:** Version bump only for package @typescript-eslint/visitor-keys diff --git a/packages/visitor-keys/package.json b/packages/visitor-keys/package.json index a637150c56a5..a5d1c226f048 100644 --- a/packages/visitor-keys/package.json +++ b/packages/visitor-keys/package.json @@ -1,6 +1,6 @@ { "name": "@typescript-eslint/visitor-keys", - "version": "4.8.2", + "version": "4.9.0", "description": "Visitor keys used to help traverse the TypeScript-ESTree AST", "keywords": [ "eslint", @@ -38,7 +38,7 @@ "typecheck": "tsc -p tsconfig.json --noEmit" }, "dependencies": { - "@typescript-eslint/types": "4.8.2", + "@typescript-eslint/types": "4.9.0", "eslint-visitor-keys": "^2.0.0" }, "devDependencies": {