運行 Typescript 編譯器外掛/轉換器後的型別檢查
我正在追蹤一個關於如何編寫 TypeScript 編譯器插件/轉換器的部落格(https://dev.doctorevidence.com/how-to-write-a-typescript-transform-plugin-fc5308fdd943)。
在應用第一個簡單的轉換後,這個轉換應該會引入類型錯誤(在沒有該屬性的對象上訪問某個屬性),我注意到沒有顯示類型錯誤。事實上,編譯器照常進行。
import * as ts from "typescript";
export const transformerFactory = (
program: ts.Program
): ts.TransformerFactory<ts.SourceFile> => {
return (context: ts.TransformationContext): ts.Transformer<ts.SourceFile> => {
const visitor: ts.Visitor = (node: ts.Node): ts.VisitResult<ts.Node> => {
if (ts.isCallExpression(node) && ts.isIdentifier(node.expression)) {
if (node.expression.escapedText === "someCall") {
return ts.createCall(
ts.createPropertyAccess(node.expression, "nonExisting"),
node.typeArguments,
node.arguments
);
}
}
return ts.visitEachChild(node, visitor, context);
};
return (sf: ts.SourceFile) => ts.visitNode(sf, visitor);
};
};
應用到 index.ts:
declare function someCall(...args: any[]): string;
console.log(someCall(1, 2, true));
產生 index.js:
console.log(someCall.nonExisting(1, 2, true));
(即使使用 noEmitOnError: true)
這是預期的行為嗎?這是我可以在某處啟用的功能嗎?
答案#1
這是預期的行為嗎?
是。
這是我可以在某處啟用的功能嗎?
不,轉換器的用途有限。不支持通用的「插件」用於編譯器。
轉換器作為「emit」階段的一部分運行,該階段從類型檢查過的 AST 生成 JavaScript 代碼。
這條在 transformers PR 的評論說
所有的轉換都發生在檢查階段之後
更新
有沒有辦法編譯兩次:一次用來轉換檔案,一次用來對整個程式進行型別檢查?如果我必須對轉換後的檔案單獨執行檢查,我也不介意。
我不知道。首先要嘗試的是讓你的轉換器像以前一樣修改 AST,然後透過呼叫手動對修改後的檔案進行型別檢查
program.getDiagnosticsProducingTypeChecker().getDiagnostics(sourceFile)
(getDiagnostics 有第二個參數 - cancellationToken - 但似乎可以安全地省略它,因為在類型檢查器的程式碼中,它總是會檢查是否為 undefined。一般來說,你可以查看各種編譯器 API 在其自身原始碼中的使用方式,例如 emit 會先透過呼叫各種 program.getNNNDiagnostics 進行型別檢查,然後再使用 transform 運行 emitter。)
這可能會有效,也可能無效,因為類型檢查器會修改 AST,而它依賴於 AST 處於正確的狀態。
那麼,你可能會想看看建構器 API——它的目的是監控源文件的修改並重新編譯已更改的文件(源代碼連結)。我不知道讓它在 AST 修改時重新編譯有多困難,而且看起來你無法使用 transformers 中可用的訪問器;你必須手動遍歷 AST。
另外,還有 ts-simple-ast 庫,其聲明的目的是「提供一種簡單的方法來導航和操作 TypeScript 和 JavaScript 代碼」。我自己沒用過,也不知道它對你的目標有多大幫助。
答案#2
你只需要創建一個 CompilerHost,並將它的 getSourceFile 方法設定為指向你轉換後的源文件。一種方法是擁有一個從文件名到轉換後源文件的 Map。之後 CompilerHost 的創建會看起來像這樣:
const compilerHost = ts.createCompilerHost(compilerOptions);
const defaultLibFileName = ts.getDefaultLibFileName(compilerOptions);
compilerHost.getSourceFile = (sourceName) => {
let sourcePath = sourceName;
if (sourceName === defaultLibFileName) {
sourcePath = ts.getDefaultLibFilePath(compilerOptions);
} else if (this.sourceFileMap.has(sourceName)) {
return this.sourceFileMap.get(sourceName);
}
if (!fs.existsSync(sourcePath)) {
return undefined;
}
const contents = fs.readFileSync(sourcePath, 'utf-8');
return ts.createSourceFile(sourceName, contents, COMPILER_OPTIONS.target);
};
然後你只需要將這個 CompilerHost 作為第三個參數傳遞給 ts.createProgram()
