/Whosbug-CI

基于Antlr语法解析能力的堆栈型bug责任人归属微服务

Primary LanguageGoDo What The F*ck You Want To Public LicenseWTFPL

Whosbug 新语法解析/算法记录

语法解析调整

放弃语法树/方法调用链的解析模式

​ 使用 Antlr 解析静态代码语法树存在先天缺陷:难以解析跨文件调用关系

​ 解析的粒度调整为:方法、类的定义行

​ 方法内的调用关系调整为仅解析名

​ Object 的唯一标识方法:path、masterObject、name(包含继承、拓展关系)

TODO: 方法重载问题

改动后,语法解析的目标能力:

  • 解析每个方法、类的声明/定义
  • 解析获得每个方法、类的声明包含关系(定义链)

Java 的变更:

  • Java 解析能力裁剪,只保留获取声明链的方法

改动后的 Java 解析数据结构:

type JavaTreeShapeListener struct {
 	AstInfoList AstResType
}

type AstResType struct {
	Classes []ClassInfoType
	Methods []MethodInfoType
}

type ClassInfoType struct {
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	ClassName string `json:"class_name"`
	Extends   string `json:"extends"`
}

type MethodInfoType struct {
	StartLine  int    `json:"start_line"`
	EndLine    int    `json:"end_line"`
	MethodName string `json:"method_name"`
	Parameters string `json:"parameters"`
}

存储的信息包含:

    • 类名(包含定义链)
    • 起止行号
    • 继承的父类
  • 方法
    • 方法名(包含定义链)
    • 起止行号
    • 参数

手动实现的方法:

  • getParamsOfMethod(ctx *javaparser.MethodDeclarationContext)

    返回方法的参数

func getParamsOfMethod(ctx *javaparser.MethodDeclarationContext) (params string) {
	if ctx.FormalParameters() != nil {
		if ctx.FormalParameters().(*javaparser.FormalParametersContext).FormalParameterList() != nil {
			for index, item := range ctx.FormalParameters().(*javaparser.FormalParametersContext).FormalParameterList().(*javaparser.FormalParameterListContext).AllFormalParameter() {
				params += item.(*javaparser.FormalParameterContext).TypeType().GetText() + " "
				params += item.(*javaparser.FormalParameterContext).VariableDeclaratorId().GetText()
				if index != len(ctx.FormalParameters().(*javaparser.FormalParametersContext).FormalParameterList().(*javaparser.FormalParameterListContext).AllFormalParameter())-1 {
					params += ", "
				}
			}
		}
	}
	return
}
  • findJavaDeclarationChain(ctx antlr.ParseTree)

    获取对象的定义链

func findJavaDeclarationChain(ctx antlr.ParseTree) (chainName string) {
	currentContext := ctx.GetParent()
	for {
		if _, ok := currentContext.(*javaparser.ClassDeclarationContext); ok {
			chainName = currentContext.(*javaparser.ClassDeclarationContext).IDENTIFIER().GetText() + "." + chainName
		}
		if _, ok := currentContext.(*javaparser.MethodDeclarationContext); ok {
			chainName = currentContext.(*javaparser.MethodDeclarationContext).IDENTIFIER().GetText() + "." + chainName
		}
		currentContext = currentContext.GetParent()
		if currentContext == nil {
			break
		}
	}
	return
}

Golang 的变更:

  • Golang 解析能力裁剪,只保留获取声明链的能力

改动后的 Golang 解析数据结构:

type GoTreeShapeListener struct {
	AstInfoList AstResType
}

type AstResType struct {
	Classes []ClassInfoType
	Methods []MethodInfoType
}

type ClassInfoType struct {
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	ClassName string `json:"class_name"`
	Extends   string `json:"extends"`
}

type MethodInfoType struct {
	StartLine  int    `json:"start_line"`
	EndLine    int    `json:"end_line"`
	MethodName string `json:"method_name"`
	Parameters string `json:"parameters"`
}

手动实现的方法:

  • getFunctionAndMethodParams(ctx antlr.ParseTree)

    获得方法或函数的参数

func getFunctionAndMethodParams(ctx antlr.ParseTree) (params string) {
	if _, ok := ctx.(*golang.FunctionDeclContext); ok {
		if ctx.(*golang.FunctionDeclContext).Signature().(*golang.SignatureContext).Parameters() != nil {
			for index, item := range ctx.(*golang.FunctionDeclContext).Signature().(*golang.SignatureContext).Parameters().(*golang.ParametersContext).AllParameterDecl() {
				params += item.(*golang.ParameterDeclContext).IdentifierList().GetText() + " "
				params += item.(*golang.ParameterDeclContext).Type_().GetText()
				if index != len(ctx.(*golang.FunctionDeclContext).Signature().(*golang.SignatureContext).Parameters().(*golang.ParametersContext).AllParameterDecl())-1 {
					params += ", "
				}
			}
		}
	}
	if _, ok := ctx.(*golang.MethodDeclContext); ok {
		if ctx.(*golang.MethodDeclContext).Signature().(*golang.SignatureContext).Parameters() != nil {
			for index, item := range ctx.(*golang.MethodDeclContext).Signature().(*golang.SignatureContext).Parameters().(*golang.ParametersContext).AllParameterDecl() {
				params += item.(*golang.ParameterDeclContext).IdentifierList().GetText() + " "
				params += item.(*golang.ParameterDeclContext).Type_().GetText()
				if index != len(ctx.(*golang.MethodDeclContext).Signature().(*golang.SignatureContext).Parameters().(*golang.ParametersContext).AllParameterDecl())-1 {
					params += ", "
				}
			}
		}
	}
	return
}
  • getRecvrTypes(ctx *golang.MethodDeclContext)

    获得方法所属的结构体类型

func getRecvrTypes(ctx *golang.MethodDeclContext) (types []string) {
	temp := ctx.Receiver().(*golang.ReceiverContext).Parameters().(*golang.ParametersContext).AllParameterDecl()
	for _, item := range temp {
		types = append(types, item.(*golang.ParameterDeclContext).Type_().GetText())
	}
	return
}

Kotlin 的变更:

  • Kotlin 解析能力裁剪,只保留获取声明链的能力

改动后的 Kotlin 解析数据结构:

type KotlinTreeShapeListener struct {
	AstInfoList AstResType
}

type AstResType struct {
	Classes []ClassInfoType
	Methods []MethodInfoType
}

type ClassInfoType struct {
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	ClassName string `json:"class_name"`
	Extends   string `json:"extends"`
}

type MethodInfoType struct {
	StartLine  int    `json:"start_line"`
	EndLine    int    `json:"end_line"`
	MethodName string `json:"method_name"`
	Parameters string `json:"parameters"`
}

手动实现的方法:

  • findKotlinClassExtends(ctx *kotlin.ClassDeclarationContext)

    获得类的继承类名

func findKotlinClassExtends(ctx *kotlin.ClassDeclarationContext) (extends string) {
	if ctx.DelegationSpecifiers() != nil {
		tempCtx := ctx.DelegationSpecifiers().(*kotlin.DelegationSpecifiersContext)
		if tempCtx.AllDelegationSpecifier() != nil {
			if tempCtx.DelegationSpecifier(0).(*kotlin.DelegationSpecifierContext).ConstructorInvocation() != nil {
				tempCtx2 := tempCtx.DelegationSpecifier(0).(*kotlin.DelegationSpecifierContext).ConstructorInvocation()
				extends = tempCtx2.(*kotlin.ConstructorInvocationContext).UserType().GetText()
			}
		}
	}
	return
}
  • findKotlinDeclarationChain(ctx antlr.ParseTree)

    获得定义链

func findKotlinDeclarationChain(ctx antlr.ParseTree) (chainName string) {
	currentContext := ctx.GetParent()
	for {
		if _, ok := currentContext.(*kotlin.ClassDeclarationContext); ok {
			chainName = currentContext.(*kotlin.ClassDeclarationContext).SimpleIdentifier().GetText() + "." + chainName
		}
		if _, ok := currentContext.(*kotlin.FunctionDeclarationContext); ok {
			chainName = currentContext.(*kotlin.FunctionDeclarationContext).Identifier().GetText() + "." + chainName
		}
		currentContext = currentContext.GetParent()
		if currentContext == nil {
			break
		}
	}
	return
}

Cpp 的变更:

  • Cpp 解析能力裁剪,只保留获取声明链的能力
  • 不解析复杂环,只解析简单定义/声明
  • 方法名的解析手动实现 更改后的数据结构:
type CppTreeShapeListener struct {
	AstInfoList AstResType
}

type AstResType struct {
	Classes []ClassInfoType
	Methods []MethodInfoType
}

type ClassInfoType struct {
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	ClassName string `json:"class_name"`
	Extends   string `json:"extends"`
}

type MethodInfoType struct {
	StartLine  int    `json:"start_line"`
	EndLine    int    `json:"end_line"`
	MethodName string `json:"method_name"`
	Parameters string `json:"parameters"`
}

Js 解析调整:

Antlr 语法树字段记录:

对于一个 Js 函数:

  • 类型:FunctionDeclarationContext
  • 子字段
    • "function"
    • 函数名 IdentifierContext
    • "("
    • 参数 FormalParameterListContext
    • ")"
    • 函数体 FunctionBodyContext

对于一个 Js 类:

  • 类型:ClassDeclarationContext
  • 子字段
    • "Class"
    • 类名 IdentifierContext
    • "("
    • 当含有 extends 字段时,有ClassTailContext
    • SingleExpressionContext/ArgumentsExpressionContext,逐层遍历取SingleExpression()可以得到IdentifierExpressionContext作为准确的 Extends 名
    • 类内部字段

手动实现的方法:

  • getExtendIdentifier(ctx *javascript.ClassDeclarationContext)(extend string)

    返回类定义中通过 Extends 继承的类名

func getExtendIdentifier(ctx *javascript.ClassDeclarationContext) (extend string) {
	if ctx.ClassTail() != nil {
		tempCtx := ctx.ClassTail().(*javascript.ClassTailContext).SingleExpression()
		for {
			if tempCtx == nil {
				return
			}
			if _, ok := tempCtx.(*javascript.ArgumentsExpressionContext); ok {
				tempCtx = tempCtx.(*javascript.ArgumentsExpressionContext).SingleExpression()
				continue
			}
			if _, ok := tempCtx.(*javascript.IdentifierExpressionContext); ok {
				extend = tempCtx.(*javascript.IdentifierExpressionContext).GetText()
				return
			}
			return
		}
	}
	return
}
  • findJsDeclChain(ctx antlr.ParseTree) (chain string)

    返回类/函数定义的定义链关系(所属类、方法等)

func findJsDeclChain(ctx antlr.ParseTree) (chain string) {
	tempCtx := ctx.GetParent()
	for {
		if _, ok := tempCtx.(*javascript.ClassDeclarationContext); ok {
			chain = tempCtx.(*javascript.ClassDeclarationContext).Identifier().GetText() + "." + chain
		}
		if _, ok := tempCtx.(*javascript.FunctionDeclarationContext); ok {
			chain = tempCtx.(*javascript.FunctionDeclarationContext).Identifier().GetText() + "." + chain
		}
		tempCtx = tempCtx.GetParent()
		if tempCtx == nil {
			return
		}
	}
}

更改后的数据结构:

type JavascriptTreeShapeListener struct {
	AstInfoList AstResType
}

type AstResType struct {
	Classes []ClassInfoType
	Methods []MethodInfoType
}

type ClassInfoType struct {
	StartLine int    `json:"start_line"`
	EndLine   int    `json:"end_line"`
	ClassName string `json:"class_name"`
	Extends   string `json:"extends"`
}

type MethodInfoType struct {
	StartLine  int    `json:"start_line"`
	EndLine    int    `json:"end_line"`
	MethodName string `json:"method_name"`
	Parameters string `json:"parameters"`
}

数据重整&插件数据流改造

我们可以看到,经过改造后的几种语言的解析数据结构已经实际统一,我们可以对其进行改造, 共用同一个类型。

改造嵌套类型,函数antlrAnalysis(diffText string, langMode string) (result AstResType)直接返回AstResType类型的结构体:

type AstResType struct {
	Classes []ClassInfoType
	Methods []MethodInfoType
}

antlrAnalysis的修改直接影响到addObjectFromChangeLineNumber()findChangedMethod()的传入参数类型,需要修改其逻辑/数据流:

type ObjectInfoType struct {
	CommitHash          string 		`json:"hash"`
	Id                  string 		`json:"object_name"`
	OldId               string 		`json:"old_object_name"`
	FilePath            string 		`json:"path"`
	OldLineCount        int    		`json:"old_line_count"`
	NewLineCount        int    		`json:"new_line_count"`
	ChangedOldLineCount int    		`json:"changed_old_line_count"`
	ChangedNewLineCount int    		`json:"changed_new_line_count"`
+	Type                string 		`json:"type"`
-	Calling				[]string
}

算法分析调整

新的算法模式将重点放置在定义链的解析,对堆栈内容的定义位置做出判断

算法侧可以获得的数据变化:

  • 减少的数据:
    • Calling 信息不再支持解析
    • MasterObject 不再支持解析
  • 新增的数据:
    • 完整的方法/类定义链
    • 起止行号的支持
    • 准确可靠的行数变动情况

算法的调整:

  • 不再依赖调用链的解析

    调用链信息解析困难,且难以提高解析准确性

  • 唯一标识对象的模式变更

    仅使用定义链+文件目录定位的模式无法准确定位对象,使用定义链条+文件目录+起始行号进行定位

  • 置信度计算依赖变更

    置信度的计算不再能依赖调用链解析,置信度的维度缩减为依靠时间、行数作为置信度判据

算法变更中的收益:

  • 不必花费极大的代价实现调用链的准确解析
  • 不必等待单次解析的所有 Object 接收完毕再启动算法

    可以在每一个 Object 上传同时进行计算,实时更新置信度

  • 插件解析的数据流可以得到简化

    插件解析的目标数据大幅度减少,可以缩减数据流中的重复流转信息

  • 插件 Antlr 语法解析的效率优化

    Antlr 解析语法树只需要获得定义信息,可以省去复杂大量的链式分析逻辑