diff --git a/README.md b/README.md index d24c3e2..af9ea5e 100644 --- a/README.md +++ b/README.md @@ -78,6 +78,7 @@ This guarantees 100% compatibility between the plugin and the parser. | [`typescript/no-non-null-assertion`](./docs/rules/no-non-null-assertion.md) | Disallows non-null assertions using the `!` postfix operator (`no-non-null-assertion` from TSLint) | | | | [`typescript/no-object-literal-type-assertion`](./docs/rules/no-object-literal-type-assertion.md) | Forbids an object literal to appear in a type assertion expression (`no-object-literal-type-assertion` from TSLint) | | | | [`typescript/no-parameter-properties`](./docs/rules/no-parameter-properties.md) | Disallow the use of parameter properties in class constructors. (`no-parameter-properties` from TSLint) | | | +| [`typescript/no-this-alias`](./docs/rules/no-this-alias.md) | Disallow aliasing `this` (`no-this-assignment` from TSLint) | | | | [`typescript/no-triple-slash-reference`](./docs/rules/no-triple-slash-reference.md) | Disallow `/// ` comments (`no-reference` from TSLint) | | | | [`typescript/no-type-alias`](./docs/rules/no-type-alias.md) | Disallow the use of type aliases (`interface-over-type-literal` from TSLint) | | | | [`typescript/no-unused-vars`](./docs/rules/no-unused-vars.md) | Prevent TypeScript-specific constructs from being erroneously flagged as unused | :heavy_check_mark: | | diff --git a/docs/rules/no-this-alias.md b/docs/rules/no-this-alias.md new file mode 100644 index 0000000..a9e68b6 --- /dev/null +++ b/docs/rules/no-this-alias.md @@ -0,0 +1,58 @@ +# Disallow aliasing `this` (no-this-alias) + +This rule prohibts assigning variables to `this`. + +## Rule Details + +Rationale from TSLint: + +> Assigning a variable to `this` instead of properly using arrow lambdas may be a symptom of pre-ES6 practices +> or not managing scope well. +> +> Instead of storing a reference to `this` and using it inside a `function () {`: +> +> ```js +> const self = this; +> +> setTimeout(function () { +> self.doWork(); +> }); +> ``` +> +> Use `() =>` arrow lambdas, as they preserve `this` scope for you: +> +> ```js +> setTimeout(() => { +> this.doWork(); +> }); +> ``` + + +Examples of **incorrect** code for this rule: + +(see the rationale above) + +Examples of **correct** code for this rule: + +(see the rationale above) + +### Options + +You can pass an object option: + +```json5 +{ + "typescript/no-this-alias": ["error", { + "allowDestructuring": true, // Allow `const { props, state } = this`; false by default + "allowedNames": ["self"], // Allow `const self = this`; `[]` by default + }], +} +``` + +## When Not To Use It + +If you need to assign `this` to variables, you shouldn’t use this rule. + +## Related to + +- TSLint: [`no-this-assignment`](https://palantir.github.io/tslint/rules/no-this-assignment/) diff --git a/lib/rules/no-this-alias.js b/lib/rules/no-this-alias.js new file mode 100644 index 0000000..6b4f44d --- /dev/null +++ b/lib/rules/no-this-alias.js @@ -0,0 +1,70 @@ +/** + * @fileoverview Disallow aliasing `this` + * @author Jed Fox + */ +"use strict"; + +const util = require("../util"); + +//------------------------------------------------------------------------------ +// Rule Definition +//------------------------------------------------------------------------------ + +module.exports = { + meta: { + docs: { + description: "Disallow aliasing `this`", + extraDescription: [util.tslintRule("no-this-assignment")], + category: "Best Practices", + recommended: false, + url: util.metaDocsUrl("no-this-alias"), + }, + fixable: null, + schema: [ + { + type: "object", + additionalProperties: false, + properties: { + allowDestructuring: { + type: "boolean", + }, + allowedNames: { + type: "array", + items: { + type: "string", + }, + }, + }, + }, + ], + messages: { + thisAssignment: "Unexpected aliasing of 'this' to local variable.", + thisDestructure: + "Unexpected aliasing of members of 'this' to local variables.", + }, + }, + + create(context) { + const { allowDestructuring = false, allowedNames = [] } = + context.options[0] || {}; + + return { + VariableDeclarator(node) { + const { id, init } = node; + + if (init.type !== "ThisExpression") return; + if (allowDestructuring && node.id.type !== "Identifier") return; + + if (!allowedNames.includes(id.name)) { + context.report({ + node: id, + messageId: + id.type === "Identifier" + ? "thisAssignment" + : "thisDestructure", + }); + } + }, + }; + }, +}; diff --git a/tests/lib/rules/no-this-alias.js b/tests/lib/rules/no-this-alias.js new file mode 100644 index 0000000..f3ad295 --- /dev/null +++ b/tests/lib/rules/no-this-alias.js @@ -0,0 +1,123 @@ +/** + * @fileoverview Disallow aliasing `this` + * Some tests taken from TSLint: https://github.com/palantir/tslint/tree/c7fc99b5/test/rules/no-this-assignment + * @author Jed Fox + */ +"use strict"; + +//------------------------------------------------------------------------------ +// Requirements +//------------------------------------------------------------------------------ + +const rule = require("../../../lib/rules/no-this-alias"), + RuleTester = require("eslint").RuleTester; + +const idError = { messageId: "thisAssignment", type: "Identifier" }; +const destructureError = { + messageId: "thisDestructure", + type: "ObjectPattern", +}; +const arrayDestructureError = { + messageId: "thisDestructure", + type: "ArrayPattern", +}; + +//------------------------------------------------------------------------------ +// Tests +//------------------------------------------------------------------------------ + +const ruleTester = new RuleTester({ + parser: "typescript-eslint-parser", +}); + +ruleTester.run("no-this-alias", rule, { + valid: [ + "const self = foo(this);", + { + code: ` +const { props, state } = this; +const { length } = this; +const { length, toString } = this; +const [foo] = this; +const [foo, bar] = this; +`.trim(), + options: [ + { + allowDestructuring: true, + }, + ], + }, + { + code: "const self = this;", + options: [ + { + allowedNames: ["self"], + }, + ], + }, + ], + + invalid: [ + { + code: "const self = this;", + options: [ + { + allowDestructuring: true, + }, + ], + errors: [idError], + }, + { + code: "const self = this;", + errors: [idError], + }, + { + code: "const { props, state } = this;", + errors: [destructureError], + }, + { + code: ` +var unscoped = this; + +function testFunction() { + let inFunction = this; +} +const testLambda = () => { + const inLambda = this; +}; +`.trim(), + errors: [idError, idError, idError], + }, + { + code: ` +class TestClass { + constructor() { + const inConstructor = this; + const asThis: this = this; + + const asString = "this"; + const asArray = [this]; + const asArrayString = ["this"]; + } + + public act(scope: this = this) { + const inMemberFunction = this; + const { act } = this; + const { act, constructor } = this; + const [foo] = this; + const [foo, bar] = this; + } +} +`.trim(), + errors: [ + idError, + idError, + idError, + destructureError, + destructureError, + arrayDestructureError, + arrayDestructureError, + ], + }, + ], +});