版本

程式碼路徑分析詳情

ESLint 的規則可以使用程式碼路徑。程式碼路徑是程式的執行路線。它會在例如 if 陳述式的地方分岔/合併。

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

Code Path Example

物件

程式由多個程式碼路徑表示。程式碼路徑由兩種物件表示:CodePathCodePathSegment

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) - 一個旗標,顯示是否可到達。當前面是 returnthrowbreakcontinue 時,這個值會變成 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.updateupdate 區段一開始會暫停。

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 是否在每個路徑中都被呼叫。CodePathCodePathSegment 的執行個體會與每個規則共用。因此,規則不得修改這些執行個體。請改用資訊對應。

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.length2。一個是正常路徑,另一個是離開路徑(throwreturn)。

 (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

變更語言