From a8db9a597672d3f9122f1cc8de8dfeb6a7f6aeac Mon Sep 17 00:00:00 2001 From: Milos Djermanovic Date: Sat, 15 Jan 2022 23:58:02 +0100 Subject: [PATCH] fix: no-invalid-this false positive in class field initializer (#15495) * fix: no-invalid-this false positive in class field initializer Fixes #15477 * fix jsdoc --- lib/rules/no-invalid-this.js | 103 ++++++++++++++--------------- tests/lib/rules/no-invalid-this.js | 42 ++++++++++++ 2 files changed, 92 insertions(+), 53 deletions(-) diff --git a/lib/rules/no-invalid-this.js b/lib/rules/no-invalid-this.js index 5f9b9f871ec..64e4d964a84 100644 --- a/lib/rules/no-invalid-this.js +++ b/lib/rules/no-invalid-this.js @@ -11,6 +11,21 @@ const astUtils = require("./utils/ast-utils"); +//------------------------------------------------------------------------------ +// Helpers +//------------------------------------------------------------------------------ + +/** + * Determines if the given code path is a code path with lexical `this` binding. + * That is, if `this` within the code path refers to `this` of surrounding code path. + * @param {CodePath} codePath Code path. + * @param {ASTNode} node Node that started the code path. + * @returns {boolean} `true` if it is a code path with lexical `this` binding. + */ +function isCodePathWithLexicalThis(codePath, node) { + return codePath.origin === "function" && node.type === "ArrowFunctionExpression"; +} + //------------------------------------------------------------------------------ // Rule Definition //------------------------------------------------------------------------------ @@ -72,71 +87,53 @@ module.exports = { return current; }; - /** - * Pushs 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) { - - // `this` can be invalid only under strict mode. - stack.push({ - init: !context.getScope().isStrict, - node, - valid: true - }); - } + return { - /** - * Pops the current checking context from the stack. - * @returns {void} - */ - function exitFunction() { - stack.pop(); - } + onCodePathStart(codePath, node) { + if (isCodePathWithLexicalThis(codePath, node)) { + return; + } - return { + if (codePath.origin === "program") { + const scope = context.getScope(); + const features = context.parserOptions.ecmaFeatures || {}; + + stack.push({ + init: true, + node, + valid: !( + scope.isStrict || + node.sourceType === "module" || + (features.globalReturn && scope.childScopes[0].isStrict) + ) + }); - /* - * `this` is invalid only under strict mode. - * Modules is always strict mode. - */ - Program(node) { - const scope = context.getScope(), - features = context.parserOptions.ecmaFeatures || {}; + return; + } + /* + * `init: false` means that `valid` isn't determined yet. + * Most functions don't use `this`, and the calculation for `valid` + * is relatively costly, so we'll calculate it lazily when the first + * `this` within the function is traversed. A special case are non-strict + * functions, because `this` refers to the global object and therefore is + * always valid, so we can set `init: true` right away. + */ stack.push({ - init: true, + init: !context.getScope().isStrict, node, - valid: !( - scope.isStrict || - node.sourceType === "module" || - (features.globalReturn && scope.childScopes[0].isStrict) - ) + valid: true }); }, - "Program:exit"() { + onCodePathEnd(codePath, node) { + if (isCodePathWithLexicalThis(codePath, node)) { + return; + } + stack.pop(); }, - FunctionDeclaration: enterFunction, - "FunctionDeclaration:exit": exitFunction, - FunctionExpression: enterFunction, - "FunctionExpression:exit": exitFunction, - - // Field initializers are implicit functions. - "PropertyDefinition > *.value": enterFunction, - "PropertyDefinition > *.value:exit": exitFunction, - - // Class static blocks are implicit functions. - StaticBlock: enterFunction, - "StaticBlock:exit": exitFunction, - // Reports if `this` of the current context is invalid. ThisExpression(node) { const current = stack.getCurrent(); diff --git a/tests/lib/rules/no-invalid-this.js b/tests/lib/rules/no-invalid-this.js index 62033fd389e..90cbdef98ac 100644 --- a/tests/lib/rules/no-invalid-this.js +++ b/tests/lib/rules/no-invalid-this.js @@ -119,6 +119,15 @@ const patterns = [ valid: [NORMAL], invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] }, + { + code: "() => { this }; this;", + parserOptions: { + ecmaVersion: 6 + }, + errors, + valid: [NORMAL], + invalid: [USE_STRICT, IMPLIED_STRICT, MODULES] + }, // IIFE. { @@ -745,6 +754,18 @@ const patterns = [ }, // Class fields. + { + code: "class C { field = this }", + parserOptions: { ecmaVersion: 2022 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, + { + code: "class C { static field = this }", + parserOptions: { ecmaVersion: 2022 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [] + }, { code: "class C { field = console.log(this); }", parserOptions: { ecmaVersion: 2022 }, @@ -776,6 +797,20 @@ const patterns = [ invalid: [USE_STRICT, IMPLIED_STRICT, MODULES], errors: [{ messageId: "unexpectedThis", type: "ThisExpression" }] }, + { + code: "class C { foo = () => this; }", + parserOptions: { ecmaVersion: 2022 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [], + errors: [{ messageId: "unexpectedThis", type: "ThisExpression" }] + }, + { + code: "class C { foo = () => { this }; }", + parserOptions: { ecmaVersion: 2022 }, + valid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], + invalid: [], + errors: [{ messageId: "unexpectedThis", type: "ThisExpression" }] + }, // Class static blocks { @@ -817,6 +852,13 @@ const patterns = [ invalid: [NORMAL, USE_STRICT, IMPLIED_STRICT, MODULES], errors: [{ messageId: "unexpectedThis", type: "ThisExpression" }] }, + { + code: "class C { static {} [this]; }", + parserOptions: { ecmaVersion: 2022 }, + valid: [NORMAL], + invalid: [USE_STRICT, IMPLIED_STRICT, MODULES], + errors: [{ messageId: "unexpectedThis", type: "ThisExpression" }] + }, { code: "class C { static {} [this.x]; }", parserOptions: { ecmaVersion: 2022 },