前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >浅尝antlr4

浅尝antlr4

作者头像
Kevinello
发布2022-08-19 11:08:46
1.7K0
发布2022-08-19 11:08:46
举报
文章被收录于专栏:Kevinello的技术小站

浅尝Antlr4

前言

Antlr是什么

In a word, 多源语言多目标语言的一个语法分析框架

以下是官方文档的解释:

ANTLR(ANother Tool for Language Recognition)是一个功能强大的解析器生成器,用于读取,处理,执行或翻译结构化文本或二进制文件。它被广泛用于构建语言,工具和框架。ANTLR从语法上生成一个解析器,该解析器可以构建解析树,还可以生成一个侦听器接口(或访问者),从而可以轻松地对所关注短语的识别做出响应。 ANTLR (ANother Tool for Language Recognition) is a powerful parser generator for reading, processing, executing, or translating structured text or binary files. It’s widely used to build languages, tools, and frameworks. From a grammar, ANTLR generates a parser that can build parse trees and also generates a listener interface (or visitor) that makes it easy to respond to the recognition of phrases of interest.

Github项目地址

这次使用antlr的诱因是whosbug中使用的ctags(另一个语法分析器)只对c系语言支持较好,对java等语言的支持欠佳(甚至可以说很差了),为了whosbug的鲁棒性我认为还是有必要换一个语法分析器的

几个需要了解的词

AST:抽象语法树

target language:antlr可以根据源语言的.g4文件生成不同语言(target language)的分析代码 各种target language的文档(有些很简略)

Lexer:antlr中的词法分析器(词法分析

Parser:antlr中的语法分析器(语法分析

Listener:是antlr中的独有概念,与传统源码分析不同,antlr提供Listener这一API供用户自定义自己的分析器,这种方式可以很大程度上使语法更易于阅读(按每位用户自己的设计),同时使得它们能避免与特定的应用程序耦合在一起,以下是官方的解释(官方文档):

其它相关概念见antlr在github上的官方文档

安装antlr4

官方文档

安装Java(1.7版或更高版本),这个不会就入土8

下载antlr4

添加antlr-4.9-complete.jarCLASSPATH

将其放入.bash_profile,就不需要每次都改环境变量了

为ANTLR Tool和 TestRig创建alias:

输入antlr4验证一下安装情况:

获取targer language为python的分析模块

获取.g4语法文件

ANTLR的GitHub项目中提供了用于不同语言的语法文件(.g4)

官方g4文件收录库

这次的需求先重点解决java的语法分析问题,所以一开始我找到了java9的g4文件,但生成分析代码的时候报错了: Incorrectly generated code for Python 3 target,google了一番找到了对应的issue:https://github.com/antlr/grammars-v4/issues/739

issue739
issue739

更换成https://github.com/antlr/grammars-v4/tree/master/java/java中的.g4文件后就没问题了

生成分析模块

按官方文档生成分析模块源码:

代码语言:javascript
复制
antlr4 -Dlanguage=Python3 JavaLexer.g4
antlr4 -Dlanguage=Python3 JavaParser.g4

生成结果见下图:

生成结果
生成结果

其中JavaLexer.py,JavaParser.py,JavaParserListener.py是我们需要重点关注的

安装antlr4-python3-runtime

这步没什么好说的,直接pip install完事

代码语言:javascript
复制
pip install antlr4-python3-runtime

创建自定义Listener

我的目录结构如下:

analyzer.py

分析模块入口,main所在位置,废话不多说,上码

代码语言:javascript
复制
import logging.config
from ast_java.ast_processor import AstProcessor
from ast_java.basic_info_listener import BasicInfoListener

logging.config.fileConfig('log/utiltools_log.conf')
AST_ANALYZER = AstProcessor(logging, BasicInfoListener())


def analyze_java(target_file_path):
    return AST_ANALYZER.execute(target_file_path)


if __name__ == '__main__':
    analyze_java('testfiles/java/AllInOne7.java')

ast_processor.py

调用antlr的语法分析模块,生成AST,供自定义Listener使用:

代码语言:javascript
复制
from antlr4 import FileStream, CommonTokenStream, ParseTreeWalker
from ast_java.JavaLexer import JavaLexer
from ast_java.JavaParser import JavaParser
from pprint import pformat


class AstProcessor:

    def __init__(self, logging, listener):
        self.logging = logging
        self.logger = logging.getLogger(self.__class__.__name__)
        self.listener = listener

    def execute(self, input_source):
        parser = JavaParser(CommonTokenStream(JavaLexer(FileStream(input_source, encoding="utf-8"))))
        walker = ParseTreeWalker()
        walker.walk(self.listener, parser.compilationUnit())
        self.logger.debug('Display all data extracted by AST. \n' + pformat(self.listener.ast_info, width=160))
        return self.listener.ast_info

basic_info_listener.py

这部分就完全是自定义的了,同时也是源码分析的关键,在这部分设计的分析模式决定了分析结果的数据结构

简单来说就是继承JavaParserListener,然后扩展自己需要的内容

具体的使用还是需要自己去读一下源码,这里放一下我写的作为参考:

代码语言:javascript
复制
from ast_java.JavaParserListener import JavaParserListener
from ast_java.JavaParser import JavaParser


class BasicInfoListener(JavaParserListener):

    def __init__(self):
        self.call_methods = []
        self.ast_info = {
            'packageName': '',
            'className': '',
            'implements': [],
            'extends': '',
            'imports': [],
            'fields': [],
            'methods': []
        }

    # Enter a parse tree produced by JavaParser#packageDeclaration.
    def enterPackageDeclaration(self, ctx: JavaParser.PackageDeclarationContext):
        self.ast_info['packageName'] = ctx.qualifiedName().getText()

    # Enter a parse tree produced by JavaParser#importDeclaration.
    def enterImportDeclaration(self, ctx: JavaParser.ImportDeclarationContext):
        import_class = ctx.qualifiedName().getText()
        self.ast_info['imports'].append(import_class)

    # Enter a parse tree produced by JavaParser#methodDeclaration.
    def enterMethodDeclaration(self, ctx: JavaParser.MethodDeclarationContext):

        print("Start line: {0} | End line: {1} | Method name: {2}".format(ctx.start.line, ctx.methodBody().stop.line, ctx.getChild(1).getText()))
        self.call_methods = []

    # Exit a parse tree produced by JavaParser#methodDeclaration.
    def exitMethodDeclaration(self, ctx: JavaParser.MethodDeclarationContext):
        c1 = ctx.getChild(0).getText()  # ---> return type
        c2 = ctx.getChild(1).getText()  # ---> method name
        params = self.parse_method_params_block(ctx.getChild(2))

        method_info = {
            'startLine': ctx.start.line,
            'endLine': ctx.methodBody().stop.line,
            'returnType': c1,
            'methodName': c2,
            'params': params,
            'depth': ctx.depth(),
            'callMethods': self.call_methods
        }
        self.ast_info['methods'].append(method_info)

    # Enter a parse tree produced by JavaParser#methodCall.
    def enterMethodCall(self, ctx: JavaParser.MethodCallContext):
        line_number = str(ctx.start.line)
        column_number = str(ctx.start.column)
        self.call_methods.append(line_number + ' ' + column_number + ' ' + ctx.parentCtx.getText())

    # Enter a parse tree produced by JavaParser#classDeclaration.
    def enterClassDeclaration(self, ctx: JavaParser.ClassDeclarationContext):
        child_count = int(ctx.getChildCount())
        if child_count == 7:
            # class Foo extends Bar implements Hoge
            # c1 = ctx.getChild(0)  # ---> class
            c2 = ctx.getChild(1).getText()  # ---> class name
            # c3 = ctx.getChild(2)  # ---> extends
            c4 = ctx.getChild(3).getChild(0).getText()  # ---> extends class name
            # c5 = ctx.getChild(4)  # ---> implements
            # c7 = ctx.getChild(6)  # ---> method body
            self.ast_info['className'] = c2
            self.ast_info['implements'] = self.parse_implements_block(ctx.getChild(5))
            self.ast_info['extends'] = c4
        elif child_count == 5:
            # class Foo extends Bar
            # or
            # class Foo implements Hoge
            # c1 = ctx.getChild(0)  # ---> class
            c2 = ctx.getChild(1).getText()  # ---> class name
            c3 = ctx.getChild(2).getText()  # ---> extends or implements

            # c5 = ctx.getChild(4)  # ---> method body
            self.ast_info['className'] = c2
            if c3 == 'implements':
                self.ast_info['implements'] = self.parse_implements_block(ctx.getChild(3))
            elif c3 == 'extends':
                c4 = ctx.getChild(3).getChild(0).getText()  # ---> extends class name or implements class name
                self.ast_info['extends'] = c4
        elif child_count == 3:
            # class Foo
            # c1 = ctx.getChild(0)  # ---> class
            c2 = ctx.getChild(1).getText()  # ---> class name
            # c3 = ctx.getChild(2)  # ---> method body
            self.ast_info['className'] = c2

    # Enter a parse tree produced by JavaParser#fieldDeclaration.
    def enterFieldDeclaration(self, ctx: JavaParser.FieldDeclarationContext):
        field = {
            'fieldType': ctx.getChild(0).getText(),
            'fieldDefinition': ctx.getChild(1).getText()
        }
        self.ast_info['fields'].append(field)

    def parse_implements_block(self, ctx):
        implements_child_count = int(ctx.getChildCount())
        result = []
        if implements_child_count == 1:
            impl_class = ctx.getChild(0).getText()
            result.append(impl_class)
        elif implements_child_count > 1:
            for i in range(implements_child_count):
                if i % 2 == 0:
                    impl_class = ctx.getChild(i).getText()
                    result.append(impl_class)
        return result

    def parse_method_params_block(self, ctx):
        params_exist_check = int(ctx.getChildCount())
        result = []
        # () ---> 2
        # (Foo foo) ---> 3
        # (Foo foo, Bar bar) ---> 3
        # (Foo foo, Bar bar, int count) ---> 3
        if params_exist_check == 3:
            params_child_count = int(ctx.getChild(1).getChildCount())
            if params_child_count == 1:
                param_type = ctx.getChild(1).getChild(0).getChild(0).getText()
                param_name = ctx.getChild(1).getChild(0).getChild(1).getText()
                param_info = {
                    'paramType': param_type,
                    'paramName': param_name
                }
                result.append(param_info)
            elif params_child_count > 1:
                for i in range(params_child_count):
                    if i % 2 == 0:
                        param_type = ctx.getChild(1).getChild(i).getChild(0).getText()
                        param_name = ctx.getChild(1).getChild(i).getChild(1).getText()
                        param_info = {
                            'paramType': param_type,
                            'paramName': param_name
                        }
                        result.append(param_info)
        return result

这里简单说明一下几个重要的点,便于理解:

  • BasicInfoListener继承JavaParserListener,供用户自定义遍历AST的方法
  • ast_info为分析结果dict
  • JavaParserListener覆盖在BasicInfoListener中定义的挂钩点分析方法,并实现其自己的分析过程 例如,enterPackageDeclaration,顾名思义,它在Java源码包定义的开头(即enter)被调用 参数ctx(上下文)具有不同的类型,但是由于存在父类,因此任何上下文类都可以访问语法解析所需的基本信息(通过getChild,getParent等方法)

还有很多的细节信息其实都有,这里就不一一赘述(都在源码里啦)

测试

到这里分析模块就完成啦,用官方提供的Java被测源码试一下效果8

命令行输出:

ast_info:

Done(antlr比ctags不知道好用多少倍)

本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2020-11-30,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 浅尝Antlr4
    • 前言
      • Antlr是什么
      • 几个需要了解的词
    • 安装antlr4
      • 获取targer language为python的分析模块
        • 获取.g4语法文件
        • 生成分析模块
        • 安装antlr4-python3-runtime
      • 创建自定义Listener
        • analyzer.py
        • ast_processor.py
        • basic_info_listener.py
      • 测试
      领券
      问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档