使用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

相关文章

C++使用栈实现括号匹配的代码详解

《C++使用栈实现括号匹配的代码详解》在编程中,括号匹配是一个常见问题,尤其是在处理数学表达式、编译器解析等任务时,栈是一种非常适合处理此类问题的数据结构,能够精确地管理括号的匹配问题,本文将通过C+... 目录引言问题描述代码讲解代码解析栈的状态表示测试总结引言在编程中,括号匹配是一个常见问题,尤其是在

Java中String字符串使用避坑指南

《Java中String字符串使用避坑指南》Java中的String字符串是我们日常编程中用得最多的类之一,看似简单的String使用,却隐藏着不少“坑”,如果不注意,可能会导致性能问题、意外的错误容... 目录8个避坑点如下:1. 字符串的不可变性:每次修改都创建新对象2. 使用 == 比较字符串,陷阱满

Python使用国内镜像加速pip安装的方法讲解

《Python使用国内镜像加速pip安装的方法讲解》在Python开发中,pip是一个非常重要的工具,用于安装和管理Python的第三方库,然而,在国内使用pip安装依赖时,往往会因为网络问题而导致速... 目录一、pip 工具简介1. 什么是 pip?2. 什么是 -i 参数?二、国内镜像源的选择三、如何

使用C++实现链表元素的反转

《使用C++实现链表元素的反转》反转链表是链表操作中一个经典的问题,也是面试中常见的考题,本文将从思路到实现一步步地讲解如何实现链表的反转,帮助初学者理解这一操作,我们将使用C++代码演示具体实现,同... 目录问题定义思路分析代码实现带头节点的链表代码讲解其他实现方式时间和空间复杂度分析总结问题定义给定

Linux使用nload监控网络流量的方法

《Linux使用nload监控网络流量的方法》Linux中的nload命令是一个用于实时监控网络流量的工具,它提供了传入和传出流量的可视化表示,帮助用户一目了然地了解网络活动,本文给大家介绍了Linu... 目录简介安装示例用法基础用法指定网络接口限制显示特定流量类型指定刷新率设置流量速率的显示单位监控多个

JavaScript中的reduce方法执行过程、使用场景及进阶用法

《JavaScript中的reduce方法执行过程、使用场景及进阶用法》:本文主要介绍JavaScript中的reduce方法执行过程、使用场景及进阶用法的相关资料,reduce是JavaScri... 目录1. 什么是reduce2. reduce语法2.1 语法2.2 参数说明3. reduce执行过程

如何使用Java实现请求deepseek

《如何使用Java实现请求deepseek》这篇文章主要为大家详细介绍了如何使用Java实现请求deepseek功能,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录1.deepseek的api创建2.Java实现请求deepseek2.1 pom文件2.2 json转化文件2.2

python使用fastapi实现多语言国际化的操作指南

《python使用fastapi实现多语言国际化的操作指南》本文介绍了使用Python和FastAPI实现多语言国际化的操作指南,包括多语言架构技术栈、翻译管理、前端本地化、语言切换机制以及常见陷阱和... 目录多语言国际化实现指南项目多语言架构技术栈目录结构翻译工作流1. 翻译数据存储2. 翻译生成脚本

C++ Primer 多维数组的使用

《C++Primer多维数组的使用》本文主要介绍了多维数组在C++语言中的定义、初始化、下标引用以及使用范围for语句处理多维数组的方法,具有一定的参考价值,感兴趣的可以了解一下... 目录多维数组多维数组的初始化多维数组的下标引用使用范围for语句处理多维数组指针和多维数组多维数组严格来说,C++语言没

在 Spring Boot 中使用 @Autowired和 @Bean注解的示例详解

《在SpringBoot中使用@Autowired和@Bean注解的示例详解》本文通过一个示例演示了如何在SpringBoot中使用@Autowired和@Bean注解进行依赖注入和Bean... 目录在 Spring Boot 中使用 @Autowired 和 @Bean 注解示例背景1. 定义 Stud