为go vet添加一个新的分析器,用于检查append后面的值缺失

缘起:


2023年5月25号,知名Go技术专家,WA语言联合发起人,多本技术书籍的合著者柴大,遇到一个append忘记写要添加的元素,只写了第一个参数(要追加的切片)的问题,可以编译通过,但大概率不是开发者本意。目前go vet缺失这样的检测项,柴大知道我对Go源码一直充满热枕,建议尝试实现。


即对于以下情况,希望在执行go vet检测时,可以抛出提示

func AppendsTest() {
 sli := []string{"a""b""c"}
 sli = append(sli) 
}

最开始我甚至不知道这种情况竟然可以编译通过,也不清楚如何为vet新增一个分析项。不过凭借一腔热情,通过分析源码,检索资料,询问ChatGPT后,几个小时后的第二天凌晨,就实现了初版功能。但后来的集成&修改较大,在此做相关记述。


目标:


添加一个新的分析器,用于在append后检查缺失值
目前,如果append时没有添加第二个参数,执行go vet时不会有提示。 添加一个分析器来检测这种情况

下面 由缓至急循循而进


go vet的作用


官方文档

Go vet 是 Go 语言内置的静态分析工具,用于检查 Go 代码中的常见错误和潜在问题。它可以帮助开发者在编译代码之前发现一些常见的错误,并提供修复建议。

以下是 Go vet 的一些常见用途和功能:

  1. 错误检查:Go vet 可以检查代码中的语法错误、类型错误、未使用的变量和导入包、无法到达的代码等。它能够捕捉到一些编译器可能会忽略的问题,从而帮助开发者在编译之前就能够发现这些潜在错误。

  2. 常见问题检测:Go vet 还可以检查代码中的常见问题,例如无用的 if 条件、无法到达的代码、错误的函数调用等。它可以帮助开发者避免一些常见的错误模式和不良实践。

  3. 代码规范检查:Go vet 还可以检查代码是否符合 Go 语言的规范和最佳实践。它可以发现一些与代码风格和约定不符的地方,例如不必要的类型转换、不恰当的命名风格等。通过检查代码的规范性,可以提高代码的可读性和可维护性。

Go vet 只是一个静态分析工具,它无法检测所有类型的问题。一些更复杂的逻辑错误、并发问题和运行时错误需要使用其他工具和技术进行检测和调试。


go vet 工具可以检查代码中的一些常见错误,例如:

  • 使用 defer 语句时传递函数参数的常见错误。
  • 在使用 sync.WaitGroup 时,忘记在协程内调用 Done 方法的常见错误。
  • 在使用 fmt.Printf 时,格式化字符串中的占位符和参数不匹配的常见错误。

除了这些常见错误之外,go vet 工具还可以检查代码中的其他一些潜在问题,例如:

  • 未使用的变量和导入包。
  • 可能的空指针解引用。
  • 可能的竞态条件等。

通过运行 go vet 工具,可以帮助开发者及时发现代码中的问题,并减少因这些问题导致的错误和不必要的麻烦。


vet 这个名称可能源于英文单词 "veterinary",意为 "兽医";或者"veto",意为 "否决"

可通过此链接,查看Go的各个版本中,vet的变化


要运行 Go vet,可以使用以下命令:

go vet <package>

其中 <package> 是要检查的 Go 包的路径。Go vet 将会分析指定包及其依赖的代码,并输出潜在的问题和修复建议。

Go vet 是一个帮助开发者在编译之前发现代码错误和潜在问题的工具,它可以提高代码的质量和可靠性,并帮助开发者编写更健壮的 Go 代码。


Go 1.12 - 2019 年 2 月 版本在analysis包基础上重写了 go vet 命令,这个包有着更大的灵活性,允许开发人员编写自己的代码检查工具。

go vet 命令可作为一系列不同源代码分析工具的基础。


举几个例子,go vet会输出提示

当使用 Go vet 进行静态代码分析时,它可能会输出一些提示来指出代码中的潜在问题。以下是一些示例:

  • 未使用的变量:
package main

import "fmt"

func main() {
    unusedVar := 42 // 声明了一个未使用的变量
    fmt.Println("Hello, Go!")
}

运行 go vet 命令后,会输出以下提示:

# command-line-arguments
./main.go:6:6: unusedVar declared but not used

这个提示表明变量 unusedVar 被声明但未被使用,可能是代码中的一个错误或者不必要的代码。


  • 错误的函数调用:
package main

import "fmt"

func main() {
    fmt.Println("Hello, Go!")
    fmt.Printl("Oops!"// 错误的函数名
}

运行 go vet 命令后,会输出以下提示:

# command-line-arguments
./main.go:7:5: Printl call has possible misspelling: fmt.Printl refers to unknown identifier 'Printl'; did you mean Println?

这个提示指出在第 7 行的函数调用 fmt.Printl("Oops!") 可能是一个拼写错误,建议使用 fmt.Println("Oops!")


  • 错误的类型转换:
package main

import "fmt"

func main() {
    var x float64 = 3.14
    y := int(x) // 错误的类型转换
    fmt.Println(y)
}

运行 go vet 命令后,会输出以下提示:

# command-line-arguments
./main.go:7:10: possible misuse of conversion

这个提示表明在第 7 行的类型转换 int(x) 可能存在问题,建议检查转换的正确性。


以上只是一些简单的示例,Go vet 可以检查更多的问题并提供相应的提示。通过运行 go vet 命令并检查输出,开发者可以及早发现代码中的潜在问题,并根据提示进行修复和改进。


append参数的问题


package main

func main() {

 sli1 := []string{"a""b""c"}
 sli2 := make([]string0)

 for _, val := range sli1 {
  print(val)
  sli2 = append(sli2)
 }
}

append的第一个参数是切片,后面是...可选参数。那如上,append()中遗漏了要追加的元素,是完全符合语法规范的,能正常编译通过。

src/buildin/buildin.go:

// The append built-in function appends elements to the end of a slice. If
// it has sufficient capacity, the destination is resliced to accommodate the
// new elements. If it does not, a new underlying array will be allocated.
// Append returns the updated slice. It is therefore necessary to store the
// result of append, often in the variable holding the slice itself:
//
// slice = append(slice, elem1, elem2)
// slice = append(slice, anotherSlice...)
//
// As a special case, it is legal to append a string to a byte slice, like this:
//
// slice = append([]byte("hello "), "world"...)
func append(slice []Type, elems ...Type) []Type

但往往这不是想要的效果,并且由于编译期不会抛错,不能马上发现问题。想必很多开发者都"中过招",甚至一些知名开源项目如docker,也有这样的bug

于是提了一个issue: proposal: cmd/vet: add a new analyzer for check missing values after append

同时写好了代码,并本地自测ok

vet这块的维护者评论了 Related #30040.

早在2019年2月份,就有开发者提出了类似提案(proposal: Go 2: ban calling append with a single argument),但最终没有定论。看起来Rob Pike不支持新增一个vet项来检测该问题,Ian Lance Taylor则较支持。

参与讨论的开发者还列举了更多这样的错误case:

Having a quick look with SQL and the Google Cloud BigQuery GitHub Activity Data - I was able to find some regexp matches (120 different lines, 1623 total matches), examples:

https://github.com/flynn/flynn/blob/1033a42b27c1acee8d8bac742a0d65291ce31b2c/test/test_cli.go#L692
https://github.com/moby/moby/blob/f8e29fdd683f8d1a79344df08342261c3991ad1b/image/rootfs.go#L41
https://github.com/yanzay/autohome/blob/9b98aa56c498e21fd580dbbcd5459cdc0c15f0c6/modules/arduino/arduino.go#L95
https://github.com/frezadev/colony-manager/blob/6ec588b081b9aab1d519cc27c3358ea26d942c92/controller/application.go#L988
https://github.com/xingzhou/kubernetes/blob/master/pkg/kubelet/rkt/rkt_test.go
https://github.com/akram/origin/blob/1b26a414ce1c2a4ca4f4491722a48a138aab6d3c/vendor/github.com/docker/docker/daemon/graphdriver/aufs/aufs_test.go#L768
https://github.com/haxorof/terraform-provider-openstack/blob/b6e0cc9732f4379ac39a1506fbbd6f5cfea1438f/vendor/github.com/hashicorp/hcl2/hcl/merged.go#L174


代码实现


4年前不了了之的问题,估计现在也不会很快有定论。

抛开这些暂且不谈,先看看代码如何实现 为go vet新增一个检测项,用来分析append切片时,是不是遗漏了要追加的元素


先借助chatgpt:

go vet的源代码是在go的哪个目录?

Go vet 工具的源代码位于 Go 语言的源代码库中,具体而言,它位于 go/src/cmd/vet 目录下。在标准的 Go 安装中,该目录会包含 vet 包和与之相关的文件。

你可以在以下路径找到 Go vet 的源代码:

go/src/cmd/vet

在这个目录中,你会找到 Go vet 工具的实现文件和其他相关文件,包括命令行解析、静态分析逻辑等。这些文件一起构成了 Go vet 工具的代码。


// Copyright 2012 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

package main

import (
 "cmd/internal/objabi"

 "golang.org/x/tools/go/analysis/unitchecker"

 "golang.org/x/tools/go/analysis/passes/asmdecl"
 "golang.org/x/tools/go/analysis/passes/assign"
 "golang.org/x/tools/go/analysis/passes/atomic"
 "golang.org/x/tools/go/analysis/passes/bools"
 "golang.org/x/tools/go/analysis/passes/buildtag"
 "golang.org/x/tools/go/analysis/passes/cgocall"
 "golang.org/x/tools/go/analysis/passes/composite"
 "golang.org/x/tools/go/analysis/passes/copylock"
 "golang.org/x/tools/go/analysis/passes/directive"
 "golang.org/x/tools/go/analysis/passes/errorsas"
 "golang.org/x/tools/go/analysis/passes/framepointer"
 "golang.org/x/tools/go/analysis/passes/httpresponse"
 "golang.org/x/tools/go/analysis/passes/ifaceassert"
 "golang.org/x/tools/go/analysis/passes/loopclosure"
 "golang.org/x/tools/go/analysis/passes/lostcancel"
 "golang.org/x/tools/go/analysis/passes/nilfunc"
 "golang.org/x/tools/go/analysis/passes/printf"
 "golang.org/x/tools/go/analysis/passes/shift"
 "golang.org/x/tools/go/analysis/passes/sigchanyzer"
 "golang.org/x/tools/go/analysis/passes/slog"
 "golang.org/x/tools/go/analysis/passes/stdmethods"
 "golang.org/x/tools/go/analysis/passes/stringintconv"
 "golang.org/x/tools/go/analysis/passes/structtag"
 "golang.org/x/tools/go/analysis/passes/testinggoroutine"
 "golang.org/x/tools/go/analysis/passes/tests"
 "golang.org/x/tools/go/analysis/passes/timeformat"
 "golang.org/x/tools/go/analysis/passes/unmarshal"
 "golang.org/x/tools/go/analysis/passes/unreachable"
 "golang.org/x/tools/go/analysis/passes/unsafeptr"
 "golang.org/x/tools/go/analysis/passes/unusedresult"
)

func main() {
 objabi.AddVersionFlag()

 unitchecker.Main(
  asmdecl.Analyzer,
  assign.Analyzer,
  atomic.Analyzer,
  bools.Analyzer,
  buildtag.Analyzer,
  cgocall.Analyzer,
  composite.Analyzer,
  copylock.Analyzer,
  directive.Analyzer,
  errorsas.Analyzer,
  framepointer.Analyzer,
  httpresponse.Analyzer,
  ifaceassert.Analyzer,
  loopclosure.Analyzer,
  lostcancel.Analyzer,
  nilfunc.Analyzer,
  printf.Analyzer,
  shift.Analyzer,
  sigchanyzer.Analyzer,
  slog.Analyzer,
  stdmethods.Analyzer,
  stringintconv.Analyzer,
  structtag.Analyzer,
  tests.Analyzer,
  testinggoroutine.Analyzer,
  timeformat.Analyzer,
  unmarshal.Analyzer,
  unreachable.Analyzer,
  unsafeptr.Analyzer,
  unusedresult.Analyzer,
 )
}
什么意思?

这段代码是 Go vet 工具的主要入口代码。它导入了多个分析器(analyzers),每个分析器都是 Go vet 工具中的一个检查功能。然后,它使用 unitchecker.Main 函数注册这些分析器,并在运行时执行它们。

这段代码的主要作用是注册和运行一系列的分析器,这些分析器将在代码静态分析过程中进行检查。每个分析器都实现了 golang.org/x/tools/go/analysis.Analyzer 接口,它定义了分析器应该提供的功能和行为。

例如,assign.Analyzer 分析器用于检查可能的赋值错误,unusedresult.Analyzer 用于检查未使用的函数调用结果,copylock.Analyzer 用于检查复制锁的错误使用等等。通过将这些分析器传递给 unitchecker.Main 函数,它们将在代码静态分析期间被执行。

整个过程的目的是通过静态分析来帮助开发者发现潜在的错误、不良实践和代码质量问题。当你在命令行中运行 go vet 命令时,实际上就是执行了这段代码,并调用了这些分析器进行代码分析和检查。

这段代码还包含了 objabi.AddVersionFlag() 的调用,它用于为 Go vet 工具添加版本标志。

总结一下,这段代码是 Go vet 工具的主入口,负责注册和执行一系列分析器来进行代码静态分析和检查。


想新增一个分析器,用于检测append(sli)后面没有追加值的情况,需要怎么写?

要新增一个分析器来检测 append(sli) 后面没有追加值的情况,你需要完成以下步骤:

创建新的分析器文件:首先,在 cmd/vet 目录下创建一个新的分析器文件,例如 appendcheck.go。

导入必要的包:在 appendcheck.go 文件中,导入所需的包,包括 "golang.org/x/tools/go/analysis" 和其他必要的包,例如 "go/ast" 和 "go/token"。

实现分析器:定义一个新的分析器类型,并实现 analysis.Analyzer 接口的方法。你需要实现 Run 方法,在其中编写检查逻辑。可以参考其他现有分析器的实现作为参考。

package main

import (
    "go/ast"
    "go/token"

    "golang.org/x/tools/go/analysis"
)

var appendCheckAnalyzer = &analysis.Analyzer{
    Name: "appendcheck",
    Doc:  "check for missing values after append",
    Run:  runAppendCheck,
}

func runAppendCheck(pass *analysis.Pass) (interface{}, error) {
    for _, file := range pass.Files {
        ast.Inspect(file, func(node ast.Node) bool {
            // 检查是否为 append 函数调用
            if call, ok := node.(*ast.CallExpr); ok {
                if sel, ok := call.Fun.(*ast.SelectorExpr); ok {
                    if ident, ok := sel.X.(*ast.Ident); ok && ident.Name == "append" {
                        // 检查 append 函数调用的参数列表是否为空
                        if len(call.Args) == 1 {
                            pass.Reportf(call.Lparen, "append called without values to append")
                        }
                    }
                }
            }
            return true
        })
    }
    return nilnil
}

func main() {
    // ...
    unitchecker.Main(appendCheckAnalyzer)
}

在上面的代码中,我们定义了一个名为 appendCheckAnalyzer 的新分析器,并实现了 Run 方法。在 Run 方法中,我们使用 ast.Inspect 函数遍历抽象语法树(AST),并检查每个函数调用是否为 append 函数调用。如果是,我们检查其参数列表的长度是否为 1。如果长度为 1,则意味着没有追加的值,我们会使用 pass.Reportf 方法报告一个问题。

注册分析器:在 main 函数中,将新的分析器 appendCheckAnalyzer 传递给 unitchecker.Main 函数,以便在运行时注册和执行该分析器。

构建和运行:将 appendcheck.go 文件和其他必要的文件添加到你的 Go 项目中,并构建执行文件。然后,你可以使用 go vet 命令来运行新的分析器,例如:

go vet ./...

这样,新的分析器就会检查代码中是否存在 append(sli) 后没有追加值的情况,并报告相应的问题。


(chatgpt给的代码有误,此处需要自行开发&调试)

实践一下:

对于

package main

func main() {

 sli1 := []string{"a""b""c"}
 sli2 := make([]string0)

 for _, val := range sli1 {
  print(val)
  sli2 = append(sli2)
 }
}

执行go vet之后不会有任何提示~

在src/cmd/vet下新建一个appendcheck.go

alt

内容如下:

package main

import (
 "fmt"
 "go/ast"
 //"go/token"

 "golang.org/x/tools/go/analysis"
 //"golang.org/x/tools/go/analysis/unitchecker"
)

var AppendCheckAnalyzer = &analysis.Analyzer{
 Name: "appendcheck",
 Doc:  "check for missing values after append",
 Run:  runAppendCheck,
}

func runAppendCheck(pass *analysis.Pass) (interface{}, error) {
 //print("到了这里了!")
 //fmt.Printf("%#v", pass)
 for _, node := range pass.Files {

  //print("也到了这里了!!!")

  ast.Inspect(node, func(n ast.Node) bool {
   if call, ok := n.(*ast.CallExpr); ok {
    if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
     argsLen := len(call.Args)
     if argsLen == 1 {
      fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
     }

    }
   }

   return true
  })

  //ast.Inspect(file, func(node ast.Node) bool {
  // // 检查是否为 append 函数调用
  // //print(66666666)
  // if call, ok := node.(*ast.CallExpr); ok {
  //  //print(7777777777)
  //  if sel, ok2 := call.Fun.(*ast.SelectorExpr); ok2 {
  //   print(888888)
  //   ident, ok3 := sel.X.(*ast.Ident)
  //   print("ident.Name:", ident.Name)
  //   //if ok3 && ident.Name == "append" {
  //   if ok3 && ident.Name == "fmt" {
  //    print("222222222222")
  //    print("参数长度为:", len(call.Args))
  //    // 检查 append 函数调用的参数列表是否为空
  //    if len(call.Args) == 1 {
  //     pass.Reportf(call.Lparen, "append called without values to append")
  //    }
  //   }
  //  }
  // }
  // return true
  //})
 }
 return nilnil
}

//func main() {
// // ...
// unitchecker.Main(AppendCheckAnalyzer)
//}

同时在同目录的main.go中新增AppendCheckAnalyzer这一行

alt

切回到src目录下,执行 ./all.bash

在IDE中配置最新的goroot,而后再执行go vet:

alt

功能算是实现了,但参考其他的Analyzer,代码都是在golang.org/x/tools/go/analysis/passes/具体的Analyzer名下面

所以不能直接将逻辑在Go主库上实现,而要在x/tools上面,Go主库import相应的功能


观察一下x/tools

golang.org/x/tools这个仓库中 go/analysis和gopls/internal/lsp/analysis有什么区别?

在 golang.org/x/tools 仓库中,go/analysis 和 gopls/internal/lsp/analysis 包是用于静态代码分析的两个不同的包,它们有以下区别:

  1. go/analysis 包:这是 Go 语言官方提供的用于编写静态代码分析工具的包。它提供了一组标准的接口和工具,用于创建、注册和执行各种代码分析器。这些分析器可以用于检查代码中的错误、不良实践、性能问题等。go/analysis 包提供了一种通用的框架,使得开发者能够方便地创建自定义的代码分析器,并与其他工具(如 go vet、golangci-lint 等)集成。这个包被广泛用于 Go 代码分析工具的开发。

  2. gopls/internal/lsp/analysis 包:这个包是 gopls(Go 语言的官方 Language Server Protocol 实现)内部使用的代码分析包。gopls 是一个提供代码编辑功能的工具,用于提供代码补全、语法检查、重构等功能。gopls 使用 analysis 包进行代码分析,以便在编辑器中提供实时的代码建议和错误提示。gopls/internal/lsp/analysis 包提供了一些特定于 gopls 的分析功能和扩展,以满足 gopls 的特定需求和集成。

总结起来,go/analysis 包是一个通用的静态代码分析框架,用于创建自定义的代码分析器和工具,而 gopls/internal/lsp/analysis 包是 gopls 工具内部使用的代码分析包,为 gopls 提供了与编辑器集成的代码分析功能。这两个包的目标和用途略有不同,但都用于静态代码分析。


想给go新增一个vet分析器,应该加在哪个目录下?


如果你想为 Go 新增一个 vet 分析器,你应该将代码放在 go/src/cmd/vet 目录下,这是 Go 语言源代码库中负责 vet 工具的目录。

在这个目录下,你可以创建一个新的 Go 源代码文件,例如 myvet.go,并在其中实现你的自定义 vet 分析器。

假设你的自定义 vet 分析器用于检查某种特定的代码模式或规范,你可以按照以下步骤进行操作:

进入 go/src/cmd/vet 目录:打开终端或命令行界面,切换到 Go 源代码库中的 cmd/vet 目录。

创建新的分析器文件:在 cmd/vet 目录下创建一个新的 Go 源代码文件,例如 myvet.go。

在 myvet.go 文件中实现分析器逻辑:在 myvet.go 文件中,你可以定义和实现你的自定义 vet 分析器。你需要导入必要的包和依赖,然后编写你的分析器逻辑。

注册分析器:在 myvet.go 文件中,确保你的分析器被注册到 unitchecker.Main 函数中。可以参考已有的分析器文件,将你的分析器添加到类似的位置。

构建和安装 vet 工具:使用适当的命令构建和安装 vet 工具,确保你的自定义分析器被包含在其中。例如,在 go/src/cmd/vet 目录下运行 go install 命令。

完成上述步骤后,你的自定义 vet 分析器就会被包含在 Go 的标准 vet 工具中,可以使用 go vet 命令来运行它。


pass.ReportRangef 报错是因为该函数名应为 pass.Reportf,而不是 pass.ReportRangef

请将以下行:

pass.ReportRangef(call.Lparen, "called without values to append")

修改为:

pass.Reportf(call.Lparen, "called without values to append")

这样修改后,应该能够正确调用 pass.Reportf 函数,并输出相应的错误报告。

在静态代码分析器中,pass.Reportf 函数用于生成问题报告,它的第一个参数是与问题相关的位置信息,例如在这种情况下是 call.Lparen,即 append 函数调用的左括号位置。第二个参数是具体的问题描述。

请注意,在你的代码中,使用 fmt.Printf 输出了一个调试信息,但最好是使用 pass.Reportf 来生成标准的静态代码分析报告。这样可以保持代码一致,并使得报告与其他分析器一起展示。


call.Lparen和call.Rparen有什么区别?

call.Lparen 和 call.Rparen 分别表示函数调用中的左括号和右括号的位置。

具体来说:

  • call.Lparen 是一个 token.Pos 类型的值,表示函数调用表达式中左括号的位置信息。
  • call.Rparen 也是一个 token.Pos 类型的值,表示函数调用表达式中右括号的位置信息。

在 Go 的抽象语法树(AST)中,函数调用表达式由函数名称和括号包围的参数列表组成。对于函数调用 append(sli) 来说:

  • call.Lparen 将指向左括号的位置。
  • call.Rparen 将指向右括号的位置。

这些位置信息可以在静态代码分析器中使用,用于报告问题的准确位置。例如,通过将 pass.Reportf 的第一个参数设置为 call.Lparen 或 call.Rparen,可以将问题报告与具体的括号位置相关联,以帮助开发人员定位代码中的问题。

总结起来,call.Lparen 和 call.Rparen 是用于表示函数调用表达式中左括号和右括号位置的标识符,用于在静态代码分析中准确定位问题的位置。


实践

在go/analysis/passes下新建一个appendcheck文件夹,新建一个appendcheck.go,内容如下:

// Copyright 2023 The Go Authors. All rights reserved.
// Use of this source code is governed by a BSD-style
// license that can be found in the LICENSE file.

// Package appendcheck defines an Analyzer that detects
// if there is only one variable in append.
package appendcheck

import (
 "go/ast"

 "golang.org/x/tools/go/analysis"
 "golang.org/x/tools/go/analysis/passes/inspect"
)

const Doc = "check for missing values after append"

var Analyzer = &analysis.Analyzer{
 Name:     "appendcheck",
 Doc:      Doc,
 URL:      "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/appendcheck",
 Requires: []*analysis.Analyzer{inspect.Analyzer},
 Run:      run,
}

func run(pass *analysis.Pass) (interface{}, error) {
 for _, node := range pass.Files {
  ast.Inspect(node, func(n ast.Node) bool {
   if call, ok := n.(*ast.CallExpr); ok {
    if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
     if len(call.Args) == 1 {
      pass.Reportf(call.Rparen, "called without values to append")
     }
    }
   }
   return true
  })
 }
 return nilnil
}

可以参考go/analysis/passes/timeformatgo/analysis/passes/bools,在相应main.go中填写

alt

此时回到Go项目中进行验证,很容易得知x/tools的代码都在和vet同目录的vendor中。如下图,将在x/tools中写好的appendcheck文件夹复制到go/src/cmd/vendor/golang.org/x/tools/go/analysis/passes下

alt

此时注释掉main.go中的AppendCheckAnalyzer,新增一行appendcheck.Analyzer,

alt

而后执行./all.bash

回到demo代码中,执行go vet

alt

当然在x/tools中还需要增加相应测试,这个先不谈。


这样就给go vet新增了一个检测项~

完整提交:

https://go-review.googlesource.com/c/tools/+/498475

https://go-review.googlesource.com/c/go/+/498416




评审阶段:


问题1: Check that it is a call to the builtin function append and not another function named append

Check that it is a call to the builtin function append and not another function named append, i.e.

if fun, ok := pass.TypesInfo.Uses[id].(*types.Builtin); ok { ... } 
是什么意思?

这段代码片段是Go语言中的一段代码,用于检查是否调用了内置函数append,而不是一个名为append的其他函数。

解释该代码段的逻辑如下:

  1. pass.TypesInfo是一个用于类型信息的结构体或对象,Uses是其中的一个字段。这个字段存储了程序中标识符(id)与其使用位置相关联的信息。

  2. 通过pass.TypesInfo.Uses[id],可以获取与标识符id关联的使用信息。

  3. 使用断言(type assertion)操作符.(*types.Builtin)将获取的使用信息转换为types.Builtin类型。这是因为内置函数append属于types.Builtin类型。

  4. 如果转换成功,那么表示使用的是内置函数append,可以在if语句块内执行相应的逻辑。

总而言之,这段代码的目的是判断一个标识符是否引用了内置函数append,并在满足条件时执行特定的逻辑。


举例说明:

下面是一个使用该代码片段的示例:

package main

import (
 "fmt"
 "go/types"
 "golang.org/x/tools/go/analysis"
)

func main() {
 analyzer := &appendAnalyzer{}
 analysis.Run(analyzer, "path/to/package")
}

type appendAnalyzer struct{}

func (a *appendAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
 for _, stmt := range pass.Nodes {
  if callExpr, ok := stmt.(*ast.CallExpr); ok {
   if id, ok := callExpr.Fun.(*ast.Ident); ok {
    if fun, ok := pass.TypesInfo.Uses[id].(*types.Builtin); ok && fun.Name() == "append" {
     fmt.Println("Calling the built-in append function.")
    }
   }
  }
 }
 return nilnil
}

在上述示例中,我们定义了一个名为appendAnalyzer的结构体,并实现了Run方法。在Run方法中,我们遍历AST中的语句,检查是否有调用内置函数append的表达式。如果是调用内置函数append,则打印一条消息。

该示例展示了如何在自定义的静态分析器(使用golang.org/x/tools/go/analysis包)中使用该代码片段来检测代码中的内置函数append的调用。请注意,这只是一个示例,实际使用时可能需要根据具体需求进行适当修改和扩展。


解决

言外之意,就是之前的代码,会把用户自定义的append方法也纳入检测。 验证一下:

假设自定义了一个append方法,如下

package main

func main() {
 sli := []int{123}
 sli = append(sli, 456)
 sli = append(sli)
}

func append(args ...interface{}) []int {

 println(args)
 return []int{}

}
alt

检测

package main

import (
 "fmt"
 "go/ast"
 "go/parser"
 "go/token"
)

func main() {
 src := `
 package main
 
 func main() {
  sli := []int{1, 2, 3}
  sli = append(sli, 4, 5, 6)
  sli = append(sli)
 }
 
 func append(args ...interface{}) []int {
 
  println(args)
  return []int{}
 
 }`


 fset := token.NewFileSet()
 node, err := parser.ParseFile(fset, "", src, parser.ParseComments)
 if err != nil {
  fmt.Println("Failed to parse file:", err)
  return
 }

 ast.Inspect(node, func(n ast.Node) bool {
  if call, ok := n.(*ast.CallExpr); ok {
   if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
    argsLen := len(call.Args)
    fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
   }
  }

  return true
 })

}

输出:

发现 append 函数调用,参数数量:4
发现 append 函数调用,参数数量:1

进行修改:

package main

import (
 "fmt"
 "go/ast"
 "go/parser"
 "go/token"
 "go/types"
)

func main() {
 src := `
 package main
 
 func main() {
  sli := []int{1, 2, 3}
  sli = append(sli, 4, 5, 6)
  sli = append(sli)
 }
 
 func append(args ...interface{}) []int {
 
  println(args)
  return []int{}
 
 }`


 fset := token.NewFileSet()
 node, err := parser.ParseFile(fset, "", src, parser.ParseComments)
 if err != nil {
  fmt.Println("Failed to parse file:", err)
  return
 }

 // 创建类型检查器(这部分为新增)
 conf := types.Config{}
 info := &types.Info{}
 _, err = conf.Check("", fset, []*ast.File{node}, info)
 if err != nil {
  fmt.Println("Failed to type-check file:", err)
  return
 }

 ast.Inspect(node, func(n ast.Node) bool {
  if call, ok := n.(*ast.CallExpr); ok {
   if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {

    fmt.Println()
    if obj, ok := info.ObjectOf(ident).(*types.Builtin); ok {
     if obj.Name() == "append" {
      argsLen := len(call.Args)
      fmt.Printf("发现内建的 append 函数调用,参数数量:%d\n", argsLen)
     }
    } else {
     argsLen := len(call.Args)
     fmt.Printf("发现用户自定义的 append 函数调用,参数数量:%d\n", argsLen)
    }
   }
  }

  return true
 })

}

运行后输出:

发现用户自定义的 append 函数调用,参数数量:4

发现用户自定义的 append 函数调用,参数数量:1

在修改后的代码中,添加了类型检查的步骤。通过使用types.Configtypes.Info,可以对Go代码进行类型检查。在检查完成后,使用info.ObjectOf函数获取标识符对应的对象,并通过类型断言判断该对象是否为types.Builtin类型(内建函数)。如果是内建函数append,则说明它不是用户自定义的函数;否则,它是用户自定义的函数。

通过这种方式,可以区分内建的append函数和用户自定义的append函数。


实际上这段代码,会一直走到 发现用户自定义的 append 函数调用这里。。所以还是有问题




问题2:Use MustExtractDoc. https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/internal/analysisutil#MustExtractDoc . See other analyzers for examples.


alt

可以参考assign.go的来改: https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/assign

这个比较好解决,即不要像 bools这个Analyzer一样,直接把文档写在上面;而要像assign一样,使用//go:embed doc.go,在doc.go中更详细地描述这个Analyzer的作用

alt
alt

问题3: Use x/tools/go/analysis/passes/inspect


See nilfunc for an example of using inspector.Preorder https://cs.opensource.google/go/x/tools/+/refs/tags/v0.10.0:go/analysis/passes/nilfunc/nilfunc.go;l=34-39

alt

好吧,搜了下tools/go/analysis/passed目录pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)这个关键字....确实应该统一“格式”

alt

golang中下面这段代码什么作用?请举例说明
 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

 nodeFilter := []ast.Node{
  (*ast.BinaryExpr)(nil),
 }
 inspect.Preorder(nodeFilter, func(n ast.Node) {}

这段代码用于在Go语言的静态分析框架中,使用inspector来对AST节点进行预定义类型的先序遍历。

让我们来逐行解释这段代码的作用:

  1. inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector):首先,我们从pass.ResultOf中获取与inspect.Analyzer相关联的结果,并将其转换为*inspector.Inspector类型。这里假设inspect.Analyzer是一个静态分析器,其执行结果包含了inspector,该inspector用于遍历AST。

  2. nodeFilter := []ast.Node{ (*ast.BinaryExpr)(nil), }:我们创建一个AST节点过滤器nodeFilter,其中只包含一个元素,即*ast.BinaryExpr类型的节点。这个过滤器指定了我们只关心二元表达式节点。

  3. inspect.Preorder(nodeFilter, func(n ast.Node) {}:使用Preorder方法进行先序遍历,遍历的起点是AST根节点。我们传递了节点过滤器nodeFilter和一个匿名函数作为参数。匿名函数会在遍历到满足过滤器条件的节点时被调用。

举个例子,假设我们有以下Go代码片段:

package main

import "fmt"

func main() {
 a := 1 + 2
 b := a * 3
 fmt.Println(b)
}

如果我们将上述代码传递给一个自定义的静态分析器,并使用上述代码片段作为分析的输入,可以使用上述代码片段中的代码来遍历AST并找到二元表达式节点。

package main

import (
 "fmt"
 "go/ast"
 "go/parser"
 "go/token"
 "golang.org/x/tools/go/analysis"
 "golang.org/x/tools/go/analysis/passes/inspect"
 "golang.org/x/tools/go/ast/inspector"
)

func main() {
 analyzer := &exampleAnalyzer{}

 analyzers := []*analysis.Analyzer{analyzer}

 analysis.Run(analyzers)
}

type exampleAnalyzer struct{}

func (a *exampleAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
 nodeFilter := []ast.Node{
  (*ast.BinaryExpr)(nil),
 }

 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)
 inspect.Preorder(nodeFilter, func(n ast.Node) {
  // 处理二元表达式节点
  binaryExpr := n.(*ast.BinaryExpr)
  fmt.Printf("发现二元表达式:%s\n", binaryExpr)
 })

 return nilnil
}

func (a *exampleAnalyzer) String() string {
 return "exampleAnalyzer"
}

在上述示例中,我们定义了一个自定义的静态分析器exampleAnalyzer,并在Run方法中使用inspect.Preorder进行AST的先序遍历。在处理函数中,我们将满足过滤器条件的节点强制转换为*ast.BinaryExpr类型,并打印出来。

当运行上述代码时,我们会发现在遍历AST期间,会找到两个二元表达式节点,并打印出它们的信息。

请注意,上述示例中涉及到了使用Go的静态分析器框架和相关库(例如golang.org/x/tools/go/analysisgolang.org/x/tools/go/ast/inspector等)。在实际使用中,您可能需要进行适当的导入和配置,以便正确运行和应用静态分析器。


给的代码完全无法编译..


帮我使用此,实现检测append后面的元素数量的功能

帮我使用

inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

 nodeFilter := []ast.Node{
  (*ast.BinaryExpr)(nil),
 }
 inspect.Preorder(nodeFilter, func(n ast.Node) {}

实现检测append后面的元素数量的功能


当我们使用inspect.Preorder结合inspector来实现检测append后面的元素数量的功能时,可以按照以下步骤进行:

  1. 导入必要的包和库:
package main

import (
 "fmt"
 "go/ast"
 "go/parser"
 "go/token"
 "golang.org/x/tools/go/analysis"
 "golang.org/x/tools/go/analysis/passes/inspect"
 "golang.org/x/tools/go/ast/inspector"
)
  1. 创建自定义的静态分析器:
type appendAnalyzer struct{}
  1. 实现静态分析器的 Run方法:
func (a *appendAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

 nodeFilter := []ast.Node{
  (*ast.CallExpr)(nil),
 }

 inspect.Preorder(nodeFilter, func(n ast.Node) {
  callExpr := n.(*ast.CallExpr)

  // 检查是否是 append 函数调用
  if ident, ok := callExpr.Fun.(*ast.Ident); ok && ident.Name == "append" {
   // 获取元素数量
   argsLen := len(callExpr.Args)
   fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
  }
 })

 return nilnil
}
  1. 实现静态分析器的 String方法:
func (a *appendAnalyzer) String() string {
 return "appendAnalyzer"
}
  1. main函数中创建静态分析器集合并运行分析器:
func main() {
 analyzer := &appendAnalyzer{}

 analyzers := []*analysis.Analyzer{
  inspect.Analyzer,
  analyzer,
 }

 analysis.Run(analyzers)
}

完整的示例代码如下:

package main

import (
 "fmt"
 "go/ast"
 "go/parser"
 "go/token"
 "golang.org/x/tools/go/analysis"
 "golang.org/x/tools/go/analysis/passes/inspect"
 "golang.org/x/tools/go/ast/inspector"
)

type appendAnalyzer struct{}

func (a *appendAnalyzer) Run(pass *analysis.Pass) (interface{}, error) {
 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

 nodeFilter := []ast.Node{
  (*ast.CallExpr)(nil),
 }

 inspect.Preorder(nodeFilter, func(n ast.Node) {
  callExpr := n.(*ast.CallExpr)

  // 检查是否是 append 函数调用
  if ident, ok := callExpr.Fun.(*ast.Ident); ok && ident.Name == "append" {
   // 获取元素数量
   argsLen := len(callExpr.Args)
   fmt.Printf("发现 append 函数调用,参数数量:%d\n", argsLen)
  }
 })

 return nilnil
}

func (a *appendAnalyzer) String() string {
 return "appendAnalyzer"
}

func main() {
 analyzer := &appendAnalyzer{}

 analyzers := []*analysis.Analyzer{
  inspect.Analyzer,
  analyzer,
 }

 analysis.Run(analyzers)
}

以上代码会在检测到append函数调用时打印出其后面的元素数量。请注意,此代码中使用了Go的静态分析器框架和相关库。确保在运行代码之前已正确导入并安装所需的包和库。


这两段质量都不咋高


直接在tools里面修改,再找个go源码项目,将改好的tools的append这个目录按照之前的操作,复制到go/src/cmd的vendor的golang.org/x/tools/go/analysis/passer目录下,并修改go/src/cmd/vet/main.go

alt

或者用最初那种方式,在go/src/cmd/vet/下新建appendcheck.go,把内容贴过去,同时在main.go中增加AppendCheckAnalyzer,

最后都要编译这个Go源码项目


关于这两种方式的差异,以及如何用新的方式实现检测append后面值缺失的功能,直接参考 使用go/analysis包实现自定义的linter


最后可用的代码:

package append

import (
 _ "embed"
 "go/ast"
 "go/types"

 "golang.org/x/tools/go/analysis"
 "golang.org/x/tools/go/analysis/passes/inspect"
 "golang.org/x/tools/go/analysis/passes/internal/analysisutil"
 "golang.org/x/tools/go/ast/inspector"
)

//go:embed doc.go
var doc string

var Analyzer = &analysis.Analyzer{
 Name:     "append",
 Doc:      analysisutil.MustExtractDoc(doc, "append"),
 URL:      "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/append",
 Requires: []*analysis.Analyzer{inspect.Analyzer},
 Run:      run,
}

func run(pass *analysis.Pass) (interface{}, error) {
 inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)

 nodeFilter := []ast.Node{
  (*ast.CallExpr)(nil),
 }
 inspect.Preorder(nodeFilter, func(n ast.Node) {
  call := n.(*ast.CallExpr)
  if ident, ok := call.Fun.(*ast.Ident); ok && ident.Name == "append" {
   if _, ok := pass.TypesInfo.Uses[ident].(*types.Builtin); ok {
    if len(call.Args) == 1 {
     pass.ReportRangef(call, "append with no values")
    }
   }
  }
 })

 return nilnil
}

go/analysis/passes/inspect分析器提供了一个构建块,可以使用它来代替ast.Inspect()或ast.Walk()来遍历语法文件。但是,如果有多个分析器,并且每个分析器都必须遍历语法树的话,则效率不是很高!

go/analysis/passes/inspect包比ast.Inspect()快得多,因为它在底层使用golang.org/x/tools/go/ast/inspector包。


最后还需要更新vendor等,在此略过。

本文由 mdnice 多平台发布

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/150406.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

应用层协议 HTTP

一、应用层协议 我们已经学过 TCP/IP , 已然知道数据能从客户端进程经过路径选择跨网络传送到服务器端进程。 我们还需要知道的是&#xff0c;我们把数据从 A 端传送到 B 端&#xff0c; TCP/IP 解决的是顺丰的功能&#xff0c;而两端还要对数据进行加工处理或者使用&#xf…

SSM - Springboot - MyBatis-Plus 全栈体系(二十一)

第四章 SpringMVC 四、RESTFUL 风格设计和实战 1. RESTFul 风格概述 1.1 RESTFul 风格简介 RESTful&#xff08;Representational State Transfer&#xff09;是一种软件架构风格&#xff0c;用于设计网络应用程序和服务之间的通信。它是一种基于标准 HTTP 方法的简单和轻量…

TCP四次挥手

四次挥手发生在断开连接的时候&#xff0c;在程序中当调用了close()会使用TCP协议进行四次挥手。 客户端和服务器端都可以主动发起断开连接&#xff0c;谁先调用close()谁就是发起。 因为在TCP连接的时候&#xff0c;采用三次握手建立的的连接是双向的&#xff0c;在断开的时候…

股票买进和卖出手续费怎么算,一文看懂,不用研究那些旧规则了

先说结论 印花税&#xff0c;上个月刚下调为0.05%&#xff08;万分之五&#xff09;&#xff0c;单向收取 过户费&#xff0c;最新政策是0.001%&#xff08;十万分之一&#xff09;&#xff0c;双向收取 券商佣金&#xff0c;各家不同&#xff0c;每个客户经理也不同0.0086%…

python笔记:pandas/geopandas DataFrame逐行遍历

在Pandas和GeoPandas中&#xff0c;可以使用几种不同的方法来遍历DataFrame的每一行 0 数据 import pandas as pddata {column1: range(1, 1001),column2: range(1001, 2001) } df pd.DataFrame(data) df 1 iterrows for index, row in df.iterrows():print(index)print(r…

ChatGPT Vision初体验

本周&#xff0c;OpenAI为其广受欢迎的 ChatGPT 平台发布了一项突破性的补充。除了处理文本之外&#xff0c;ChatGPT 现在还可以处理图像并聊天。 很难夸大这件事的重要性。目前多达70% 的内容是视觉内容而不是书面内容。人们每年会生成数千张照片&#xff0c;而当今许多最大的…

FastThreadLocal 快在哪里 ?

FastThreadLocal 快在哪里 &#xff1f; 引言FastThreadLocalset如何获取当前线程私有的InternalThreadLocalMap &#xff1f;如何知道当前线程使用到了哪些FastThreadLocal实例 ? get垃圾回收 小结 引言 FastThreadLocal 是 Netty 中造的一个轮子&#xff0c;那么为什么放着…

JavaScript系列从入门到精通系列第十八篇:JavaScript中的函数作用域

文章目录 前言 一&#xff1a;函数作用域 前言 我们刚才提到了&#xff0c;在<Script>标签当中进行定义的变量、对象、函数对象都属于全局作用域&#xff0c;全局作用域在页面打开的时候生效在页面关闭的时候失效。 一&#xff1a;函数作用域 调用函数时创建函数作用域…

【Linux学习】05-1Linux上安装部署各类软件

Linux&#xff08;B站黑马&#xff09;学习笔记 01Linux初识与安装 02Linux基础命令 03Linux用户和权限 04Linux实用操作 05-1Linux上安装部署各类软件 05-2Linux上部署项目 文章目录 Linux&#xff08;B站黑马&#xff09;学习笔记前言05-1Linux上安装部署各类软件JDK安装部署…

Linux高性能服务器编程 学习笔记 第十一章 定时器

网络程序需要处理定时事件&#xff0c;如定期检测一个客户连接的活动状态。服务器进程通常管理着众多定时事件&#xff0c;有效地组织这些定时事件&#xff0c;使其在预期的时间被触发且不影响服务器的主要逻辑&#xff0c;对于服务器的性能有至关重要的影响。为此&#xff0c;…

【计算机网络-自顶向下方法】应用层(HTTP、FTP)

1. Principles of network applications 创建一个网络应用 创建一个网络应用的核心&#xff0c;是编写一个分布式程序&#xff0c;使其可以运行在不同的端系统上&#xff0c;并能通过网络相互通信。&#xff08;例如&#xff0c;web服务器软件与浏览器软件&#xff09;   应…

MongoEngine 简介安装、连接、数据类型及其参数详解

文章目录 前言一、MongoEngine 简介二、MongoEngine的安装与连接1. 安装MongoEngine2. 连接到MongoDB3. 定义数据模型 三、MongoEngine模型介绍1. 常见数据类型2. 数据类型参数 总结 前言 为了巩固所学的知识&#xff0c;作者尝试着开始发布一些学习笔记类的博客&#xff0c;方…

归并排序与非比较排序详解

W...Y的主页 &#x1f60a; 代码仓库分享 &#x1f495; &#x1f354;前言&#xff1a; 上篇博客我们讲解了非常重要的快速排序&#xff0c;相信大家已经学会了。最后我们再学习一种特殊的排序手法——归并排序。话不多说我们直接上菜。 目录 归并排序 基本思想 递归思路…

[NewStarCTF 2023 公开赛道] week1

最近没什么正式比赛&#xff0c;都是入门赛&#xff0c;有moectf,newstar,SHCTF,0xGame都是漫长的比赛。一周一堆制。 这周newstar第1周结束了&#xff0c;据说py得很厉害&#xff0c;第2周延期了&#xff0c;什么时候开始还不一定&#xff0c;不过第一周已经结束提交了&#…

林沛满-TCP之在途字节数

本文整理自&#xff1a;《Wireshark网络分析的艺术 第1版》 作者&#xff1a;林沛满 著 出版时间&#xff1a;2016-02 我一直谨记斯蒂芬霍金的金玉良言—每写一道数学公式就会失去一半读者。不过为了深度分析网络包&#xff0c;有时候是不得不计算的&#xff0c;好在小学一年级…

socket简介

套接字&#xff08;Socket&#xff09;实质上就是对网络中不同主机上的应用进程之间进行双向通信的端点的抽象。一个套接字就是网络上进程通信的一端&#xff0c;为应用层进程利网络协议交换数据提供了相应机制。套接字出于承上启下的作用&#xff0c;向上连接应用进程&#xf…

【QT5-程序控制电源-[GPIB-USB-HS]-SCPI协议-上位机-基础样例【2】】

【QT5-程序控制电源-[GPIB-USB-HS]-SCPI协议-上位机-基础样例【2】】 1、前言2、实验环境3、自我总结1、基础了解仪器控制-熟悉仪器2、连接SCPI协议3、了解GPIB-USB-HS4、软件调试-代码编写 4、熟悉协议-SCPI协议5、实验过程-熟悉软件&#xff08;1&#xff09;去官网NI&#x…

【Java项目推荐之黑马头条】自媒体文章实现异步上下架(使用Kafka中间件实现)

自媒体文章上下架功能完成 需求分析 流程说明 接口定义 说明接口路径/api/v1/news/down_or_up请求方式POST参数DTO响应结果ResponseResult DTO Data public class WmNewsDto {private Integer id;/*** 是否上架 0 下架 1 上架*/private Short enable;}ResponseResult 自媒…

使用华为eNSP组网试验⑷-OSPF多区域组网

今天进行了OSPF的多区域组网试验&#xff0c;本来这是个很简单的操作&#xff0c;折腾了好长时间&#xff0c;根本原因只是看了别人写的配置代码&#xff0c;没有真正弄明白里面对应的规则。 一般情况下&#xff0c;很多单位都使用OSPF进行多区域的组网&#xff0c;大体分为1个…

2023-IDEA插件推荐

CamelCase 链接 https://plugins.jetbrains.com/plugin/7160-camelcase https://github.com/netnexus/camelcaseplugin 介绍 提供下划线、驼峰等代码风格的切换。快捷键是⇧ ⌥ U / Shift Alt U GsonFormatPlus 链接 https://plugins.jetbrains.com/plugin/14949-gs…