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

Update: Code path analysis for class fields (fixes #14343) #14886

Merged
merged 8 commits into from Aug 26, 2021
Merged
145 changes: 110 additions & 35 deletions lib/linter/code-path-analysis/code-path-analyzer.js
Expand Up @@ -29,6 +29,18 @@ function isCaseNode(node) {
return Boolean(node.test);
}

/**
* Checks if a given node appears as the value of a PropertyDefinition node.
* @param {ASTNode} node THe node to check.
* @returns {boolean} `true` if the node is a PropertyDefinition value,
* false if not.
*/
function isPropertyDefinitionValue(node) {
const parent = node.parent;

return parent && parent.type === "PropertyDefinition" && parent.value === node;
}

/**
* Checks whether the given logical operator is taken into account for the code
* path analysis.
Expand Down Expand Up @@ -138,6 +150,7 @@ function isIdentifierReference(node) {
return parent.id !== node;

case "Property":
case "PropertyDefinition":
case "MethodDefinition":
return (
parent.key !== node ||
Expand Down Expand Up @@ -388,29 +401,59 @@ function processCodePathToEnter(analyzer, node) {
let state = codePath && CodePath.getState(codePath);
const parent = node.parent;

/**
* Creates a new code path and trigger the onCodePathStart event
* based on the currently selected node.
* @returns {void}
*/
function startCodePath() {
if (codePath) {

// Emits onCodePathSegmentStart events if updated.
forwardCurrentToHead(analyzer, node);
debug.dumpState(node, state, false);
}

// Create the code path of this scope.
codePath = analyzer.codePath = new CodePath(
analyzer.idGenerator.next(),
codePath,
analyzer.onLooped
);
state = CodePath.getState(codePath);

// Emits onCodePathStart events.
debug.dump(`onCodePathStart ${codePath.id}`);
analyzer.emitter.emit("onCodePathStart", codePath, node);
}

/*
* Special case: The right side of class field initializer is considered
* to be its own function, so we need to start a new code path in this
* case.
*/
if (isPropertyDefinitionValue(node)) {
startCodePath();

/*
* Intentional fall through because `node` needs to also be
* processed by the code below. For example, if we have:
*
* class Foo {
* a = () => {}
* }
*
* In this case, we also need start a second code path.
*/

}

switch (node.type) {
case "Program":
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression":
if (codePath) {

// Emits onCodePathSegmentStart events if updated.
forwardCurrentToHead(analyzer, node);
debug.dumpState(node, state, false);
}

// Create the code path of this scope.
codePath = analyzer.codePath = new CodePath(
analyzer.idGenerator.next(),
codePath,
analyzer.onLooped
);
state = CodePath.getState(codePath);

// Emits onCodePathStart events.
debug.dump(`onCodePathStart ${codePath.id}`);
analyzer.emitter.emit("onCodePathStart", codePath, node);
startCodePath();
break;

case "ChainExpression":
Expand Down Expand Up @@ -503,6 +546,7 @@ function processCodePathToEnter(analyzer, node) {
* @returns {void}
*/
function processCodePathToExit(analyzer, node) {

const codePath = analyzer.codePath;
const state = CodePath.getState(codePath);
let dontForward = false;
Expand Down Expand Up @@ -627,28 +671,38 @@ function processCodePathToExit(analyzer, node) {
* @returns {void}
*/
function postprocess(analyzer, node) {

/**
* Ends the code path for the current node.
* @returns {void}
*/
function endCodePath() {
let codePath = analyzer.codePath;

// Mark the current path as the final node.
CodePath.getState(codePath).makeFinal();

// Emits onCodePathSegmentEnd event of the current segments.
leaveFromCurrentSegment(analyzer, node);

// Emits onCodePathEnd event of this code path.
debug.dump(`onCodePathEnd ${codePath.id}`);
analyzer.emitter.emit("onCodePathEnd", codePath, node);
debug.dumpDot(codePath);

codePath = analyzer.codePath = analyzer.codePath.upper;
if (codePath) {
debug.dumpState(node, CodePath.getState(codePath), true);
}

}

switch (node.type) {
case "Program":
case "FunctionDeclaration":
case "FunctionExpression":
case "ArrowFunctionExpression": {
let codePath = analyzer.codePath;

// Mark the current path as the final node.
CodePath.getState(codePath).makeFinal();

// Emits onCodePathSegmentEnd event of the current segments.
leaveFromCurrentSegment(analyzer, node);

// Emits onCodePathEnd event of this code path.
debug.dump(`onCodePathEnd ${codePath.id}`);
analyzer.emitter.emit("onCodePathEnd", codePath, node);
debug.dumpDot(codePath);

codePath = analyzer.codePath = analyzer.codePath.upper;
if (codePath) {
debug.dumpState(node, CodePath.getState(codePath), true);
}
endCodePath();
break;
}

Expand All @@ -662,6 +716,27 @@ function postprocess(analyzer, node) {
default:
break;
}

/*
* Special case: The right side of class field initializer is considered
* to be its own function, so we need to end a code path in this
* case.
*
* We need to check after the other checks in order to close the
* code paths in the correct order for code like this:
*
*
* class Foo {
* a = () => {}
* }
*
* In this case, The ArrowFunctionExpression code path is closed first
* and then we need to close the code path for the PropertyDefinition
* value.
*/
if (isPropertyDefinitionValue(node)) {
endCodePath();
}
}

//------------------------------------------------------------------------------
Expand Down
@@ -0,0 +1,37 @@
/*expected
initial->s3_1->final;
*/
/*expected
initial->s2_1->final;
*/
/*expected
initial->s1_1->final;
*/

class Foo { a = () => b }

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s3_1[label="ArrowFunctionExpression:enter\nIdentifier (b)\nArrowFunctionExpression:exit"];
initial->s3_1->final;
}

digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s2_1[label="ArrowFunctionExpression"];
initial->s2_1->final;
}

digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program:enter\nClassDeclaration:enter\nIdentifier (Foo)\nClassBody:enter\nPropertyDefinition:enter\nIdentifier (a)\nArrowFunctionExpression\nPropertyDefinition:exit\nClassBody:exit\nClassDeclaration:exit\nProgram:exit"];
initial->s1_1->final;
}
*/
@@ -0,0 +1,26 @@
/*expected
initial->s2_1->final;
*/
/*expected
initial->s1_1->final;
*/

class Foo { a = b() }

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s2_1[label="CallExpression:enter\nIdentifier (b)\nCallExpression:exit"];
initial->s2_1->final;
}
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program:enter\nClassDeclaration:enter\nIdentifier (Foo)\nClassBody:enter\nPropertyDefinition:enter\nIdentifier (a)\nCallExpression\nPropertyDefinition:exit\nClassBody:exit\nClassDeclaration:exit\nProgram:exit"];
initial->s1_1->final;
}
*/
@@ -0,0 +1,32 @@
/*expected
initial->s2_1->s2_2->s2_4;
s2_1->s2_3->s2_4->final;
*/
/*expected
initial->s1_1->final;
*/


class Foo { a = b ? c : d }

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s2_1[label="ConditionalExpression:enter\nIdentifier (b)"];
s2_2[label="Identifier (c)"];
s2_4[label="ConditionalExpression:exit"];
s2_3[label="Identifier (d)"];
initial->s2_1->s2_2->s2_4;
s2_1->s2_3->s2_4->final;
}

digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program:enter\nClassDeclaration:enter\nIdentifier (Foo)\nClassBody:enter\nPropertyDefinition:enter\nIdentifier (a)\nConditionalExpression\nPropertyDefinition:exit\nClassBody:exit\nClassDeclaration:exit\nProgram:exit"];
initial->s1_1->final;
}
*/
28 changes: 28 additions & 0 deletions tests/fixtures/code-path-analysis/class-fields-init--simple.js
@@ -0,0 +1,28 @@
/*expected
initial->s2_1->final;
*/
/*expected
initial->s1_1->final;
*/

class Foo {
a = b;
}

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s2_1[label="Identifier (b)"];
initial->s2_1->final;
}

digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program:enter\nClassDeclaration:enter\nIdentifier (Foo)\nClassBody:enter\nPropertyDefinition:enter\nIdentifier (a)\nIdentifier (b)\nPropertyDefinition:exit\nClassBody:exit\nClassDeclaration:exit\nProgram:exit"];
initial->s1_1->final;
}
*/
30 changes: 30 additions & 0 deletions tests/fixtures/code-path-analysis/function--new.js
@@ -0,0 +1,30 @@
/*expected
initial->s2_1->s2_2->s2_4;
s2_1->s2_3->s2_4->final;
*/
/*expected
initial->s1_1->final;
*/
function Foo() { this.a = b ? c : d }; new Foo()

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s2_1[label="FunctionDeclaration:enter\nIdentifier (Foo)\nBlockStatement:enter\nExpressionStatement:enter\nAssignmentExpression:enter\nMemberExpression:enter\nThisExpression\nIdentifier (a)\nMemberExpression:exit\nConditionalExpression:enter\nIdentifier (b)"];
s2_2[label="Identifier (c)"];
s2_4[label="ConditionalExpression:exit\nAssignmentExpression:exit\nExpressionStatement:exit\nBlockStatement:exit\nFunctionDeclaration:exit"];
s2_3[label="Identifier (d)"];
initial->s2_1->s2_2->s2_4;
s2_1->s2_3->s2_4->final;
}

digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program:enter\nFunctionDeclaration\nEmptyStatement\nExpressionStatement:enter\nNewExpression:enter\nIdentifier (Foo)\nNewExpression:exit\nExpressionStatement:exit\nProgram:exit"];
initial->s1_1->final;
}
*/
20 changes: 20 additions & 0 deletions tests/fixtures/code-path-analysis/object-literal--conditional.js
@@ -0,0 +1,20 @@
/*expected
initial->s1_1->s1_2->s1_4;
s1_1->s1_3->s1_4->final;
*/

x = { a: b ? c : d }

/*DOT
digraph {
node[shape=box,style="rounded,filled",fillcolor=white];
initial[label="",shape=circle,style=filled,fillcolor=black,width=0.25,height=0.25];
final[label="",shape=doublecircle,style=filled,fillcolor=black,width=0.25,height=0.25];
s1_1[label="Program:enter\nExpressionStatement:enter\nAssignmentExpression:enter\nIdentifier (x)\nObjectExpression:enter\nProperty:enter\nIdentifier (a)\nConditionalExpression:enter\nIdentifier
(b)"];
s1_2[label="Identifier (c)"];
s1_4[label="ConditionalExpression:exit\nProperty:exit\nObjectExpression:exit\nAssignmentExpression:exit\nExpressionStatement:exit\nProgram:exit"];
s1_3[label="Identifier (d)"];
initial->s1_1->s1_2->s1_4;
s1_1->s1_3->s1_4->final;
}*/
4 changes: 2 additions & 2 deletions tests/lib/linter/code-path-analysis/code-path-analyzer.js
Expand Up @@ -561,11 +561,11 @@ describe("CodePathAnalyzer", () => {
}
}));
const messages = linter.verify(source, {
parserOptions: { ecmaVersion: 2021 },
parserOptions: { ecmaVersion: 2022 },
rules: { test: 2 }
});

assert.strictEqual(messages.length, 0);
assert.strictEqual(messages.length, 0, "Unexpected linting error in code.");
assert.strictEqual(actual.length, expected.length, "a count of code paths is wrong.");

for (let i = 0; i < actual.length; ++i) {
Expand Down