版本

代码路径分析详情

ESLint 的规则可以使用代码路径。代码路径是程序的执行路线。它在诸如 `if` 语句处分叉/合并。

if (a && b) {
    foo();
}
bar();

Code Path Example

对象

程序用多个代码路径表示。代码路径用两种类型的对象表示:`CodePath` 和 `CodePathSegment`。

CodePath

`CodePath` 表示一条完整代码路径。此对象存在于每个函数和全局范围内。它包含代码路径的初始段和最终段的引用。

`CodePath` 具有以下属性

  • id(`string`) - 一个唯一的字符串。相应的规则可以使用 `id` 为每个代码路径保存其他信息。
  • origin(`string`) - 代码路径启动的原因。可能是 `“program”`、`“function”`、`“class-field-initializer”` 或 `“class-static-block”`。
  • initialSegment(`CodePathSegment`) - 此代码路径的初始段。
  • finalSegments(`CodePathSegment[]`) - 最终段,包括返回和抛出。
  • returnedSegments(`CodePathSegment[]`) - 最终段,仅包括返回。
  • thrownSegments(`CodePathSegment[]`) - 最终段,仅包括抛出。
  • upper(`CodePath|null`) - 上层函数/全局作用域的代码路径。
  • childCodePaths(`CodePath[]`) - 此代码路径包含的函数的代码路径。

CodePathSegment

`CodePathSegment` 是代码路径的一部分。代码路径由多个 `CodePathSegment` 对象表示,类似于双向链表。与双向链表的不同之处在于存在分叉和合并(next/prev 是复数)。

`CodePathSegment` 具有以下属性

  • id(`string`) - 一个唯一的字符串。相应的规则可以使用 `id` 为每个段保存其他信息。
  • nextSegments(`CodePathSegment[]`) - 下一段。如果分叉,则有两个或多个。如果是最终段,则没有。
  • prevSegments(`CodePathSegment[]`) - 前一段。如果合并,则有两个或多个。如果是初始段,则没有。
  • reachable(`boolean`) - 一个标志,指示该段是否可达。当前面有 `return`、`throw`、`break` 或 `continue` 时,它将变为 `false`。

事件

有七个与代码路径相关的事件,您可以通过在规则的 `create()` 方法导出的对象中添加它们以及节点访问器来定义事件处理程序。

module.exports = {
    meta: {
        // ...
    },
    create(context) {

        return {
            /**
             * This is called at the start of analyzing a code path.
             * In this time, the code path object has only the initial segment.
             *
             * @param {CodePath} codePath - The new code path.
             * @param {ASTNode} node - The current node.
             * @returns {void}
             */
            onCodePathStart(codePath, node) {
                // do something with codePath
            },

            /**
             * This is called at the end of analyzing a code path.
             * In this time, the code path object is complete.
             *
             * @param {CodePath} codePath - The completed code path.
             * @param {ASTNode} node - The current node.
             * @returns {void}
             */
            onCodePathEnd(codePath, node) {
                // do something with codePath
            },

            /**
             * This is called when a reachable code path segment was created.
             * It meant the code path is forked or merged.
             * In this time, the segment has the previous segments and has been
             * judged reachable or not.
             *
             * @param {CodePathSegment} segment - The new code path segment.
             * @param {ASTNode} node - The current node.
             * @returns {void}
             */
            onCodePathSegmentStart(segment, node) {
                // do something with segment
            },

            /**
             * This is called when a reachable code path segment was left.
             * In this time, the segment does not have the next segments yet.
             *
             * @param {CodePathSegment} segment - The left code path segment.
             * @param {ASTNode} node - The current node.
             * @returns {void}
             */
            onCodePathSegmentEnd(segment, node) {
                // do something with segment
            },

            /**
             * This is called when an unreachable code path segment was created.
             * It meant the code path is forked or merged.
             * In this time, the segment has the previous segments and has been
             * judged reachable or not.
             *
             * @param {CodePathSegment} segment - The new code path segment.
             * @param {ASTNode} node - The current node.
             * @returns {void}
             */
            onUnreachableCodePathSegmentStart(segment, node) {
                // do something with segment
            },

            /**
             * This is called when an unreachable code path segment was left.
             * In this time, the segment does not have the next segments yet.
             *
             * @param {CodePathSegment} segment - The left code path segment.
             * @param {ASTNode} node - The current node.
             * @returns {void}
             */
            onUnreachableCodePathSegmentEnd(segment, node) {
                // do something with segment
            },

            /**
             * This is called when a code path segment was looped.
             * Usually segments have each previous segments when created,
             * but when looped, a segment is added as a new previous segment into a
             * existing segment.
             *
             * @param {CodePathSegment} fromSegment - A code path segment of source.
             * @param {CodePathSegment} toSegment - A code path segment of destination.
             * @param {ASTNode} node - The current node.
             * @returns {void}
             */
            onCodePathSegmentLoop(fromSegment, toSegment, node) {
                // do something with segment
            }
        };

    }
}

关于 `onCodePathSegmentLoop`

当下一段已经存在时,此事件总是被触发。这个时间点主要是循环的结束。

例如 1

while (a) {
    a = foo();
}
bar();
  1. 首先,分析推进到循环的末尾。

Loop Event's Example 1

  1. 其次,它创建循环路径。此时,下一段已经存在,因此不会触发 `onCodePathSegmentStart` 事件。它会触发 `onCodePathSegmentLoop`。

Loop Event's Example 2

  1. 最后,它推进到末尾。

Loop Event's Example 3

例如 2

for (let i = 0; i < 10; ++i) {
    foo(i);
}
bar();
  1. `for` 语句比较复杂。首先,分析推进到 `ForStatement.update`。`update` 段首先被悬停。

Loop Event's Example 1

  1. 其次,它推进到 `ForStatement.body`。当然,`body` 段由 `test` 段之前置。它保持 `update` 段悬停。

Loop Event's Example 2

  1. 第三,它从 `body` 段到 `update` 段创建循环路径。此时,下一段已经存在,因此不会触发 `onCodePathSegmentStart` 事件。它会触发 `onCodePathSegmentLoop`。

Loop Event's Example 3

  1. 第四,它还从 `update` 段到 `test` 段创建循环路径。此时,下一段已经存在,因此不会触发 `onCodePathSegmentStart` 事件。它会触发 `onCodePathSegmentLoop`。

Loop Event's Example 4

  1. 最后,它推进到末尾。

Loop Event's Example 5

使用示例

跟踪当前段位置

要跟踪当前代码路径段位置,您可以定义如下规则

module.exports = {
    meta: {
        // ...
    },
    create(context) {

        // tracks the code path we are currently in
        let currentCodePath;

        // tracks the segments we've traversed in the current code path
        let currentSegments;

        // tracks all current segments for all open paths
        const allCurrentSegments = [];

        return {

            onCodePathStart(codePath) {
                currentCodePath = codePath;
                allCurrentSegments.push(currentSegments);
                currentSegments = new Set();
            },

            onCodePathEnd(codePath) {
                currentCodePath = codePath.upper;
                currentSegments = allCurrentSegments.pop();
            },

            onCodePathSegmentStart(segment) {
                currentSegments.add(segment);
            },

            onCodePathSegmentEnd(segment) {
                currentSegments.delete(segment);
            },

            onUnreachableCodePathSegmentStart(segment) {
                currentSegments.add(segment);
            },

            onUnreachableCodePathSegmentEnd(segment) {
                currentSegments.delete(segment);
            }
        };

    }
};

在此示例中,`currentCodePath` 变量用于访问当前正在遍历的代码路径,`currentSegments` 变量跟踪到目前为止已遍历的该代码路径中的段。请注意,`currentSegments` 都是以空集开始和结束,在遍历过程中不断更新。

跟踪当前段位置有助于分析导致特定节点的代码路径,如下一个示例所示。

查找不可达节点

要查找不可达节点,请跟踪当前段位置,然后使用节点访问器检查任何段是否可达。例如,以下代码查找任何不可达的 `ExpressionStatement`。

function areAnySegmentsReachable(segments) {
    for (const segment of segments) {
        if (segment.reachable) {
            return true;
        }
    }

    return false;
}

module.exports = {
    meta: {
        // ...
    },
    create(context) {

        // tracks the code path we are currently in
        let currentCodePath;

        // tracks the segments we've traversed in the current code path
        let currentSegments;

        // tracks all current segments for all open paths
        const allCurrentSegments = [];

        return {

            onCodePathStart(codePath) {
                currentCodePath = codePath;
                allCurrentSegments.push(currentSegments);
                currentSegments = new Set();
            },

            onCodePathEnd(codePath) {
                currentCodePath = codePath.upper;
                currentSegments = allCurrentSegments.pop();
            },

            onCodePathSegmentStart(segment) {
                currentSegments.add(segment);
            },

            onCodePathSegmentEnd(segment) {
                currentSegments.delete(segment);
            },

            onUnreachableCodePathSegmentStart(segment) {
                currentSegments.add(segment);
            },

            onUnreachableCodePathSegmentEnd(segment) {
                currentSegments.delete(segment);
            },

            ExpressionStatement(node) {

                // check all the code path segments that led to this node
                if (!areAnySegmentsReachable(currentSegments)) {
                    context.report({ message: "Unreachable!", node });
                }
            }

        };

    }
};

另请参阅:no-unreachableno-fallthroughconsistent-return

检查函数是否在每条路径中都被调用

此示例检查参数 `cb` 是否在每条路径中都被调用。`CodePath` 和 `CodePathSegment` 的实例被共享到每个规则。因此,规则不得修改这些实例。请改用信息映射。

function hasCb(node, context) {
    if (node.type.indexOf("Function") !== -1) {
        const sourceCode = context.sourceCode;
        return sourceCode.getDeclaredVariables(node).some(function(v) {
            return v.type === "Parameter" && v.name === "cb";
        });
    }
    return false;
}

function isCbCalled(info) {
    return info.cbCalled;
}

module.exports = {
    meta: {
        // ...
    },
    create(context) {

        let funcInfo;
        const funcInfoStack = [];
        const segmentInfoMap = Object.create(null);

        return {
            // Checks `cb`.
            onCodePathStart(codePath, node) {
                funcInfoStack.push(funcInfo);

                funcInfo = {
                    codePath: codePath,
                    hasCb: hasCb(node, context),
                    currentSegments: new Set()
                };
            },

            onCodePathEnd(codePath, node) {
                funcInfo = funcInfoStack.pop();

                // Checks `cb` was called in every paths.
                const cbCalled = codePath.finalSegments.every(function(segment) {
                    const info = segmentInfoMap[segment.id];
                    return info.cbCalled;
                });

                if (!cbCalled) {
                    context.report({
                        message: "`cb` should be called in every path.",
                        node: node
                    });
                }
            },

            // Manages state of code paths and tracks traversed segments
            onCodePathSegmentStart(segment) {

                funcInfo.currentSegments.add(segment);

                // Ignores if `cb` doesn't exist.
                if (!funcInfo.hasCb) {
                    return;
                }

                // Initialize state of this path.
                const info = segmentInfoMap[segment.id] = {
                    cbCalled: false
                };

                // If there are the previous paths, merges state.
                // Checks `cb` was called in every previous path.
                if (segment.prevSegments.length > 0) {
                    info.cbCalled = segment.prevSegments.every(isCbCalled);
                }
            },

            // Tracks unreachable segment traversal
            onUnreachableCodePathSegmentStart(segment) {
                funcInfo.currentSegments.add(segment);
            },

            // Tracks reachable segment traversal
            onCodePathSegmentEnd(segment) {
                funcInfo.currentSegments.delete(segment);
            },

            // Tracks unreachable segment traversal
            onUnreachableCodePathSegmentEnd(segment) {
                funcInfo.currentSegments.delete(segment);
            },

            // Checks reachable or not.
            CallExpression(node) {

                // Ignores if `cb` doesn't exist.
                if (!funcInfo.hasCb) {
                    return;
                }

                // Sets marks that `cb` was called.
                const callee = node.callee;
                if (callee.type === "Identifier" && callee.name === "cb") {
                    funcInfo.currentSegments.forEach(segment => {
                        const info = segmentInfoMap[segment.id];
                        info.cbCalled = true;
                    });
                }
            }
        };
    }
};

另请参阅:constructor-superno-this-before-super

代码路径示例

Hello World

console.log("Hello world!");

Hello World

IfStatement

if (a) {
    foo();
} else {
    bar();
}

IfStatement(链式)

if (a) {
    foo();
} else if (b) {
    bar();
} else if (c) {
    hoge();
}

 (chain)

SwitchStatement

switch (a) {
    case 0:
        foo();
        break;

    case 1:
    case 2:
        bar();
        // fallthrough

    case 3:
        hoge();
        break;
}

SwitchStatement(具有 `default`)

switch (a) {
    case 0:
        foo();
        break;

    case 1:
    case 2:
        bar();
        // fallthrough

    case 3:
        hoge();
        break;

    default:
        fuga();
        break;
}

 (has )

TryStatement(try-catch)

try {
    foo();
    if (a) {
        throw new Error();
    }
    bar();
} catch (err) {
    hoge(err);
}
last();

它在以下位置创建从 `try` 块到 `catch` 块的路径:

  • `throw` 语句。
  • `try` 块中的第一个可抛出节点(例如,函数调用)。
  • `try` 块的末尾。

 (try-catch)

TryStatement(try-finally)

try {
    foo();
    bar();
} finally {
    fuga();
}
last();

如果没有 `catch` 块,`finally` 块有两个当前段。此时,当运行前面的查找不可达节点的示例时,`currentSegments.length` 为 `2`。一个是正常路径,另一个是离开路径(`throw` 或 `return`)。

 (try-finally)

TryStatement(try-catch-finally)

try {
    foo();
    bar();
} catch (err) {
    hoge(err);
} finally {
    fuga();
}
last();

 (try-catch-finally)

WhileStatement

while (a) {
    foo();
    if (b) {
        continue;
    }
    bar();
}

DoWhileStatement

do {
    foo();
    bar();
} while (a);

ForStatement

for (let i = 0; i < 10; ++i) {
    foo();
    if (b) {
        break;
    }
    bar();
}

ForStatement(无限循环)

for (;;) {
    foo();
}
bar();

 (for ever)

ForInStatement

for (let key in obj) {
    foo(key);
}

当存在函数时

function foo(a) {
    if (a) {
        return;
    }
    bar();
}

foo(false);

它创建两个代码路径。

  • 全局的

When there is a function

  • 函数的

When there is a function

更改语言