diff --git a/packages/eslint-plugin/README.md b/packages/eslint-plugin/README.md index fa049b2cfb27..18f5b8797272 100644 --- a/packages/eslint-plugin/README.md +++ b/packages/eslint-plugin/README.md @@ -184,6 +184,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 000000000000..34e5fb8e8754 --- /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 supports all options and features of the base rule. + +## 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 c5575d4970d6..990ddbd5aec2 100644 --- a/packages/eslint-plugin/src/configs/all.json +++ b/packages/eslint-plugin/src/configs/all.json @@ -45,6 +45,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", "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 29db58b681ab..d3c450bae235 100644 --- a/packages/eslint-plugin/src/rules/index.ts +++ b/packages/eslint-plugin/src/rules/index.ts @@ -38,6 +38,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'; @@ -133,6 +134,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 000000000000..d192931b72c5 --- /dev/null +++ b/packages/eslint-plugin/src/rules/no-invalid-this.ts @@ -0,0 +1,86 @@ +import { TSESTree } from '@typescript-eslint/experimental-utils'; +import baseRule from 'eslint/lib/rules/no-invalid-this'; +import { + InferOptionsTypeFromRule, + InferMessageIdsTypeFromRule, + createRule, + deepMerge, +} from '../util'; + +export type Options = InferOptionsTypeFromRule; +export type MessageIds = InferMessageIdsTypeFromRule; + +const schema = deepMerge( + Array.isArray(baseRule.meta.schema) + ? baseRule.meta.schema[0] + : baseRule.meta.schema, + { + properties: { + capIsConstructor: { + type: 'boolean', + default: true, + }, + }, + }, +); + +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: [schema], + }, + defaultOptions: [{ capIsConstructor: true }], + create(context) { + const rules = baseRule.create(context); + let argList: Array = []; + + return { + ...rules, + FunctionDeclaration(node: TSESTree.FunctionDeclaration): void { + const names = node?.params.map( + (param: TSESTree.Parameter) => param?.name, + ); + argList.push(names); + // 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 { + const names = node?.params.map( + (param: TSESTree.Parameter) => param?.name, + ); + argList.push(names); + // 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) { + const lastFnArg = argList[argList.length - 1]; + + if (lastFnArg?.some((name: string) => name === 'this')) { + 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 000000000000..0070fefb1bea --- /dev/null +++ b/packages/eslint-plugin/tests/rules/no-invalid-this.test.ts @@ -0,0 +1,465 @@ +import { AST_NODE_TYPES } from '@typescript-eslint/experimental-utils'; +import rule from '../../src/rules/no-invalid-this'; +import { RuleTester, getFixturesRootDir } from '../RuleTester'; + +const rootPath = getFixturesRootDir(); + +const ruleTester = new RuleTester({ + parser: '@typescript-eslint/parser', + parserOptions: { + sourceType: 'module', + tsconfigRootDir: rootPath, + project: './tsconfig.json', + }, +}); + +const error = { + messageId: 'unexpectedThis', +}; + +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)); });', + + '/**\n * @returns {void}\n * @this Obj\n */\nfunction 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: [error], + }, + // Global. + { + code: 'console.log(this); z(x => console.log(x, this));', + + errors: [error, error], + }, + { + code: 'console.log(this); z(x => console.log(x, this));', + parserOptions: { + ecmaFeatures: { globalReturn: true }, + }, + errors: [error, error], + }, + + // IIFE. + { + code: + '(function() { console.log(this); z(x => console.log(x, this)); })();', + + errors: [error, error], + }, + + // Just functions. + { + code: + 'function foo() { console.log(this); z(x => console.log(x, this)); }', + + errors: [error, error], + }, + { + 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: [error, error], + }, + { + code: + 'function Foo() { console.log(this); z(x => console.log(x, this)); }', + + options: [{ capIsConstructor: false }], + errors: [error, error], + }, + { + code: + 'function foo() { "use strict"; console.log(this); z(x => console.log(x, this)); }', + + errors: [error, error], + }, + { + code: + 'function Foo() { "use strict"; console.log(this); z(x => console.log(x, this)); }', + + options: [{ capIsConstructor: false }], + errors: [error, error], + }, + { + code: + 'return function() { console.log(this); z(x => console.log(x, this)); };', + parserOptions: { + ecmaFeatures: { globalReturn: true }, + }, + errors: [error, error], + }, + { + code: + 'var foo = (function() { console.log(this); z(x => console.log(x, this)); }).bar(obj);', + + errors: [error, error], + }, + + // Functions in methods. + { + code: + 'var obj = {foo: function() { function foo() { console.log(this); z(x => console.log(x, this)); } foo(); }};', + + errors: [error, error], + }, + { + code: + 'var obj = {foo() { function foo() { console.log(this); z(x => console.log(x, this)); } foo(); }};', + + errors: [error, error], + }, + { + code: + 'var obj = {foo: function() { return function() { console.log(this); z(x => console.log(x, this)); }; }};', + + errors: [error, error], + }, + { + code: + 'var obj = {foo: function() { "use strict"; return function() { console.log(this); z(x => console.log(x, this)); }; }};', + + errors: [error, error], + }, + { + code: + 'obj.foo = function() { return function() { console.log(this); z(x => console.log(x, this)); }; };', + + errors: [error, error], + }, + { + code: + 'obj.foo = function() { "use strict"; return function() { console.log(this); z(x => console.log(x, this)); }; };', + + errors: [error, error], + }, + { + code: + 'class A { foo() { return function() { console.log(this); z(x => console.log(x, this)); }; } }', + + errors: [error, error], + }, + + // Class Static methods. + + { + code: + 'obj.foo = (function() { return () => { console.log(this); z(x => console.log(x, this)); }; })();', + + errors: [error, error], + }, + { + code: + 'obj.foo = (() => () => { console.log(this); z(x => console.log(x, this)); })();', + + errors: [error, error], + }, + // Bind/Call/Apply + + { + code: + 'var foo = function() { console.log(this); z(x => console.log(x, this)); }.bind(null);', + + errors: [error, error], + }, + + { + code: + '(function() { console.log(this); z(x => console.log(x, this)); }).call(undefined);', + + errors: [error, error], + }, + + { + code: + '(function() { console.log(this); z(x => console.log(x, this)); }).apply(void 0);', + + errors: [error, error], + }, + + // Array methods. + { + code: + 'Array.from([], function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + { + code: + 'foo.every(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + { + code: + 'foo.filter(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + { + code: + 'foo.find(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + { + code: + 'foo.findIndex(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + { + code: + 'foo.forEach(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + { + code: + 'foo.map(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + { + code: + 'foo.some(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + + { + code: + 'foo.forEach(function() { console.log(this); z(x => console.log(x, this)); }, null);', + + errors: [error, error], + }, + + // @this tag. + + { + code: + '/** @returns {void} */ function foo() { console.log(this); z(x => console.log(x, this)); }', + + errors: [error, error], + }, + { + code: + '/** @this Obj */ foo(function() { console.log(this); z(x => console.log(x, this)); });', + + errors: [error, error], + }, + + // https://github.com/eslint/eslint/issues/3254 + { + code: + 'function foo() { console.log(this); z(x => console.log(x, this)); }', + + errors: [error, error], + }, + + { + code: + 'var Ctor = function() { console.log(this); z(x => console.log(x, this)); }', + + options: [{ capIsConstructor: false }], + errors: [error, error], + }, + { + code: + 'var func = function() { console.log(this); z(x => console.log(x, this)); }', + + errors: [error, error], + }, + { + code: + 'var func = function() { console.log(this); z(x => console.log(x, this)); }', + + options: [{ capIsConstructor: false }], + errors: [error, error], + }, + + { + code: + 'Ctor = function() { console.log(this); z(x => console.log(x, this)); }', + + options: [{ capIsConstructor: false }], + errors: [error, error], + }, + { + code: + 'func = function() { console.log(this); z(x => console.log(x, this)); }', + + errors: [error, error], + }, + { + code: + 'func = function() { console.log(this); z(x => console.log(x, this)); }', + + options: [{ capIsConstructor: false }], + errors: [error, error], + }, + + { + code: + 'function foo(func = function() { console.log(this); z(x => console.log(x, this)); }) {}', + + errors: [error, error], + }, + + { + code: + '[func = function() { console.log(this); z(x => console.log(x, this)); }] = a', + + errors: [error, error], + }, + ], +}); diff --git a/packages/eslint-plugin/typings/eslint-rules.d.ts b/packages/eslint-plugin/typings/eslint-rules.d.ts index ea60d9b31697..076ed5d13e02 100644 --- a/packages/eslint-plugin/typings/eslint-rules.d.ts +++ b/packages/eslint-plugin/typings/eslint-rules.d.ts @@ -543,3 +543,22 @@ declare module 'eslint/lib/rules/no-extra-semi' { >; export = rule; } + +declare module 'eslint/lib/rules/no-invalid-this' { + import { TSESLint, TSESTree } from '@typescript-eslint/experimental-utils'; + + const rule: TSESLint.RuleModule< + 'unexpectedThis', + [{ 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; +}