진단 TypeScript 언어 서비스 플러그인은 어떻게 작성합니까?

코드에 추가한 예쁜 TODO나 FIXME 주석이 잊혀지고 영원히 남아 있다는 사실을 깨달았습니까?저희 회사의 혁신의 날에 TypeScript 컴파일러 플러그인을 작성하려고 합니다. TODO 주석에서 제공한 조건이 사실로 평가될 때 구축에 실패할 수 있습니다.불행하게도, TypeScript에서 컴파일러 플러그인을 작성할 수 없다는 결론을 내렸습니다.그러나 언어 서비스 플러그인을 작성할 수 있도록 해 줍니다.
TypeScript 언어 서비스 플러그인을 사용하여 TypeScript 사용자의 편집 환경을 변경할 수 있습니다.그것은 컴파일러를 방해하지 않을 것이다.강제 집행이 아니라 지도나 도움을 줄 수밖에 없다는 뜻이다.본고는 저희Todo Or Die use case를 예로 삼아 진단 플러그인을 만드는 방법을 소개합니다.

코드 때문에?이것 좀 보세요repository.

설치 프로그램


우선 공장 함수와 장식기가 있는 간단한 TypeScript 프로젝트가 필요합니다. 공장 함수로 돌아가야 합니다.
TypeScript wikiSetup and Initialization의 단계Decorator CreationWriting 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의 언어 서비스 기능을 덮어쓸 수 있습니다.진단 서비스 유형을 변경하려면 스크립트에서 세 가지 진단 유형을 정의합니다.
  • 문법
  • 의 의미
  • 권장 사항
  • 그것들 중 하나는 모두 상응하는 기능을 가지고 있기 때문에 우리는 그것들을 덮어쓸 수 있다.

    덮어쓸 함수 결정하기


    type files에서 TypeScript에서는 이러한 함수를 사용하는 시기를 자세히 설명합니다.이 세 가지 방법은 현재 파일 이름을 매개 변수로 전달하고 되돌려주며, 진단 그룹을 되돌려 달라고 요구합니다.유형에 따라 진단의 모양을 정해야 합니다.덮어쓸 항목을 선택합니다.

    구법의

    getSyntacticDiagnostics(fileName: string): DiagnosticWithLocation[]"지시 파일의 잘못된 문법에 대한 오류를 가져옵니다.
    영어에서'this cdeo have,erorrs'는 맞춤법 오류, 문법 오류, 문장부호 오류가 있기 때문에 문법적으로 무효입니다.마찬가지로 TypeScript의 구문 오류 예로는 if문에 괄호가 없고 일치하지 않는 괄호가 있으며 변수 이름으로 보존 키워드를 사용하는 것이 있습니다.
    이 진단의 계산 비용은 매우 낮아서 다른 문서를 이해할 필요가 없다.비공식 결과는 getSemanticDiagnostics의 오보 가능성을 증가시킬 수 있으니 주의하세요.
    대부분 문법과 관련된 진단을 대표하지만 유형 시스템이 필요한 경우도 있다getSemanticDiagnostics에 나타난다."

    의미의

    getSemanticDiagnostics(fileName: string): Diagnostic[]주어진 파일의 형식 시스템 문제를 표시하는 경고나 오류를 가져옵니다.
    의미 진단을 요청하면 유형 시스템을 시작하고 지연 작업을 실행할 수 있기 때문에 첫 번째 호출은 후속 호출보다 시간이 더 오래 걸릴 수 있습니다.
    다른 get*Diagnostics 함수와 달리 이러한 진단에는 소스 파일에 대한 참조가 포함되지 않을 수 있습니다.구체적으로 말하면, 처음 그것을 호출할 때, 그것은 관련 위치가 없는 전역 진단으로 되돌아갈 것이다.
    의미 진단과 문법 진단 사이의 차이를 비교하기 위해'태양은 녹색이다'는 말을 고려할 수 있다.문법이 정확하다.이것들은 진정한 영어 단어로 문장의 구조가 정확하다.그러나 그것은 의미가 무효다. 왜냐하면 그것은 사실이 아니기 때문이다."

    권장 사항

    getSuggestionDiagnostics(fileName: string): DiagnosticWithLocation[]특정 파일에 대한 권장 진단을 받습니다. 이러한 진단은 잠재적인 오류 실행을 지시하는 것이 아니라, 주동적으로 재구성을 권장하는 경향이 있습니다.

    덮어쓰기 함수


    현재, 우리는 설정된 proxy 을 사용하여 상술한 함수 중의 하나를 덮어쓸 수 있다.덮어쓰겠습니다getSemanticDiagnostics. 그러나 원하는 기능을 가장 잘 선택해야 합니다.getSemanticDiagnostics는 사용자가 사용하는 현재 파일의 이름을 매개 변수로 보여 줍니다. 이것은 우리가 ts.Diagnostic의 목록을 되돌려 주기를 바랍니다.
    이것은 이렇게 보인다.
    proxy.getSemanticDiagnostics = (filename) => {
      return [];
    }
    
    지금 우리가 해야 할 첫 번째 일은 TypeScript 자체나 다른 언어 서비스 플러그인에 기록된 다른 진단을 되돌려 주는 것이다.우리는 info.languageService.getSemanticDiagnostics를 사용하여 이 점을 실현할 수 있다.
    proxy.getSemanticDiagnostics = (filename) => {
      const prior = languageService.getSemanticDiagnostics(filename);
    
      return [...prior];
    }
    
    마지막으로, 우리는 자신의 논리를 추가하여 진단을 되돌릴 수 있다.우선 filename 매개 변수에 따라 파일의 내용을 가져와야 합니다.이를 위해 우리는 사용할 수 있다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-do나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:로 시작하는 전체 줄을 표시하고 "이 처리할 사항은 복구되어야 합니다!"를 표시합니다.오류 세부 정보의 메시지입니다.

    최종 결과


    현재 설정 코드 세션과 변이한 프록시 행위를 결합하면 작업 진단 플러그인이 있습니다!
    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"
    
  • TypeScript 프로젝트에 플러그인을 추가tsconfig합니다.
  • {
      "plugins": [
        { "name": "your-plugin-name" }
      ]
    }
    
  • 편집기를 다시 시작합니다.
  • 저장소의 주석TODO을 검토합니다.
  • 참고: Visual Studio 코드를 사용하는 경우 TypeScript: TypeScript 버전 선택 명령을 실행하고 작업공간 버전 사용을 선택하거나 오른쪽 아래 모서리의 TypeScript 옆에 있는 버전 번호를 클릭해야 합니다.그렇지 않으면 VS 코드에서 플러그인을 찾을 수 없습니다.

    플러그 인 해제


    플러그인을 풀려면 package.json 파일에서 상기 코드를 포함하는 파일을 참조해야 합니다.예: { "main": "dist/index.js" }.현재 플러그인을 npm에 발표하고 다른 프로젝트에 설치합니다!

    결론


    본문을 읽어 주셔서 감사합니다!이것repository의 샘플 플러그인을 보세요.대기 사항이나 사망 플러그인에 적용하는 방법에 관심이 있으시면 https://github.com/ngnijland/typescript-todo-or-die-plugin를 방문하십시오.

    좋은 웹페이지 즐겨찾기