diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index f6e70ef98ac..dd269dc2484 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -192,6 +192,7 @@ In these cases, we create what we call an extension rule; a rule within our plug | [`@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 | | :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-magic-numbers`](./docs/rules/no-magic-numbers.md) | Disallow magic numbers | | | | | [`@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: | | | 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..ac9dc30122c --- /dev/null +++ b/packages/eslint-plugin/docs/rules/no-invalid-this.md @@ -0,0 +1,26 @@ +# disallow `this` keywords outside of classes or class-like objects (`no-invalid-this`) + +## Rule Details + +This rule extends the base [`eslint/no-invalid-this`](https://eslint.org/docs/rules/no-invalid-this) rule. +It adds support for TypeScript's `this` parameters. + +## How to use + +```cjson +{ + // note you must disable the base rule as it can report incorrect errors + "no-invalid-this": "off", + "@typescript-eslint/no-invalid-this": ["error"] +} +``` + +## Options + +See [`eslint/no-invalid-this` options](https://eslint.org/docs/rules/no-invalid-this#options). + +## When Not To Use It + +When you are indifferent as to how your variables are initialized. + +Taken with ❤️ [from ESLint core](https://github.com/eslint/eslint/blob/master/docs/rules/no-invalid-this.md) diff --git a/packages/eslint-plugin/src/configs/all.json b/packages/eslint-plugin/src/configs/all.json index 0e103162f5f..cf27dc57be5 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -52,6 +52,8 @@ "@typescript-eslint/no-for-in-array": "error", "@typescript-eslint/no-implied-eval": "error", "@typescript-eslint/no-inferrable-types": "error", + "no-invalid-this": "off", + "@typescript-eslint/no-invalid-this": "error", "@typescript-eslint/no-invalid-void-type": "error", "no-magic-numbers": "off", "@typescript-eslint/no-magic-numbers": "error", diff --git a/packages/eslint-plugin/src/rules/index.ts b/packages/eslint-plugin/src/rules/index.ts index 2d8e6830c4f..9c1c26444d7 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -41,6 +41,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 noInvalidVoidType from './no-invalid-void-type'; import noMagicNumbers from './no-magic-numbers'; import noMisusedNew from './no-misused-new'; @@ -145,6 +146,7 @@ export default { 'no-for-in-array': noForInArray, 'no-implied-eval': noImpliedEval, 'no-inferrable-types': noInferrableTypes, + 'no-invalid-this': noInvalidThis, 'no-invalid-void-type': noInvalidVoidType, 'no-magic-numbers': noMagicNumbers, 'no-misused-new': noMisusedNew, 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..186d3378846 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-invalid-this.ts @@ -0,0 +1,78 @@ +import { + TSESTree, + AST_NODE_TYPES, +} from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/no-invalid-this'; +import { + InferOptionsTypeFromRule, + createRule, + InferMessageIdsTypeFromRule, +} from '../util'; + +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; + +export default 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, + extendsBaseRule: true, + }, + messages: baseRule.meta.messages, + schema: baseRule.meta.schema, + }, + defaultOptions: [{ capIsConstructor: true }], + create(context) { + const rules = baseRule.create(context); + const argList: boolean[] = []; + + return { + ...rules, + FunctionDeclaration(node: TSESTree.FunctionDeclaration): void { + argList.push( + node.params.some( + param => + param.type === AST_NODE_TYPES.Identifier && param.name === 'this', + ), + ); + // baseRule's work + rules.FunctionDeclaration(node); + }, + 'FunctionDeclaration:exit'(node: TSESTree.FunctionDeclaration): void { + argList.pop(); + // baseRule's work + rules['FunctionDeclaration:exit'](node); + }, + FunctionExpression(node: TSESTree.FunctionExpression): void { + argList.push( + node.params.some( + param => + param.type === AST_NODE_TYPES.Identifier && param.name === 'this', + ), + ); + // baseRule's work + rules.FunctionExpression(node); + }, + 'FunctionExpression:exit'(node: TSESTree.FunctionExpression): void { + argList.pop(); + // baseRule's work + rules['FunctionExpression:exit'](node); + }, + ThisExpression(node: TSESTree.ThisExpression): void { + const lastFnArg = argList[argList.length - 1]; + + if (lastFnArg) { + return; + } + + // baseRule's work + rules.ThisExpression(node); + }, + }; + }, +}); 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..93701a2dc32 --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-invalid-this.test.ts @@ -0,0 +1,899 @@ +import rule from '../../src/rules/no-invalid-this'; +import { RuleTester } from '../RuleTester'; + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + }, +}); + +// eslint-disable-next-line @typescript-eslint/no-explicit-any +const errors: any = [ + { message: "Unexpected 'this'." }, + { message: "Unexpected 'this'." }, +]; + +ruleTester.run('no-invalid-this', rule, { + valid: [ + ` +describe('foo', () => { + it('does something', function(this: Mocha.Context) { + this.timeout(100); + // done + }); +}); + `, + ` + interface SomeType { + prop: string; + } + function foo(this: SomeType) { + this.prop; + } + `, + ` +function foo(this: prop) { + this.propMethod(); +} + `, + ` +z(function(x, this: context) { + console.log(x, this); +}); + `, + // https://github.com/eslint/eslint/issues/3287 + + ` +function foo() { + /** @this Obj*/ return function bar() { + console.log(this); + z(x => console.log(x, this)); + }; +} + `, + + // https://github.com/eslint/eslint/issues/6824 + + ` +var Ctor = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + // Constructors. + { + code: ` +function Foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + }, + { + code: ` +function Foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + options: [{}], // test the default value in schema + }, + { + code: ` +function Foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + options: [{ capIsConstructor: true }], // test explicitly set option to the default value + }, + { + code: ` +var Foo = function Foo() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + }, + { + code: ` +class A { + constructor() { + console.log(this); + z(x => console.log(x, this)); + } +} + `, + }, + + // On a property. + { + code: ` +var obj = { + foo: function() { + console.log(this); + z(x => console.log(x, this)); + }, +}; + `, + }, + { + code: ` +var obj = { + foo() { + console.log(this); + z(x => console.log(x, this)); + }, +}; + `, + }, + { + code: ` +var obj = { + foo: + foo || + function() { + console.log(this); + z(x => console.log(x, this)); + }, +}; + `, + }, + { + code: ` +var obj = { + foo: hasNative + ? foo + : function() { + console.log(this); + z(x => console.log(x, this)); + }, +}; + `, + }, + { + code: ` +var obj = { + foo: (function() { + return function() { + console.log(this); + z(x => console.log(x, this)); + }; + })(), +}; + `, + }, + { + code: ` +Object.defineProperty(obj, 'foo', { + value: function() { + console.log(this); + z(x => console.log(x, this)); + }, +}); + `, + }, + { + code: ` +Object.defineProperties(obj, { + foo: { + value: function() { + console.log(this); + z(x => console.log(x, this)); + }, + }, +}); + `, + }, + + // Assigns to a property. + { + code: ` +obj.foo = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + }, + { + code: ` +obj.foo = + foo || + function() { + console.log(this); + z(x => console.log(x, this)); + }; + `, + }, + { + code: ` +obj.foo = foo + ? bar + : function() { + console.log(this); + z(x => console.log(x, this)); + }; + `, + }, + { + code: ` +obj.foo = (function() { + return function() { + console.log(this); + z(x => console.log(x, this)); + }; +})(); + `, + }, + { + code: ` +obj.foo = (() => + function() { + console.log(this); + z(x => console.log(x, this)); + })(); + `, + }, + + // Bind/Call/Apply + ` +(function() { + console.log(this); + z(x => console.log(x, this)); +}.call(obj)); + `, + ` +var foo = function() { + console.log(this); + z(x => console.log(x, this)); +}.bind(obj); + `, + ` +Reflect.apply( + function() { + console.log(this); + z(x => console.log(x, this)); + }, + obj, + [], +); + `, + ` +(function() { + console.log(this); + z(x => console.log(x, this)); +}.apply(obj)); + `, + + // Class Instance Methods. + ` +class A { + foo() { + console.log(this); + z(x => console.log(x, this)); + } +} + `, + + // Array methods. + + ` +Array.from( + [], + function() { + console.log(this); + z(x => console.log(x, this)); + }, + obj, +); + `, + + ` +foo.every(function() { + console.log(this); + z(x => console.log(x, this)); +}, obj); + `, + + ` +foo.filter(function() { + console.log(this); + z(x => console.log(x, this)); +}, obj); + `, + + ` +foo.find(function() { + console.log(this); + z(x => console.log(x, this)); +}, obj); + `, + + ` +foo.findIndex(function() { + console.log(this); + z(x => console.log(x, this)); +}, obj); + `, + + ` +foo.forEach(function() { + console.log(this); + z(x => console.log(x, this)); +}, obj); + `, + + ` +foo.map(function() { + console.log(this); + z(x => console.log(x, this)); +}, obj); + `, + + ` +foo.some(function() { + console.log(this); + z(x => console.log(x, this)); +}, obj); + `, + + // @this tag. + + ` +/** @this Obj */ function foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + ` +foo( + /* @this Obj */ function() { + console.log(this); + z(x => console.log(x, this)); + }, +); + `, + + ` +/** + * @returns {void} + * @this Obj + */ +function foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + ` +Ctor = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + + ` +function foo( + Ctor = function() { + console.log(this); + z(x => console.log(x, this)); + }, +) {} + `, + + ` +[ + obj.method = function() { + console.log(this); + z(x => console.log(x, this)); + }, +] = a; + `, + + // Static + + ` +class A { + static foo() { + console.log(this); + z(x => console.log(x, this)); + } +} + `, + ], + + invalid: [ + { + code: ` +interface SomeType { + prop: string; +} +function foo() { + this.prop; +} + `, + errors: [{ message: "Unexpected 'this'." }], + }, + // Global. + { + code: ` +console.log(this); +z(x => console.log(x, this)); + `, + + errors, + }, + { + code: ` +console.log(this); +z(x => console.log(x, this)); + `, + parserOptions: { + ecmaFeatures: { globalReturn: true }, + }, + errors, + }, + + // IIFE. + { + code: ` +(function() { + console.log(this); + z(x => console.log(x, this)); +})(); + `, + + errors, + }, + + // Just functions. + { + code: ` +function foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + errors, + }, + { + code: ` +function foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + options: [{ capIsConstructor: false }], // test that the option doesn't reverse the logic and mistakenly allows lowercase functions + errors, + }, + { + code: ` +function Foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + options: [{ capIsConstructor: false }], + errors, + }, + { + code: ` +function foo() { + 'use strict'; + console.log(this); + z(x => console.log(x, this)); +} + `, + + errors, + }, + { + code: ` +function Foo() { + 'use strict'; + console.log(this); + z(x => console.log(x, this)); +} + `, + + options: [{ capIsConstructor: false }], + errors, + }, + { + code: ` +return function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + parserOptions: { + ecmaFeatures: { globalReturn: true }, + }, + errors, + }, + { + code: ` +var foo = function() { + console.log(this); + z(x => console.log(x, this)); +}.bar(obj); + `, + + errors, + }, + + // Functions in methods. + { + code: ` +var obj = { + foo: function() { + function foo() { + console.log(this); + z(x => console.log(x, this)); + } + foo(); + }, +}; + `, + + errors, + }, + { + code: ` +var obj = { + foo() { + function foo() { + console.log(this); + z(x => console.log(x, this)); + } + foo(); + }, +}; + `, + + errors, + }, + { + code: ` +var obj = { + foo: function() { + return function() { + console.log(this); + z(x => console.log(x, this)); + }; + }, +}; + `, + + errors, + }, + { + code: ` +var obj = { + foo: function() { + 'use strict'; + return function() { + console.log(this); + z(x => console.log(x, this)); + }; + }, +}; + `, + + errors, + }, + { + code: ` +obj.foo = function() { + return function() { + console.log(this); + z(x => console.log(x, this)); + }; +}; + `, + + errors, + }, + { + code: ` +obj.foo = function() { + 'use strict'; + return function() { + console.log(this); + z(x => console.log(x, this)); + }; +}; + `, + + errors, + }, + { + code: ` +class A { + foo() { + return function() { + console.log(this); + z(x => console.log(x, this)); + }; + } +} + `, + + errors, + }, + + // Class Static methods. + + { + code: ` +obj.foo = (function() { + return () => { + console.log(this); + z(x => console.log(x, this)); + }; +})(); + `, + + errors, + }, + { + code: ` +obj.foo = (() => () => { + console.log(this); + z(x => console.log(x, this)); +})(); + `, + + errors, + }, + // Bind/Call/Apply + + { + code: ` +var foo = function() { + console.log(this); + z(x => console.log(x, this)); +}.bind(null); + `, + + errors, + }, + + { + code: ` +(function() { + console.log(this); + z(x => console.log(x, this)); +}.call(undefined)); + `, + + errors, + }, + + { + code: ` +(function() { + console.log(this); + z(x => console.log(x, this)); +}.apply(void 0)); + `, + + errors, + }, + + // Array methods. + { + code: ` +Array.from([], function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + { + code: ` +foo.every(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + { + code: ` +foo.filter(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + { + code: ` +foo.find(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + { + code: ` +foo.findIndex(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + { + code: ` +foo.forEach(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + { + code: ` +foo.map(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + { + code: ` +foo.some(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + + { + code: ` +foo.forEach(function() { + console.log(this); + z(x => console.log(x, this)); +}, null); + `, + + errors, + }, + + // @this tag. + + { + code: ` +/** @returns {void} */ function foo() { + console.log(this); + z(x => console.log(x, this)); +} + `, + + errors, + }, + { + code: ` +/** @this Obj */ foo(function() { + console.log(this); + z(x => console.log(x, this)); +}); + `, + + errors, + }, + + { + code: ` +var Ctor = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + + options: [{ capIsConstructor: false }], + errors, + }, + { + code: ` +var func = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + + errors, + }, + { + code: ` +var func = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + + options: [{ capIsConstructor: false }], + errors, + }, + + { + code: ` +Ctor = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + + options: [{ capIsConstructor: false }], + errors, + }, + { + code: ` +func = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + + errors, + }, + { + code: ` +func = function() { + console.log(this); + z(x => console.log(x, this)); +}; + `, + + options: [{ capIsConstructor: false }], + errors, + }, + + { + code: ` +function foo( + func = function() { + console.log(this); + z(x => console.log(x, this)); + }, +) {} + `, + + errors, + }, + + { + code: ` +[ + func = function() { + console.log(this); + z(x => console.log(x, this)); + }, +] = a; + `, + + errors, + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index 0bac88823e2..b4ec293e4f5 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -641,6 +641,28 @@ declare module 'eslint/lib/rules/init-declarations' { export = rule; } +declare module 'eslint/lib/rules/no-invalid-this' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + never, + [ + { + capIsConstructor?: boolean; + }?, + ], + { + Program(node: TSESTree.Program): void; + 'Program:exit'(node: TSESTree.Program): void; + FunctionDeclaration(node: TSESTree.FunctionDeclaration): void; + 'FunctionDeclaration:exit'(node: TSESTree.FunctionDeclaration): void; + FunctionExpression(node: TSESTree.FunctionExpression): void; + 'FunctionExpression:exit'(node: TSESTree.FunctionExpression): void; + ThisExpression(node: TSESTree.ThisExpression): void; + } + >; + export = rule; +} declare module 'eslint/lib/rules/dot-notation' { import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils';