Skip to content

Commit

Permalink
Update: Code path analysis for class fields (fixes #14343) (#14886)
Browse files Browse the repository at this point in the history
* Fix: Code path analysis for class fields (fixes #14343)

* PropertyDefinition code paths

* Add test and comments

* Reverse order of code path closing

* Added CodePath#origin

* Update tests/lib/linter/code-path-analysis/code-path.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

* Update lib/linter/code-path-analysis/code-path.js

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>

Co-authored-by: Milos Djermanovic <milos.djermanovic@gmail.com>
  • Loading branch information
nzakas and mdjermanovic committed Aug 26, 2021
1 parent db15183 commit 05ca24c
Show file tree
Hide file tree
Showing 11 changed files with 353 additions and 42 deletions.
1 change: 1 addition & 0 deletions docs/developer-guide/code-path-analysis/README.md
Expand Up @@ -27,6 +27,7 @@ This has references of both the initial segment and the final segments of a code
`CodePath` has the following properties:

* `id` (`string`) - A unique string. Respective rules can use `id` to save additional information for each code path.
* `origin` (`string`) - The reason that the code path was started. May be `"program"`, `"function"`, or `"class-field-initializer"`.
* `initialSegment` (`CodePathSegment`) - The initial segment of this code path.
* `finalSegments` (`CodePathSegment[]`) - The final segments which includes both returned and thrown.
* `returnedSegments` (`CodePathSegment[]`) - The final segments which includes only returned.
Expand Down
150 changes: 115 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,64 @@ 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.
* @param {string} origin The reason the code path was started.
* @returns {void}
*/
function startCodePath(origin) {
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({
id: analyzer.idGenerator.next(),
origin,
upper: codePath,
onLooped: 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("class-field-initializer");

/*
* 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":
startCodePath("program");
break;

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("function");
break;

case "ChainExpression":
Expand Down Expand Up @@ -503,6 +551,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 +676,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 +721,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
18 changes: 14 additions & 4 deletions lib/linter/code-path-analysis/code-path.js
Expand Up @@ -22,11 +22,14 @@ const IdGenerator = require("./id-generator");
class CodePath {

/**
* @param {string} id An identifier.
* @param {CodePath|null} upper The code path of the upper function scope.
* @param {Function} onLooped A callback function to notify looping.
* Creates a new instance.
* @param {Object} options Options for the function (see below).
* @param {string} options.id An identifier.
* @param {string} options.origin The type of code path origin.
* @param {CodePath|null} options.upper The code path of the upper function scope.
* @param {Function} options.onLooped A callback function to notify looping.
*/
constructor(id, upper, onLooped) {
constructor({ id, origin, upper, onLooped }) {

/**
* The identifier of this code path.
Expand All @@ -35,6 +38,13 @@ class CodePath {
*/
this.id = id;

/**
* The reason that this code path was started. May be "program",
* "function", or "class-field-initializer".
* @type {string}
*/
this.origin = origin;

/**
* The code path of the upper function scope.
* @type {CodePath|null}
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;
}
*/

0 comments on commit 05ca24c

Please sign in to comment.