通用语法校验器tree-sitter——C++语法校验实践

0 阅读3分钟

tree-sitter介绍

以下内容来自于官方文档:tree-sitter.github.io/tree-sitter…

Tree-sitter 是一个解析器生成工具和增量解析库,用于为源代码文件构建具体的语法树,并在源文件编辑时高效更新语法树。它旨在提供一个通用、快速且鲁棒的解决方案,用于解析编程语言,即使在存在语法错误的情况下也能正常工作。

主要特点:

  • 通用性:能够解析任何编程语言。
  • 高效性:足够快,可以在文本编辑器中每按一个键就进行解析。
  • 鲁棒性:即使有语法错误,也能提供有用的结果。
  • 无依赖:运行时库使用纯 C11 编写,可以嵌入到任何应用程序中。

工作原理:

Tree-sitter 生成解析器并维护一个增量解析库,随着源文件的编辑实时更新语法树,从而支持如文本编辑器中的实时解析。

支持的语言:

  • 语言绑定:支持 C# (.NET)、C++、Crystal、D、Delphi、ELisp、Go、Guile、Janet、Java (JDK 8+ 和 11+)、Julia、Lua、OCaml、Odin、Perl、Pharo、PHP、R 和 Ruby 等语言的绑定(部分绑定可能不完整或过时)。

tree-sitter的缺点

Tree-sitter 不是利用编程语言(如 C++、JavaScript 等)的现有或官方解析器来进行解析的。它是一个独立的解析器生成工具,使用自己的框架和语法定义来为各种语言生成专属的解析器。这些解析器基于 GLR(广义 LR)算法构建,并通过 Tree-sitter 的工具包预先编译或运行时生成,而不是依赖语言的内置运行时环境(如 V8 对于 JavaScript)。这种设计允许 Tree-sitter 在编辑器中实现高效的增量解析,但也可能导致与官方解析器在某些边缘情况下的不一致。

因此,如果需要高质量的语法解析,请不要用tree-sitter。 虽然tree-sitter提供了api让开发者编写更精细的解析,但不如考虑其他wasm方案或Language Server Protocol (LSP)

测例

以下c++代码中有4处错误,包括使用了关键字/错误的声明/使用了未定义变量/错误语法(a+++),但只识别到了最后一个。

int main()  
{  
    int int = 1;  
    inb a = 1;  
    int a = 0, b = 1, c = 2, d = 3, e = 4;

    if (x > 1)  
    {  
    }  
    a++ + ;  
    if (a || (b < c && e >= d))  
    { /* ... */  
    }

    return 0;  
}

playground

tree-sitter.github.io/tree-sitter…

在浏览器环境中使用tree-sitter

tree-sitter支持在浏览器环境中使用,方法也很简单。

安装依赖

npm install web-tree-sitter

生成wasm

语法校验需要对应语言的wasm,生成步骤如下:

  1. 安装依赖
npm install --save-dev tree-sitter-cli tree-sitter-cpp

将node_modules中的web-tree-sitter.wasm文件复制到public目录下 2. 执行命令,在当前目录下生成tree-sitter-cpp.wasm

npx tree-sitter build --wasm node_modules/tree-sitter-cpp

3. 将生成的tree-sitter-cpp.wasm放入public目录下

实践demo

代码如下:

import { code } from "./code";
import { Parser, Language, Query } from "web-tree-sitter";
async function main() {
  await Parser.init({
    locateFile(scriptName: string, scriptDirectory: string) {
      return scriptName;
    },
  });
  const cpp = await Language.load("tree-sitter-cpp.wasm");

  const parser = new Parser();
  parser.setLanguage(cpp);
  const tree = parser.parse(code);
  console.log(tree);
  console.log(tree?.rootNode);
  if (!tree!.rootNode.hasError) {
    console.log("没有错误");
    return;
  } else {
    console.log("存在错误");
  }
  // 异常查询
  const queryString = "(ERROR) @error-node (MISSING) @missing-node"; 

  const language = parser.language;
  const query = new Query(language!, queryString);
  const root = tree!.rootNode;
  // Execute query and get matches
  const matches = query.matches(root!);

  const errorNodes = [];
  for (const match of matches) {
    for (const capture of match.captures) {
      errorNodes.push(capture.node);
    }
  }

  console.log(errorNodes);
  if (errorNodes.length) {
    const { row, column } = errorNodes[0]!.startPosition;
    console.log(`${row + 1}行,${column + 1}列存在错误`);
  }
}
main();

错误节点捕获:

image.png