批评家:Go上最顽固的静态分析器


我们即将发布Go的新的linter(静态分析器),这也是在静态分析领域中为您的想法制作原型的沙箱。


评论家围绕以下观察结果而建立:


  • 有一个“足够好”的测试实现比根本没有测试更好
  • 如果支票有争议,这并不意味着它没有用。 我们标记为“调皮”,然后倒入
  • 如果框架本身易于理解,那么从头开始编写linter通常比向现有框架添加新检查要困难得多。

在这篇文章中,我们将研究go-critic的用法和体系结构,以及其中实现的一些检查 ,并描述向其添加分析器功能的主要步骤。


快速上手


$ cd $GOPATH $ go get -u github.com/go-critic/go-critic/... $ ./bin/gocritic check-package strings $GOROOT/src/strings/replace.go:450:22: unslice: could simplify s[:] to s $GOROOT/src/strings/replace.go:148:2: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:156:3: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:219:3: elseif: should rewrite if-else to switch statement $GOROOT/src/strings/replace.go:370:1: paramTypeCombine: func(pattern string, value string) *singleStringReplacer could be replaced with func(pattern, value string) *singleStringReplacer $GOROOT/src/strings/replace.go:259:2: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/replace.go:264:2: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/strings.go:791:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:800:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:809:1: paramTypeCombine: func(s string, cutset string) string could be replaced with func(s, cutset string) string $GOROOT/src/strings/strings.go:44:1: unnamedResult: consider to give name to results $GOROOT/src/strings/strings.go:61:1: unnamedResult: consider to give name to results $GOROOT/src/strings/export_test.go:28:3: rangeExprCopy: copy of r.mapping (256 bytes) can be avoided with &r.mapping $GOROOT/src/strings/export_test.go:42:1: unnamedResult: consider to give name to results 

(警告的格式已被编辑;原稿位于gist中 。)


gocritic实用程序可以通过其导入路径( check-package )来检查各个软件包,还可以递归遍历所有目录( check-project )。 例如,您可以使用单个命令检查整个$GOROOT$GOPATH


 $ gocritic check-project $GOROOT/src $ gocritic check-project $GOPATH/src 

支持检查“白名单”,以便明确列出应执行的检查( -enable标志)。 默认情况下,将运行所有未标记为“ Experimental或“ VeryOpinionated图标的检查。


计划整合golangci-lintgometalinter


一切如何开始


对Go项目进行下一次代码审查,或者在审核某些第三方库时,您会一遍又一遍地注意到相同的问题。


令您遗憾的是,找不到能够诊断此类问题的棉绒。


您的第一步可能是尝试对问题进行分类,并与现有短毛绒的作者联系,建议他们添加新的支票。 您的提案被接受的机会在很大程度上取决于项目,并且可能会非常低。 此外,很可能会持续数月的预期。


但是,如果支票完全模棱两可并且被某人认为过于主观或不够准确怎么办?


尝试自己写这张支票也许有意义吗?


go-critic存在是为了成为实验测试的家,与将其连接到现有的静态分析仪相比,我们更容易实现这些测试。 go-critic设备本身将添加新检查所需的上下文和操作数量减至最少-可以说,您只需要添加一个文件(不计算测试)。


评论家如何工作


批判者是一组描述检查属性的规则 ,而微型检查器则执行代码检查以符合规则。


嵌入了linter的应用程序(例如, cmd / gocriticgolangci-lint )将接收受支持规则的列表,以特定方式对其进行过滤,为每个选定规则创建检查功能,然后在正在研究的程序包上启动每个规则。


添加新检查器的工作可分为三个主要步骤:


  1. 添加测试。
  2. 验证本身的执行。
  3. 为短绒棉添加文档。

我们将使用示例captLocal规则遍历所有这些点,该规则要求缺少以大写字母开头的本地名称。



添加测试


要为新检查添加测试数据,您需要在lint / testdata中创建一个新目录。


每个此类目录都应具有一个positive_tests.go文件,该文件描述了检查应在其上进行的代码示例。 为了测试是否没有误报,在测试中添加了“正确”代码,在该代码中,新检查不应发现任何问题( negative_tests.go )。


范例:


 // lint/testdata/positive_tests.go /// consider `in' name instead of `IN' /// `X' should not be capitalized /// `Y' should not be capitalized /// `Z' should not be capitalized func badFunc1(IN, X int) (Y, Z int) { /// `V' should not be capitalized V := 1 return V, 0 } 

 // lint/testdata/negative_tests.go func goodFunc1(in, x int) (x, y int) { v := 1 return v, 0 } 

您可以在添加新的lint之后运行测试。


验证实施


创建一个具有检查器名称的文件: lint/captLocal_checker.go
按照惯例,所有微_checker文件都带有_checker后缀。


 package lint //  “Checker”    . type captLocalChecker struct { checkerBase upcaseNames map[string]bool } 

checkerBase是必须嵌入每个检查器中的类型。
它提供了默认的实现,允许您在每个linter中编写更少的代码。
其中,checkerBase包含一个指向lint.context的指针,该指针包含类型信息和有关要检查文件的其他元数据。


upcaseNames字段将包含一个已知名称的表,我们将使用该表替换为strings.ToLower(name)版本。 对于地图中未包含的那些名称,建议不要使用大写字母,但不会提供正确的替代名称。


每个实例的内部状态均初始化一次。
仅需要为那些需要进行初步初始化的linter定义Init()方法。


 func (c *captLocalChecker) Init() { c.upcaseNames = map[string]bool{ "IN": true, "OUT": true, "INOUT": true, } } 

现在,您需要定义检查功能本身。
对于captLocal ,我们需要检查所有引入新变量的本地ast.Ident


为了检查名称的所有本地定义,您应该在检查器中实现带有以下签名的方法:


 VisitLocalDef(name astwalk.Name, initializer ast.Expr) 

可用的访客接口列表可在文件lint / internal / visitor.go中找到
captLocal实现LocalDefVisitor


 //  ast.Expr,         //  .      . func (c *captLocalChecker) VisitLocalDef(name astwalk.Name, _ ast.Expr) { switch { case c.upcaseNames[name.ID.String()]: c.warnUpcase(name.ID) case ast.IsExported(name.ID.String()): c.warnCapitalized(name.ID) } } func (c *captLocalChecker) warnUpcase(id *ast.Ident) { c.ctx.Warn(id, "consider `%s' name instead of `%s'", strings.ToLower(id.Name), id) } func (c *captLocalChecker) warnCapitalized(id ast.Node) { c.ctx.Warn(id, "`%s' should not be capitalized", id) } 

按照惯例,生成警告的方法通常放在单独的方法中。 很少有例外,但是遵循此规则被认为是良好做法。


添加文件


另一个必要的实现方法是InitDocumentation


 func (c *captLocalChecker) InitDocumentation(d *Documentation) { d.Summary = "Detects capitalized names for local variables" d.Before = `func f(IN int, OUT *int) (ERR error) {}` d.After = `func f(in int, out *int) (err error) {}` } 

通常,只需填写3个字段:


  • Summary -用一句话描述验证动作。
  • Before -更正Before代码。
  • After -更正After代码(不应引起警告)。

文档生成

重新生成文档不是新短绒的先决条件,也许在不久的将来,这一步骤将完全自动化。 但是,如果您仍然想检查输出markdown文件的外观,请使用make docs命令。 docs/overview.md文件将被更新。


注册一个新的linter并运行测试


最后一点是注册一个新的lint:


 //   captLocal_checker.go init . func init() { addChecker(&captLocalChecker{}, attrExperimental, attrSyntaxOnly) } 

addChecker需要一个指向新的addChecker的零值的指针。 接下来是可变参数,您可以传递零个或多个描述规则实施属性的属性。


attrSyntaxOnly是用于在实现中不使用类型信息的attrSyntaxOnly的可选标记,它允许您在不执行类型检查的情况下运行它们。 golangci-lint用“快速”标志标记这样的短绒(因为它们运行得更快)。


attrExperimental是分配给所有新实现的属性。 仅在稳定已实现检查之后才可以删除此属性。


现在,新的linter已通过addChecker注册,您可以运行测试:


 #  GOPATH: $ go test -v github.com/go-critic/go-critic/lint #  GOPATH/src/github.com/go-critic/go-critic: $ go test -v ./lint #  ,      make: $ make test 

乐观合并(几乎)


在考虑请求请求时,我们尝试坚持乐观的合并策略。 这主要表现为接受审稿人可能具有某些(尤其是纯粹主观的)主张的PR。 注入此类补丁后,审阅者会立即进行PR,以纠正这些缺陷,然后将原始补丁的作者添加到CC(副本)中。


在没有完全达成共识的情况下,我们还有两个linter标记可用于避免出现危险信号:


  1. Experimental :实施可能会有大量的误报,无效(发现问题的根源)或在某些情况下“失败”。 如果使用attrExperimental属性标记该实现,则可以注入该实现。 有时,在实验的帮助下,那些检查被指示未能从第一次提交中找到好名字。
  2. VeryOpinionated :如果支票可以同时有防御者和敌人,则应该使用属性attrVeryOpinionated标记它。 这样,我们可以避免拒绝关于代码风格的想法,这些想法可能与某些地鼠的口味不符。

Experimental是潜在的临时且可修复的实现属性。 VeryOpinionated是更基本的规则属性,与实现无关。


建议在提交实现之前在github上创建一个[checker-request]票证,但是如果您已经提交了pull请求,则可以为您打开相应的问题。


有关开发过程的更多详细信息,请参见CONTRIBUTING.md
基本规则在“ 主要规则”部分列出。


分词


您不仅可以添加新的linter,还可以参与该项目。
还有许多其他方法:


  • 在您的项目或大型/知名的开源项目上尝试一下,并报告误报,误报和其他缺点。 如果您也将有关已发现/已解决问题的注释添加到奖杯页面,我们将不胜感激。
  • 为新的检查提出建议。 在我们的跟踪器上创建一个问题就足够了。
  • 添加对现有短绒的测试。

go-critic使用参与其开发的所有程序员的声音来批评您的Go代码。 每个人都可以批评,因此-加入!


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


All Articles