VSCode外掛開發實戰:實現一個程式碼診斷外掛

2022-02-16 22:00:21
本篇文章給大家分享一個外掛開發實戰,開發一個程式碼診斷外掛,分析一下基本原理,並一步步實現,希望對大家有所幫助!

最近,我們內部出了一份 Code Review 指南,但是 Code Review 過程非常佔時間,大家不會太仔細去 review 程式碼,因此想通過一個外掛讓開發者在開發階段就能感知到寫法的錯誤,做出的效果如下圖

Kapture 2022-02-10 at 15.36.49.gif

接下來將介紹如何從 0 實現這麼一個功能。

基本原理

Visual Studio Code 的程式語言功能擴充套件是有 Language Server 來實現的,這很好理解,畢竟檢查語言功能是耗費效能的,需要另起一個程序來作為語言服務,這就是 Language Server 語言伺服器。【推薦學習:《》】

Language Server 是一種特殊的 Visual Studio Code 擴充套件,可為許多程式語言提供編輯體驗。使用語言伺服器,您可以實現自動完成、錯誤檢查(診斷)、跳轉到定義以及VS Code 支援的許多其他語言功能。

既然有了伺服器提供的語法檢查功能,就需要使用者端去連線語言伺服器,然後和伺服器進行互動,比如使用者在使用者端進行程式碼編輯時,進行語言檢查。具體互動如下:

2.gif

當開啟 Vue 檔案時會啟用外掛,此時就會啟動 Language Server,當檔案發生變化時,語言伺服器就會重新診斷程式碼,並把診斷結果傳送給使用者端。

程式碼診斷的效果是出現波浪線,滑鼠移上顯示提示訊息,如果有快速修復,會在彈出提示的視窗下出現快速修復的按鈕

動手實現

瞭解了程式碼診斷的基本原理之後,開始動手實現,從上面的基本原理可知,我們需要實現兩大部分的功能:

  • 使用者端與語言伺服器互動

  • 語言伺服器的診斷和快速修復功能

使用者端與語言伺服器互動

官方檔案 提供了一個範例 - 用於純文字檔案的簡單語言伺服器,我們可以在這個範例的基礎上去修改。

> git clone https://github.com/microsoft/vscode-extension-samples.git
> cd vscode-extension-samples/lsp-sample
> npm install
> npm run compile
> code .

首先在 client 建立伺服器

// client/src/extension.ts
export function activate(context: ExtensionContext) {
    ...
    const clientOptions: LanguageClientOptions = {
        documentSelector: [{ scheme: 'file', language: 'vue' }], // 開啟 vue 檔案時才啟用
        ...
    };
    client = new LanguageClient(...);
    client.start();
}

接著在 server/src/server.ts 中,編寫於使用者端的互動邏輯,比如在使用者端檔案發生變化的時候,校驗程式碼:

// server/src/server.ts
import {
    createConnection
    TextDocuments,
    ProposedFeatures,
    ...
} from 'vscode-languageserver/node';
const connection = createConnection(ProposedFeatures.all);
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
documents.onDidChangeContent(change => {
    // 檔案發生變化時,校驗檔案
    validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument): Promise<void> {
    ...
    // 拿到診斷結果
    const diagnostics = getDiagnostics(textDocument, settings);
    // 發給使用者端
    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
// 提供快速修復的操作
connection.onCodeAction(provideCodeActions);

async function provideCodeActions(params: CodeActionParams): Promise<CodeAction[]> {
    ...
    return quickfix(textDocument, params);
}

在完成上面使用者端與伺服器端互動之後,可以注意到這兩個方法 getDiagnostics(textDocument, settings)quickfix(textDocument, params)。 這兩個方法分別是為檔案提供診斷資料和快速修復的操作。

程式碼診斷

整體流程

3.gif

1. 將程式碼檔案轉成 AST 語法樹

在處理使用者端傳遞過來的 Vue 程式碼文字的,需要通過 vue/compiler-dom 解析成三部分 ast 格式的資料結構,分別是 template、JS、CSS, 由於現在前端程式碼使用的都是 TypeScript,JS 部分沒有解析成 AST,因此需要使用 babel/parser 去解析 TypeScript 程式碼生成最終的 JS 的 AST 資料結構。

const VueParser = require('@vue/compiler-dom');
// 該函數返回診斷結果使用者端
function getDiagnostics(textDocument: TextDocument, settings: any): Diagnostic[] {
	const text = textDocument.getText();
	const res = VueParser.parse(text);
	const [template, script] = res.children;
	return [
		...analyzeTemplate(template), // 解析 template 得到診斷結果
		...analyzeScript(script, textDocument), // 解析 js 得到診斷結果
	];
}
// 分析 js 語法
function analyzeScript(script: any, textDocument: TextDocument) {
  const scriptAst = parser.parse(script.children[0]?.content, {
    sourceType: 'module',
    plugins: [
      'typescript', // typescript
      ['decorators', { decoratorsBeforeExport: true }], // 裝飾器
      'classProperties', // ES6 class 寫法
      'classPrivateProperties',
    ],
  });

得到的 AST 語法樹結構如下:

Template AST

4.gif

JS AST

5.gif

2. 遍歷語法樹對程式碼校驗

在得到程式碼的語法樹之後,我們需要對每一個程式碼節點進行檢查,來判斷是否符合 Code Review 的要求,因此需要遍歷語法樹來對每個節點處理。

使用深度優先搜尋對 template 的 AST 進行遍歷:

function deepLoopData(
  data: AstTemplateInterface[],
  handler: Function,
  diagnostics: Diagnostic[],
) {
  function dfs(data: AstTemplateInterface[]) {
    for (let i = 0; i < data.length; i++) {
      handler(data[i], diagnostics); // 在這一步對程式碼進行處理
      if (data[i]?.children?.length) {
        dfs(data[i].children);
      } else {
        continue;
      }
    }
  }
  dfs(data);
}

function analyzeTemplate(template: any) {
  const diagnostics: Diagnostic[] = [];
  deepLoopData(template.children, templateHandler, diagnostics);
  return diagnostics;
}
function templateHandler(currData: AstTemplateInterface, diagnostics: Diagnostic[]){
   // ...對程式碼節點檢查
}

而對於 JS AST 遍歷,可以使用 babel/traverse 遍歷:

 traverse(scriptAst, {
    enter(path: any) {
      ...
    }
 }

3. 發現不合規程式碼,生成診斷

根據 ast 語法節點去判斷語法是否合規,如果不符合要求,需要在程式碼處生成診斷,一個基礎的診斷物件(diagnostics)包括下面幾個屬性:

  • range: 診斷有問題的範圍,也就是畫波浪線的地方

  • severity: 嚴重性,分別有四個等級,不同等級標記的顏色不同,分別是:

    • Error: 1
    • Warning: 2
    • Information:3
    • Hint:4
  • message: 診斷的提示資訊

  • source: 來源,比如說來源是 Eslint

  • data:攜帶資料,可以將修復好的資料放在這裡,用於後面的快速修復功能

比如實現一個提示函數過長的診斷:

function isLongFunction(node: Record<string, any>) {
  return (
    // 如果結束位置的行 - 開始位置的行 > 80 的話,我們認為這個函數寫得太長了
    node.type === 'ClassMethod' && node.loc.end.line - node.loc.start.line > 80
  );
}

在遍歷 AST 時如果遇到某個節點是出現函數過長的時候,就往診斷資料中新增此診斷

traverse(scriptAst, {
    enter(path: any) {
        const { node } = path;
        if (isLongFunction(node)) {
            const diagnostic: Diagnostic ={
                severity: DiagnosticSeverity.Warning,
                range: getPositionRange(node, scriptStart),
                message: '儘可能保持一個函數的單一職責原則,單個函數不宜超過 80 行',
                source: 'Code Review 指南',
            }
            diagnostics.push(diagnostic);
        }
        ...   
    }
});

檔案中所有的診斷結果會儲存在 diagnostics 陣列中,最後通過互動返回給使用者端。

4. 提供快速修復

上面那個函數過長的診斷沒辦法快速修復,如果能快速修復的話,可以將修正後的結果放在 diagnostics.data 。換個例子寫一個快速修復, 比如 Vue template 屬性排序不正確,我們需要把程式碼自動修復

// attributeOrderValidator 得到判斷結果 和 修復後的程式碼
const {isGoodSort, newText} = attributeOrderValidator(props, currData.loc.source);
    if (!isGoodSort) {
      const range = {
        start: {
          line: props[0].loc.start.line - 1,
          character: props[0].loc.start.column - 1,
        },
        end: {
          line: props[props.length - 1].loc.end.line - 1,
          character: props[props.length - 1].loc.end.column - 1,
        },
      }
      let diagnostic: Diagnostic = genDiagnostics(
        'vue template 上的屬性順序',
        range
      );
      if (newText) { // 如果有修復後的程式碼
        // 將快速修復資料儲存在 diagnostic.data
        diagnostic.data = {
          title: '按照 Code Review 指南的順序修復',
          newText,
        }
      }
      diagnostics.push(diagnostic);
    }

quickfix(textDocument, params)

export function quickfix(
  textDocument: TextDocument,
  params: CodeActionParams
): CodeAction[] {
  const diagnostics = params.context.diagnostics;
  if (isNullOrUndefined(diagnostics) || diagnostics.length === 0) {
    return [];
  }
  const codeActions: CodeAction[] = [];
  diagnostics.forEach((diag) => {
    if (diag.severity === DiagnosticSeverity.Warning) {
      if (diag.data) { // 如果有快速修復資料
        // 新增快速修復
        codeActions.push({
          title: (diag.data as any)?.title,
          kind: CodeActionKind.QuickFix, // 快速修復
          diagnostics: [diag], // 屬於哪個診斷的操作
          edit: {
            changes: {
                [params.textDocument.uri]: [
                  {
                    range: diag.range,
                    newText: (diag.data as any)?.newText, // 修復後的內容
                  },
                ],
              },
           },
        });
    } 
  }
});

有快速修復的診斷會儲存在 codeActions 中,並且返回給使用者端, 重新回看互動的程式碼,在 documents.onDidChangeContent 事件中,通過 connection.sendDiagnostics({ uri: textDocument.uri, diagnostics }) 把診斷傳送給使用者端。quickfix 結果通過 connection.onCodeAction 發給使用者端。

import {
    createConnection
    TextDocuments,
    ProposedFeatures,
    ...
} from 'vscode-languageserver/node';
const connection = createConnection(ProposedFeatures.all);
const documents: TextDocuments<TextDocument> = new TextDocuments(TextDocument);
documents.onDidChangeContent(change => {
    ...
    // 拿到診斷結果
    const diagnostics = getDiagnostics(textDocument, settings);
    // 發給使用者端
    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
});

// 提供快速修復的操作
connection.onCodeAction(provideCodeActions);

async function provideCodeActions(params: CodeActionParams): Promise<CodeAction[]> {
    ...
    return quickfix(textDocument, params);
}

總結

實現一個程式碼診斷的外掛功能,需要兩個步驟,首先建立語言伺服器,並且建立使用者端與語言伺服器的互動。接著需要 伺服器根據使用者端的程式碼進行校驗,把診斷結果放入 Diagnostics,快速修復結果放在 CodeActions,通過與使用者端的通訊,把兩個結果返回給使用者端,使用者端即可出現黃色波浪線的問題提示。

更多關於VSCode的相關知識,請存取:!!

以上就是VSCode外掛開發實戰:實現一個程式碼診斷外掛的詳細內容,更多請關注TW511.COM其它相關文章!