Skip to content

Commit

Permalink
fix: no-invalid-this false positive in class field initializer (#15495)
Browse files Browse the repository at this point in the history
* fix: no-invalid-this false positive in class field initializer

Fixes #15477

* fix jsdoc
  • Loading branch information
mdjermanovic committed Jan 15, 2022
1 parent 19ad061 commit a8db9a5
Show file tree
Hide file tree
Showing 2 changed files with 92 additions and 53 deletions.
103 changes: 50 additions & 53 deletions lib/rules/no-invalid-this.js
Expand Up @@ -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
//------------------------------------------------------------------------------
Expand Down Expand Up @@ -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();
Expand Down
42 changes: 42 additions & 0 deletions tests/lib/rules/no-invalid-this.js
Expand Up @@ -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.
{
Expand Down Expand Up @@ -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 },
Expand Down Expand Up @@ -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
{
Expand Down Expand Up @@ -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 },
Expand Down

0 comments on commit a8db9a5

Please sign in to comment.