使用golang的AST编写定制化lint

2024-09-03 17:20

本文主要是介绍使用golang的AST编写定制化lint,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

什么是lint

(来自wiki)在计算机科学中,lint是一种工具程序的名称,它用来标记源代码中,某些可疑的、不具结构性(可能造成bug)的段落。它是一种静态程序分析工具,最早适用于C语言,在UNIX平台上开发出来。后来它成为通用术语,可用于描述在任何一种计算机程序语言中,用来标记源代码中有疑义段落的工具。

什么是AST

(来自wiki)在计算机科学中,抽象语法树Abstract Syntax Tree,AST),或简称语法树(Syntax tree),是源代码语法结构的一种抽象表示。它以树状的形式表现编程语言的语法结构,树上的每个节点都表示源代码中的一种结构。之所以说语法是“抽象”的,是因为这里的语法并不会表示出真实语法中出现的每个细节。比如,嵌套括号被隐含在树的结构中,并没有以节点的形式呈现;而类似于 if-condition-then 这样的条件跳转语句,可以使用带有三个分支的节点来表示。

(来自网络)在Go语言中,AST是通过Go语言的内置包go/ast来实现的。该包提供了一系列类型和函数,可以用于生成和操作AST。

使用 ast 包提供的一些函数,我们可以非常方便地将如下的规则字符串:

orders > 10000 && driving_years > 5

解析成一棵这样的二叉树:

规则二叉树

其中,ast.BinaryExpr 代表一个二元表达式,它由 X 和 Y 以及符号 OP 三部分组成。最上面的一个 BinaryExpr 表示规则的左半部分和右半部分相与。

很明显,左半部分就是:orders > 10000,而右半部分则是:driving_years > 5。神奇的是,左半部分和右半部分恰好又都是一个二元表达式。

左半部分的 orders > 10000 其实也是最小的叶子节点,它可以算出来一个 bool 值。把它拆开来之后,又可以分成 X、Y、OP。X 是 orders,OP 是 “>",Y 则是 “10000”。其中 X 表示一个标识符,是 ast.Ident 类型,Y 表示一个基本类型的字面量,例如 int 型、字符串型……是 ast.BasicLit 类型。

右半部分的 driving_years > 18 也可以照此拆分。

然后,从 json 中取出这个司机的 orders 字段的值为 100000,它比 10000 大,所以左半部分算出来为 true。同理,右半部分算出来也为 true。最后,再算最外层的 “&&",结果仍然为 true。

至此,直接根据规则字符串,我们就可以算出来结果。

如果写成程序的话,就是一个 dfs 的遍历过程。如果不是叶子结点,那就是二元表达式结点,那就一定有 X、Y、OP 部分。递归地遍历 X,如果 X 是叶子结点,那就结束递归,并计算出 X 的值……

我们可以使用AST做什么

AST一般可以被用来做linter。现在常用的golangci-lint已经集成了一些linter了,但是我们有可能会有一些自己定制化的代码静态检查规则,就可以通过自己解析AST来实现。

之前在对接现网问题和做code review的时候,发现有可能有些编码习惯会导致一些问题,其中一个问题就是遇到某个接口报错时,返回出来的异常里面却没有包含异常的详情,导致问题无法定位,大致可能是这样的代码:

err := someFunc()
if err != nil {return fmt.Errorf("call someFunc failed")
}

上面这段代码里面,调用someFunc这个接口出错了,但是最后返回的异常里面却没有包含具体的错误,最终可能会导致问题无法被定位。

实际案例就是之前定位线上告警邮件通知时,所有的参数错误都被包装成一个“参数错误”的异常被跑出来,但是具体的参数错误就没有被包含了,所以当时通过日志是完全没法定位具体是哪个参数报错了。所以正确的代码应该是这样

err := someFunc()
if err != nil {return fmt.Errorf("call someFunc failed, %s", err) // 或者是 return err之类的,总之return的内容里面一定得包含err才行
}

所以基于上面的内容,我们可以使用AST做的就是静态检查是否有类似的错误,这种错误就可以被抽象为:

在err != nil的if分支内,是否有return语句未包含err相关信息

我们可以把这个规则命名为NotReturnErr检查。

但是这个规则也不完善,因为有可能有这种情况:

err := someFunc()
if err != nil {errMsg := fmt.Sprintf("call someFunc failed, %s", err)return fmt.Errorf("%s", errMsg)
}

这种如果要通过规则去检查可能就会复杂一点,目前选择扫描出来之后通过人工去筛查一下是否有这种误判的情况了。

如何实现NotReturnErr检查

基本概念

先具体介绍一些这里可能会用到的AST的具体概念:

ast.Node

在Go语言的抽象语法树(AST)中,Node是所有AST节点类型的接口。所有的AST节点,无论是表达式、声明、语句,还是其他类型的节点,都实现了Node接口。这个接口定义如下:

 
type Node interface {// Pos方法返回节点的第一个字符的位置。Pos() token.Pos// End方法返回节点的最后一个字符的下一个位置。End() token.Pos
}

ast.IfStmt

ast.Node的实现之一,If Statement的缩写,代表一个if语句,其定义如下:

type IfStmt struct {If   token.Pos // "if"的位置Init Stmt      // 初始化语句;可能为空Cond Expr      // 条件表达式Body *BlockStmt // "then"部分Else Stmt      // "else"部分;可能为空
}

ast.BinaryExpr

在Go语言的抽象语法树(AST)中,BinaryExpr代表一个二元表达式。二元表达式是一个包含两个操作数和一个操作符的表达式。例如,a + bc * de == f都是二元表达式。
在Go语言的AST中,BinaryExpr是一个结构体,其定义如下:

type BinaryExpr struct {X     Expr // 左操作数OpPos token.Pos // 操作符的位置Op    token.Token // 操作符Y     Expr // 右操作数
}

ast.Ident

在Go语言的抽象语法树(AST)中,ast.Ident代表一个标识符。标识符在编程中广泛使用,它可以是变量名、函数名、类型名等。

在Go语言的AST中,ast.Ident是一个结构体,其定义如下:

type Ident struct {NamePos token.Pos // 标识符的位置Name    string    // 标识符的名字Obj     *Object   // 对应的对象;可能为空
}

ast.ReturnStmt

在Go语言的抽象语法树(AST)中,ReturnStmt代表一个return语句。return语句用于从函数中返回,并且可以携带返回值。

在Go语言的AST中,ReturnStmt是一个结构体,其定义如下:

type ReturnStmt struct {Return  token.Pos // "return"的位置Results []Expr    // 返回值列表;可能为空
}

ast.CallExpr

在Go语言的抽象语法树(AST)中,CallExpr代表一个函数调用表达式。函数调用表达式是一种特殊的表达式,它表示对一个函数的调用。

在Go语言的AST中,CallExpr是一个结构体,其定义如下:

type CallExpr struct {Fun      Expr      // 被调用的函数Lparen   token.Pos // 左括号的位置Args     []Expr    // 函数调用的参数列表Ellipsis token.Pos // 省略号的位置(如果存在)Rparen   token.Pos // 右括号的位置
}

ast.SelectorExpr

在Go语言的抽象语法树(AST)中,SelectorExpr代表一个选择器表达式。选择器表达式用于访问结构体的字段或者调用包的函数或变量。

在Go语言的AST中,SelectorExpr是一个结构体,其定义如下:

type SelectorExpr struct {X   Expr   // 表达式Sel *Ident // 选择器
}

实现逻辑

实现逻辑用一句话概括就是,遍历目录下的所有go文件(除vendor以外),然后对所有遍历的文件生成AST语法树,找到同时满足以下条件的AST节点:

  1. 所处函数为返回值有error的函数
  2. 有if语句,且if语句包含err != nil的判断
  3. if语句的body里有return语句

判断此节点是否返回error相关内容,例如return err或者return fmt.Errorf("%s", err.Error()),如果没有的话就打印记录。

代码仓

运行效果

然后就可以根据具体的情况来看是否需要对当前代码进行修改,我们以这个为例:

打开具体文件发现内容如下所示:

这种如果不是逻辑上就需要忽略这个err的话,那么这里可能就会有问题,这种就是需要排查是否需要修改的。

ChangeLog

下一步计划

  • 优化检测逻辑,当前会检测到一些error派生的类生成的新error,这种暂时没法识别成error
  • 计划将此lint和其他lint看能否集成到gitlab提交代码流程内,

这篇关于使用golang的AST编写定制化lint的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/1133565

相关文章

Mybatis官方生成器的使用方式

《Mybatis官方生成器的使用方式》本文详细介绍了MyBatisGenerator(MBG)的使用方法,通过实际代码示例展示了如何配置Maven插件来自动化生成MyBatis项目所需的实体类、Map... 目录1. MyBATis Generator 简介2. MyBatis Generator 的功能3

Python中使用defaultdict和Counter的方法

《Python中使用defaultdict和Counter的方法》本文深入探讨了Python中的两个强大工具——defaultdict和Counter,并详细介绍了它们的工作原理、应用场景以及在实际编... 目录引言defaultdict的深入应用什么是defaultdictdefaultdict的工作原理

使用Python进行文件读写操作的基本方法

《使用Python进行文件读写操作的基本方法》今天的内容来介绍Python中进行文件读写操作的方法,这在学习Python时是必不可少的技术点,希望可以帮助到正在学习python的小伙伴,以下是Pyth... 目录一、文件读取:二、文件写入:三、文件追加:四、文件读写的二进制模式:五、使用 json 模块读写

Python使用qrcode库实现生成二维码的操作指南

《Python使用qrcode库实现生成二维码的操作指南》二维码是一种广泛使用的二维条码,因其高效的数据存储能力和易于扫描的特点,广泛应用于支付、身份验证、营销推广等领域,Pythonqrcode库是... 目录一、安装 python qrcode 库二、基本使用方法1. 生成简单二维码2. 生成带 Log

Python如何使用seleniumwire接管Chrome查看控制台中参数

《Python如何使用seleniumwire接管Chrome查看控制台中参数》文章介绍了如何使用Python的seleniumwire库来接管Chrome浏览器,并通过控制台查看接口参数,本文给大家... 1、cmd打开控制台,启动谷歌并制定端口号,找不到文件的加环境变量chrome.exe --rem

Oracle数据库使用 listagg去重删除重复数据的方法汇总

《Oracle数据库使用listagg去重删除重复数据的方法汇总》文章介绍了在Oracle数据库中使用LISTAGG和XMLAGG函数进行字符串聚合并去重的方法,包括去重聚合、使用XML解析和CLO... 目录案例表第一种:使用wm_concat() + distinct去重聚合第二种:使用listagg,

使用C#代码计算数学表达式实例

《使用C#代码计算数学表达式实例》这段文字主要讲述了如何使用C#语言来计算数学表达式,该程序通过使用Dictionary保存变量,定义了运算符优先级,并实现了EvaluateExpression方法来... 目录C#代码计算数学表达式该方法很长,因此我将分段描述下面的代码片段显示了下一步以下代码显示该方法如

Go语言使用Buffer实现高性能处理字节和字符

《Go语言使用Buffer实现高性能处理字节和字符》在Go中,bytes.Buffer是一个非常高效的类型,用于处理字节数据的读写操作,本文将详细介绍一下如何使用Buffer实现高性能处理字节和... 目录1. bytes.Buffer 的基本用法1.1. 创建和初始化 Buffer1.2. 使用 Writ

redis-cli命令行工具的使用小结

《redis-cli命令行工具的使用小结》redis-cli是Redis的命令行客户端,支持多种参数用于连接、操作和管理Redis数据库,本文给大家介绍redis-cli命令行工具的使用小结,感兴趣的... 目录基本连接参数基本连接方式连接远程服务器带密码连接操作与格式参数-r参数重复执行命令-i参数指定命

PyTorch使用教程之Tensor包详解

《PyTorch使用教程之Tensor包详解》这篇文章介绍了PyTorch中的张量(Tensor)数据结构,包括张量的数据类型、初始化、常用操作、属性等,张量是PyTorch框架中的核心数据结构,支持... 目录1、张量Tensor2、数据类型3、初始化(构造张量)4、常用操作5、常用属性5.1 存储(st