0. 前言
在《淡如止水 TypeScript》中,我們研究了 TypeScript 源碼的一些基本概念柑晒,
例如暗甥,TypeScript 是如何進(jìn)行詞法分析框往、語法分析的,如何進(jìn)行類型檢查的知允,
如何進(jìn)行代碼轉(zhuǎn)換撒蟀,以及 tsserver 是如何作為獨(dú)立的進(jìn)程提供語言服務(wù)的。
本系列文章温鸽,我們將繼續(xù)深入探索保屯,
TypeScript 源碼量比較大,短時(shí)間內(nèi)通讀一遍也不太現(xiàn)實(shí)涤垫,更無必要姑尺,
因此,打算以專題的形式蝠猬,從問題出發(fā)總結(jié)成文切蟋。
TypeScript 的調(diào)試方式,我已經(jīng)整理到了 github: debug-typescript 中榆芦。
上一個(gè)系列的文章中已詳細(xì)介紹過了柄粹,本系列文章直接使用它。
TypeScript 源碼的版本匆绣,我用的是 TypeScipt v3.7.3驻右。
1. 問題
參考 github: debug-typescript 的使用說明,
安裝完畢后崎淳,它會在 TypeScript 源碼目錄新建一個(gè) debug/index.ts
文件堪夭。
這是我們用來試驗(yàn) TypeScript 各項(xiàng)功能的源代碼文件。
我們修改 debug/index.ts
的內(nèi)容如下拣凹,并用 VSCode 打開這個(gè)文件茵瘾,
function f() {
x
}
鼠標(biāo)移動到 x
上,我們會看到 VSCode 會提示診斷信息(Diagnostics)咐鹤,
Cannot find name 'x'. ts(2304)
VSCode 是如何知道 x
是未定義的呢?
這還要從 TypeScript 診斷過程中的 resolveName
說起圣絮。
2. 啟動調(diào)試
Cannot find name 'x'. ts(2304)
我們已經(jīng)知道 VSCode 是通過與 tsserver 通信祈惶,實(shí)現(xiàn) TypeScript 的各項(xiàng)語言支持的,
那么,以上診斷信息捧请,應(yīng)該是由 TypeScript 源碼中反饋回來的凡涩。
本來我們需要 debug tsserver 來找到結(jié)果,但其實(shí) tsc 命令也會返回錯(cuò)誤信息疹蛉。
$ tsc debug/index.ts
debug/index.ts:2:3 - error TS2304: Cannot find name 'x'.
2 x
~
Found 1 error.
tsc 相較于 tsserver 調(diào)試起來會簡單一些活箕,所以下文我們用 tsc 來進(jìn)行調(diào)試。
因?yàn)?x
是代碼相關(guān)的可款,肯定是一個(gè)占位符育韩,所以我們只搜索 Cannot find name
。
全局搜索后闺鲸,第一個(gè)結(jié)果中筋讨,我們看到了
2304
錯(cuò)誤碼,還看到了
Cannot find name '{0}'.
模板形式的字符串摸恍,{0}
應(yīng)該是占位符了悉罕,最后被替換為 x
。
接著搜索 Cannot_find_name_0
立镶,
位于 src/compiler/checker.ts#L18085壁袄,在這里打個(gè)斷點(diǎn),
然后按照 github: debug-typescript 介紹的方式媚媒,按 F5
進(jìn)行調(diào)試嗜逻。
注意這里在調(diào)試的時(shí)候,要先
Step Into
進(jìn)入到 src/compiler/core.ts#L1 代碼中欣范,再按 Continue
变泄,否則可能會出現(xiàn) VSCode 無法跳轉(zhuǎn)到 TypeScript 源碼的情形。
再按 Continue
恼琼,果然來到了斷點(diǎn)處妨蛹。
3. 預(yù)加載的 .d.ts 文件
然而,不幸的是晴竞,這并不是我們示例代碼中 x
變量報(bào)錯(cuò)的時(shí)間點(diǎn)蛙卤,
通過檢查調(diào)用棧 checkExpressionWorker
,src/compiler/checker.ts#L17575噩死,
我們發(fā)現(xiàn) node.escapedText
為 Symbol
颤难,不是我們的變量 x
。
原來
checkSourceFile
已维,src/compiler/checker.ts#L33009行嗤,所檢查的并非我們的源碼文件
debug/index.ts
,而是這個(gè)文件垛耳,
/Users/.../Microsoft/TypeScript/built/local/lib.es5.d.ts
其中栅屏,/Users/.../Microsoft/TypeScript
是我本地 TypeScript 源碼倉庫地址飘千,
我們來看一下這個(gè)文件的內(nèi)容,
它是一個(gè) TypeScript 的聲明文件栈雳,用于聲明 es5 中內(nèi)置對象的類型护奈,
TypeScript 會預(yù)加載很多內(nèi)置的聲明文件。
我們可以從這里 src/compiler/program.ts#L1653 獲取 TypeScript 總共預(yù)先加載了哪些文件哥纫,
program.getSourceFiles().map(({fileName})=>fileName).length
> 114
包括 debug/index.ts
在內(nèi)霉旗,總共有 114
個(gè),除了 built/local/
目錄下的蛀骇,還有 node_modules/
中的厌秒。
4. 條件斷點(diǎn)
為了能定位到 debug/index.ts
中的 x
變量的報(bào)錯(cuò)信息,我們需要使用條件斷點(diǎn)(Conditional Breakpoint)松靡,
在 src/compiler/checker.ts#L27575 行打斷點(diǎn)的位置简僧,右鍵添加條件斷點(diǎn)。
然后輸入條件雕欺,回車岛马,
node.escapedText === 'x'
再把最初 src/compiler/checker.ts#L18085,Cannot_find_name_0
報(bào)錯(cuò)位置的斷點(diǎn)去掉屠列,按 F5
繼續(xù)調(diào)試啦逆。
這是不是我們的 debug/index.ts
文件中的 x
呢?
查看調(diào)用棧信息笛洛,發(fā)現(xiàn)很幸運(yùn)剛好是夏志,其他預(yù)加載的文件中,沒有
x
苛让。
然后我們再到 src/compiler/checker.ts#L27575 把斷點(diǎn)再打上沟蔑,應(yīng)該會跑到這里,
5. 跟蹤
(1)查找符號
現(xiàn)在我們來分析 TypeScript 是怎么 x
未定義的狱杰,這才是問題的關(guān)鍵瘦材。
以下我從上到下,列舉了調(diào)用棧中幾個(gè)主要的函數(shù)仿畸,
executeCommandLine # 執(zhí)行 tsc
performCompilation # 開始編譯
getSemanticDiagnostics # 語義分析
checkSourceFile # 檢查加載的各個(gè)文件
checkSourceElement # 從 ast 的根元素開始檢查
checkIdentifier # 檢查標(biāo)識符 x
getResolvedSymbol # 從符號表中獲取與 x 相關(guān)的信息
resolveName # 查找 x
getCannotFindNameDiagnosticForName # 獲取 “無法找到名字” 的診斷文案
resolveName
食棕,是由 getResolvedSymbol
調(diào)用的,src/compiler/checker.ts#L18094错沽,
function getResolvedSymbol(node: Identifier): Symbol {
const links = getNodeLinks(node);
if (!links.resolvedSymbol) {
links.resolvedSymbol = !nodeIsMissing(node) &&
resolveName(
node,
node.escapedText,
SymbolFlags.Value | SymbolFlags.ExportValue,
getCannotFindNameDiagnosticForName(node),
node,
!isWriteOnlyAccess(node),
/*excludeGlobals*/ false,
Diagnostics.Cannot_find_name_0_Did_you_mean_1) || unknownSymbol;
}
return links.resolvedSymbol;
}
可見簿晓,不論是否能找到 x
,都會先調(diào)用 getCannotFindNameDiagnosticForName
獲取報(bào)錯(cuò)文案千埃。
(2)局部變量
resolveName
憔儿,src/compiler/checker.ts#L1430,會調(diào)用 resolveNameHelper
放可,
然后跑到一個(gè)很長的帶
loop
標(biāo)簽的 while
循環(huán)中皿曲,src/compiler/checker.ts#L1463唱逢,整個(gè)
resolveNameHelper
有 407
行,src/compiler/checker.ts#L1442屋休,結(jié)構(gòu)如下,
function resolveNameHelper(
...
): ... {
...
loop: while (location) {
// Locals of a source file are not in scope (because they get merged into the global symbol table)
if (location.locals && !isGlobalSourceFile(location)) {
if (result = lookup(location.locals, name, meaning)) {
...
}
}
...
switch (location.kind) {
...
}
...
lastLocation = location;
location = location.parent;
}
...
if (!result) {
...
if (!excludeGlobals) {
result = lookup(globals, name, meaning);
}
}
if (!result) {
...
}
if (!result) {
...
}
...
if (nameNotFoundMessage) {
...
}
return result;
}
while
循環(huán)做的主要事情就是备韧,從 x
節(jié)點(diǎn)開始不斷的向父節(jié)點(diǎn)搜索劫樟,
檢查祖先節(jié)點(diǎn)的 locals
屬性,其中保存了這個(gè)祖先節(jié)點(diǎn)作用域內(nèi)的詞法變量织堂。
location
指的是當(dāng)前正在查找的節(jié)點(diǎn)叠艳。
在 src/compiler/checker.ts#L1466 打個(gè)斷點(diǎn),
發(fā)現(xiàn)了第一個(gè)具有
locals
屬性的父節(jié)點(diǎn)易阳,函數(shù)聲明 FunctionDeclaration
附较,pos: 0
,end: 19
潦俺,
function f(){
x
}
函數(shù)沒有形參拒课,因此函數(shù)聲明創(chuàng)建的詞法作用域中沒有符號,locals
為空 Map
事示。
如果我們修改一下 debug/index.ts
早像,給 f
加上形參 y
,在進(jìn)行調(diào)試肖爵,
function f(y){
x
}
發(fā)現(xiàn)這里的
locals
已經(jīng)不再為空了卢鹦,Map
中有與 y
相關(guān)的信息。
(2)全局變量
局部變量保存在了 FunctionDeclaration
節(jié)點(diǎn)的 locals
屬性中劝堪,
全局變量也是一樣冀自,也在祖先節(jié)點(diǎn)的 locals
屬性中,
位于
FunctionDeclaration
節(jié)點(diǎn)的父節(jié)點(diǎn)的 locals
中秒啦。
只是 TypeScript 中熬粗,在不同的源碼位置對全局變量進(jìn)行查找,
位于 src/compiler/checker.ts#L1752帝蒿,
result = lookup(globals, name, meaning);
為什么要區(qū)分開來呢荐糜?
這是因?yàn)椋暶髟谧钔鈱拥娜肿兞扛鸪c TypeScript 語言內(nèi)置的一些變量進(jìn)行合并暴氏,例如 Array
,Date
這些绣张。
resolveNameHelper
局部變量 lookup
前的注釋進(jìn)行了說明答渔,
Locals of a source file are not in scope (because they get merged into the global symbol table)
局部變量與全局變量,lookup
調(diào)用位置關(guān)系如下侥涵,
function resolveNameHelper(
...
): ... {
...
loop: while (location) {
// Locals of a source file are not in scope (because they get merged into the global symbol table)
if (location.locals && !isGlobalSourceFile(location)) {
if (result = lookup(location.locals, name, meaning)) {
...
}
}
...
lastLocation = location;
location = location.parent;
}
...
if (!result) {
...
if (!excludeGlobals) {
result = lookup(globals, name, meaning);
}
}
...
return result;
}
現(xiàn)在我們在全局變量查找位置 src/compiler/checker.ts#L1752 打個(gè)條件斷點(diǎn)沼撕,按 F5
執(zhí)行宋雏,
name === 'x'
我們看到全局范圍內(nèi)有 1812
個(gè)名字,包含函數(shù) f
务豺,不包含變量 x
磨总。
6. 后記
上文我們研究了 TypeScript 變量是否定義的診斷過程,從報(bào)錯(cuò)文案出發(fā)笼沥,
順藤摸瓜的跟蹤了蚪燕,局部變量和全局變量的查找過程。
一個(gè)意外的發(fā)現(xiàn)是奔浅,ast 節(jié)點(diǎn)中可能包含了 locals
屬性馆纳,其中保存了相關(guān)詞法作用域中定義的全部變量。
因此我們就可以靜態(tài)分析出汹桦,源碼的作用域?qū)哟谓Y(jié)構(gòu)了鲁驶。
然而,從 program
中直接得到的 ast 中是不包含 locals
信息的舞骆,
const ts = require('typescript');
const main = filePath => {
const rootNames = [filePath];
const options = {};
const program = ts.createProgram(rootNames, options);
// program.getGlobalDiagnostics();
const sourceFile = program.getSourceFile(filePath);
const { locals } = sourceFile;
locals;
};
其中钥弯,filePath
是待編譯源碼的絕對地址,我們傳入 debug/index.ts
文件地址葛作,
文件內(nèi)容如下寿羞,
function f(){
x
}
通過對比 tsc 的執(zhí)行過程,
我們發(fā)現(xiàn)是因?yàn)?tsc 在編譯的時(shí)候執(zhí)行了 program.getGlobalDiagnostics
赂蠢,src/compiler/watch.ts#L165绪穆,
位于語義分析 program.getSemanticDiagnostics
之前。
上述代碼虱岂,我們把注釋解除玖院,在獲取 sourceFile
之前先執(zhí)行,
program.getGlobalDiagnostics();
果然
locals
屬性有值了第岖,正是我們?nèi)致暶鞯暮瘮?shù) f
难菌。
事實(shí)上,不執(zhí)行 program.getGlobalDiagnostics
的話蔑滓,
節(jié)點(diǎn)的 parent
屬性也是沒有的郊酒,我們無法通過葉子節(jié)點(diǎn),向上追溯到 ast 根節(jié)點(diǎn)键袱。
至于 program.getGlobalDiagnostics
是怎樣為每個(gè)節(jié)點(diǎn)添加 parent
屬性燎窘,
又怎樣為部分節(jié)點(diǎn)計(jì)算出 locals
的,等到必要時(shí)遇到阻礙時(shí)蹄咖,再詳細(xì)探究吧褐健。
參考
github: debug-typescript
TypeScipt v3.7.3
TypeScript Compiler API