Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix: no-invalid-this false positive in class field initializer #15495

Merged
merged 2 commits into from Jan 15, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
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