diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index 809aac848a6..8bb80f10ff7 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -119,6 +119,7 @@ Pro Tip: For larger codebases you may want to consider splitting our linting int | [`@typescript-eslint/no-for-in-array`](./docs/rules/no-for-in-array.md) | Disallow iterating over an array with a for-in loop | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-implied-eval`](./docs/rules/no-implied-eval.md) | Disallow the use of `eval()`-like methods | | | :thought_balloon: | | [`@typescript-eslint/no-inferrable-types`](./docs/rules/no-inferrable-types.md) | Disallows explicit type declarations for variables or parameters initialized to a number, string, or boolean | :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-misused-new`](./docs/rules/no-misused-new.md) | Enforce valid definition of `new` and `constructor` | :heavy_check_mark: | | | | [`@typescript-eslint/no-misused-promises`](./docs/rules/no-misused-promises.md) | Avoid using promises in places not designed to handle them | :heavy_check_mark: | | :thought_balloon: | | [`@typescript-eslint/no-namespace`](./docs/rules/no-namespace.md) | Disallow the use of custom TypeScript modules and namespaces | :heavy_check_mark: | | | diff --git a/packages/eslint-plugin/docs/rules/no-invalid-this.md b/packages/eslint-plugin/docs/rules/no-invalid-this.md new file mode 100644 index 00000000000..9b713a1fca8 --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-invalid-this.md @@ -0,0 +1 @@ +# Disallow `this` keywords outside of classes or class-like objects (`no-invalid-this`) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 85cf2e1ecc6..ed7c8efafc9 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -46,6 +46,7 @@ "@typescript-eslint/no-for-in-array": "error", "@typescript-eslint/no-implied-eval": "error", "@typescript-eslint/no-inferrable-types": "error", + "@typescript-eslint/no-invalid-this": "error", "no-magic-numbers": "off", "@typescript-eslint/no-magic-numbers": "error", "@typescript-eslint/no-misused-new": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 15ce8dd1a82..701b57611b6 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -39,6 +39,7 @@ import noFloatingPromises from './no-floating-promises'; import noForInArray from './no-for-in-array'; import noImpliedEval from './no-implied-eval'; import noInferrableTypes from './no-inferrable-types'; +import noInvalidThis from './no-invalid-this'; import noMagicNumbers from './no-magic-numbers'; import noMisusedNew from './no-misused-new'; import noMisusedPromises from './no-misused-promises'; @@ -136,6 +137,7 @@ export default { 'no-for-in-array': noForInArray, 'no-implied-eval': noImpliedEval, 'no-inferrable-types': noInferrableTypes, + 'no-invalid-this': noInvalidThis, 'no-magic-numbers': noMagicNumbers, 'no-misused-new': noMisusedNew, 'no-misused-promises': noMisusedPromises, diff --git a/packages/eslint-plugin/src/rules/no-invalid-this.ts b/packages/eslint-plugin/src/rules/no-invalid-this.ts new file mode 100644 index 00000000000..7f7c18c7586 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-invalid-this.ts @@ -0,0 +1,159 @@ +import baseRule from 'eslint/lib/rules/no-invalid-this'; +import astUtils from 'eslint/lib/rules/utils/ast-utils'; +import { + AST_NODE_TYPES, + TSESTree, +} from '@typescript-eslint/experimental-utils'; +import * as util from '../util'; + +type CheckingContextNode = + | TSESTree.Program + | TSESTree.FunctionDeclaration + | TSESTree.FunctionExpression + | TSESTree.ArrowFunctionExpression; + +interface CheckingContext { + init: boolean; + node: CheckingContextNode; + valid: boolean; +} + +type Options = util.InferOptionsTypeFromRule; +type MessageIds = util.InferMessageIdsTypeFromRule; + +export default util.createRule({ + name: 'no-invalid-this', + meta: { + type: 'suggestion', + docs: { + description: + 'Disallow `this` keywords outside of classes or class-like objects', + category: 'Best Practices', + recommended: false, + }, + messages: { + noInvalidThis: "Unexpected 'this'.", + }, + schema: baseRule.meta.schema, + }, + defaultOptions: [ + { + capIsConstructor: true, + }, + ], + create(context, [options]) { + /** Modified version of "eslint/lib/rules/no-invalid-this" */ + + const capIsConstructor = options.capIsConstructor !== false; + const stack: CheckingContext[] = [], + sourceCode = context.getSourceCode(); + + /** + * Gets the current checking context. + * + * The return value has a flag that whether or not `this` keyword is valid. + * The flag is initialized when got at the first time. + * @returns {{valid: boolean}} + * an object which has a flag that whether or not `this` keyword is valid. + */ + function getCurrentCheckingContext(): CheckingContext { + const current = stack[stack.length - 1]; + + if (!current.init) { + current.init = true; + current.valid = !astUtils.isDefaultThisBinding( + current.node, + sourceCode, + { capIsConstructor }, + ); + } + return current; + } + + /** + * Pushes new checking context into the stack. + * + * The checking context is not initialized yet. + * Because most functions don't have `this` keyword. + * When `this` keyword was found, the checking context is initialized. + * @param {ASTNode} node A function node that was entered. + * @returns {void} + */ + function enterFunction(node: CheckingContextNode): void { + // `this` can be invalid only under strict mode. + stack.push({ + init: !context.getScope().isStrict, + node, + valid: true, + }); + } + + /** + * Pushes new checking context into the stack. + * + * The checking context is not initialized yet. + * Because most functions don't have `this` keyword. + * When `this` keyword was found, the checking context is initialized. + * @param {ASTNode} node A function node that was entered. + * @returns {void} + */ + function enterArrowFunction(node: CheckingContextNode): void { + // `this` can only be valid inside class methods. + stack.push({ + init: true, + node, + valid: node.parent?.type === AST_NODE_TYPES.ClassProperty, + }); + } + + /** + * Pops the current checking context from the stack. + * @returns {void} + */ + function exitFunction(): void { + stack.pop(); + } + + return { + /* + * `this` is invalid only under strict mode. + * Modules is always strict mode. + */ + Program(node): void { + const scope = context.getScope(), + features = context.parserOptions.ecmaFeatures ?? {}; + + stack.push({ + init: true, + node, + valid: !( + scope.isStrict || + node.sourceType === 'module' || + (features.globalReturn && scope.childScopes[0].isStrict) + ), + }); + }, + + 'Program:exit'(): void { + stack.pop(); + }, + + FunctionDeclaration: enterFunction, + 'FunctionDeclaration:exit': exitFunction, + FunctionExpression: enterFunction, + 'FunctionExpression:exit': exitFunction, + + // Introduce handling of ArrowFunctionExpressions not present in baseRule + ArrowFunctionExpression: enterArrowFunction, + 'ArrowFunctionExpression:exit': exitFunction, + + // Reports if `this` of the current context is invalid. + ThisExpression(node): void { + const current = getCurrentCheckingContext(); + if (current && !current.valid) { + context.report({ node, messageId: 'noInvalidThis' }); + } + }, + }; + }, +}); diff --git a/packages/eslint-plugin/tests/rules/no-invalid-this.test.ts b/packages/eslint-plugin/tests/rules/no-invalid-this.test.ts new file mode 100644 index 00000000000..f7c2d2eb56b --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-invalid-this.test.ts @@ -0,0 +1,93 @@ +import rule from '../../src/rules/no-invalid-this'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parserOptions: { + sourceType: 'module', + }, + parser: '@typescript-eslint/parser', +}); + +ruleTester.run('no-invalid-this', rule, { + valid: [ + `class A { + private barA = 0; + fooA = () => { + this.barA = 1; + } + }`, + `class B { + private barB() {} + fooB = () => { + this.barB(); + } + }`, + `const C = class { + private barC = 0; + fooC = () => { + this.barC = 1; + } + }`, + `const D = class { + private barD() {} + fooD = () => { + this.barD(); + } + }`, + ], + invalid: [ + { + code: `function invalidFoo() { + this.x = 1; + }`, + errors: [ + { + messageId: 'noInvalidThis', + line: 2, + column: 11, + }, + ], + }, + { + code: `function invalidBar() { + this.invalidMethod(); + }`, + errors: [ + { + messageId: 'noInvalidThis', + line: 2, + column: 11, + }, + ], + }, + { + code: `const invalidBazz = () => { + this.x = 1; + }`, + errors: [ + { + messageId: 'noInvalidThis', + line: 2, + column: 11, + }, + ], + }, + { + code: `const invalidBuzz = () => { + this.invalidMethod(); + }`, + errors: [ + { + messageId: 'noInvalidThis', + line: 2, + column: 11, + }, + ], + }, + ], +}); + +/* + + +*/ diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index ea60d9b3169..22e9e87c7f9 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -203,6 +203,28 @@ declare module 'eslint/lib/rules/no-implicit-globals' { export = rule; } +declare module 'eslint/lib/rules/no-invalid-this' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'noInvalidThis', + [ + { + capIsConstructor?: boolean; + }, + ], + { + Program(node: TSESTree.Program): void; + FunctionDeclaration(node: TSESTree.FunctionDeclaration): void; + FunctionExpression(node: TSESTree.FunctionExpression): void; + 'Program:exit'(): void; + 'FunctionDeclaration:exit'(): void; + 'FunctionExpression:exit'(): void; + } + >; + export = rule; +} + declare module 'eslint/lib/rules/no-magic-numbers' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; @@ -543,3 +565,13 @@ declare module 'eslint/lib/rules/no-extra-semi' { >; export = rule; } + +declare module 'eslint/lib/rules/utils/ast-utils' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + export function isDefaultThisBinding( + node: TSESTree.Node, + sourceCode: TSESLint.SourceCode, + options?: { capIsConstructor: boolean }, + ): boolean; +}