Ruleguard:Go的动态检查


在本文中,我将讨论新的go-ruleguard静态分析库(和实用程序),该库将gogrep为可用于短绒gogrep


独特的功能:您可以在特殊的Go-like DSL上描述静态分析的规则,在ruleguard的一开始, ruleguard就会变成一组诊断。 也许这是用于实施Go的自定义检查的最容易配置的工具之一。


作为奖励,我们将讨论go/analysis及其前身


静态分析可扩展性


Go有许多短毛猫,其中一些可以扩展。 通常,要扩展linter,您需要使用特殊的linter API编写Go代码。


主要有两种方式: Go插件和Monolith。 整体意味着所有支票(包括您的个人支票)在编译阶段都可用。


revive要求其内核中包括新的检查以进行扩展。 除了这个可以插入插件的go-critic插件,它使您可以收集扩展名而与主要代码无关。 这两种方法都暗示您go/ast使用linter API在Go上实现go/astgo/types操作。 即使是简单的检查也需要大量代码


go/analysis旨在通过以下方案简化图片:皮棉机的“框架”几乎完全相同,但这并不能解决诊断程序本身的技术实施复杂性的问题。


`loader`和`go / packages'的题外话


当您为Go编写分析器时,您的最终目标是与AST和类型进行交互,但是在执行此操作之前,需要以正确的方式“加载”源代码。 为简化起见,加载的概念包括解析 ,类型检查和导入依赖项


简化此管道的第一步是go/loader包,它将允许您通过几个调用来“下载”所需的一切。 一切都差不多了,然后他不赞成使用go/packagesgo/packages API略有改进,从理论上讲,它与模块配合良好。


现在,最好不要直接使用以上任何一种来编写分析器,因为go/analysis提供了go/packages以前的解决方案所没有的东西-程序的结构。 现在,我们可以使用指定的go/analysis范例,并更有效地重用分析器。 这种范例具有争议性,例如, go/analysis非常适合于单个软件包及其依赖项级别的分析,但是要在没有狡猾的工程技巧的情况下对其进行全局分析并不容易。


go/analysis还可以简化分析仪的测试




什么是规则守卫?



go-ruleguard是静态分析实用程序,默认情况下不包含单个检查。


ruleguard规则从一个特殊的gorules文件开始加载,该文件以声明方式描述了应向其发出警告的代码模式。 ruleguard用户可以自由编辑此文件。


不必gorules用于连接新支票gorules控制程序,因此可以将gorules的规则称为dynamic


ruleguard控制程序如下所示:


 package main import ( "github.com/quasilyte/go-ruleguard/analyzer" "golang.org/x/tools/go/analysis/singlechecker" ) func main() { singlechecker.Main(analyzer.Analyzer) } 

同时, analyzer通过ruleguard程序包实现的,如果要将它用作库,则必须使用该程序。


守护者VS复兴


举一个简单但真实的示例:假设我们要避免在程序中调用runtime.GC() 。 为了复兴,已经有一个单独的诊断程序,称为"call-to-gc"


Call-to-gc实现(Elven中的70行)


 package rule import ( "go/ast" "github.com/mgechev/revive/lint" ) // CallToGCRule lints calls to the garbage collector. type CallToGCRule struct{} // Apply applies the rule to given file. func (r *CallToGCRule) Apply(file *lint.File, _ lint.Arguments) []lint.Failure { var failures []lint.Failure onFailure := func(failure lint.Failure) { failures = append(failures, failure) } var gcTriggeringFunctions = map[string]map[string]bool{ "runtime": map[string]bool{"GC": true}, } w := lintCallToGC{onFailure, gcTriggeringFunctions} ast.Walk(w, file.AST) return failures } // Name returns the rule name. func (r *CallToGCRule) Name() string { return "call-to-gc" } type lintCallToGC struct { onFailure func(lint.Failure) gcTriggeringFunctions map[string]map[string]bool } func (w lintCallToGC) Visit(node ast.Node) ast.Visitor { ce, ok := node.(*ast.CallExpr) if !ok { return w // nothing to do, the node is not a call } fc, ok := ce.Fun.(*ast.SelectorExpr) if !ok { return nil // nothing to do, the call is not of the form pkg.func(...) } id, ok := fc.X.(*ast.Ident) if !ok { return nil // in case X is not an id (it should be!) } fn := fc.Sel.Name pkg := id.Name if !w.gcTriggeringFunctions[pkg][fn] { return nil // it isn't a call to a GC triggering function } w.onFailure(lint.Failure{ Confidence: 1, Node: node, Category: "bad practice", Failure: "explicit call to the garbage collector", }) return w } 



现在,与go-ruleguard操作方式进行比较:


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func callToGC(m fluent.Matcher) { m.Match(`runtime.GC()`).Report(`explicit call to the garbage collector`) } 

没什么,真正重要的是runtime.GC和万一触发规则需要发出的消息。


您可能会问:仅此而已吗? 我特别从一个简单的示例开始,以显示在使用传统方法的情况下进行非常简单的诊断可能需要多少代码。 我保证会有更多令人兴奋的例子。


快速上手


go-critic有一个rangeExprCopy诊断程序,可以在代码中查找潜在的意外数组副本。


此代码遍历数组的副本


 var xs [2048]byte for _, x := range xs { // Copies 2048 bytes // Loop body. } 

解决此问题的方法是添加一个字符:


  var xs [2048]byte - for _, x := range xs { // Copies 2048 bytes + for _, x := range &xs { // No copy // Loop body. } 

最有可能的是,您不需要此复制,并且更正版本的性能始终更好。 您可以等到Go编译器变得更好为止,或者可以在代码中检测到此类位置,并立即使用相同的go-critic对其进行更正。


可以使用gorules语言( rules.go文件)实施此诊断:


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func _(m fluent.Matcher) { m.Match(`for $_, $_ := range $x { $*_ }`, `for $_, $_ = range $x { $*_ }`). Where(m["x"].Addressable && m["x"].Type.Size >= 128). Report(`$x copy can be avoided with &$x`). At(m["x"]). Suggest(`&$x`) } 

该规则找到所有使用两个可迭代变量的for-range循环(这种情况导致复制)。 可迭代表达式$x必须是addressable并且必须大于所选阈值(以字节为单位)。


Report()定义了要发送给用户的消息, Suggest()描述了一个快速quickfix模板,该模板可以通过gopls (LSP)在编辑器中使用,并且如果使用-fix参数调用ruleguard可以交互使用(我们将返回此内容)。 At()将警告和快速 quickfix附加到模板的特定部分。 我们需要用$x &$x代替$x ,而不是重写整个循环。


Report()Suggest()接受一个字符串,该字符串可以插入模板从Match()捕获的表达式。 预定义的变量$$表示“所有捕获的片段”(在正则表达式中为$0 )。


创建rangecopy.go文件:


 package example // sizeof(builtins[...]) = 240 on x86-64 var builtins = [...]string{ "append", "cap", "close", "complex", "copy", "delete", "imag", "len", "make", "new", "panic", "print", "println", "real", "recover", } func builtinID(name string) int { for i, s := range builtins { if s == name { return i } } return -1 } 

现在我们可以运行ruleguard


 $ ruleguard -rules rules.go -fix rangecopy.go rangecopy.go:12:20: builtins copy can be avoided with &builtins 

如果之后再查看rangecopy.go ,则会看到一个固定版本,因为使用-fix参数调用了ruleguard


无需创建gorules文件即可调试最简单的规则:


 $ ruleguard -c 1 -e 'm.Match(`return -1`)' rangecopy.go rangecopy.go:17:2: return -1 16 } 17 return -1 18 } 

由于使用了go/analysis/singlechecker ,我们有了-c选项,它使我们可以显示指定的上下文行以及警告本身。 控制此参数有点违反直觉:缺省值为-c=-1 ,表示“无上下文”,而-c=0将输出上下文的一行(诊断所指示的一行)。


这是一些更有趣的gorules


  • 类型模板 ,可用于指定期望的类型。 例如,表达式map[$t]$t描述了所有具有与键类型匹配的值类型的map,而*[$len]$elem捕获了所有指向数组的指针。
  • 在一个函数中,可能有多个规则,
    函数本身应称为规则组
  • 组中的规则按照定义的顺序依次应用。 触发的第一个规则取消与其余规则的比较。 对于优化而言,这不重要,而对于特定情况下的专用规则而言,则不那么重要。 一个有用的例子是将$x=$x+$y重写$x=$x+$y $x+=$y ,对于$y=1的情况,您要提供$x++ ,而不是$x+=1

可以在docs/gorules.md找到有关所用DSL的更多信息。


更多例子


 package gorules import "github.com/quasilyte/go-ruleguard/dsl/fluent" func exampleGroup(m fluent.Matcher) { //     json.Decoder. // . http://golang.org/issue/36225 m.Match(`json.NewDecoder($_).Decode($_)`). Report(`this json.Decoder usage is erroneous`) //   unconvert,    . m.Match(`time.Duration($x) * time.Second`). Where(m["x"].Const). Suggest(`$x * time.Second`) //   fmt.Sprint()    String(), //   $x  . m.Match(`fmt.Sprint($x)`). Where(m["x"].Type.Implements(`fmt.Stringer`)). Suggest(`$x.String()`) //   . m.Match(`!($x != $y)`).Suggest(`$x == $y`) m.Match(`!($x == $y)`).Suggest(`$x != $y`) } 

如果没有对规则的Report()调用,则将使用Suggest()的消息输出。 在某些情况下,这可以避免重复。


类型过滤器和子表达式可以检查各种属性。 例如, PureConst属性非常有用:


  • Var.Pure表示该表达没有副作用。
  • Var.Const表示可以在恒定上下文(例如,数组的维)中使用表达式。

对于Where()条件中的package-qualified名称,您需要使用Import()方法。 为了方便起见,所有标准软件包都是为您导入的,因此在上面的示例中,我们不需要进行其他导入。


go/analysis修复动作


go/analysis为我们quickfixquickfix支持。


go/analysis模型中,分析器生成诊断事实 。 诊断将发送给用户,并且事实将供其他分析仪使用。


诊断程序可以具有一组建议的修复程序 ,每个修复程序都描述了如何在指定范围内更改源代码,以修复诊断程序发现的问题。


官方说明位于go/analysis/doc/suggested_fixes.md


结论



在您的项目上尝试使用ruleguard ,如果您发现错误或想请求新功能,请打开issue


如果您仍然很难使用ruleguard ,请参考以下示例:


  • 实施自己的Go诊断程序。
  • 使用-fix自动升级或重构代码。
  • 使用-json处理-json结果的代码统计信息收集。

ruleguard的近期发展计划:



有用的链接和资源


Source: https://habr.com/ru/post/zh-CN481696/


All Articles