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
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
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;
}
*/