1. 背景

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

2. 初识Antlr4

Antlr4简介

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

Antlr4安装

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

  1. $ cd /usr/local/lib
  2. $ curl -O https://www.antlr.org/download/antlr-4.7.1-complete.jar
  • 添加安装包到CLASSPATH:
  1. $ export CLASSPATH=".:/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH"
  • 创建 ANTLR Tool、 TestRig别名
  1. $ alias antlr4='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" org.antlr.v4.Tool'
  2. $ alias grun='java -Xmx500M -cp "/usr/local/lib/antlr-4.7.1-complete.jar:$CLASSPATH" org.antlr.v4.gui.TestRig'
  • 验证是否正确安装
  1. $ 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就能实现这些数据库之间的关联查询。交互体验可以前往 https://dms.aliyun.com/
帮助文档如下:https://help.aliyun.com/document_detail/127641.html

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

编写g4文件

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

  1. grammar SqlBase;
  2. tokens {
  3. DELIMITER
  4. }
  5. singleStatement
  6. : statement EOF
  7. ;
  8. singleExpression
  9. : expression EOF
  10. ;
  11. statement
  12. : query #statementDefault
  13. | USE schema=identifier #use
  14. | USE catalog=identifier '.' schema=identifier #use
  15. | CREATE SCHEMA (IF NOT EXISTS)? qualifiedName
  16. ....

Java语法树生成

在SqlBase.g4目录运行 $ antlr4 SqlBase.g4 即可生成对应的lexer、parse及java解析程序(注意这里的antlr4命令即上文通过别名生成的命令,等价于org.antlr.v4.Tool)
avatar
编译java程序,同样在该目录运行 $ javac SqlBase*.java 此时会生成一堆编译后的class文件,下面我们就可以输入对应的DSQL语法,检查我们的语法树能否正确生成。首先,我们先输入正确的SQL,如下:

  1. $ grun SqlBase statement -tree
  2. (Now enter some SQL like below)
  3. SELECT * FROM `adb_mysql_dblink`.`adb_mysql_1124qie`.`courses`
  4. (now,do:)
  5. ^D
  6. (The output:)
  7. ((statement (query (queryNoWith (queryTerm (queryPrimary (querySpecification SELECT (selectItem *) FROM (relation (sampledRelation (aliasedRelation (relationPrimary (qualifiedName (identifier `adb_mysql_dblink`) . (identifier `adb_mysql_1124qie`) . (identifier `courses`)))))))))))))
  8. // gui
  9. $ grun SqlBase statement -gui
  10. SELECT * FROM `adb_mysql_dblink`.`adb_mysql_1124qie`.`courses`
  11. ^D

gui 语法树
avatar

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

  1. $ grun SqlBase statement -gui
  2. (Now enter some SQL like below)
  3. SELECT * FROM aa where id = 1
  4. (now,do:)
  5. ^D
  6. (The output:)
  7. 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`<br />SELECT * FROM aa where id = 1` 虽然在MySQL的语法中是合法的,但是在我们的DSQL语法中却是错误的,FROM关键字后面必须是上面括号里面的符号或者关键字,语法当中给出了错误提示。

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

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

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

  1. $ antlr4 -Dlanguage=JavaScript MyGrammar.g4

对应的解析文件如下
avatar

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

  1. /* eslint-disable react-hooks/rules-of-hooks */
  2. import { SqlBaseLexer } from './antlr4/SqlBaseLexer';
  3. import { SqlBaseParser } from './antlr4/SqlBaseParser';
  4. var SqlBaseListener = require('./antlr4/SqlBaseListener').SqlBaseListener;
  5. var antlr4 = require('antlr4');
  6. function ParseTree = (sql) => {
  7. // sql = "SELECT * FROM `adb_mysql_dblink`.`adb_mysql_1124qie`.`courses`"
  8. const chars = new antlr4.InputStream(sql);
  9. const lexer = new SqlBaseLexer(chars);
  10. const tokens = new antlr4.CommonTokenStream(lexer);
  11. const parser = new SqlBaseParser(tokens);
  12. parser.buildParseTrees = true;
  13. const tree = parser.statement();
  14. const walker = new tree.ParseTreeWalker();
  15. // 自定义的监听器,采用Listener模式
  16. const extractor = new DsqlListener({
  17. enterAliasedRelation: this.enterAliasedRelation, // this.enterAliasedRelatio是具体的业务逻辑
  18. enterQualifiedName: this.enterQualifiedName,
  19. });
  20. walker.walk(extractor, tree);
  21. }

两种访问ParseTree的方法

  • Listener模式

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

  • Visitor模式

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

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

  1. class DsqlListener extends SqlBaseListener {
  2. constructor(opts) {
  3. super();
  4. this.configs = opts || {};
  5. }
  6. enterQualifiedName(ctx) {
  7. const { enterQualifiedName } = this.configs;
  8. setFunction(enterQualifiedName, ctx);
  9. }
  10. enterAliasedRelation(ctx) {
  11. const { enterAliasedRelation } = this.configs;
  12. setFunction(enterAliasedRelation, ctx);
  13. }
  14. }
  15. function setFunction(fun, params) {
  16. if (fun && typeof fun === 'function') {
  17. fun(params);
  18. }
  19. }
  20. export default DsqlListener;

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

5. 结束

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

参考资料:
https://github.com/antlr/grammars-v4
https://github.com/antlr/antlr4
《antlr4权威指南》