阅读 936

Antlr4 前端应用与实践

1. 背景

在前端开发中,通常提到语法解析等功能,一般都是由后端负责提供接口,前端调用;或者如果要执行也是直接扔给服务端去处理;但是在一些特殊的情况下,譬如使用编辑器的时候,往往需要具备一些错误提醒、自动完成等的功能;虽然市面上也有现成的编辑器可以直接拿来使用,但是在一些特殊或者复杂的业务场景下时,这些编辑器都不太能满足我们的需求,这时候就需要我们进行定制化开发了,比如在我们业务中需要使用到的SQL编辑器。

2. 初识Antlr4

Antlr4简介

Antlr4是ANother Tool for Language Recognition即另一个语言识别工具,官方介绍为Antlr4是一款强大的解析器生成工具,可用来读取、处理、执行和翻译结构化文本或二进制文件。 Antlr4生成的解析器包含了词法分析程序和语法分析程序,词法分析程序是将输入的代码字符序列转换成标记(Token)序列的程序,而语法分析程序则是将标记序列转换成语法树的程序。

Antlr4安装

关于Antlr4的安装,大家可以参照Github上的Getting Started with ANTLR v4,这里不作详细介绍。只简单列举一下macos环境下的安装步骤:

  • 下载antlr包
$ cd /usr/local/lib
$ curl -O https://www.antlr.org/download/antlr-4.7.1-complete.jar
复制代码
  • 添加安装包到CLASSPATH:
$ export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH"
复制代码
  • 创建 ANTLR Tool、 TestRig别名
$ alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
$ alias grun='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig'
复制代码
  • 验证是否正确安装
$ java org.antlr.v4.Tool
复制代码

3. 使用步骤

环境搭建好以后,我们基本就可以按照以下三个步骤来使用我们的antlr4了。

  • 自定义g4语法文件

ANTLR4 的语法规则分为词法(Lexer)规则和语法(Parser)规则,词法规则定义了怎么将代码字符串序列转换成标记序列;语法规则定义怎么将标记序列转换成语法树。通常,词法规则的规则名以大写字母命名,而语法规则的规则名以小写字母开始。主流语言的 ANTLR4 语法定义可以到语法仓库中找到。

  • 使用 ANTLR 4 生成目标编程语言代码的词法分析器(Lexer)和语法分析器(Parser),支持的编程语言有:Java、JavaScript、Python、C 和 C++ 等;
  • 遍历 AST(Abstract Syntax Tree 抽象语法树),ANTLR 4 支持两种模式:访问者模式(Visitor)和监听器模式(Listener)

4. 实现DSQL编辑器

什么是DSQL?

跨数据库查询(DSQL)为不同环境下的在线异构数据源,提供及时的关联查询服务。不论数据库是MySQL、SQLServer、PostgreSQL还是Redis,不论数据库实例部署在哪个region哪个环境,通过一条SQL就能实现这些数据库之间的关联查询。交互体验可以前往 dms.aliyun.com/ 帮助文档如下:help.aliyun.com/document_de…

接下来就以DSQL里面的sql编辑器为例子,详细介绍下antlr4的具体实现。先来看下最终的实现效果:

avatar

编写g4文件

由于文件太长,在此就不具体展示了;文件命名为SqlBase.g4

grammar SqlBase;

tokens {
    DELIMITER
}

singleStatement
    : statement EOF
    ;

singleExpression
    : expression EOF
    ;

statement
        : query                                                            #statementDefault
    | USE schema=identifier                                            #use
    | USE catalog=identifier '.' schema=identifier                     #use
    | CREATE SCHEMA (IF NOT EXISTS)? qualifiedName
    ....
复制代码

Java语法树生成

在SqlBase.g4目录运行 $ antlr4 SqlBase.g4   即可生成对应的lexer、parse及java解析程序(注意这里的antlr4命令即上文通过别名生成的命令,等价于org.antlr.v4.Tool)

avatar
编译java程序,同样在该目录运行 $ javac SqlBase*.java 此时会生成一堆编译后的class文件,下面我们就可以输入对应的DSQL语法,检查我们的语法树能否正确生成。首先,我们先输入正确的SQL,如下:

$ grun SqlBase statement -tree
(Now enter some SQL like below)
SELECT * FROM `adb_mysql_dblink`.`adb_mysql_1124qie`.`courses`
(now,do:)
^D
(The output:)
((statement (query (queryNoWith (queryTerm (queryPrimary (querySpecification SELECT (selectItem *) FROM (relation (sampledRelation (aliasedRelation (relationPrimary (qualifiedName (identifier `adb_mysql_dblink`) . (identifier `adb_mysql_1124qie`) . (identifier `courses`)))))))))))))

// gui
$ grun SqlBase statement -gui
SELECT * FROM `adb_mysql_dblink`.`adb_mysql_1124qie`.`courses`
^D
复制代码

gui 语法树

avatar

假如我们的SQL不符合规范呢?

$ grun SqlBase statement -gui
(Now enter some SQL like below)
SELECT * FROM aa where id = 1
(now,do:)
^D
(The output:)
line 1:14 mismatched input 'a' expecting {'(', 'ADD', 'ALL', 'ANALYZE', 'ANY', 'ARRAY', 'ASC', 'AT', 'BERNOULLI', 'CALL', 'CASCADE', 'CATALOGS', 'COLUMN', 'COLUMNS', 'COMMENT', 'COMMIT', 'COMMITTED', 'CURRENT', 'DATA', 'DATE', 'DAY', 'DESC', 'DISTRIBUTED', 'EXCLUDING', 'EXPLAIN', 'FILTER', 'FIRST', 'FOLLOWING', 'FORMAT', 'FUNCTIONS', 'GRANT', 'GRANTS', 'GRAPHVIZ', 'HOUR', 'IF', 'INCLUDING', 'INPUT', 'INTERVAL', 'ISOLATION', 'LAST', 'LATERAL', 'LEVEL', 'LIMIT', 'LOGICAL', 'MAP', 'MINUTE', 'MONTH', 'NFC', 'NFD', 'NFKC', 'NFKD', 'NO', 'NULLIF', 'NULLS', 'ONLY', 'OPTION', 'ORDINALITY', 'OUTPUT', 'OVER', 'PARTITION', 'PARTITIONS', 'POSITION', 'PRECEDING', 'PRIVILEGES', 'PROPERTIES', 'PUBLIC', 'RANGE', 'READ', 'RENAME', 'REPEATABLE', 'REPLACE', 'RESET', 'RESTRICT', 'REVOKE', 'ROLLBACK', 'ROW', 'ROWS', 'SCHEMA', 'SCHEMAS', 'SECOND', 'SERIALIZABLE', 'SESSION', 'SET', 'SETS', 'SHOW', 'SOME', 'START', 'STATS', 'SUBSTRING', 'SYSTEM', 'TABLES', 'TABLESAMPLE', 'TEXT', 'TIME', 'TIMESTAMP', 'TO', 'TRANSACTION', 'TRY_CAST', 'TYPE', 'UNBOUNDED', 'UNCOMMITTED', 'UNNEST', 'USE', 'VALIDATE', 'VERBOSE', 'VIEW', 'WORK', 'WRITE', 'YEAR', 'ZONE', IDENTIFIER, DIGIT_IDENTIFIER, BACKQUOTED_IDENTIFIER}
复制代码

生成了错误的AST树

avatar
` SELECT * FROM aa where id = 1` 虽然在MySQL的语法中是合法的,但是在我们的DSQL语法中却是错误的,FROM关键字后面必须是上面括号里面的符号或者关键字,语法当中给出了错误提示。

通过以上命令行的方式,可以对我们编写的g4文件做测试,当然你也可以通过 Idea 的antlr4插件来生成查看,这里就不再讲述了,大家可以去尝试一下。

生成js词法解析器和语法解析器

终于到了我们本文的重点环节了,如何在前端生成解析文件,前端的使用其实也很简单,可以参看官网教程github.com/antlr/antlr… 这里我重点讲如何使用,运行如下命令

$ antlr4 -Dlanguage=JavaScript MyGrammar.g4
复制代码

对应的解析文件如下

avatar

接下来就可以通过编码来生成ParseTree语法树了

/* eslint-disable react-hooks/rules-of-hooks */
import { SqlBaseLexer } from './antlr4/SqlBaseLexer';
import { SqlBaseParser } from './antlr4/SqlBaseParser';
var SqlBaseListener = require('./antlr4/SqlBaseListener').SqlBaseListener;

var antlr4 = require('antlr4');

function ParseTree = (sql) => {
  // sql = "SELECT * FROM `adb_mysql_dblink`.`adb_mysql_1124qie`.`courses`"
  const chars = new antlr4.InputStream(sql);
  const lexer = new SqlBaseLexer(chars);
  const tokens  = new antlr4.CommonTokenStream(lexer);
  const parser = new SqlBaseParser(tokens);
  parser.buildParseTrees = true;
  const tree = parser.statement();
  const walker = new tree.ParseTreeWalker();
  // 自定义的监听器,采用Listener模式
  const extractor = new DsqlListener({
    enterAliasedRelation: this.enterAliasedRelation, // this.enterAliasedRelatio是具体的业务逻辑
    enterQualifiedName: this.enterQualifiedName,
  });
  walker.walk(extractor, tree);
}
复制代码

两种访问ParseTree的方法

  • Listener模式

1) Listener模式会由ANTLR提供的walker对象自动调用;在遇到不同的节点中,会调用提供的listener的不同方法 2)Listener模式没有返回值,只能用一些变量来存储中间值 3)Listener模式是对整棵树的遍历

  • Visitor模式

1)visitor需要自己来指定访问特定类型的节点,在使用过程中,只需要对感兴趣的节点实现visit方法即可 2)visitor模式可以自定义返回值 3)visitor模式是对指定节点的访问

根据我们的业务场景,我们选择的是Listener模式,我们定义了自己的监听器 DsqlListener,部分代码如下:

class DsqlListener extends SqlBaseListener {
  constructor(opts) {
    super();

    this.configs = opts || {};
  }

  enterQualifiedName(ctx) {
    const { enterQualifiedName } = this.configs;
    setFunction(enterQualifiedName, ctx);
  }

  enterAliasedRelation(ctx) {
    const { enterAliasedRelation } = this.configs;
    setFunction(enterAliasedRelation, ctx);
  }
}

function setFunction(fun, params) {
  if (fun && typeof fun === 'function') {
    fun(params);
  }
}

export default DsqlListener;
复制代码

注意:这里的 enterQualifiedName 和 enterAliasedRelation等方法名都是我们在定义g4文件的时候指定生成的。

5. 结束

以上就是我们利用antlr4实现DSQL编辑器语法智能提示的大致思路,想要了解更多内部实现欢迎加入我们吧~

参考资料: github.com/antlr/gramm… github.com/antlr/antlr… 《antlr4权威指南》