From e5224b7fe4fa8c64303feaad80c060a3dbe103ba Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Thu, 20 Feb 2020 17:54:56 -0800 Subject: [PATCH] WIP --- .eslintrc.js | 1 + packages/eslint-plugin/src/configs/all.json | 2 +- .../src/rules/func-call-spacing.ts | 2 +- .../src/rules/no-array-constructor.ts | 2 +- .../eslint-plugin/src/rules/no-unsafe-any.ts | 486 ++++++++++++++++ packages/eslint-plugin/src/util/astUtils.ts | 43 +- packages/eslint-plugin/src/util/types.ts | 10 +- .../tests/rules/no-unsafe-any.test.ts | 536 ++++++++++++++++++ .../src/ts-estree/ts-estree.ts | 9 + 9 files changed, 1085 insertions(+), 6 deletions(-) create mode 100644 packages/eslint-plugin/src/rules/no-unsafe-any.ts create mode 100644 packages/eslint-plugin/tests/rules/no-unsafe-any.test.ts diff --git a/.eslintrc.js b/.eslintrc.js index 39ca61de516..8bfe56bdd88 100644 --- a/.eslintrc.js +++ b/.eslintrc.js @@ -29,6 +29,7 @@ module.exports = { '@typescript-eslint/no-non-null-assertion': 'off', '@typescript-eslint/no-throw-literal': 'off', '@typescript-eslint/no-use-before-define': 'off', + '@typescript-eslint/no-unsafe-any': 'error', '@typescript-eslint/no-var-requires': 'off', '@typescript-eslint/prefer-nullish-coalescing': 'error', '@typescript-eslint/prefer-optional-chain': 'error', diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 23e9db2d974..8db751c518b 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -41,7 +41,6 @@ "@typescript-eslint/no-extraneous-class": "error", "@typescript-eslint/no-floating-promises": "error", "@typescript-eslint/no-for-in-array": "error", - "@typescript-eslint/no-implied-eval": "error", "@typescript-eslint/no-inferrable-types": "error", "no-magic-numbers": "off", "@typescript-eslint/no-magic-numbers": "error", @@ -60,6 +59,7 @@ "@typescript-eslint/no-unnecessary-qualifier": "error", "@typescript-eslint/no-unnecessary-type-arguments": "error", "@typescript-eslint/no-unnecessary-type-assertion": "error", + "@typescript-eslint/no-unsafe-any": "error", "no-unused-expressions": "off", "@typescript-eslint/no-unused-expressions": "error", "no-unused-vars": "off", diff --git a/packages/eslint-plugin/src/rules/func-call-spacing.ts b/packages/eslint-plugin/src/rules/func-call-spacing.ts index 8e11d58592b..4ccb064d41a 100644 --- a/packages/eslint-plugin/src/rules/func-call-spacing.ts +++ b/packages/eslint-plugin/src/rules/func-call-spacing.ts @@ -79,7 +79,7 @@ export default util.createRule({ | TSESTree.OptionalCallExpression | TSESTree.NewExpression, ): void { - const isOptionalCall = util.isOptionalOptionalChain(node); + const isOptionalCall = util.isOptionalOptionalCallExpression(node); const closingParenToken = sourceCode.getLastToken(node)!; const lastCalleeTokenWithoutPossibleParens = sourceCode.getLastToken( diff --git a/packages/eslint-plugin/src/rules/no-array-constructor.ts b/packages/eslint-plugin/src/rules/no-array-constructor.ts index dddae97f663..c9465e70799 100644 --- a/packages/eslint-plugin/src/rules/no-array-constructor.ts +++ b/packages/eslint-plugin/src/rules/no-array-constructor.ts @@ -37,7 +37,7 @@ export default util.createRule({ node.callee.type === AST_NODE_TYPES.Identifier && node.callee.name === 'Array' && !node.typeParameters && - !util.isOptionalOptionalChain(node) + !util.isOptionalOptionalCallExpression(node) ) { context.report({ node, diff --git a/packages/eslint-plugin/src/rules/no-unsafe-any.ts b/packages/eslint-plugin/src/rules/no-unsafe-any.ts new file mode 100644 index 00000000000..45e4f937b19 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-unsafe-any.ts @@ -0,0 +1,486 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import { isTypeReference } from 'tsutils'; +import * as ts from 'typescript'; +import * as util from '../util'; + +type Options = [ + { + allowVariableAnnotationFromAny?: boolean; + }, +]; +type MessageIds = + | 'typeReferenceResolvesToAny' + | 'variableDeclarationInitializedToAnyWithoutAnnotation' + | 'variableDeclarationInitializedToAnyWithAnnotation' + | 'patternVariableDeclarationInitializedToAnyWithoutAnnotation' + | 'patternVariableDeclarationInitializedToAny' + | 'letVariableInitializedToNullishAndNoAnnotation' + | 'letVariableWithNoInitialAndNoAnnotation' + | 'loopVariableInitializedToAny' + | 'returnAny' + | 'passedArgumentIsAny' + | 'assignmentValueIsAny' + | 'updateExpressionIsAny' + | 'booleanTestIsAny' + | 'switchDiscriminantIsAny' + | 'switchCaseTestIsAny'; + +export default util.createRule({ + name: 'no-unsafe-any', + meta: { + type: 'problem', + docs: { + description: + 'Detects usages of any which can cause type safety holes within your codebase', + category: 'Possible Errors', + recommended: false, + }, + messages: { + typeReferenceResolvesToAny: + 'Referenced type {{typeName}} resolves to `any`.', + variableDeclarationInitializedToAnyWithAnnotation: + 'Variable declaration is initialized to `any` with an explicit type annotation, which is potentially unsafe. Prefer explicit type narrowing via type guards.', + variableDeclarationInitializedToAnyWithoutAnnotation: + 'Variable declaration {{name}} is initialized to `any` without a type annotation.', + patternVariableDeclarationInitializedToAnyWithoutAnnotation: + 'Variable declaration {{name}} is initialized to `any` without a type annotation.', + patternVariableDeclarationInitializedToAny: + 'Variable declaration is initialized to `any`.', + letVariableInitializedToNullishAndNoAnnotation: + 'Variable declared with {{kind}} and initialized to `null` or `undefined` is implicitly typed as `any`. Add an explicit type annotation.', + letVariableWithNoInitialAndNoAnnotation: + 'Variable declared with {{kind}} and no initial value is implicitly typed as `any`.', + loopVariableInitializedToAny: 'Loop variable is typed as `any`.', + returnAny: 'The type of the return is `any`.', + passedArgumentIsAny: 'The passed argument is `any`.', + assignmentValueIsAny: 'The value being assigned is `any`.', + updateExpressionIsAny: 'The update expression variable is `any`.', + booleanTestIsAny: 'The {{kind}} test is `any`.', + switchDiscriminantIsAny: 'The switch discriminant is `any`.', + switchCaseTestIsAny: 'The switch case test is `any`.', + }, + schema: [ + { + type: 'object', + additionalProperties: false, + properties: { + allowVariableAnnotationFromAny: { + type: 'boolean', + }, + }, + }, + ], + }, + defaultOptions: [ + { + allowVariableAnnotationFromAny: false, + }, + ], + create(context, [{ allowVariableAnnotationFromAny }]) { + const { program, esTreeNodeToTSNodeMap } = util.getParserServices(context); + const checker = program.getTypeChecker(); + const sourceCode = context.getSourceCode(); + + /** + * @returns true if the type is `any` + */ + function isAnyType(node: ts.Node): boolean { + const type = checker.getTypeAtLocation(node); + return util.isTypeFlagSet(type, ts.TypeFlags.Any); + } + /** + * @returns true if the type is `any[]` or `readonly any[]` + */ + function isAnyArrayType(node: ts.Node): boolean { + const type = checker.getTypeAtLocation(node); + return ( + checker.isArrayType(type) && + isTypeReference(type) && + util.isTypeFlagSet(checker.getTypeArguments(type)[0], ts.TypeFlags.Any) + ); + } + + function isAnyOrAnyArrayType(node: ts.Node): boolean { + return isAnyType(node) || isAnyArrayType(node); + } + + function reportVariableDeclarationInitializedToAny( + node: TSESTree.VariableDeclarator, + name?: string, + ): void { + if (!node.id.typeAnnotation) { + return context.report({ + node, + messageId: name + ? 'variableDeclarationInitializedToAnyWithoutAnnotation' + : 'patternVariableDeclarationInitializedToAnyWithoutAnnotation', + data: { + name, + // todo + }, + }); + } + + // there is a type annotation + + if (allowVariableAnnotationFromAny) { + // there is an annotation on the type, and the user indicated they are okay with the "unsafe" conversion + return; + } + if ( + node.id.typeAnnotation.typeAnnotation.type === + AST_NODE_TYPES.TSUnknownKeyword + ) { + // annotation with unknown is as safe as can be + return; + } + + return context.report({ + node, + messageId: 'variableDeclarationInitializedToAnyWithAnnotation', + }); + } + + function checkDestructuringPattern( + node: TSESTree.Node, + messageId: MessageIds, + ): void { + if (node.type === AST_NODE_TYPES.ObjectPattern) { + node.properties.forEach(prop => { + checkDestructuringPattern(prop.value ?? prop, messageId); + }); + } else if (node.type === AST_NODE_TYPES.ArrayPattern) { + node.elements.forEach(el => { + if (el) { + checkDestructuringPattern(el, messageId); + } + }); + } else if (node.type === AST_NODE_TYPES.AssignmentPattern) { + checkDestructuringPattern(node.left, messageId); + } else { + const tsNode = esTreeNodeToTSNodeMap.get(node); + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node, + messageId, + }); + } + } + } + + return { + // Handled by the no-explicit-any rule (with a fixer) + //TSAnyKeyword(node): void {}, + + // #region typeReferenceResolvesToAny + + TSTypeReference(node): void { + // const is a special keyword, but typescript resolves its type to any + if (util.isConstIdentifier(node.typeName)) { + return; + } + + const tsNode = esTreeNodeToTSNodeMap.get(node); + if (!isAnyType(tsNode)) { + return; + } + + const typeName = sourceCode.getText(node); + context.report({ + node, + messageId: 'typeReferenceResolvesToAny', + data: { + typeName, + }, + }); + }, + + // #endregion typeReferenceResolvesToAny + + // #region letVariableWithNoInitialAndNoAnnotation + + 'VariableDeclaration:matches([kind = "let"], [kind = "var"]) > VariableDeclarator:not([init])'( + node: TSESTree.VariableDeclarator, + ): void { + if (node.id.typeAnnotation) { + return; + } + + const parent = node.parent as TSESTree.VariableDeclaration; + context.report({ + node, + messageId: 'letVariableWithNoInitialAndNoAnnotation', + data: { + kind: parent.kind, + }, + }); + }, + + // #endregion letVariableWithNoInitialAndNoAnnotation + + // #region letVariableInitializedToNullishAndNoAnnotation + + 'VariableDeclaration:matches([kind = "let"], [kind = "var"]) > VariableDeclarator[init]'( + node: TSESTree.VariableDeclarator, + ): void { + if (node.id.typeAnnotation) { + return; + } + + const parent = node.parent as TSESTree.VariableDeclaration; + if ( + util.isNullLiteral(node.init) || + util.isUndefinedIdentifier(node.init) + ) { + context.report({ + node, + messageId: 'letVariableInitializedToNullishAndNoAnnotation', + data: { + kind: parent.kind, + }, + }); + } + }, + + // #endregion letVariableInitializedToNullishAndNoAnnotation + + // #region variableDeclarationInitializedToAnyWithAnnotation, variableDeclarationInitializedToAnyWithoutAnnotation, patternVariableDeclarationInitializedToAny + + // const x = ...; + 'VariableDeclaration > VariableDeclarator[init] > Identifier.id'( + node: TSESTree.Identifier, + ): void { + const parent = node.parent as TSESTree.VariableDeclarator; + /* istanbul ignore if */ if (!parent.init) { + return; + } + + const tsNode = esTreeNodeToTSNodeMap.get(parent.init); + if (!isAnyType(tsNode) && !isAnyArrayType(tsNode)) { + return; + } + + // the variable is initialized to any | any[]... + + reportVariableDeclarationInitializedToAny(parent, node.name); + }, + // const x = []; + // this is a special case, because the type of [] is never[], but the variable gets typed as any[]. + // this means it can't be caught by the above selector + 'VariableDeclaration > VariableDeclarator > ArrayExpression[elements.length = 0].init'( + node: TSESTree.ArrayExpression, + ): void { + const parent = node.parent as TSESTree.VariableDeclarator; + + if (parent.id.typeAnnotation) { + // note that there is no way to fix the type, so you have to use a type annotation + // so we don't report variableDeclarationInitializedToAnyWithAnnotation + return; + } + + context.report({ + node: parent, + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + }); + }, + // const { x } = ...; + // const [x] = ...; + 'VariableDeclaration > VariableDeclarator[init] > :matches(ObjectPattern, ArrayPattern).id'( + node: TSESTree.ObjectPattern, + ): void { + const parent = node.parent as TSESTree.VariableDeclarator; + /* istanbul ignore if */ if (!parent.init) { + return; + } + + const tsNode = esTreeNodeToTSNodeMap.get(parent.init); + if (isAnyOrAnyArrayType(tsNode)) { + // the entire init value is any, so report the entire declaration + return reportVariableDeclarationInitializedToAny(parent); + } + + checkDestructuringPattern( + node, + 'patternVariableDeclarationInitializedToAny', + ); + }, + + // #endregion variableDeclarationInitializedToAnyWithAnnotation, variableDeclarationInitializedToAnyWithoutAnnotation, patternVariableDeclarationInitializedToAny + + // #region loopVariableInitializedToAny + + 'ForOfStatement > VariableDeclaration.left > VariableDeclarator'( + node: TSESTree.VariableDeclarator, + ): void { + if (node.id.type === AST_NODE_TYPES.Identifier) { + const tsNode = esTreeNodeToTSNodeMap.get(node); + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node, + messageId: 'loopVariableInitializedToAny', + }); + } + } else { + checkDestructuringPattern(node.id, 'loopVariableInitializedToAny'); + } + }, + + // #endregion loopVariableInitializedToAny + + // #region returnAny + + 'ReturnStatement[argument]'(node: TSESTree.ReturnStatement): void { + const argument = util.nullThrows( + node.argument, + util.NullThrowsReasons.MissingToken( + 'argument', + AST_NODE_TYPES.ReturnStatement, + ), + ); + const tsNode = esTreeNodeToTSNodeMap.get(argument); + + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node, + messageId: 'returnAny', + }); + } + }, + // () => 1 + 'ArrowFunctionExpression > :not(BlockStatement).body'( + node: TSESTree.Expression, + ): void { + const tsNode = esTreeNodeToTSNodeMap.get(node); + + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node, + messageId: 'returnAny', + }); + } + }, + + // #endregion returnAny + + // #region passedArgumentIsAny + + 'CallExpression[arguments.length > 0]'( + node: TSESTree.CallExpression, + ): void { + for (const argument of node.arguments) { + const tsNode = esTreeNodeToTSNodeMap.get(argument); + + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node: argument, + messageId: 'passedArgumentIsAny', + }); + } + } + }, + + // #endregion passedArgumentIsAny + + // #region assignmentValueIsAny + + AssignmentExpression(node): void { + const tsNode = esTreeNodeToTSNodeMap.get(node.right); + + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node, + messageId: 'assignmentValueIsAny', + }); + } + }, + + // #endregion assignmentValueIsAny + + // #region updateExpressionIsAny + + UpdateExpression(node): void { + const tsNode = esTreeNodeToTSNodeMap.get(node.argument); + + if (isAnyType(tsNode)) { + context.report({ + node, + messageId: 'updateExpressionIsAny', + }); + } + }, + + // #endregion updateExpressionIsAny + + // #region booleanTestIsAny + + 'IfStatement, WhileStatement, DoWhileStatement, ConditionalExpression'( + node: + | TSESTree.IfStatement + | TSESTree.WhileStatement + | TSESTree.DoWhileStatement + | TSESTree.ConditionalExpression, + ): void { + const tsNode = esTreeNodeToTSNodeMap.get(node.test); + const typeToText = { + [AST_NODE_TYPES.IfStatement]: 'if', + [AST_NODE_TYPES.WhileStatement]: 'while', + [AST_NODE_TYPES.DoWhileStatement]: 'do while', + [AST_NODE_TYPES.ConditionalExpression]: 'ternary', + }; + + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node: node.test, + messageId: 'booleanTestIsAny', + data: { + kind: typeToText[node.type], + }, + }); + } + }, + + // #endregion booleanTestIsAny + + // #region switchDiscriminantIsAny + + SwitchStatement(node): void { + const tsNode = esTreeNodeToTSNodeMap.get(node.discriminant); + + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node: node.discriminant, + messageId: 'switchDiscriminantIsAny', + }); + } + }, + + // #endregion switchDiscriminantIsAny + + // #region switchCaseTestIsAny + + 'SwitchCase[test]'(node: TSESTree.SwitchCase): void { + const tsNode = esTreeNodeToTSNodeMap.get( + util.nullThrows( + node.test, + util.NullThrowsReasons.MissingToken( + 'test', + AST_NODE_TYPES.SwitchCase, + ), + ), + ); + + if (isAnyOrAnyArrayType(tsNode)) { + context.report({ + node, + messageId: 'switchCaseTestIsAny', + }); + } + }, + + // #endregion switchCaseTestIsAny + }; + }, +}); diff --git a/packages/eslint-plugin/src/util/astUtils.ts b/packages/eslint-plugin/src/util/astUtils.ts index 29dcdc0b459..c8578fc558e 100644 --- a/packages/eslint-plugin/src/util/astUtils.ts +++ b/packages/eslint-plugin/src/util/astUtils.ts @@ -31,7 +31,7 @@ function isNotNonNullAssertionPunctuator( /** * Returns true if and only if the node represents: foo?.() or foo.bar?.() */ -function isOptionalOptionalChain( +function isOptionalOptionalCallExpression( node: TSESTree.Node, ): node is TSESTree.OptionalCallExpression & { optional: true } { return ( @@ -132,19 +132,58 @@ function isAwaitKeyword( return node?.type === AST_TOKEN_TYPES.Identifier && node.value === 'await'; } +/** + * Checks if a node is the null literal + */ +function isNullLiteral( + node: TSESTree.Node | undefined | null, +): node is TSESTree.NullLiteral { + if (!node) { + return false; + } + return node.type === AST_NODE_TYPES.Literal && node.value === null; +} + +/** + * Checks if a node is the undefined identifier + */ +function isUndefinedIdentifier( + node: TSESTree.Node | undefined | null, +): node is TSESTree.UndefinedIdentifier { + if (!node) { + return false; + } + return node.type === AST_NODE_TYPES.Identifier && node.name === 'undefined'; +} + +/** + * Checks if a node is the const identifier + */ +function isConstIdentifier( + node: TSESTree.Node | undefined | null, +): node is TSESTree.ConstIdentifier { + if (!node) { + return false; + } + return node.type === AST_NODE_TYPES.Identifier && node.name === 'const'; +} + export { isAwaitExpression, isAwaitKeyword, + isConstIdentifier, isConstructor, isIdentifier, isLogicalOrOperator, isNonNullAssertionPunctuator, isNotNonNullAssertionPunctuator, isNotOptionalChainPunctuator, + isNullLiteral, isOptionalChainPunctuator, - isOptionalOptionalChain, + isOptionalOptionalCallExpression, isSetter, isTokenOnSameLine, isTypeAssertion, + isUndefinedIdentifier, LINEBREAK_MATCHER, }; diff --git a/packages/eslint-plugin/src/util/types.ts b/packages/eslint-plugin/src/util/types.ts index eae3519ea21..1a67214542a 100644 --- a/packages/eslint-plugin/src/util/types.ts +++ b/packages/eslint-plugin/src/util/types.ts @@ -169,7 +169,8 @@ export function getTypeFlags(type: ts.Type): ts.TypeFlags { } /** - * Checks if the given type is (or accepts) the given flags + * Checks if the given type is (or accepts) the given flags. + * This collects all types across a union. * @param isReceiver true if the type is a receiving type (i.e. the type of a called function's parameter) */ export function isTypeFlagSet( @@ -186,6 +187,13 @@ export function isTypeFlagSet( return (flags & flagsToCheck) !== 0; } +export function isTypeFlagSetNonUnion( + type: ts.Type, + flagsToCheck: ts.TypeFlags, +): boolean { + return (type.flags & flagsToCheck) !== 0; +} + /** * @returns Whether a type is an instance of the parent type, including for the parent's base types. */ diff --git a/packages/eslint-plugin/tests/rules/no-unsafe-any.test.ts b/packages/eslint-plugin/tests/rules/no-unsafe-any.test.ts new file mode 100644 index 00000000000..f4bd75d4890 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-unsafe-any.test.ts @@ -0,0 +1,536 @@ +import rule from '../../src/rules/no-unsafe-any'; +import { + RuleTester, + batchedSingleLineTests, + getFixturesRootDir, +} from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + project: './tsconfig.json', + tsconfigRootDir: getFixturesRootDir(), + }, +}); + +ruleTester.run('no-unsafe-any', rule, { + valid: [ + ` + /// + async function foo() { + const x = await fetch(''); + } + `, + 'const a = null', + 'const a = undefined', + 'let b: string | null = null', + 'var b: number | null = null', + 'const x = "foo" as const;', + { + code: ` + const x: any = 1; + const y: number = x; + `, + options: [{ allowVariableAnnotationFromAny: true }], + }, + 'const a = Array("str");', + 'const a = ["str"];', + ` + declare const arg: string | number; + const a = Array(arg); // typeof === (string | number)[]; + `, + 'for (const x of [1]) {}', + 'foo(1)', + 'x = 1', + 'function foo(arg: string) { return arg; }', + 'function foo() { return 1; }', + 'const foo = () => { return 1; }', + '1++;', + 'if (1) {}', + 'while (1) {}', + 'do {} while (1)', + '(1) ? 1 : 2;', + 'switch (1) { case 1: }', + 'switch (1) { default: }', + ], + invalid: [ + // typeReferenceResolvesToAny + { + code: ` +type T = any; + +let x: T = 1; +let y: string | T = 1; +const z = new Map(); +function foo(): T {} + `, + errors: [ + { + messageId: 'typeReferenceResolvesToAny', + line: 4, + }, + { + messageId: 'typeReferenceResolvesToAny', + line: 5, + }, + { + messageId: 'typeReferenceResolvesToAny', + line: 6, + }, + { + messageId: 'typeReferenceResolvesToAny', + line: 6, + }, + { + messageId: 'typeReferenceResolvesToAny', + line: 7, + }, + ], + }, + + // letVariableWithNoInitialAndNoAnnotation + ...batchedSingleLineTests({ + code: ` +let x; +var x; +let x, y; +var x, y; +let x, y = 1; + `, + errors: [ + { + messageId: 'letVariableWithNoInitialAndNoAnnotation', + line: 2, + }, + { + messageId: 'letVariableWithNoInitialAndNoAnnotation', + line: 3, + }, + { + messageId: 'letVariableWithNoInitialAndNoAnnotation', + line: 4, + }, + { + messageId: 'letVariableWithNoInitialAndNoAnnotation', + line: 4, + }, + { + messageId: 'letVariableWithNoInitialAndNoAnnotation', + line: 5, + }, + { + messageId: 'letVariableWithNoInitialAndNoAnnotation', + line: 5, + }, + { + messageId: 'letVariableWithNoInitialAndNoAnnotation', + line: 6, + }, + ], + }), + + // letVariableInitializedToNullishAndNoAnnotation + ...batchedSingleLineTests({ + code: ` +let a = null; +let a = undefined; +var a = undefined, b = null; +let a: string | null = null, b = null; + `, + errors: [ + { + messageId: 'letVariableInitializedToNullishAndNoAnnotation', + line: 2, + data: { + kind: 'let', + }, + }, + { + messageId: 'letVariableInitializedToNullishAndNoAnnotation', + line: 3, + data: { + kind: 'let', + }, + }, + { + messageId: 'letVariableInitializedToNullishAndNoAnnotation', + line: 4, + data: { + kind: 'var', + }, + }, + { + messageId: 'letVariableInitializedToNullishAndNoAnnotation', + line: 4, + data: { + kind: 'var', + }, + }, + { + messageId: 'letVariableInitializedToNullishAndNoAnnotation', + line: 5, + data: { + kind: 'let', + }, + }, + ], + }), + + // variableDeclarationInitializedToAnyWithoutAnnotation + ...batchedSingleLineTests({ + code: ` +const b = (1 as any); +const c = (1 as any) + 1; +const { baz } = (1 as any); + +const a = []; +const b = Array(); +const c = new Array(); +const d = Array(1); +const e = new Array(1); + `, + errors: [ + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 2, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 3, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 4, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 6, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 7, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 8, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 9, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 10, + }, + ], + }), + { + code: ` + const a = 1; + const b = new Array(a); + `, + errors: [ + { + messageId: 'variableDeclarationInitializedToAnyWithoutAnnotation', + line: 3, + }, + ], + }, + + // variableDeclarationInitializedToAnyWithAnnotation + ...batchedSingleLineTests({ + code: ` +const bbbb: number = (1 as any); +const c: number = (1 as any) + 1; + `, + options: [{ allowVariableAnnotationFromAny: false }], + errors: [ + { + messageId: 'variableDeclarationInitializedToAnyWithAnnotation', + line: 2, + }, + { + messageId: 'variableDeclarationInitializedToAnyWithAnnotation', + line: 3, + }, + ], + }), + + // patternVariableDeclarationInitializedToAny + ...batchedSingleLineTests({ + code: ` +const { x, y } = { x: 1 as any, y: 1 as any }; +const { x, y } = { x: 1 } as { x: number, y: any }; +const { x: { y } } = { x: { y: 1 as any } }; +const { x: { y: [a, b] } } = { x: { y: [1] } } as { x: { y: any[] } }; +const [a,b] = [1 as any, 2 as any]; +const [{ a }] = [1 as any]; +const [{ a } = { a: false }] = [1 as any]; + `, + errors: [ + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 2, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 2, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 3, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 4, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 5, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 5, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 6, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 6, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 7, + }, + { + messageId: 'patternVariableDeclarationInitializedToAny', + line: 8, + }, + ], + }), + + // loopVariableInitializedToAny + ...batchedSingleLineTests({ + code: ` +for (const x of ([] as any)) {} +for (const x of ([] as any[])) {} +for (const [index, argument] of [[1 as any, 2 as any]]) {} + `, + errors: [ + { + messageId: 'loopVariableInitializedToAny', + line: 2, + }, + { + messageId: 'loopVariableInitializedToAny', + line: 3, + }, + { + messageId: 'loopVariableInitializedToAny', + line: 4, + }, + { + messageId: 'loopVariableInitializedToAny', + line: 4, + }, + ], + }), + + // returnAny + ...batchedSingleLineTests({ + code: ` +function foo(arg: any) { return arg; } +function foo(arg: any[]) { return arg; } +function foo() { return (1 as any); } +const foo = () => (1 as any); +const foo = (arg: any[]) => arg; + `, + errors: [ + { + messageId: 'returnAny', + line: 2, + }, + { + messageId: 'returnAny', + line: 3, + }, + { + messageId: 'returnAny', + line: 4, + }, + { + messageId: 'returnAny', + line: 5, + }, + { + messageId: 'returnAny', + line: 6, + }, + ], + }), + + // passedArgumentIsAny + ...batchedSingleLineTests({ + code: ` +foo((1 as any)); +foo((1 as any[])); +foo((1 as any[]), (1 as any)); +foo(1, (1 as any)); + `, + errors: [ + { + messageId: 'passedArgumentIsAny', + line: 2, + }, + { + messageId: 'passedArgumentIsAny', + line: 3, + }, + { + messageId: 'passedArgumentIsAny', + line: 4, + }, + { + messageId: 'passedArgumentIsAny', + line: 4, + }, + { + messageId: 'passedArgumentIsAny', + line: 5, + }, + ], + }), + + // assignmentValueIsAny + ...batchedSingleLineTests({ + code: ` +x = (1 as any); +x = (1 as any[]); +x += (1 as any); +x -= (1 as any); +x |= (1 as any); +x.y = (1 as any); + `, + errors: [ + { + messageId: 'assignmentValueIsAny', + line: 2, + }, + { + messageId: 'assignmentValueIsAny', + line: 3, + }, + { + messageId: 'assignmentValueIsAny', + line: 4, + }, + { + messageId: 'assignmentValueIsAny', + line: 5, + }, + { + messageId: 'assignmentValueIsAny', + line: 6, + }, + { + messageId: 'assignmentValueIsAny', + line: 7, + }, + ], + }), + + // updateExpressionIsAny + ...batchedSingleLineTests({ + code: ` +(1 as any)++; +(1 as any)--; +++(1 as any); +--(1 as any); + `, + errors: [ + { + messageId: 'updateExpressionIsAny', + line: 2, + }, + { + messageId: 'updateExpressionIsAny', + line: 3, + }, + { + messageId: 'updateExpressionIsAny', + line: 4, + }, + { + messageId: 'updateExpressionIsAny', + line: 5, + }, + ], + }), + + // booleanTestIsAny + ...batchedSingleLineTests({ + code: ` +if (1 as any) {} +while (1 as any) {} +do {} while (1 as any) +;(1 as any) ? 1 : 2; + `, + errors: [ + { + messageId: 'booleanTestIsAny', + line: 2, + }, + { + messageId: 'booleanTestIsAny', + line: 3, + }, + { + messageId: 'booleanTestIsAny', + line: 4, + }, + { + messageId: 'booleanTestIsAny', + line: 5, + }, + ], + }), + + // switchDiscriminantIsAny + ...batchedSingleLineTests({ + code: ` +switch (1 as any) {} +switch (1 as any[]) {} + `, + errors: [ + { + messageId: 'switchDiscriminantIsAny', + line: 2, + }, + { + messageId: 'switchDiscriminantIsAny', + line: 3, + }, + ], + }), + + // switchCaseTestIsAny + ...batchedSingleLineTests({ + code: ` +switch (1) { case (1 as any): } +switch (1) { case (1 as any[]): } + `, + errors: [ + { + messageId: 'switchCaseTestIsAny', + line: 2, + }, + { + messageId: 'switchCaseTestIsAny', + line: 3, + }, + ], + }), + ], +}); diff --git a/packages/typescript-estree/src/ts-estree/ts-estree.ts b/packages/typescript-estree/src/ts-estree/ts-estree.ts index 94d4b7c2440..498490c2a3e 100644 --- a/packages/typescript-estree/src/ts-estree/ts-estree.ts +++ b/packages/typescript-estree/src/ts-estree/ts-estree.ts @@ -505,6 +505,7 @@ export type TSUnaryExpression = | TSTypeAssertion | UnaryExpression | UpdateExpression; +export type TypeAssertion = TSAsExpression | TSTypeAssertion; export type TypeElement = | TSCallSignatureDeclaration | TSConstructSignatureDeclaration @@ -841,6 +842,10 @@ export interface ConditionalExpression extends BaseNode { alternate: Expression; } +export interface ConstIdentifier extends Identifier { + name: 'const'; +} + export interface ContinueStatement extends BaseNode { type: AST_NODE_TYPES.ContinueStatement; label: Identifier | null; @@ -1660,6 +1665,10 @@ export interface TSVoidKeyword extends BaseNode { type: AST_NODE_TYPES.TSVoidKeyword; } +export interface UndefinedIdentifier extends Identifier { + name: 'undefined'; +} + export interface UpdateExpression extends UnaryExpressionBase { type: AST_NODE_TYPES.UpdateExpression; operator: '++' | '--';