程式碼路徑分析詳情
ESLint 的規則可以使用程式碼路徑。程式碼路徑是程式的執行路線。它會在例如 if
陳述式的地方分岔/合併。
if (a && b) {
foo();
}
bar();
物件
程式由多個程式碼路徑表示。程式碼路徑由兩種物件表示: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();
- 首先,分析會進行到迴圈的結束。
- 其次,它會建立迴圈路徑。此時,下一個區段已存在,因此不會觸發
onCodePathSegmentStart
事件。它會改為觸發onCodePathSegmentLoop
。
- 最後,它會進行到結束。
例如 2
for (let i = 0; i < 10; ++i) {
foo(i);
}
bar();
for
陳述式更複雜。首先,分析會進行到ForStatement.update
。update
區段一開始會暫停。
- 其次,它會進行到
ForStatement.body
。當然,body
區段前面是test
區段。它會讓update
區段保持暫停。
- 第三,它會建立從
body
區段到update
區段的迴圈路徑。此時,下一個區段已存在,因此不會觸發onCodePathSegmentStart
事件。它會改為觸發onCodePathSegmentLoop
。
- 第四,它也會建立從
update
區段到test
區段的迴圈路徑。此時,下一個區段已存在,因此不會觸發onCodePathSegmentStart
事件。它會改為觸發onCodePathSegmentLoop
。
- 最後,它會進行到結束。
使用範例
追蹤目前的區段位置
若要追蹤目前的程式碼路徑區段位置,您可以定義如下的規則
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-unreachable、no-fallthrough、consistent-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-super、no-this-before-super
程式碼路徑範例
Hello World
console.log("Hello world!");
IfStatement
if (a) {
foo();
} else {
bar();
}
IfStatement
(鏈)
if (a) {
foo();
} else if (b) {
bar();
} else if (c) {
hoge();
}
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;
}
TryStatement
(try-catch)
try {
foo();
if (a) {
throw new Error();
}
bar();
} catch (err) {
hoge(err);
}
last();
它會在下列位置建立從 try
區塊到 catch
區塊的路徑
throw
陳述式。try
區塊中第一個可拋出的節點(例如函式呼叫)。try
區塊的結尾。
TryStatement
(try-finally)
try {
foo();
bar();
} finally {
fuga();
}
last();
如果沒有 catch
區塊,finally
區塊會有兩個目前的區段。此時,當執行先前的範例來尋找無法到達的節點時,currentSegments.length
為 2
。一個是正常路徑,另一個是離開路徑(throw
或 return
)。
TryStatement
(try-catch-finally)
try {
foo();
bar();
} catch (err) {
hoge(err);
} finally {
fuga();
}
last();
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();
ForInStatement
for (let key in obj) {
foo(key);
}
當有函式時
function foo(a) {
if (a) {
return;
}
bar();
}
foo(false);
它會建立兩個程式碼路徑。
- 全域的
- 函式的