網站搜尋

運行 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()