diff --git a/packages/eslint-plugin/docs/rules/no-unsafe-enum-assignment.md b/packages/eslint-plugin/docs/rules/no-unsafe-enum-assignment.md new file mode 100644 index 000000000000..030b9ec29359 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-unsafe-enum-assignment.md @@ -0,0 +1,62 @@ +--- +description: 'Disallow providing non-enum values to enum typed locations.' +--- + +> 🛑 This file is source code, not the primary documentation location! 🛑 +> +> See **https://typescript-eslint.io/rules/no-unsafe-enum-assignment** for documentation. + +The TypeScript compiler can be surprisingly lenient when working with enums. +For example, it will allow you to assign any `number` value to a variable containing a numeric enum: + +```ts +enum Fruit { + Apple, + Banana, +} + +let fruit = Fruit.Apple; +fruit = 999; // No error +``` + +This rule flags when a `number` value is provided in a location that expects an enum type. + + + +### ❌ Incorrect + +```ts +let fruit = Fruit.Apple; +fruit++; +``` + +```ts +const fruit: Fruit = 0; +``` + +```ts +function useFruit(fruit: Fruit) {} +useFruit(0); +``` + +### ✅ Correct + +```ts +let fruit = Fruit.Apple; +fruit = Fruit.Banana; +``` + +```ts +const fruit: Fruit = Fruit.Apple; +``` + +```ts +function useFruit(fruit: Fruit) {} +useFruit(Fruit.Apple); +``` + + + +## When Not to Use It + +If you use enums as shorthands for numbers and don't mind potentially unsafe `number`-typed values assigned to them, you likely don't need this rule. diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index eb3856f10c3b..d5dd90ddf5ab 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -110,6 +110,7 @@ export = { '@typescript-eslint/no-unsafe-assignment': 'error', '@typescript-eslint/no-unsafe-call': 'error', '@typescript-eslint/no-unsafe-declaration-merging': 'error', + '@typescript-eslint/no-unsafe-enum-assignment': 'error', '@typescript-eslint/no-unsafe-member-access': 'error', '@typescript-eslint/no-unsafe-return': 'error', 'no-unused-expressions': 'off', diff --git a/packages/eslint-plugin/src/configs/strict.ts b/packages/eslint-plugin/src/configs/strict.ts index 99b4e83b5081..0d6aad0bb4cc 100644 --- a/packages/eslint-plugin/src/configs/strict.ts +++ b/packages/eslint-plugin/src/configs/strict.ts @@ -28,6 +28,7 @@ export = { '@typescript-eslint/no-unnecessary-condition': 'warn', '@typescript-eslint/no-unnecessary-type-arguments': 'warn', '@typescript-eslint/no-unsafe-declaration-merging': 'warn', + '@typescript-eslint/no-unsafe-enum-assignment': 'warn', 'no-useless-constructor': 'off', '@typescript-eslint/no-useless-constructor': 'warn', '@typescript-eslint/non-nullable-type-assertion-style': 'warn', diff --git a/packages/eslint-plugin/src/rules/enum-utils/shared.ts b/packages/eslint-plugin/src/rules/enum-utils/shared.ts new file mode 100644 index 000000000000..1a3850101966 --- /dev/null +++ b/packages/eslint-plugin/src/rules/enum-utils/shared.ts @@ -0,0 +1,46 @@ +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; + +import * as util from '../../util'; + +/* + * If passed an enum member, returns the type of the parent. Otherwise, + * returns itself. + * + * For example: + * - `Fruit` --> `Fruit` + * - `Fruit.Apple` --> `Fruit` + */ +export function getBaseEnumType( + typeChecker: ts.TypeChecker, + type: ts.Type, +): ts.Type { + const symbol = type.getSymbol(); + if ( + !symbol?.valueDeclaration?.parent || + !tsutils.isSymbolFlagSet(symbol, ts.SymbolFlags.EnumMember) + ) { + return type; + } + + return typeChecker.getTypeAtLocation(symbol.valueDeclaration.parent); +} + +/** + * A type can have 0 or more enum types. For example: + * - 123 --> [] + * - {} --> [] + * - Fruit.Apple --> [Fruit] + * - Fruit.Apple | Vegetable.Lettuce --> [Fruit, Vegetable] + * - Fruit.Apple | Vegetable.Lettuce | 123 --> [Fruit, Vegetable] + * - T extends Fruit --> [Fruit] + */ +export function getEnumTypes( + typeChecker: ts.TypeChecker, + type: ts.Type, +): ts.Type[] { + return tsutils + .unionTypeParts(type) + .filter(subType => util.isTypeFlagSet(subType, ts.TypeFlags.EnumLike)) + .map(type => getBaseEnumType(typeChecker, type)); +} diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index bbddfc8d4709..4f899a640863 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -81,6 +81,7 @@ import noUnsafeArgument from './no-unsafe-argument'; import noUnsafeAssignment from './no-unsafe-assignment'; import noUnsafeCall from './no-unsafe-call'; import noUnsafeDeclarationMerging from './no-unsafe-declaration-merging'; +import noUnsafeEnumAssignment from './no-unsafe-enum-assignment'; import noUnsafeMemberAccess from './no-unsafe-member-access'; import noUnsafeReturn from './no-unsafe-return'; import noUnusedExpressions from './no-unused-expressions'; @@ -214,6 +215,7 @@ export default { 'no-unsafe-assignment': noUnsafeAssignment, 'no-unsafe-call': noUnsafeCall, 'no-unsafe-declaration-merging': noUnsafeDeclarationMerging, + 'no-unsafe-enum-assignment': noUnsafeEnumAssignment, 'no-unsafe-member-access': noUnsafeMemberAccess, 'no-unsafe-return': noUnsafeReturn, 'no-unused-expressions': noUnusedExpressions, diff --git a/packages/eslint-plugin/src/rules/no-redeclare.ts b/packages/eslint-plugin/src/rules/no-redeclare.ts index 2b10c97c8e76..1eb2baa6de8d 100644 --- a/packages/eslint-plugin/src/rules/no-redeclare.ts +++ b/packages/eslint-plugin/src/rules/no-redeclare.ts @@ -1,3 +1,4 @@ +import { ScopeType } from '@typescript-eslint/scope-manager'; import type { TSESLint, TSESTree } from '@typescript-eslint/utils'; import { AST_NODE_TYPES } from '@typescript-eslint/utils'; @@ -256,7 +257,7 @@ export default util.createRule({ // Node.js or ES modules has a special scope. if ( - scope.type === 'global' && + scope.type === ScopeType.global && scope.childScopes[0] && // The special scope's block is the Program node. scope.block === scope.childScopes[0].block diff --git a/packages/eslint-plugin/src/rules/no-unsafe-enum-assignment.ts b/packages/eslint-plugin/src/rules/no-unsafe-enum-assignment.ts new file mode 100644 index 000000000000..edc8c1c4f906 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-enum-assignment.ts @@ -0,0 +1,486 @@ +import { AST_NODE_TYPES, TSESLint, TSESTree } from '@typescript-eslint/utils'; +import * as tsutils from 'tsutils'; +import * as ts from 'typescript'; + +import * as util from '../util'; +import { getBaseEnumType, getEnumTypes } from './enum-utils/shared'; + +type MakeRequiredNonNullable = Omit & { + [K in Key]: NonNullable; +}; + +const ALLOWED_TYPES_FOR_ANY_ENUM_ARGUMENT = + ts.TypeFlags.Unknown | ts.TypeFlags.Number | ts.TypeFlags.String; + +type MessageIds = 'operation' | 'provided' | 'providedProperty'; + +export default util.createRule<[], MessageIds>({ + name: 'no-unsafe-enum-assignment', + meta: { + type: 'suggestion', + docs: { + description: 'Disallow providing non-enum values to enum typed locations', + recommended: 'strict', + requiresTypeChecking: true, + }, + messages: { + operation: + 'This {{ operator }} may change the enum value to one not present in its enum type.', + provided: 'Unsafe non enum type provided to an enum value.', + providedProperty: + 'Unsafe non enum type provided to an enum value for property {{ property }}.', + }, + schema: [], + }, + defaultOptions: [], + create(context) { + const sourceCode = context.getSourceCode(); + const parserServices = util.getParserServices(context); + const typeChecker = parserServices.program.getTypeChecker(); + + /** + * Similar to `getEnumTypes`, but returns early as soon as it finds one. + */ + function hasEnumType(node: TSESTree.Node): boolean { + return tsutils + .unionTypeParts(getTypeFromNode(node)) + .some(subType => util.isTypeFlagSet(subType, ts.TypeFlags.EnumLiteral)); + } + + function getTypeFromNode(node: TSESTree.Node): ts.Type { + return typeChecker.getTypeAtLocation( + parserServices.esTreeNodeToTSNodeMap.get(node), + ); + } + + /** + * @returns Whether the recipient type is an enum and the provided type + * unsafely provides it a number. + */ + function isProvidedTypeUnsafe( + providedType: ts.Type, + recipientType: ts.Type, + ): boolean { + // This short-circuits most logic: if the types are the same, we're happy. + if (providedType === recipientType) { + return false; + } + + // `any` types can't be reasoned with + if ( + tsutils.isTypeFlagSet(recipientType, ts.TypeFlags.Any) || + tsutils.isTypeFlagSet(providedType, ts.TypeFlags.Any) + ) { + return false; + } + + // If the two types are containers, check each matching type recursively. + // + // ```ts + // declare let fruits: Fruit[]; + // fruits = [0, 1]; + // ``` + if ( + util.isTypeReferenceType(recipientType) && + util.isTypeReferenceType(providedType) + ) { + return isProvidedReferenceValueMismatched(providedType, recipientType); + } + + // If the recipient is not an enum, we don't care about it. + const recipientEnumTypes = new Set( + getEnumTypes(typeChecker, recipientType), + ); + if (recipientEnumTypes.size === 0) { + return false; + } + + const providedUnionTypes = tsutils.unionTypeParts(providedType); + + // Either every provided type should match the recipient enum... + if ( + providedUnionTypes.every(providedType => + recipientEnumTypes.has(getBaseEnumType(typeChecker, providedType)), + ) + ) { + return false; + } + + // ...or none of them can be an enum at all + return !providedUnionTypes.every(tsutils.isEnumType); + } + + /** + * Finds the first mismatched type reference: meaning, a non-enum type in + * the provided type compared to an enum type in the recipient type. + */ + function isProvidedReferenceValueMismatched( + providedType: ts.TypeReference, + recipientType: ts.TypeReference, + ): boolean { + const providedTypeArguments = typeChecker.getTypeArguments(providedType); + const recipientTypeArguments = + typeChecker.getTypeArguments(recipientType); + const checkableArguments = Math.min( + recipientTypeArguments.length, + providedTypeArguments.length, + ); + + for (let i = 0; i < checkableArguments; i += 1) { + if ( + isProvidedTypeUnsafe( + providedTypeArguments[i], + recipientTypeArguments[i], + ) + ) { + return true; + } + } + + return false; + } + + /** + * @returns The type of the parameter to the node, accounting for generic + * type parameters. + */ + function getParameterType( + node: TSESTree.CallExpression | TSESTree.NewExpression, + signature: ts.Signature, + index: number, + ): ts.Type { + // If possible, try to find the original parameter to retrieve any generic + // type parameter. For example: + // + // ```ts + // declare function useFruit(fruitType: FruitType); + // useFruit(0) + // ``` + const parameter = signature.getDeclaration()?.parameters[index]; + if (parameter !== undefined) { + const parameterType = typeChecker.getTypeAtLocation(parameter); + const constraint = parameterType.getConstraint(); + if (constraint !== undefined) { + return constraint; + } + } + + // Failing that, defer to whatever TypeScript sees as the contextual type. + return typeChecker.getContextualTypeForArgumentAtIndex( + parserServices.esTreeNodeToTSNodeMap.get(node), + index, + ); + } + + function isMismatchedEnumFunctionArgument( + argumentType: ts.Type, + parameterType: ts.Type, + ): boolean { + // First, recursively check for functions with type containers like: + // + // ```ts + // declare function useFruits(fruits: Fruit[]); + // useFruits([0, 1]); + // ``` + if (util.isTypeReferenceType(argumentType)) { + const argumentTypeArguments = + typeChecker.getTypeArguments(argumentType); + + const parameterSubTypes = tsutils.unionTypeParts(parameterType); + for (const parameterSubType of parameterSubTypes) { + if (!util.isTypeReferenceType(parameterSubType)) { + continue; + } + const parameterTypeArguments = + typeChecker.getTypeArguments(parameterSubType); + + for (let i = 0; i < argumentTypeArguments.length; i++) { + if ( + isMismatchedEnumFunctionArgument( + argumentTypeArguments[i], + parameterTypeArguments[i], + ) + ) { + return true; + } + } + } + + return false; + } + + // Allow function calls that have nothing to do with enums, like: + // + // ```ts + // declare function useNumber(num: number); + // useNumber(0); + // ``` + const parameterEnumTypes = getEnumTypes(typeChecker, parameterType); + if (parameterEnumTypes.length === 0) { + return false; + } + + // Allow passing enum values into functions that take in the "any" type + // and similar types that should basically match any enum, like: + // + // ```ts + // declare function useNumber(num: number); + // useNumber(Fruit.Apple); + // ``` + const parameterSubTypes = new Set(tsutils.unionTypeParts(parameterType)); + for (const parameterSubType of parameterSubTypes) { + if ( + util.isTypeFlagSet( + parameterSubType, + ALLOWED_TYPES_FOR_ANY_ENUM_ARGUMENT, + ) + ) { + return false; + } + } + + // Disallow passing number literals into enum parameters, like: + // + // ```ts + // declare function useFruit(fruit: Fruit); + // declare const fruit: Fruit.Apple | 1; + // useFruit(fruit) + // ``` + return tsutils.unionTypeParts(argumentType).some( + argumentSubType => + argumentSubType.isLiteral() && + !util.isTypeFlagSet(argumentSubType, ts.TypeFlags.EnumLiteral) && + // Permit the argument if it's a number the parameter allows, like: + // + // ```ts + // declare function useFruit(fruit: Fruit | -1); + // useFruit(-1) + // ``` + // that's ok too + !parameterSubTypes.has(argumentSubType), + ); + } + + /** + * Checks whether a provided node mismatches + */ + function compareProvidedNode( + provided: TSESTree.Node, + recipient: TSESTree.Node, + ): void { + compareProvidedType( + provided, + getTypeFromNode(provided), + getTypeFromNode(recipient), + ); + } + + /** + * Checks whether a provided type mismatches + */ + function compareProvidedType( + provided: TSESTree.Node, + providedType: ts.Type, + recipientType: ts.Type, + data: Omit, 'node'> = { + messageId: 'provided', + }, + ): void { + if (isProvidedTypeUnsafe(providedType, recipientType)) { + context.report({ + node: provided, + ...data, + }); + } + } + + const alreadyCheckedObjects = new Set(); + + function deduplicateObjectsCheck(node: TSESTree.Node): boolean { + if (alreadyCheckedObjects.has(node)) { + return false; + } + + alreadyCheckedObjects.add(node); + return true; + } + + function compareObjectType(node: TSESTree.Expression): void { + if (!deduplicateObjectsCheck(node)) { + return; + } + + const type = getTypeFromNode(node); + const contextualType = + typeChecker.getContextualType( + parserServices.esTreeNodeToTSNodeMap.get(node) as ts.Expression, + ) ?? type; + + for (const property of type.getProperties()) { + if (!property.valueDeclaration) { + continue; + } + + const contextualProperty = contextualType.getProperty(property.name); + if (!contextualProperty?.valueDeclaration) { + continue; + } + + const propertyValueDeclaration = + parserServices.tsNodeToESTreeNodeMap.get( + property.valueDeclaration, + ) as TSESTree.PropertyDefinition | undefined; + + const propertyValueType = typeChecker.getTypeOfSymbolAtLocation( + property, + property.valueDeclaration, + ); + const contextualValueType = typeChecker.getTypeOfSymbolAtLocation( + contextualProperty, + contextualProperty.valueDeclaration, + ); + + // If this is an inline object literal, we're able to complain on the specific property key + if (propertyValueDeclaration?.parent === node) { + compareProvidedType( + propertyValueDeclaration.key, + propertyValueType, + contextualValueType, + ); + } + // Otherwise, complain on the whole node and name the property + else { + { + compareProvidedType(node, propertyValueType, contextualValueType, { + data: { name: property.name }, + messageId: 'providedProperty', + }); + } + } + } + } + + return { + AssignmentPattern(node): void { + if (hasEnumType(node.left)) { + compareProvidedNode(node.left, node.right); + } else { + compareObjectType(node.right); + } + }, + + 'CallExpression, NewExpression'( + node: TSESTree.CallExpression | TSESTree.NewExpression, + ): void { + const signature = typeChecker.getResolvedSignature( + parserServices.esTreeNodeToTSNodeMap.get(node), + undefined, + node.arguments.length, + )!; + + // Iterate through the arguments provided to the call function and cross + // reference their types to the types of the "real" function parameters. + for (let i = 0; i < node.arguments.length; i++) { + // any-typed arguments can be ignored altogether + const argumentType = getTypeFromNode(node.arguments[i]); + if ( + !argumentType || + tsutils.isTypeFlagSet(argumentType, ts.TypeFlags.Any) + ) { + continue; + } + + const parameterType = getParameterType(node, signature, i); + + // Disallow mismatched function calls, like: + // + // ```ts + // declare function useFruit(fruit: Fruit); + // useFruit(0); + // ``` + if (isMismatchedEnumFunctionArgument(argumentType, parameterType)) { + context.report({ + messageId: 'provided', + node: node.arguments[i], + }); + } + } + }, + + 'ClassBody > PropertyDefinition[value]:not([typeAnnotation])'( + node: TSESTree.PropertyDefinition & { + parent: TSESTree.ClassBody; + }, + ): void { + const parentClass = node.parent.parent as + | TSESTree.ClassDeclaration + | TSESTree.ClassExpression; + if (!parentClass.implements) { + return; + } + + const { name } = util.getNameFromMember(node, sourceCode); + + for (const baseName of parentClass.implements) { + const baseType = getTypeFromNode(baseName); + const basePropertySymbol = typeChecker.getPropertyOfType( + baseType, + name, + ); + if (!basePropertySymbol) { + continue; + } + + compareProvidedType( + node.value!, + getTypeFromNode(node.value!), + typeChecker.getTypeOfSymbolAtLocation( + basePropertySymbol, + basePropertySymbol.valueDeclaration as ts.Declaration, + ), + ); + } + }, + + ObjectExpression(node): void { + compareObjectType(node); + }, + + 'PropertyDefinition[typeAnnotation][value]'( + node: MakeRequiredNonNullable< + TSESTree.PropertyDefinition, + 'typeAnnotation' | 'value' + >, + ): void { + compareProvidedNode(node.value, node.key); + }, + + UpdateExpression(node): void { + if (hasEnumType(node.argument)) { + context.report({ + data: { + operator: node.operator, + }, + messageId: 'operation', + node, + }); + } + }, + + 'VariableDeclarator[id.typeAnnotation][init]'( + node: TSESTree.VariableDeclarator & { + id: { + typeAnnotation: object; + }; + init: object; + }, + ): void { + if (hasEnumType(node.id.typeAnnotation)) { + compareProvidedNode(node.init, node.id); + } else { + compareObjectType(node.init); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/src/rules/unbound-method.ts b/packages/eslint-plugin/src/rules/unbound-method.ts index 6741f4df09f2..a261afe3c202 100644 --- a/packages/eslint-plugin/src/rules/unbound-method.ts +++ b/packages/eslint-plugin/src/rules/unbound-method.ts @@ -277,7 +277,7 @@ function checkMethod( const firstParam = decl.parameters[0]; const firstParamIsThis = firstParam?.name.kind === ts.SyntaxKind.Identifier && - firstParam?.name.escapedText === 'this'; + firstParam?.name.escapedText === ts.InternalSymbolName.This; const thisArgIsVoid = firstParamIsThis && firstParam?.type?.kind === ts.SyntaxKind.VoidKeyword; diff --git a/packages/eslint-plugin/src/util/collectUnusedVariables.ts b/packages/eslint-plugin/src/util/collectUnusedVariables.ts index ba3beb6861df..5ffe503eb1a1 100644 --- a/packages/eslint-plugin/src/util/collectUnusedVariables.ts +++ b/packages/eslint-plugin/src/util/collectUnusedVariables.ts @@ -1,4 +1,7 @@ -import { ImplicitLibVariable } from '@typescript-eslint/scope-manager'; +import { + ImplicitLibVariable, + ScopeType, +} from '@typescript-eslint/scope-manager'; import { Visitor } from '@typescript-eslint/scope-manager/dist/referencer/Visitor'; import type { TSESTree } from '@typescript-eslint/utils'; import { @@ -98,7 +101,7 @@ class UnusedVarsVisitor< const scope = this.#scopeManager.acquire(node, inner); if (scope) { - if (scope.type === 'function-expression-name') { + if (scope.type === ScopeType.functionExpressionName) { return scope.childScopes[0] as T; } return scope as T; diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-enum-assignment.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-enum-assignment.test.ts new file mode 100644 index 000000000000..a159c02789a8 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-enum-assignment.test.ts @@ -0,0 +1,971 @@ +import rule from '../../src/rules/no-unsafe-enum-assignment'; +import { getFixturesRootDir, RuleTester } from '../RuleTester'; + +const rootDir = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parserOptions: { + ecmaVersion: 2015, + tsconfigRootDir: rootDir, + project: './tsconfig.json', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-unsafe-enum-assignment', rule, { + valid: [ + ` + let fruit = 0; + fruit++; + `, + ` + let fruit = 1; + fruit--; + `, + ` + let fruit = 0; + ++fruit; + `, + ` + let fruit = 1; + --fruit; + `, + ` + let fruit = 1; + ~fruit; + `, + ` + let fruit = 1; + !fruit; + `, + ` + let fruit = 0; + fruit += 1; + `, + ` + let fruit = 0; + fruit *= 1; + `, + ` + let fruit = 0; + fruit /= 1; + `, + ` + let fruit = 1; + fruit -= 1; + `, + ` + let fruits; + fruits = [0]; + `, + ` + enum Fruit { + Apple, + } + let fruits; + fruits = [Fruit.Apple]; + `, + ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + !fruit; + `, + ` + enum Fruit { + Apple, + } + let fruits: Fruit[]; + fruits = [Fruit.Apple]; + `, + ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit = Fruit.Apple; + `, + ` + enum Fruit { + Apple, + } + let fruit: Fruit; + fruit = Fruit.Apple; + `, + ` + enum Fruit { + Apple, + } + let fruit; + fruit = Fruit.Apple; + `, + ` + enum Fruit { + Apple, + Banana, + } + let fruits: Fruit[]; + fruits = [Fruit.Apple, Fruit.Banana]; + `, + ` + enum Fruit { + Apple, + } + const x: { prop: Fruit } = { prop: Fruit.Apple }; + `, + ` + enum Fruit { + Apple, + } + const values = { prop: Fruit.Apple }; + const x: { prop: Fruit } = { ...values }; + `, + ` + enum Fruit { + Apple, + } + const values = { unrelated: 0, prop: 1 }; + const overrides = { prop: Fruit.Apple }; + const x: { prop: Fruit } = { ...values, ...overrides }; + `, + ` + enum Fruit { + Apple, + } + const values = { prop: Fruit.Apple }; + const x: { prop: Fruit }[] = [{ ...values }]; + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit): void; + useFruit(Fruit.Apple); + `, + ` + enum Fruit { + Apple, + Banana = '', + } + declare function useFruit(fruit: Fruit.Apple): void; + useFruit(Fruit.Apple); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit | -1): void; + useFruit(-1); + `, + ` + enum Fruit { + Apple = 0, + Banana = '', + } + declare function useFruit(fruit: Fruit | any): void; + useFruit(1); + `, + ` + enum Fruit { + Apple = 0, + Banana = '', + } + declare function useFruit(fruit: Fruit | number): void; + useFruit(1); + `, + ` + enum Fruit { + Apple = 0, + Banana = '', + } + declare function useFruit(fruit: Fruit | string): void; + useFruit(1); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit | null): void; + useFruit(Fruit.Apple); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit | string): void; + useFruit(Math.random() > 0.5 ? Fruit.Apple : ''); + `, + ` + enum Fruit { + Apple, + Banana, + } + declare function useFruit(fruit: Fruit): void; + useFruit(Math.random() > 0.5 ? Fruit.Apple : Fruit.Banana); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit | null): void; + useFruit(Math.random() > 0.5 ? Fruit.Apple : null); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit): void; + useFruit({} as any); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit | undefined): void; + useFruit(undefined); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit, a: number): void; + useFruit(Fruit.Apple, -1); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit, a: number, b: number): void; + useFruit(Fruit.Apple, -1, -1); + `, + ` + enum Fruit { + Apple, + } + declare function useFruit(a: number, fruit: Fruit, b: number): void; + useFruit(-1, Fruit.Apple, -1); + `, + ` + enum Fruit { + Apple, + } + declare function useFruits(fruits: Fruit[]): void; + useFruit([Fruit.Apple]); + `, + ` + enum Fruit { + Apple, + Banana, + } + declare function useFruits(...fruits: Fruit[]): void; + useFruit(Fruit.Apple, Fruit.Banana); + `, + ` + enum Fruit { + Apple, + Banana, + } + declare function useFruits(a: number, ...fruits: Fruit[]): void; + useFruit(1, Fruit.Apple, Fruit.Banana); + `, + ` + enum Fruit { + Apple, + } + declare function giveFruit(): Fruit { + return Fruit.Apple; + }; + `, + ` + enum Fruit { + Apple, + } + declare function giveFruit(): Fruit[] { + return [Fruit.Apple]; + }; + `, + ` + enum Fruit { + Apple, + } + class F { + prop: Fruit = Fruit.Apple; + } + `, + ` + enum Fruit { + Apple, + } + interface F { + prop: Fruit; + } + class Foo implements F { + prop = Fruit.Apple; + } + `, + ` + declare enum Fruit { + Apple, + } + interface F { + prop: Fruit; + } + class Foo implements F { + prop = Fruit.Apple; + } + `, + ` + declare enum Fruit { + Apple, + } + interface Unrelated { + other: Fruit; + } + class Foo implements Unrelated { + other = Fruit.Apple; + prop = 1; + } + `, + { + code: ` + enum Fruit { + Apple, + } + const Component = (props: { fruit: Fruit }) => null; + ; + `, + filename: 'react.tsx', + }, + ], + invalid: [ + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit++; + `, + errors: [{ data: { operator: '++' }, messageId: 'operation' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit--; + `, + errors: [{ data: { operator: '--' }, messageId: 'operation' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + ++fruit; + `, + errors: [{ data: { operator: '++' }, messageId: 'operation' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + --fruit; + `, + errors: [{ data: { operator: '--' }, messageId: 'operation' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit += 1; + `, + + errors: [{ messageId: 'provided' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit -= 1; + `, + + errors: [{ messageId: 'provided' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit |= 1; + `, + errors: [{ messageId: 'provided' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit &= 1; + `, + errors: [{ messageId: 'provided' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit ^= 1; + `, + errors: [{ messageId: 'provided' }], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruit = Fruit.Apple; + fruit = 1; + `, + errors: [ + { + column: 17, + endColumn: 18, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruits: Fruit[]; + fruits = [0]; + `, + errors: [ + { + column: 18, + endColumn: 21, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruits: Fruit[]; + fruits = [0, 1]; + `, + errors: [ + { + column: 18, + endColumn: 24, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + let fruits: Fruit[]; + fruits = [0, Fruit.Apple]; + `, + errors: [ + { + column: 18, + endColumn: 34, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const x: { prop: Fruit } = { prop: 1 }; + `, + errors: [ + { + data: { name: 'prop' }, + column: 38, + endColumn: 42, + line: 5, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const values = { prop: 1 }; + const x: { prop: Fruit } = { ...values }; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 49, + line: 6, + messageId: 'providedProperty', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare const values: { prop: 1 }; + const x: { prop: Fruit } = { ...values }; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 49, + line: 6, + messageId: 'providedProperty', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare const values: { prop: number }; + const x: { prop: Fruit } = { ...values }; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 49, + line: 6, + messageId: 'providedProperty', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const values = { prop: 1 }; + const unrelated = { other: 2 }; + const x: { prop: Fruit } = { ...unrelated, ...values }; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 63, + line: 7, + messageId: 'providedProperty', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const values = { prop: 1 }; + const unrelated = { other: 2 }; + const x: { prop: Fruit } = { ...values, ...unrelated }; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 63, + line: 7, + messageId: 'providedProperty', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const values = { prop: 1 }; + const overridden = { prop: Fruit.Apple }; + const x: { prop: Fruit } = { ...overridden, ...values }; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 64, + line: 7, + messageId: 'providedProperty', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const values = { prop: 1 }; + const x: { prop: Fruit } = values; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 42, + line: 6, + messageId: 'providedProperty', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const values = { prop: 1 }; + const x: { prop: Fruit }[] = [values]; + `, + errors: [ + { + data: { name: 'prop' }, + column: 36, + endColumn: 42, + line: 6, + messageId: 'providedProperty', + }, + ], + }, + // todo: handle object nesting, and nested arrays of objects... + { + code: ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: Fruit): void; + useFruit(0); + `, + errors: [ + { + column: 18, + endColumn: 19, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare function useFruit(fruit: FruitType): void; + useFruit(0); + `, + errors: [ + { + column: 18, + endColumn: 19, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare function useFruits(fruits: Fruit[]): void; + useFruits([0]); + `, + errors: [ + { + column: 19, + endColumn: 22, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare function useFruits(fruits: Fruit[]): void; + useFruits([0, Fruit.Apple]); + `, + errors: [ + { + column: 19, + endColumn: 35, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare function useFruits(...fruits: Fruit[]): void; + useFruits(0); + `, + errors: [ + { + column: 19, + endColumn: 20, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare function useFruits(a: number, ...fruits: Fruit[]): void; + useFruits(0, Fruit.Apple, 0); + `, + errors: [ + { + column: 35, + endColumn: 36, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare function giveFruit(): Fruit { + return 1; + }; + `, + errors: [ + { + // (todo: correct numbers) + column: -1, + endColumn: -1, + line: -1, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + declare function giveFruit(): Fruit[] { + return [1]; + }; + `, + errors: [ + { + // (todo: correct numbers) + column: -1, + endColumn: -1, + line: -1, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const values = new Set(); + values.add(0); + `, + errors: [ + { + column: 20, + endColumn: 21, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + class F { + prop: Fruit = 1; + } + `, + errors: [ + { + column: 25, + endColumn: 26, + line: 6, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + interface F { + prop: Fruit; + } + class Foo implements F { + prop = 1; + } + `, + errors: [ + { + column: 18, + endColumn: 19, + line: 9, + messageId: 'provided', + }, + ], + }, + { + code: ` + declare enum Fruit { + Apple, + } + interface F { + prop: Fruit; + } + class Foo implements F { + prop = 1; + } + `, + errors: [ + { + column: 18, + endColumn: 19, + line: 9, + messageId: 'provided', + }, + ], + }, + { + code: ` + declare enum Fruit { + Apple, + } + interface F { + prop: Fruit; + } + interface Unrelated { + other: Fruit; + } + class Foo implements F, Unrelated { + prop = 1; + } + `, + errors: [ + { + column: 18, + endColumn: 19, + line: 12, + messageId: 'provided', + }, + ], + }, + { + code: ` + declare enum Fruit { + Apple, + } + interface F { + prop: Fruit; + } + interface Unrelated { + other: Fruit; + } + class Foo implements Unrelated, F { + prop = 1; + } + `, + errors: [ + { + column: 18, + endColumn: 19, + line: 12, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const Component = (props: { fruit: Fruit }) => null; + ; + `, + errors: [ + { + // (todo: correct numbers) + column: -1, + endColumn: -1, + line: -1, + messageId: 'provided', + }, + ], + }, + { + code: ` + enum Fruit { + Apple, + } + const Component = (props: { fruit: Fruit[] }) => null; + ; + `, + errors: [ + { + // (todo: correct numbers) + column: -1, + endColumn: -1, + line: -1, + messageId: 'provided', + }, + ], + }, + ], +}); + +// For more todos: https://github.com/typescript-eslint/typescript-eslint/pull/6091#issuecomment-1407857817 diff --git a/packages/eslint-plugin/typings/typescript.d.ts b/packages/eslint-plugin/typings/typescript.d.ts index 0f0b38a5ba6f..e08a8fb42267 100644 --- a/packages/eslint-plugin/typings/typescript.d.ts +++ b/packages/eslint-plugin/typings/typescript.d.ts @@ -4,6 +4,8 @@ declare module 'typescript' { interface TypeChecker { // internal TS APIs + getContextualTypeForArgumentAtIndex(node: Node, argIndex: number): Type; + /** * @returns `true` if the given type is an array type: * - `Array` diff --git a/packages/scope-manager/src/ScopeManager.ts b/packages/scope-manager/src/ScopeManager.ts index 7f4b2a5f7052..3a63b1d9da4b 100644 --- a/packages/scope-manager/src/ScopeManager.ts +++ b/packages/scope-manager/src/ScopeManager.ts @@ -14,6 +14,7 @@ import { GlobalScope, MappedTypeScope, ModuleScope, + ScopeType, SwitchScope, TSEnumScope, TSModuleScope, @@ -106,7 +107,10 @@ class ScopeManager { */ public acquire(node: TSESTree.Node, inner = false): Scope | null { function predicate(testScope: Scope): boolean { - if (testScope.type === 'function' && testScope.functionExpressionScope) { + if ( + testScope.type === ScopeType.function && + testScope.functionExpressionScope + ) { return false; } return true; diff --git a/packages/scope-manager/src/referencer/VisitorBase.ts b/packages/scope-manager/src/referencer/VisitorBase.ts index 5a7a8bbebe02..04880d5ec7e7 100644 --- a/packages/scope-manager/src/referencer/VisitorBase.ts +++ b/packages/scope-manager/src/referencer/VisitorBase.ts @@ -35,7 +35,7 @@ abstract class VisitorBase { node: T | null | undefined, excludeArr: (keyof T)[] = [], ): void { - if (node == null || node.type == null) { + if (node == null || node.type === null) { return; } diff --git a/packages/scope-manager/src/scope/ScopeBase.ts b/packages/scope-manager/src/scope/ScopeBase.ts index ae26d129cb56..6d7e3422900f 100644 --- a/packages/scope-manager/src/scope/ScopeBase.ts +++ b/packages/scope-manager/src/scope/ScopeBase.ts @@ -363,7 +363,7 @@ abstract class ScopeBase< if (this.shouldStaticallyClose()) { closeRef = this.#staticCloseRef; - } else if (this.type !== 'global') { + } else if (this.type !== ScopeType.global) { closeRef = this.#dynamicCloseRef; } else { closeRef = this.#globalCloseRef; diff --git a/packages/type-utils/src/typeFlagUtils.ts b/packages/type-utils/src/typeFlagUtils.ts index 134fdcf4ece1..c74a6ccae9cb 100644 --- a/packages/type-utils/src/typeFlagUtils.ts +++ b/packages/type-utils/src/typeFlagUtils.ts @@ -1,11 +1,13 @@ import { unionTypeParts } from 'tsutils'; import * as ts from 'typescript'; +const ANY_OR_UNKNOWN = ts.TypeFlags.Any | ts.TypeFlags.Unknown; + /** - * Gets all of the type flags in a type, iterating through unions automatically + * Gets all of the type flags in a type, iterating through unions automatically. */ export function getTypeFlags(type: ts.Type): ts.TypeFlags { - let flags: ts.TypeFlags = 0; + let flags = 0 as ts.TypeFlags; for (const t of unionTypeParts(type)) { flags |= t.flags; } @@ -13,8 +15,13 @@ export function getTypeFlags(type: ts.Type): ts.TypeFlags { } /** - * Checks if the given type is (or accepts) the given flags - * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) + * @param flagsToCheck The composition of one or more `ts.TypeFlags`. + * @param isReceiver Whether the type is a receiving type (e.g. the type of a + * called function's parameter). + * @remarks + * Note that if the type is a union, this function will decompose it into the + * parts and get the flags of every union constituent. If this is not desired, + * use the `isTypeFlag` function from tsutils. */ export function isTypeFlagSet( type: ts.Type, @@ -23,7 +30,7 @@ export function isTypeFlagSet( ): boolean { const flags = getTypeFlags(type); - if (isReceiver && flags & (ts.TypeFlags.Any | ts.TypeFlags.Unknown)) { + if (isReceiver && flags & ANY_OR_UNKNOWN) { return true; }