[Node] 隨遇而安 TypeScript(一):查找符號

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)用棧 checkExpressionWorkersrc/compiler/checker.ts#L17575噩死,
我們發(fā)現(xiàn) node.escapedTextSymbol颤难,不是我們的變量 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#L18085Cannot_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è) resolveNameHelper407 行,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: 0end: 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)行合并暴氏,例如 ArrayDate 這些绣张。

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

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個(gè)濱河市澜汤,隨后出現(xiàn)的幾起案子蚜迅,更是在濱河造成了極大的恐慌舵匾,老刑警劉巖,帶你破解...
    沈念sama閱讀 221,635評論 6 515
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件谁不,死亡現(xiàn)場離奇詭異坐梯,居然都是意外死亡,警方通過查閱死者的電腦和手機(jī)刹帕,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 94,543評論 3 399
  • 文/潘曉璐 我一進(jìn)店門烛缔,熙熙樓的掌柜王于貴愁眉苦臉地迎上來,“玉大人轩拨,你說我怎么就攤上這事≡合玻” “怎么了亡蓉?”我有些...
    開封第一講書人閱讀 168,083評論 0 360
  • 文/不壞的土叔 我叫張陵,是天一觀的道長喷舀。 經(jīng)常有香客問我砍濒,道長,這世上最難降的妖魔是什么硫麻? 我笑而不...
    開封第一講書人閱讀 59,640評論 1 296
  • 正文 為了忘掉前任爸邢,我火速辦了婚禮,結(jié)果婚禮上拿愧,老公的妹妹穿的比我還像新娘杠河。我一直安慰自己,他們只是感情好浇辜,可當(dāng)我...
    茶點(diǎn)故事閱讀 68,640評論 6 397
  • 文/花漫 我一把揭開白布券敌。 她就那樣靜靜地躺著,像睡著了一般柳洋。 火紅的嫁衣襯著肌膚如雪待诅。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 52,262評論 1 308
  • 那天熊镣,我揣著相機(jī)與錄音卑雁,去河邊找鬼。 笑死绪囱,一個(gè)胖子當(dāng)著我的面吹牛测蹲,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播毕箍,決...
    沈念sama閱讀 40,833評論 3 421
  • 文/蒼蘭香墨 我猛地睜開眼弛房,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了而柑?” 一聲冷哼從身側(cè)響起文捶,我...
    開封第一講書人閱讀 39,736評論 0 276
  • 序言:老撾萬榮一對情侶失蹤荷逞,失蹤者是張志新(化名)和其女友劉穎,沒想到半個(gè)月后粹排,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體种远,經(jīng)...
    沈念sama閱讀 46,280評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,369評論 3 340
  • 正文 我和宋清朗相戀三年顽耳,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了坠敷。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片。...
    茶點(diǎn)故事閱讀 40,503評論 1 352
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡射富,死狀恐怖膝迎,靈堂內(nèi)的尸體忽然破棺而出,到底是詐尸還是另有隱情胰耗,我是刑警寧澤限次,帶...
    沈念sama閱讀 36,185評論 5 350
  • 正文 年R本政府宣布,位于F島的核電站柴灯,受9級特大地震影響卖漫,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜赠群,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,870評論 3 333
  • 文/蒙蒙 一羊始、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧查描,春花似錦突委、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,340評論 0 24
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至长豁,卻和暖如春钧唐,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背匠襟。 一陣腳步聲響...
    開封第一講書人閱讀 33,460評論 1 272
  • 我被黑心中介騙來泰國打工钝侠, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人酸舍。 一個(gè)月前我還...
    沈念sama閱讀 48,909評論 3 376
  • 正文 我出身青樓帅韧,卻偏偏與公主長得像,于是被迫代替她去往敵國和親啃勉。 傳聞我的和親對象是個(gè)殘疾皇子忽舟,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,512評論 2 359

推薦閱讀更多精彩內(nèi)容