Recently, we issued an internal Code Review guide, but the Code Review process takes a lot of time, and people do not Review the Code carefully. Therefore, we want to use a plug-in to let developers feel the writing errors in the development stage, and the effect is shown in the following figure

Here’s how to implement such a feature from 0.

The basic principle of

Visual Studio Code’s programming Language functionality extensions are implemented by Language Server, which makes sense because checking Language functionality is performance-costly and requires a separate process as a Language service, the Language Server.

Language Server is a special Visual Studio Code extension that provides an editing experience for many programming languages. Using the language server, you can implement auto-complete, error checking (diagnostics), jump to definitions, and many other language features supported by VS Code.

With the syntax checking function provided by the server, it is necessary for the client to connect to the language server and interact with the server, for example, when the user is editing the code on the client, the language checking is carried out. Specific interactions are as follows:

When the plug-in is activated when the Vue file is opened, the Language Server starts, and when the document changes, the Language Server rediagnoses the code and sends the diagnosis to the client.

The effect of code diagnosis is the appearance of wavy lines, mouse over the display prompt message, if there is a quick fix, will pop up in the prompt window under the quick fix button

implement

Now that we know the basics of code diagnostics, we can start implementing them. From the basics above, we need to implement two main functions:

  1. The client interacts with the language server
  2. Language server diagnostics and quick fixes

The client interacts with the language server

The official documentation provides an example – a simple language server for plain text files – that we can build on.

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

First set up the server in client

// client/src/extension.ts
export function activate(context: ExtensionContext) {...const clientOptions: LanguageClientOptions = {
        documentSelector: [{ scheme: 'file'.language: 'vue'}].// Only activated when the vue file is opened. }; client =newLanguageClient(...) ; client.start(); }Copy the code

Then, in server/ SRC /server.ts, write the client-side interaction logic, such as checking the code when the client document changes:

// server/src/server.ts
documents.onDidChangeContent(change= > {
    // Verify the document when it changes
    validateTextDocument(change.document);
});

async function validateTextDocument(textDocument: TextDocument) :Promise<void> {...// Get the diagnosis result
    const diagnostics = getDiagnostics(textDocument, settings);
    // Send to the client
    connection.sendDiagnostics({ uri: textDocument.uri, diagnostics });
}
// Provide quick fixes
connection.onCodeAction(provideCodeActions);

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

After completing the client-server interaction above, notice the two methods getDiagnostics(textDocument, Settings) and QuickFix (textDocument, Params). These are operations that provide diagnostic data for documents and quick fixes, respectively.

The code in the diagnosis of

The whole process

1. Turn the code document into an AST syntax tree

In processing the Vue code text passed by the client, Vue/Compiler-DOM needs to be parsed into three ast format data structures, namely template, JS and CSS. Since the front-end code is now using TypeScript, The JS part is not parsed to AST, so Babel/Parser is used to parse the AST data structure of the TypeScript code to produce the final JS.

const VueParser = require('@vue/compiler-dom');
// This function returns the diagnostic result client
function getDiagnostics(textDocument: TextDocument, settings: any) :Diagnostic[] {
	const text = textDocument.getText();
	const res = VueParser.parse(text);
	const [template, script] = res.children;
	return [
		...analyzeTemplate(template), // Parse template to get diagnostic results. analyzeScript(script, textDocument),// Parse the js to get the diagnostic result
	];
}
// Parse the js syntax
function analyzeScript(script: any, textDocument: TextDocument) {
  const scriptAst = parser.parse(script.children[0]? .content, {sourceType: 'module'.plugins: [
      'typescript'.// typescript
      ['decorators', { decoratorsBeforeExport: true}]./ / a decorator
      'classProperties'.// ES6 class
      'classPrivateProperties',]});Copy the code

The AST syntax tree structure is as follows:

Template AST

JS AST

2. Traverses the syntax tree to verify the code

After obtaining the syntax tree of the Code, we need to check each Code node to determine whether it meets the requirements of Code Review, so we need to traverse the syntax tree to process each node.

Traversal the AST of the Template using depth-first search:

function deepLoopData(
  data: AstTemplateInterface[],
  handler: Function,
  diagnostics: Diagnostic[],
) {
  function dfs(data: AstTemplateInterface[]) {
    for (let i = 0; i < data.length; i++) {
      handler(data[i], diagnostics); // The code is processed in this step
      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[]){
   / /... Check the code node
} 
Copy the code

3. Find non-compliance codes and generate diagnosis

The AST syntax node is used to determine whether the syntax is compliant. If it is not, a diagnosis needs to be generated in the code. A basic diagnostics object consists of the following attributes:

  1. Range: The area where the wavy line is drawn
  2. Severity: severity. There are four grades respectively. The colors of the marks of different grades are different.
    • Error: 1.
    • Warning: 2
    • Information: 3
    • Hint: 4
  3. “Message” : diagnosis message
  4. Source: source, for example Eslint
  5. Data: Carries data. You can store the repaired data here for later quick repair

For example, implementing a diagnosis where the prompt function is too long:

function isLongFunction(node: Record<string, any>) {
  return (
    // If the end line - start line > 80, we consider this function to be too long
    node.type === 'ClassMethod' && node.loc.end.line - node.loc.start.line > 80
  );
}
Copy the code

If you encounter a node where the function is too long while traversing the AST, you add that diagnosis to the diagnostic data

traverse(scriptAst, {
    enter(path: any) {
        const { node } = path;
        if (isLongFunction(node)) {
            const diagnostic: Diagnostic ={
                severity: DiagnosticSeverity.Warning,
                range: getPositionRange(node, scriptStart),
                message: 'As far as possible, maintain a single responsibility principle for a function, no more than 80 lines per function'.source: 'Code Review Guide ', } diagnostics.push(diagnostic); }... }});Copy the code

All diagnostics in the document are stored in the Diagnostics array and returned to the client via interaction.

4. Provide quick fixes

The above function is too long for a quick fix. If it can be fixed quickly, you can place the corrected result in diagnosis.data. Another example to write a quick fix, such as the Vue template property is not sorted correctly, we need to fix the code automatically

// attributeOrderValidator gets the result and fixed code
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(
        'Order of attributes on vue Template',
        range
      );
      if (newText) { // If there is fixed code
        // Save the quick fix data in diagnostic.data
        diagnostic.data = {
          title: 'Fix in the order of the Code Review Guide',
          newText,
        }
      }
      diagnostics.push(diagnostic);
    }
Copy the code

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) { // If there is a quick fix data
            // Add quick fixes
            codeActions.push({
              title: (diag.data asany)? .title,kind: CodeActionKind.QuickFix, // Quick fix
              diagnostics: [diag], // Which operation belongs to the diagnosis
              edit: {
                changes: {
                    [params.textDocument.uri]: [
                      {
                        range: diag.range,
                        newText: (diag.data asany)? .newText,// The fixed content},],},},},}); }}});Copy the code

conclusion

Implementing a plug-in for code diagnostics requires two steps, first setting up the language server and setting up the client’s interaction with the language server. Then, the server needs to verify the code of the client, place the diagnosis result in Diagnostics, and place the quick repair result in CodeActions. By communicating with the client, the two results are returned to the client, and the problem prompt of yellow wavy line can appear on the client.