どのように診断のタイプスクリプト言語のプラグインを書くには?


あなたのコードに追加するこれらの美しいtodoまたはfixmeコメントが忘れられているという事実を認識し、永遠にまで残って?当社の企業革新日では、あなたのTODOコメントで提供する条件がtrueに評価されたときにビルド失敗になるTypeScriptコンパイラプラグインを書きたかったです.残念なことに、typescriptはコンパイラプラグインを書くことができないという結論に達しました.しかし、言語サービスプラグインを書くことができます.
TypeScript言語サービスプラグインを使用すると、入力スクリプトの編集経験を変更することができます.これはコンパイラと干渉しません.これは、ガイドするか、助けることができるだけでなく、実施することを意味します.この記事では、私たちのTodo Or Die use caseを例として診断プラグインを作成する方法について説明します.

ここだけのコードですか?repository .

セットアップ
まず、ファクトリ関数を使用して簡単なTypeScriptプロジェクトを必要とします.
typescript wiki上のSetup and InitializationのステップDecorator CreationおよびWriting a Language Service Plugin articleに従ってください.
次のコードが終わります.
function init(modules: { typescript: typeof import("typescript/lib/tsserverlibrary") }) {
  const ts = modules.typescript;

  function create(info: ts.server.PluginCreateInfo) {
    const proxy: ts.LanguageService = Object.create(null);

    for (let k of Object.keys(info.languageService) as Array<keyof ts.LanguageService>) {
      const x = info.languageService[k]!;
      // @ts-expect-error - JS runtime trickery which is tricky to type tersely
      proxy[k] = (...args: Array<{}>) => x.apply(info.languageService, args);
    }

    return proxy;
  }

  return { create };
}

export = init;
これは今ではパススループラグインですが、それは私たちのコメントのカスタム動作を追加を開始することができます.

カスタム動作
このデコレータを設定した後に、私たちは今、typescriptの言語サービス機能を上書きすることができます.診断サービスTypescriptの変更を行うには、3種類の診断を定義します.
  • の統語
  • 意味
  • 提案
  • それぞれには、上書きできる関数があります.

    上書きする関数の決定
    タイプファイルTypesScriptでは、これらの関数を使用するときの詳細な説明を行います.すべての3つの引数として、現在のファイル名を渡すと、診断の配列を返す必要があります.型に応じて診断を整形する必要があります.上書きしたいものを選びなさい.

    統語getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]ファイル内で無効な構文を示すエラーを取得します.
    英語では、“このcdeoを持っている、erorrs”は、それが誤植、文法エラー、および誤った句読点を持っているので、構文的に無効です.同様に、typescriptの構文エラーの例はif文、不釣合い括弧括弧、および変数名として予約されたキーワードを使用する括弧を欠いています.
    これらの診断は、計算するために安価であり、他のファイルの知識を必要としない.非空の結果はgetSemanticDiagnosticsから偽陽性の可能性を増加させることに注意してください.
    これらは構文関連診断の大多数を表しますが、getSemanticDiagnosticsに存在するタイプシステムを必要とするものもあります."

    意味getSemanticDiagnostics(fileName: string): Diagnostic[]指定したファイルの型のシステムの問題を示す警告またはエラーを取得します.
    意味の診断を要求すると、型システムを起動し、遅延された作業を実行することができます.したがって、最初の呼び出しは、以降の呼び出しよりも長くなる可能性があります.
    他のget *診断機能とは異なり、これらの診断は潜在的にソースファイルへのリファレンスを含んでいません.具体的には、これが初めて呼び出されると、関連付けられていない場所でグローバル診断を返します.
    意味と構文診断の違いを対照するために、「太陽は緑です」という文を考えてみましょうは構文的に正しいそれらは正しい文構造を持つ本当の英単語です.しかし、それが真実でないので、それは意味論的に無効です."

    提言getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[]特定のファイルに対する提案診断を取得します.これらの診断は、潜在的に誤った実行時動作を示す診断に対して、積極的にリファクタを提案する傾向があります

    関数を上書きする
    我々は現在、上記の機能の上書きするために設定したproxyを使用することができます.私はgetSemanticDiagnosticsを上書きするつもりですが、あなたが達成したいものと一致する機能を選択するようにしてください.getSemanticDiagnosticsはユーザが引数として動作する現在のファイルの名前を与え、ts.Diagnosticのリストを返すことを期待しています.
    これは次のようになります.
    proxy.getSemanticDiagnostics = (filename) => {
      return [];
    }
    
    今私たちがしたいことは、他の診断を返すことを確認することです.これを行うにはinfo.languageService.getSemanticDiagnosticsを使用できます.
    proxy.getSemanticDiagnostics = (filename) => {
      const prior = languageService.getSemanticDiagnostics(filename);
    
      return [...prior];
    }
    
    最後に、診断を返すために独自のロジックを追加することができます.まず、ファイル名の引数に基づいてファイルの内容を取得する必要があります.このため、info.languageService.getProgram()?.getSourceFile(filename)を使用できます.この関数の結果を未定義にすることができますので、このケースをキャッチし、代わりにpriorを返すようにします.
    proxy.getSemanticDiagnostics = (filename) => {
      const prior = info.languageService.getSemanticDiagnostics(filename);
      const doc = info.languageService.getProgram()?.getSourceFile(filename);
    
      if (!doc) {
        return prior;
      }
    
      return [...prior];
    }
    
    その後、ファイルを分析し、それに基づいて診断を生成することができます.私たちの場合、私たちはto -またはdie文のためにファイルのすべての線をチェックしたいです.この例のために可能な限り簡単にするために、// TODO:から始まる任意の行を探し、それぞれの診断を作成します.ts.Diagnosticの型は以下の通りです.
    enum DiagnosticCategory {
      Warning = 0,
      Error = 1,
      Suggestion = 2,
      Message = 3
    }
    
    interface DiagnosticMessageChain {
      messageText: string;
      category: DiagnosticCategory;
      code: number;
      next?: DiagnosticMessageChain[];
    }
    
    interface Diagnostic {
      category: DiagnosticCategory;
      code: number;
      file: SourceFile | undefined;
      start: number | undefined; // Index of `doc` to start error from
      length: number | undefined;
      messageText: string | DiagnosticMessageChain;
    }
    
    診断を一緒にすることができるすべての必要なデータを収集するためにこの情報を使用してください.私たちの場合、行番号とTODOコメントを含む行自体を追跡したいと思います.
    // Context
    import { DiagnosticCategory } from "typescript";
    // Context
    
    proxy.getSemanticDiagnostics = (filename) => {
      const prior = info.languageService.getSemanticDiagnostics(filename);
      const doc = info.languageService.getProgram()?.getSourceFile(filename);
    
      if (!doc) {
        return prior;
      }
    
      return [
        ...prior,
        ...doc.text
          .split("\n")
          .reduce<[string, number]][]>((acc, line, index) => {
            if (line.trim().startsWith("// TODO:")) {
              return [...acc, [line, index]];
            }
    
            return acc;
          }, [])
          .map(([line, lineNumber]) => ({
            file: doc,
            start: doc.getPositionOfLineAndCharacter(lineNumber, 0),
            length: line.length,
            messageText: "This TODO comment should be fixed!",
            category: DiagnosticCategory.Error,
            source: "Your plugin name",
            code: 666
          }))
      ];
    }
    
    このコードは、// TODO:から始まる完全な行をマークし、“このtodo commendを修正する必要があります!”エラーの詳細のメッセージ.

    結果終了
    今すぐセットアップコードスニペットを変異プロキシの動作を組み合わせるし、自分自身の作業診断プラグインを持っている!
    import { DiagnosticCategory } from "typescript";
    
    function init(modules: { typescript: typeof import("typescript/lib/tsserverlibrary") }) {
      const ts = modules.typescript;
    
      function create(info: ts.server.PluginCreateInfo) {
        const proxy: ts.LanguageService = Object.create(null);
    
        for (let k of Object.keys(info.languageService) as Array<keyof ts.LanguageService>) {
          const x = info.languageService[k]!;
          // @ts-expect-error - JS runtime trickery which is tricky to type tersely
          proxy[k] = (...args: Array<{}>) => x.apply(info.languageService, args);
        }
    
        proxy.getSemanticDiagnostics = (filename) => {
          const prior = info.languageService.getSemanticDiagnostics(filename);
          const doc = info.languageService.getProgram()?.getSourceFile(filename);
    
          if (!doc) {
            return prior;
          }
    
          return [
            ...prior,
            ...doc.text
              .split("\n")
              .reduce<[string, number][]>((acc, line, index) => {
                if (line.trim().startsWith("// TODO:")) {
                  return [...acc, [line, index]];
                }
    
                return acc;
              }, [])
              .map(([line, lineNumber]) => ({
                file: doc,
                start: doc.getPositionOfLineAndCharacter(lineNumber, 0),
                length: line.length,
                messageText: "This TODO comment should be fixed!",
                category: DiagnosticCategory.Error,
                source: "Your plugin name",
                code: 666
              }))
          ];
        }
    
        return proxy;
      }
    
      return { create };
    }
    
    export = init;
    

    ローカルテスト
  • あなたのパッケージに少なくとも以下を加えてください.JSON
  • {
      "name": "your-plugin-name",
      "version": "1.0.0",
      "main": "dist/index.js",
      "scripts": {
        "build": "tsc -p .",
      },
      "dependencies": {
        "typescript": "^4.5.4"
      }
    }
    
  • 依存関係をインストールする.
  • npm install
    
  • ビルドプラグイン.
  • npm run build
    
  • セットアップリンク.
  • npm link
    
  • リンクを別のリポジトリにリンクします.
  • cd ../path-to-repository
    npm link "your-plugin-name"
    
  • タイプスクリプトプロジェクトでtsconfigにプラグインを追加します.
  • {
      "plugins": [
        { "name": "your-plugin-name" }
      ]
    }
    
  • あなたのエディタを再起動します.
  • あなたの倉庫のどんなTODOコメントをチェックしてください.
  • 注意: Visual Studioのコードを使用している場合は、「TypeScript : Select TypeScript Version」コマンドを実行し、「Workspace Versionを使用」を選択するか、右下隅の「TypeScript」の横のバージョン番号をクリックします.さもなければ、VSコードはプラグインを見つけることができません.

    プラグインのリリース
    プラグインをリリースするには、メインキーを使用してpackage.jsonファイルに上記のコードを含むファイルを参照する必要があります.例えば、{ "main": "dist/index.js" }.今NPMにプラグインを公開し、別のプロジェクトにインストール!

    結論
    この記事を読んでくれてありがとう!このrepositoryでBoilerPlateプラグインをチェックしてください.あなたが私たちがどのように我々がそれを適用するか、または興味があるならば、あなたはhttps://github.com/ngnijland/typescript-todo-or-die-pluginでそれを見つけることができます.