From 36305df74b3c26b60364f7ec13390be492b4b2ec Mon Sep 17 00:00:00 2001 From: Brad Zacher Date: Sat, 5 Sep 2020 14:57:33 -0700 Subject: [PATCH] feat(eslint-plugin): add extension rule `no-loop-func` (#2490) --- packages/eslint-plugin/README.md | 61 +- .../eslint-plugin/docs/rules/no-loop-func.md | 22 + packages/eslint-plugin/src/configs/all.ts | 2 + packages/eslint-plugin/src/rules/index.ts | 18 +- .../eslint-plugin/src/rules/no-loop-func.ts | 220 ++++++ .../tests/rules/no-loop-func.test.ts | 738 ++++++++++++++++++ .../eslint-plugin/typings/eslint-rules.d.ts | 15 + 7 files changed, 1038 insertions(+), 38 deletions(-) create mode 100644 packages/eslint-plugin/docs/rules/no-loop-func.md create mode 100644 packages/eslint-plugin/src/rules/no-loop-func.ts create mode 100644 packages/eslint-plugin/tests/rules/no-loop-func.test.ts diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index fd77a7dceba..63b5b6e7f32 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -185,36 +185,37 @@ In these cases, we create what we call an extension rule; a rule within our plug **Key**: :heavy_check_mark: = recommended, :wrench: = fixable, :thought_balloon: = requires type information -| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: | -| ----------------------------------------------------------------------------------------------- | ----------------------------------------------------------------------------------- | ------------------ | -------- | ----------------- | -| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | -| [`@typescript-eslint/comma-spacing`](./docs/rules/comma-spacing.md) | Enforces consistent spacing before and after commas | | :wrench: | | -| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | -| [`@typescript-eslint/dot-notation`](./docs/rules/dot-notation.md) | enforce dot notation whenever possible | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | -| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | -| [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | | -| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | | -| [`@typescript-eslint/lines-between-class-members`](./docs/rules/lines-between-class-members.md) | Require or disallow an empty line between class members | | :wrench: | | -| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | | -| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | -| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | -| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | :heavy_check_mark: | :wrench: | | -| [`@typescript-eslint/no-invalid-this`](./docs/rules/no-invalid-this.md) | disallow `this` keywords outside of classes or class-like objects | | | | -| [`@typescript-eslint/no-loss-of-precision`](./docs/rules/no-loss-of-precision.md) | Disallow literal numbers that lose precision | | | | -| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | | -| [`@typescript-eslint/no-redeclare`](./docs/rules/no-redeclare.md) | Disallow variable redeclaration | | | | -| [`@typescript-eslint/no-shadow`](./docs/rules/no-shadow.md) | Disallow variable declarations from shadowing variables declared in the outer scope | | | | -| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | -| [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | -| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | | | | -| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | -| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | -| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | -| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | :wrench: | :thought_balloon: | -| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | -| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | +| Name | Description | :heavy_check_mark: | :wrench: | :thought_balloon: | +| ----------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ | ------------------ | -------- | ----------------- | +| [`@typescript-eslint/brace-style`](./docs/rules/brace-style.md) | Enforce consistent brace style for blocks | | :wrench: | | +| [`@typescript-eslint/comma-spacing`](./docs/rules/comma-spacing.md) | Enforces consistent spacing before and after commas | | :wrench: | | +| [`@typescript-eslint/default-param-last`](./docs/rules/default-param-last.md) | Enforce default parameters to be last | | | | +| [`@typescript-eslint/dot-notation`](./docs/rules/dot-notation.md) | enforce dot notation whenever possible | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/func-call-spacing`](./docs/rules/func-call-spacing.md) | Require or disallow spacing between function identifiers and their invocations | | :wrench: | | +| [`@typescript-eslint/indent`](./docs/rules/indent.md) | Enforce consistent indentation | | :wrench: | | +| [`@typescript-eslint/init-declarations`](./docs/rules/init-declarations.md) | require or disallow initialization in variable declarations | | | | +| [`@typescript-eslint/keyword-spacing`](./docs/rules/keyword-spacing.md) | Enforce consistent spacing before and after keywords | | :wrench: | | +| [`@typescript-eslint/lines-between-class-members`](./docs/rules/lines-between-class-members.md) | Require or disallow an empty line between class members | | :wrench: | | +| [`@typescript-eslint/no-array-constructor`](./docs/rules/no-array-constructor.md) | Disallow generic `Array` constructors | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-dupe-class-members`](./docs/rules/no-dupe-class-members.md) | Disallow duplicate class members | | | | +| [`@typescript-eslint/no-empty-function`](./docs/rules/no-empty-function.md) | Disallow empty functions | :heavy_check_mark: | | | +| [`@typescript-eslint/no-extra-parens`](./docs/rules/no-extra-parens.md) | Disallow unnecessary parentheses | | :wrench: | | +| [`@typescript-eslint/no-extra-semi`](./docs/rules/no-extra-semi.md) | Disallow unnecessary semicolons | :heavy_check_mark: | :wrench: | | +| [`@typescript-eslint/no-invalid-this`](./docs/rules/no-invalid-this.md) | disallow `this` keywords outside of classes or class-like objects | | | | +| [`@typescript-eslint/no-loop-func`](./docs/rules/no-loop-func.md) | Disallow function declarations that contain unsafe references inside loop statements | | | | +| [`@typescript-eslint/no-loss-of-precision`](./docs/rules/no-loss-of-precision.md) | Disallow literal numbers that lose precision | | | | +| [`@typescript-eslint/no-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | | +| [`@typescript-eslint/no-redeclare`](./docs/rules/no-redeclare.md) | Disallow variable redeclaration | | | | +| [`@typescript-eslint/no-shadow`](./docs/rules/no-shadow.md) | Disallow variable declarations from shadowing variables declared in the outer scope | | | | +| [`@typescript-eslint/no-unused-expressions`](./docs/rules/no-unused-expressions.md) | Disallow unused expressions | | | | +| [`@typescript-eslint/no-unused-vars`](./docs/rules/no-unused-vars.md) | Disallow unused variables | :heavy_check_mark: | | | +| [`@typescript-eslint/no-use-before-define`](./docs/rules/no-use-before-define.md) | Disallow the use of variables before they are defined | | | | +| [`@typescript-eslint/no-useless-constructor`](./docs/rules/no-useless-constructor.md) | Disallow unnecessary constructors | | | | +| [`@typescript-eslint/quotes`](./docs/rules/quotes.md) | Enforce the consistent use of either backticks, double, or single quotes | | :wrench: | | +| [`@typescript-eslint/require-await`](./docs/rules/require-await.md) | Disallow async functions which have no `await` expression | :heavy_check_mark: | | :thought_balloon: | +| [`@typescript-eslint/return-await`](./docs/rules/return-await.md) | Enforces consistent returning of awaited values | | :wrench: | :thought_balloon: | +| [`@typescript-eslint/semi`](./docs/rules/semi.md) | Require or disallow semicolons instead of ASI | | :wrench: | | +| [`@typescript-eslint/space-before-function-paren`](./docs/rules/space-before-function-paren.md) | Enforces consistent spacing before function parenthesis | | :wrench: | | diff --git a/packages/eslint-plugin/docs/rules/no-loop-func.md b/packages/eslint-plugin/docs/rules/no-loop-func.md new file mode 100644 index 00000000000..d48d38c20f6 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-loop-func.md @@ -0,0 +1,22 @@ +# Disallow function declarations that contain unsafe references inside loop statements (`no-loop-func`) + +## Rule Details + +This rule extends the base [`eslint/no-loop-func`](https://eslint.org/docs/rules/no-loop-func) rule. +It adds support for TypeScript types. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "no-loop-func": "off", + "@typescript-eslint/no-loop-func": ["error"] +} +``` + +## Options + +See [`eslint/no-loop-func` options](https://eslint.org/docs/rules/no-loop-func#options). + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-loop-func.md) diff --git a/packages/eslint-plugin/src/configs/all.ts b/packages/eslint-plugin/src/configs/all.ts index c3edea830e8..7e457d7aa1f 100644 --- a/packages/eslint-plugin/src/configs/all.ts +++ b/packages/eslint-plugin/src/configs/all.ts @@ -65,6 +65,8 @@ export = { 'no-invalid-this': 'off', '@typescript-eslint/no-invalid-this': 'error', '@typescript-eslint/no-invalid-void-type': 'error', + 'no-loop-func': 'off', + '@typescript-eslint/no-loop-func': 'error', 'no-loss-of-precision': 'off', '@typescript-eslint/no-loss-of-precision': 'error', 'no-magic-numbers': 'off', diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index f909be42d4e..fa8dba93ed1 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -45,6 +45,7 @@ import noInferrableTypes from './no-inferrable-types'; import noInvalidThis from './no-invalid-this'; import noInvalidVoidType from './no-invalid-void-type'; import noLossOfPrecision from './no-loss-of-precision'; +import noLoopFunc from './no-loop-func'; import noMagicNumbers from './no-magic-numbers'; import noMisusedNew from './no-misused-new'; import noMisusedPromises from './no-misused-promises'; @@ -114,7 +115,6 @@ export default { 'brace-style': braceStyle, 'class-literal-property-style': classLiteralPropertyStyle, 'comma-spacing': commaSpacing, - 'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual, 'consistent-type-assertions': consistentTypeAssertions, 'consistent-type-definitions': consistentTypeDefinitions, 'consistent-type-imports': consistentTypeImports, @@ -124,31 +124,34 @@ export default { 'explicit-member-accessibility': explicitMemberAccessibility, 'explicit-module-boundary-types': explicitModuleBoundaryTypes, 'func-call-spacing': funcCallSpacing, - indent: indent, 'init-declarations': initDeclarations, 'keyword-spacing': keywordSpacing, + 'lines-between-class-members': linesBetweenClassMembers, 'member-delimiter-style': memberDelimiterStyle, 'member-ordering': memberOrdering, 'method-signature-style': methodSignatureStyle, 'naming-convention': namingConvention, 'no-array-constructor': noArrayConstructor, 'no-base-to-string': noBaseToString, + 'no-confusing-non-null-assertion': confusingNonNullAssertionLikeNotEqual, 'no-dupe-class-members': noDupeClassMembers, 'no-dynamic-delete': noDynamicDelete, 'no-empty-function': noEmptyFunction, 'no-empty-interface': noEmptyInterface, 'no-explicit-any': noExplicitAny, - 'no-implicit-any-catch': noImplicitAnyCatch, 'no-extra-non-null-assertion': noExtraNonNullAssertion, 'no-extra-parens': noExtraParens, 'no-extra-semi': noExtraSemi, 'no-extraneous-class': noExtraneousClass, 'no-floating-promises': noFloatingPromises, 'no-for-in-array': noForInArray, + 'no-implicit-any-catch': noImplicitAnyCatch, 'no-implied-eval': noImpliedEval, 'no-inferrable-types': noInferrableTypes, 'no-invalid-this': noInvalidThis, 'no-invalid-void-type': noInvalidVoidType, + 'no-loop-func': noLoopFunc, + 'no-loss-of-precision': noLossOfPrecision, 'no-magic-numbers': noMagicNumbers, 'no-misused-new': noMisusedNew, 'no-misused-promises': noMisusedPromises, @@ -193,21 +196,20 @@ export default { 'prefer-string-starts-ends-with': preferStringStartsEndsWith, 'prefer-ts-expect-error': preferTsExpectError, 'promise-function-async': promiseFunctionAsync, - quotes: quotes, 'require-array-sort-compare': requireArraySortCompare, 'require-await': requireAwait, 'restrict-plus-operands': restrictPlusOperands, 'restrict-template-expressions': restrictTemplateExpressions, 'return-await': returnAwait, - semi: semi, 'space-before-function-paren': spaceBeforeFunctionParen, 'strict-boolean-expressions': strictBooleanExpressions, 'switch-exhaustiveness-check': switchExhaustivenessCheck, 'triple-slash-reference': tripleSlashReference, 'type-annotation-spacing': typeAnnotationSpacing, - typedef: typedef, 'unbound-method': unboundMethod, 'unified-signatures': unifiedSignatures, - 'lines-between-class-members': linesBetweenClassMembers, - 'no-loss-of-precision': noLossOfPrecision, + indent: indent, + quotes: quotes, + semi: semi, + typedef: typedef, }; diff --git a/packages/eslint-plugin/src/rules/no-loop-func.ts b/packages/eslint-plugin/src/rules/no-loop-func.ts new file mode 100644 index 00000000000..44d0178e867 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-loop-func.ts @@ -0,0 +1,220 @@ +import { + AST_NODE_TYPES, + TSESLint, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/no-loop-func'; +import * as util from '../util'; + +type Options = util.InferOptionsTypeFromRule; +type MessageIds = util.InferMessageIdsTypeFromRule; + +export default util.createRule({ + name: 'no-loop-func', + meta: { + type: 'suggestion', + docs: { + description: + 'Disallow function declarations that contain unsafe references inside loop statements', + category: 'Best Practices', + recommended: false, + extendsBaseRule: true, + }, + schema: [], + messages: baseRule?.meta.messages ?? { + unsafeRefs: + 'Function declared in a loop contains unsafe references to variable(s) {{ varNames }}.', + }, + }, + defaultOptions: [], + create(context) { + /** + * Reports functions which match the following condition: + * - has a loop node in ancestors. + * - has any references which refers to an unsafe variable. + * + * @param node The AST node to check. + * @returns Whether or not the node is within a loop. + */ + function checkForLoops( + node: + | TSESTree.ArrowFunctionExpression + | TSESTree.FunctionExpression + | TSESTree.FunctionDeclaration, + ): void { + const loopNode = getContainingLoopNode(node); + + if (!loopNode) { + return; + } + + const references = context.getScope().through; + const unsafeRefs = references + .filter(r => !isSafe(loopNode, r)) + .map(r => r.identifier.name); + + if (unsafeRefs.length > 0) { + context.report({ + node, + messageId: 'unsafeRefs', + data: { varNames: `'${unsafeRefs.join("', '")}'` }, + }); + } + } + + return { + ArrowFunctionExpression: checkForLoops, + FunctionExpression: checkForLoops, + FunctionDeclaration: checkForLoops, + }; + }, +}); + +/** + * Gets the containing loop node of a specified node. + * + * We don't need to check nested functions, so this ignores those. + * `Scope.through` contains references of nested functions. + * + * @param node An AST node to get. + * @returns The containing loop node of the specified node, or `null`. + */ +function getContainingLoopNode(node: TSESTree.Node): TSESTree.Node | null { + for ( + let currentNode = node; + currentNode.parent; + currentNode = currentNode.parent + ) { + const parent = currentNode.parent; + + switch (parent.type) { + case AST_NODE_TYPES.WhileStatement: + case AST_NODE_TYPES.DoWhileStatement: + return parent; + + case AST_NODE_TYPES.ForStatement: + // `init` is outside of the loop. + if (parent.init !== currentNode) { + return parent; + } + break; + + case AST_NODE_TYPES.ForInStatement: + case AST_NODE_TYPES.ForOfStatement: + // `right` is outside of the loop. + if (parent.right !== currentNode) { + return parent; + } + break; + + case AST_NODE_TYPES.ArrowFunctionExpression: + case AST_NODE_TYPES.FunctionExpression: + case AST_NODE_TYPES.FunctionDeclaration: + // We don't need to check nested functions. + return null; + + default: + break; + } + } + + return null; +} + +/** + * Gets the containing loop node of a given node. + * If the loop was nested, this returns the most outer loop. + * @param node A node to get. This is a loop node. + * @param excludedNode A node that the result node should not include. + * @returns The most outer loop node. + */ +function getTopLoopNode( + node: TSESTree.Node, + excludedNode: TSESTree.Node | null | undefined, +): TSESTree.Node { + const border = excludedNode ? excludedNode.range[1] : 0; + let retv = node; + let containingLoopNode: TSESTree.Node | null = node; + + while (containingLoopNode && containingLoopNode.range[0] >= border) { + retv = containingLoopNode; + containingLoopNode = getContainingLoopNode(containingLoopNode); + } + + return retv; +} + +/** + * Checks whether a given reference which refers to an upper scope's variable is + * safe or not. + * @param loopNode A containing loop node. + * @param reference A reference to check. + * @returns `true` if the reference is safe or not. + */ +function isSafe( + loopNode: TSESTree.Node, + reference: TSESLint.Scope.Reference, +): boolean { + const variable = reference.resolved; + const definition = variable?.defs[0]; + const declaration = definition?.parent; + const kind = + declaration?.type === AST_NODE_TYPES.VariableDeclaration + ? declaration.kind + : ''; + + // type references are all safe + // this only really matters for global types that haven't been configured + if (reference.isTypeReference) { + return true; + } + + // Variables which are declared by `const` is safe. + if (kind === 'const') { + return true; + } + + /* + * Variables which are declared by `let` in the loop is safe. + * It's a different instance from the next loop step's. + */ + if ( + kind === 'let' && + declaration && + declaration.range[0] > loopNode.range[0] && + declaration.range[1] < loopNode.range[1] + ) { + return true; + } + + /* + * WriteReferences which exist after this border are unsafe because those + * can modify the variable. + */ + const border = getTopLoopNode(loopNode, kind === 'let' ? declaration : null) + .range[0]; + + /** + * Checks whether a given reference is safe or not. + * The reference is every reference of the upper scope's variable we are + * looking now. + * + * It's safe if the reference matches one of the following condition. + * - is readonly. + * - doesn't exist inside a local function and after the border. + * + * @param upperRef A reference to check. + * @returns `true` if the reference is safe. + */ + function isSafeReference(upperRef: TSESLint.Scope.Reference): boolean { + const id = upperRef.identifier; + + return ( + !upperRef.isWrite() || + (variable?.scope?.variableScope === upperRef.from.variableScope && + id.range[0] < border) + ); + } + + return variable?.references.every(isSafeReference) ?? false; +} diff --git a/packages/eslint-plugin/tests/rules/no-loop-func.test.ts b/packages/eslint-plugin/tests/rules/no-loop-func.test.ts new file mode 100644 index 00000000000..be22ae267c0 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-loop-func.test.ts @@ -0,0 +1,738 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; +import rule from '../../src/rules/no-loop-func'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-loop-func', rule, { + valid: [ + ` +let someArray: MyType[] = []; +for (let i = 0; i < 10; i += 1) { + someArray = someArray.filter((item: MyType) => !!item); +} + `, + { + code: ` +let someArray: MyType[] = []; +for (let i = 0; i < 10; i += 1) { + someArray = someArray.filter((item: MyType) => !!item); +} + `, + globals: { + MyType: 'readonly', + }, + }, + { + code: ` +let someArray: MyType[] = []; +for (let i = 0; i < 10; i += 1) { + someArray = someArray.filter((item: MyType) => !!item); +} + `, + globals: { + MyType: 'writable', + }, + }, + ` +type MyType = 1; +let someArray: MyType[] = []; +for (let i = 0; i < 10; i += 1) { + someArray = someArray.filter((item: MyType) => !!item); +} + `, + ], + invalid: [], +}); + +// Forked from https://github.com/eslint/eslint/blob/bf2e367bf4f6fde9930af9de8b8d8bc3d8b5782f/tests/lib/rules/no-loop-func.js +ruleTester.run('no-loop-func ESLint tests', rule, { + valid: [ + "string = 'function a() {}';", + ` +for (var i = 0; i < l; i++) {} +var a = function () { + i; +}; + `, + ` +for ( + var i = 0, + a = function () { + i; + }; + i < l; + i++ +) {} + `, + ` +for (var x in xs.filter(function (x) { + return x != upper; +})) { +} + `, + { + code: ` +for (var x of xs.filter(function (x) { + return x != upper; +})) { +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // no refers to variables that declared on upper scope. + ` +for (var i = 0; i < l; i++) { + (function () {}); +} + `, + ` +for (var i in {}) { + (function () {}); +} + `, + { + code: ` +for (var i of {}) { + (function () {}); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + + // functions which are using unmodified variables are OK. + { + code: ` +for (let i = 0; i < l; i++) { + (function () { + i; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +for (let i in {}) { + i = 7; + (function () { + i; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +for (const i of {}) { + (function () { + i; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +for (let i = 0; i < 10; ++i) { + for (let x in xs.filter(x => x != i)) { + } +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +let a = 0; +for (let i = 0; i < l; i++) { + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +let a = 0; +for (let i in {}) { + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +let a = 0; +for (let i of {}) { + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +let a = 0; +for (let i = 0; i < l; i++) { + (function () { + (function () { + a; + }); + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +let a = 0; +for (let i in {}) { + function foo() { + (function () { + a; + }); + } +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +let a = 0; +for (let i of {}) { + () => { + (function () { + a; + }); + }; +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var a = 0; +for (let i = 0; i < l; i++) { + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var a = 0; +for (let i in {}) { + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ` +var a = 0; +for (let i of {}) { + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + }, + { + code: [ + 'let result = {};', + 'for (const score in scores) {', + ' const letters = scores[score];', + " letters.split('').forEach(letter => {", + ' result[letter] = score;', + ' });', + '}', + 'result.__default = 6;', + ].join('\n'), + parserOptions: { ecmaVersion: 6 }, + }, + { + code: ['while (true) {', ' (function() { a; });', '}', 'let a;'].join( + '\n', + ), + parserOptions: { ecmaVersion: 6 }, + }, + ], + invalid: [ + { + code: ` +for (var i = 0; i < l; i++) { + (function () { + i; + }); +} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for (var i = 0; i < l; i++) { + for (var j = 0; j < m; j++) { + (function () { + i + j; + }); + } +} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i', 'j'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for (var i in {}) { + (function () { + i; + }); +} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for (var i of {}) { + (function () { + i; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for (var i = 0; i < l; i++) { + () => { + i; + }; +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.ArrowFunctionExpression, + }, + ], + }, + { + code: ` +for (var i = 0; i < l; i++) { + var a = function () { + i; + }; +} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for (var i = 0; i < l; i++) { + function a() { + i; + } + a(); +} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionDeclaration, + }, + ], + }, + { + code: ` +for ( + var i = 0; + (function () { + i; + })(), + i < l; + i++ +) {} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for ( + var i = 0; + i < l; + (function () { + i; + })(), + i++ +) {} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +while (i) { + (function () { + i; + }); +} + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +do { + (function () { + i; + }); +} while (i); + `, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + + // Warns functions which are using modified variables. + { + code: ` +let a; +for (let i = 0; i < l; i++) { + a = 1; + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +let a; +for (let i in {}) { + (function () { + a; + }); + a = 1; +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +let a; +for (let i of {}) { + (function () { + a; + }); +} +a = 1; + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +let a; +for (let i = 0; i < l; i++) { + (function () { + (function () { + a; + }); + }); + a = 1; +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +let a; +for (let i in {}) { + a = 1; + function foo() { + (function () { + a; + }); + } +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionDeclaration, + }, + ], + }, + { + code: ` +let a; +for (let i of {}) { + () => { + (function () { + a; + }); + }; +} +a = 1; + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.ArrowFunctionExpression, + }, + ], + }, + { + code: ` +for (var i = 0; i < 10; ++i) { + for (let x in xs.filter(x => x != i)) { + } +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'i'" }, + type: AST_NODE_TYPES.ArrowFunctionExpression, + }, + ], + }, + { + code: ` +for (let x of xs) { + let a; + for (let y of ys) { + a = 1; + (function () { + a; + }); + } +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for (var x of xs) { + for (let y of ys) { + (function () { + x; + }); + } +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'x'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +for (var x of xs) { + (function () { + x; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'x'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +var a; +for (let x of xs) { + a = 1; + (function () { + a; + }); +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +var a; +for (let x of xs) { + (function () { + a; + }); + a = 1; +} + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +let a; +function foo() { + a = 10; +} +for (let x of xs) { + (function () { + a; + }); +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + { + code: ` +let a; +function foo() { + a = 10; + for (let x of xs) { + (function () { + a; + }); + } +} +foo(); + `, + parserOptions: { ecmaVersion: 6 }, + errors: [ + { + messageId: 'unsafeRefs', + data: { varNames: "'a'" }, + type: AST_NODE_TYPES.FunctionExpression, + }, + ], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index fa9fc6170a6..9117f06c6c1 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -286,6 +286,21 @@ declare module 'eslint/lib/rules/no-implicit-globals' { export = rule; } +declare module 'eslint/lib/rules/no-loop-func' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'unsafeRefs', + [], + { + ArrowFunctionExpression(node: TSESTree.ArrowFunctionExpression): void; + FunctionExpression(node: TSESTree.FunctionExpression): void; + FunctionDeclaration(node: TSESTree.FunctionDeclaration): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/no-magic-numbers' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';