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

相关文章

springboot security快速使用示例详解

《springbootsecurity快速使用示例详解》:本文主要介绍springbootsecurity快速使用示例,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝... 目录创www.chinasem.cn建spring boot项目生成脚手架配置依赖接口示例代码项目结构启用s

Python如何使用__slots__实现节省内存和性能优化

《Python如何使用__slots__实现节省内存和性能优化》你有想过,一个小小的__slots__能让你的Python类内存消耗直接减半吗,没错,今天咱们要聊的就是这个让人眼前一亮的技巧,感兴趣的... 目录背景:内存吃得满满的类__slots__:你的内存管理小助手举个大概的例子:看看效果如何?1.

java中使用POI生成Excel并导出过程

《java中使用POI生成Excel并导出过程》:本文主要介绍java中使用POI生成Excel并导出过程,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录需求说明及实现方式需求完成通用代码版本1版本2结果展示type参数为atype参数为b总结注:本文章中代码均为

golang 日志log与logrus示例详解

《golang日志log与logrus示例详解》log是Go语言标准库中一个简单的日志库,本文给大家介绍golang日志log与logrus示例详解,感兴趣的朋友一起看看吧... 目录一、Go 标准库 log 详解1. 功能特点2. 常用函数3. 示例代码4. 优势和局限二、第三方库 logrus 详解1.

Spring Boot3虚拟线程的使用步骤详解

《SpringBoot3虚拟线程的使用步骤详解》虚拟线程是Java19中引入的一个新特性,旨在通过简化线程管理来提升应用程序的并发性能,:本文主要介绍SpringBoot3虚拟线程的使用步骤,... 目录问题根源分析解决方案验证验证实验实验1:未启用keep-alive实验2:启用keep-alive扩展建

使用Java实现通用树形结构构建工具类

《使用Java实现通用树形结构构建工具类》这篇文章主要为大家详细介绍了如何使用Java实现通用树形结构构建工具类,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一下... 目录完整代码一、设计思想与核心功能二、核心实现原理1. 数据结构准备阶段2. 循环依赖检测算法3. 树形结构构建4. 搜索子

GORM中Model和Table的区别及使用

《GORM中Model和Table的区别及使用》Model和Table是两种与数据库表交互的核心方法,但它们的用途和行为存在著差异,本文主要介绍了GORM中Model和Table的区别及使用,具有一... 目录1. Model 的作用与特点1.1 核心用途1.2 行为特点1.3 示例China编程代码2. Tab

SpringBoot使用OkHttp完成高效网络请求详解

《SpringBoot使用OkHttp完成高效网络请求详解》OkHttp是一个高效的HTTP客户端,支持同步和异步请求,且具备自动处理cookie、缓存和连接池等高级功能,下面我们来看看SpringB... 目录一、OkHttp 简介二、在 Spring Boot 中集成 OkHttp三、封装 OkHttp

使用Python实现获取网页指定内容

《使用Python实现获取网页指定内容》在当今互联网时代,网页数据抓取是一项非常重要的技能,本文将带你从零开始学习如何使用Python获取网页中的指定内容,希望对大家有所帮助... 目录引言1. 网页抓取的基本概念2. python中的网页抓取库3. 安装必要的库4. 发送HTTP请求并获取网页内容5. 解

使用Python实现网络设备配置备份与恢复

《使用Python实现网络设备配置备份与恢复》网络设备配置备份与恢复在网络安全管理中起着至关重要的作用,本文为大家介绍了如何通过Python实现网络设备配置备份与恢复,需要的可以参考下... 目录一、网络设备配置备份与恢复的概念与重要性二、网络设备配置备份与恢复的分类三、python网络设备配置备份与恢复实