使用golang的AST编写定制化lint

news/2024/9/15 17:22:36/ 标签: golang, 开发语言, 后端

什么是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提交代码流程内,


http://www.ppmy.cn/news/1519914.html

相关文章

【13年12月CCF计算机软件能力认证】:出现次数最多的数、ISBN号码、最大的矩形、有趣的数、I‘m stuck!

题目概括出现次数最多的数暴力枚举,非常简单ISBN号码直接模拟,非常简单最大的矩形用到双指针(优化枚举),非常简单有趣的数用到了数学知识排列组合,有一定思维难度I’m stuck!我用到了两个dfs来解决&#xf…

【区块链 + 供应链】广汽本田区块链合同供应链管理系统 | FISCO BCOS应用案例

广汽本田是国内汽车制造的龙头,每年销售额超千亿级别,每年的合同采购规模量在百亿以上。企业内部采用传 统的中心化方式管理合同,由于涉及部门众多,需要管理的合同要素也各不相同,造成信息不集中、合同版本众多、 合同…

C#中lock(this)与lock(private object)区别

前言 在使用多线程编程时,我们会对代码关键部分确保其一次只由一个线程执行,对于防止争用条件和保持数据完整性至关重要。在C#中,lock 语句就是用于通过同步对共享资源的访问来实现此目的工具。本文介绍lock(this) 与lock(private object) 两…

重新修改 Qt 项目的 Kit 配置

要重新修改 Qt 项目的 Kit 配置,你可以按照以下步骤进行操作: 1. 打开 Qt Creator 首先,启动 Qt Creator,确保你的项目已经打开。 2. 进入项目设置 在 Qt Creator 中,点击菜单栏的 “Projects” 标签(通…

Spark MLlib模型训练—回归算法 Decision tree regression

Spark MLlib模型训练—回归算法 Decision tree regression 在机器学习中,决策树是一种常用且直观的模型,广泛应用于分类和回归任务。决策树回归 (Decision Tree Regression) 通过将数据集分割成多个区域,构建一棵树形结构,以预测目标变量的连续值。本文将详细探讨 Spark 中…

【Eureka】搭建Eureka Server,实现服务注册和服务发现

1. Eureka介绍 Eureka是NetflixOSS套件中关于服务注册和发现的解决⽅案.SpringCloud对Eureka进⾏了集成,并作为优先推荐⽅案进⾏宣传,虽然⽬前Eureka2.0已经停⽌维护,新的微服务架构设计中,也不再建议使用,但是⽬前依然有⼤量公司的微服务系统使⽤Eureka作为注册中⼼. 官方文…

数据访问:JPA

文章目录 JPA的由来JPA是什么Spring Data JPA快速上手 JPA的由来 ORM框架能够将Java对象映射到关系型数据库中,能够直接持久化复杂的 Java对象。ORM框架的出现,可以让开发者从数据库编程中解脱出来,把更多的精力放在业务模型与业务逻辑上。目…

k8s-pod 实战八 (gRPC 探测详细分析)

gRPC 探测详细分析 在 Kubernetes 中,探针(Probe)用于检查应用程序的健康状态和就绪状态。尽管 Kubernetes 原生支持 HTTP 和 TCP 探针,但对于 gRPC 服务,你需要借助第三方工具来实现探测。grpc-health-probe 是一个常用的工具,它专门用于探测 gRPC 服务的健康状态。 实…

KeePassXC软件简介

KeePassXC 是一款开源且免费的跨平台密码管理器,它允许用户在不同的网站和服务上使用多个不同的密码,而无需记住它们。用户只需要记住一个主密码或者持有一个密钥文件,就可以访问所有密码的加密数据库。KeePassXC 支持 AES 加密算法&#xff…

《C++20 特性综述》

《C20 特性综述》 在编程世界中,C一直以其强大的性能和灵活性占据着重要地位。随着时间的推移,C不断发展和演进,C20 带来了一系列令人瞩目的新特性,为开发者提供了更强大的工具和更高效的编程方式。 一、概念(Concep…

大模型技术 | 基于 Langchain 和 Streamlit,构建多 PDF RAG 聊天机器人

与 PDF 互动是很酷的。你可以与你的笔记、书籍和文档等进行聊天。 本文将帮助你构建一个基于 Multi RAG Streamlit 的 Web 应用程序,通过对话 AI 聊天机器人来读取、处理和互动PDF数据。 以下是该应用程序的工作步骤,用简单的语言进行说明。 配置必要的…

JDK原理

当我们谈论JDK(Java Development Kit)的原理时,实际上是在探讨Java语言及其开发环境背后的技术和设计思想。JDK是Java编程语言的核心工具包,它包含了Java运行环境(JRE)、Java编译器(javac&#…

2 html5 浏览器已经支持的新API

HTML5规范下很多API浏览器都已经支持,这里我们列举几个很常用的浏览器支持的API: 1 tab页之间通信: BroadcastChannel(channelName); 可用于多个不同浏览器tab页之间通信。实例化的时候Channel名称必须相同。 const broadcastChannel new BroadcastChannel(myC…

39次8.29(了解docker-compose,docker-compose编排容器,配置harbor服务)

1.使用使用docker-compose编排容器 1.YAML ⽂件的格式和语法 1)YAML ⽂件格式 yaml 是⼀种标记语⾔很直观的数据序列化格式,可读性很⾼。 类似于 xml 描述性语⾔,语法⽐xml简单的很多。 yaml 数据结构通过缩进进⾏表示,连续的…

金九银十来了,你准备好了吗?——迎接技术行业的旺季

每年的九月和十月,对于技术行业来说,是一个特别的时期。这个时期被业界称为“金九银十”,意味着招聘和项目开发的高峰期。对于技术人员而言,这不仅是一个职业发展的黄金时期,也是技术能力提升和职业规划的关键时刻。那…

RAG中pdf解析的方法全览

RAG中解析PDF的方法 一 pdf格式都有哪些 1.机器生成的pdf文件,包含图像,文本,可以被编辑 2.传统扫描文档,表现为图像,不能被编辑 3.带OCR的扫描文档。可能转OCR的过程中带入了错误。 二 pdf解析全科指南 全面指南…

设计模式 8 组合模式

设计模式 8 创建型模式(5):工厂方法模式、抽象工厂模式、单例模式、建造者模式、原型模式结构型模式(7):适配器模式、桥接模式、组合模式、装饰者模式、外观模式、享元模式、代理模式行为型模式&#xff0…

golang 于 goland 无法运行

命令行 go build -o main.go 等等 报出 # command-line-arguments runtime.main_mainf: function main is undeclared in the main package 直接goland运行 爆出 command-line-arguements 等等 goland中直接删除配置( Edit Configurations ),或者把运行模式从 Fil…

redis主从+高可用切换+负载均衡

1. redis主从配置 # 在master中 cp sentinel.conf /etc/redis/ vim /etc/redis/sentinel.conf scp /etc/redis/sentinel.conf server2:/etc/redis/ scp /etc/redis/sentinel.conf server3:/etc/redis/ redis-sentinel /etc/redis/sentinel.conf # 启动监控# 在slave中 redis-s…

第十三节:学习Springboot整合mybatis——完整篇(自学Spring boot 3.x的第三天)

大家好,今天记录下学习springboot的第三天。​网创有方 这节详细记录了如何使用springboot整合mybatis方法,并成功实现将请求信息插入本地的mysql数据库。 由于只是为了方便本地验证,实际项目是需要部署到服务器上去的,所以就采用…