前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >使用go/analysis自己实现linter

使用go/analysis自己实现linter

原创
作者头像
王沛文
发布2019-12-30 15:02:19
1.8K1
发布2019-12-30 15:02:19
举报
文章被收录于专栏:王沛文的专栏王沛文的专栏

golang虽然是门很火的语言,但是其缺点也是很明显的。由于最初目标就是替换C语言,考虑到复杂性等各种原因没有引入泛型,而是采用了interface{}这个带了类型的void*

所以可以看得到golang中极度恶心的sort.Interfacesync.Map等接口。

虽然go是门静态强类型语言,但是套上interface{}这个大锅之后,代码可以写的风生水起,堪比脚本语言。当然与其相应的就是各种运行时的panic也是层出不穷,很大程度上失去了静态强类型语言的优势。

如何解决这些问题呢?完善的测试是一方面,之前在看KotlinConf 2019的视频是有一页ppt很让我印象深刻。

宏观来看,类型检查、编译、单元测试、集成测试、系统测试都可以是测试的一方面。而越底层的方式成本越低,所以相比真正的跑单元测试、系统测试,如果能在编译时解决这些问题当然更加高效。

golang中编译时没有检查的东西,一是reflect相关的代码,二是struct tag。本文就使用golang官方提供的代码分析库实现一个tag检查的工具。在构建时以linter的方式检查出代码中的错误。

golang.org/x/tools/go/analysis

这个包是golang专门用来做代码静态检查的包,使用很简单。只需要实现analysis.Analyzer这个结构体即可

这个结构体最核心的就是一个Run func(*Pass) (interface{}, error)方法

这个方法传入一个Pass,对应这静态分析器的一个Pass

而我们就可以通过这个Pass来遍历整个所有代码做检查。这次我们的目标是检查tag的时候是否正确。

encoding/json

golang自带的json库支持自定义json key的名字,同时自带两个option:stringomitempty

string代表序列化的时候使用json字符串格式,但是golang结构体中使用别的标量类型,比如intboolean

omitempty则代表如果该字段是零值那就不序列化到json中。

这里常见的误用就是将string option添加到类型就是string的字段上去。

动手吧

接下来的事情就是遍历代码,检查tag了。

代码语言:txt
复制
func(pass *analysis.Pass) (interface{}, error) {
        for _, file := range pass.Files {
            ast.Inspect(file, func(node ast.Node) bool {
                st, ok := node.(*ast.StructType)
                if !ok {
                    return true
                }
                if st.Fields == nil {
                    return true
                }
                for _, f := range st.Fields.List {
                    if f.Tag == nil {
                        continue
                    }
                    tv, err := strconv.Unquote(f.Tag.Value)
                    if err != nil {
                        pass.Reportf(f.Tag.Pos(), "invalid tag:%q", render(pass.Fset, f.Tag))
                        continue
                    }
                    tags := reflect.StructTag(tv)
                    tag, ok := tags.Lookup(key())
                    if !ok {
                        continue
                    }
                      //TODO do some check
                }
                return true
            })
        }
        return nil, nil
    }

核心逻辑其实很简单,就是遍历pass中的每一个文件,然后遍历文件中的每一个语法元素,当遇到结构体时就检查结构体的字段的tag,这套代码对于检查任何tag都是通用的。

核心的check逻辑则如下

代码语言:txt
复制
            jsonTag := ParseJsonTag(tag)
            if jsonTag.Skip {
                return
            }
            //check tag name
            if !isValidTag(jsonTag.Name) {
                pass.Reportf(f.Tag.Pos(), "invalid name:%q", render(pass.Fset, f.Tag))
            }
            //check for string option
            if jsonTag.String {
                if !isStringable(pass.TypesInfo.TypeOf(f.Type)) {
                    pass.Report(analysis.Diagnostic{
                        Pos:     f.Tag.Pos(),
                        End:     f.Tag.End(),
                        Message: fmt.Sprintf("string must use on scalar field:%q", render(pass.Fset, f.Tag)),
                    })
                }
            }

这里的ParseJsonTagisStringable其实是从go标准库里抠出来的,为了保持和标准库的逻辑一致。

如果不是string可以使用的类型,就调用Report上报错误。

核心函数包装成结构体之后,再使用golang.org/x/tools/go/analysis/singlechecker包装一把,一个简单的linter就是实现了。

完整代码可以参见https://github.com/okhowang/gotaglint

后记

其实golang的tag和java的annotation几乎一模一样,绝大部分场景都是运行时解析然后做一些特殊逻辑,差就差在golang的tag没有任何检查逻辑,而java的annotation是强类型。最终每个库的tag语法真的是千差万别。有像json这种逗号分隔的,复杂点像https://github.com/go-playground/validator这种带层级的,还有xorm这种带括号的,一个比一个复杂。

真正写代码的时候除非对库有很深入的了解,很难整体把握tag的使用方式。而第三方来实现tag的lint也缺少具体的解析代码,需要库作者把tag解析代码声明成大写...

最好的方式还是库作者同时实现一个tag lint来方便开发者检查自己的tag编写是否正确。避免真正跑到具体逻辑的时候再panic。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • golang.org/x/tools/go/analysis
  • encoding/json
  • 动手吧
  • 后记
相关产品与服务
腾讯云代码分析
腾讯云代码分析(内部代号CodeDog)是集众多代码分析工具的云原生、分布式、高性能的代码综合分析跟踪管理平台,其主要功能是持续跟踪分析代码,观测项目代码质量,支撑团队传承代码文化。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档