diff --git a/README.md b/README.md index 173af52..a147bef 100644 --- a/README.md +++ b/README.md @@ -1,25 +1,34 @@ [日本語版](./README_ja.md) -# skeleton +# skeleton -skeleton creates skeleton codes for a modularized static analysis tool with [x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package. +skeleton is skeleton codes generator for Go's static analysis tools. skeleton makes easy to develop static analysis tools with [x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package and [x/tools/go/packages](https://golang.org/x/tools/go/packages) package. -## x/tools/go/analysis pacakge +## x/tools/go/analysis package -[x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package provides a type `analysis.Analyzer` which is unit of analyzers in modularized static analysis tool. +[x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package is for modularizing static analysis tools. x/tools/go/analysis package provides [analysis.Analyzer](https://golang.org/x/tools/go/analysis/#Analyzer) type which represents a unit of modularized static analysis tool. -If you want to create new analyzer, you should provide a package variable which type is `*analysis.Analyzer`. -`skeleton` creates skeleton codes of the package and directories including test codes and main.go. +`x/tools/go/analysis` package also provides common works of a static analysis tool. Just run the `skeleton mylinter` command, skeleton generates an `*analyzer.Analyzer` type initialization code, a test code, and a `main.go` for an executable which may be run with the `go vet` command. -## Install +The following blog helps to learn about the skeleton. -### Go version < 1.16 +* [Go static analysis starting with skeleton](https://engineering.mercari.com/blog/entry/20220406-eea588f493/) (Japanese) + +The following slides describes details of Go's static analysis including the `x/tools/go/analysis` package. + +* [A complete introduction of the programming language Go, Chapter 14: Static Analysis and Code Generation](http://tenn.in/analysis) (Japanese) + +## Installation + +There are two diffrent ways to install skeleton by Go versions. + +### Less than Go1.16 ``` $ go get -u github.com/gostaticanalysis/skeleton/v2 ``` -### Go 1.16+ +### Go1.16 or higher ``` $ go install github.com/gostaticanalysis/skeleton/v2@latest @@ -27,17 +36,21 @@ $ go install github.com/gostaticanalysis/skeleton/v2@latest ## How to use -### Create skeleton codes with module path +### Create a skeleton code with a module path + +skeleton receives a module path and generates a skeleton code with the module path. All generated codes are located in a directory which name is the last element of the module path. + +When you run skeleton with `example.com/mylinter` as a module path, skeleton generates the following files. ``` -$ skeleton example.com/pkgname -pkgname +$ skeleton example.com/mylinter +mylinter ├── cmd -│   └── pkgname +│   └── mylinter │   └── main.go ├── go.mod -├── pkgname.go -├── pkgname_test.go +├── mylinter.go +├── mylinter_test.go └── testdata └── src └── a @@ -45,82 +58,280 @@ pkgname └── go.mod ``` +#### Analyzer + +A static analysis tool which developed with `x/tools/go/analysis`, is represented by value of `*analysis.Analyzer` type. In the mylinter case, the value is defined in `mylinter.go` as a variable which name is `Analyzer`. + +The generated code provides toy implement with `inspect.Analyzer`. It finds identifiers which name are `gopher`. + +```go +package mylinter + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +const doc = "mylinter is ..." + +// Analyzer is ... +var Analyzer = &analysis.Analyzer{ + Name: "mylinter", + Doc: doc, + Run: run, + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + }, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.Ident)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + switch n := n.(type) { + case *ast.Ident: + if n.Name == "gopher" { + pass.Reportf(n.Pos(), "identifier is gopher") + } + } + }) + + return nil, nil +} +``` + +#### Test codes + +skeleton also generates test codes. `x/tools/go/analysis` package provides a testing library in `analysistest` sub package. `analysistest.Run` runs tests with source codes in `testdata/src` directory. The second parameter is a path for `testdata` directory. The third parameter is test target analyzer and remains are packages which are used in tests. + +```go +package mylinter_test + +import ( + "testing" + + "github.com/gostaticanalysis/example.com/mylinter" + "github.com/gostaticanalysis/testutil" + "golang.org/x/tools/go/analysis/analysistest" +) + +// TestAnalyzer is a test for Analyzer. +func TestAnalyzer(t *testing.T) { + testdata := testutil.WithModules(t, analysistest.TestData(), nil) + analysistest.Run(t, testdata, mylinter.Analyzer, "a") +} +``` + +In the mylinter case, the test uses `testdata/src/a/a.go` file as a test data. `mylinter.Analyzer` finds `gopher` identifiers in the source code and report them. In the test side, expected reports are described in comments. The comments must be start with `want` and a reporting message follows. The reporting message is represented by a regular expression. When the analyzer reports unexpected diagnostics or does not report expected diagnostics, the test will be failed. + +```go +package a + +func f() { + // The pattern can be written in regular expression. + var gopher int // want "pattern" + print(gopher) // want "identifier is gopher" +} +``` + +When you run `go mod tidy` and `go test`, the test will be failed because the analyzer does not report a diagnostic with "pattern". + +``` +$ go mod tidy +go: finding module for package golang.org/x/tools/go/analysis +go: finding module for package github.com/gostaticanalysis/testutil +go: finding module for package golang.org/x/tools/go/analysis/passes/inspect +go: finding module for package golang.org/x/tools/go/analysis/unitchecker +go: finding module for package golang.org/x/tools/go/ast/inspector +go: finding module for package golang.org/x/tools/go/analysis/analysistest +go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10 +go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0 +go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10 + +$ go test +--- FAIL: TestAnalyzer (0.06s) + analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern` + analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern` +FAIL +exit status 1 +FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s +``` + +#### Executable file + +skeleton generates `main.go` in `cmd` directory. When you build it and generate an executable file, the executable file must be run via `go vet` command such as the following. `-vettool` flag for `go vet` command specifies an absoluted path for an executable file of own static analysis tool. + +``` +$ go vet -vettool=`which mylinter` ./... +``` + +### Overwrite a directory + +If the directory already exists, skeleton gives you with following options. + +``` +$ skeleton example.com/mylinter +mylinter already exists, overwrite? +[1] No (Exit) +[2] Remove and create new directory +[3] Overwrite existing files with confirmation +[4] Create new files only +``` + +### Without cmd directory -### Create skeleton codes without cmd directory +If you don't need `cmd` directory, you can set `false` to `-cmd` flag. ``` -$ skeleton -cmd=false example.com/pkgname -pkgname +$ skeleton -cmd=false example.com/mylinter +mylinter ├── go.mod -├── pkgname.go -├── pkgname_test.go -└── testdata - └── src - └── a - ├── a.go - └── go.mod +mylinter.go +├── mylinter_test.go +└─ testdata + └─ testdata + testdata └── src + Go.mod + go.mod +``` + +### Without go.mod file + +skeleton generates a `go.mod` file by default. When you would like to use skeleton in a directory which is already under Go Modules management, you can set `false` to `-gomod` option as following. + +``` +$ skeleton -gomod=false example.com/mylinter +mylinter +├── cmd +│└── mylinter +└─ main.go +├── mylinter.go +mylinter_test.go +└─ testdata + testdata └── src + testdata └─ a + Go.mod + go.mod ``` -### Change the checker from unitchecker to singlechecker or multichecker +### SKELETON_PREFIX environment variable + +When `SKELETON_PREFIX` environment variable is set, skeleton puts it as a prefix to a module path. + +``` +$ SKELETON_PREFIX=example.com skeleton mylinter +$ head -1 mylinter/go.mod +module example.com/mylinter +``` -You can change the checker from unitchecker to singlechecker or multichecker. +It is useful with [direnv](https://github.com/direnv/direnv) such as following. ``` -$ skeleton -checker=single example.com/pkgname -$ cat cmd/pkgname/main.go +$ cat ~/repos/gostaticanalysis/.envrc +export SKELETON_PREFIX=github.com/gostaticanalysis +``` + +If `SKELETON_PREFIX` environment variable is specified but the `-gomod` flag is `false`, skeleton prioritizes `-gomod` flag. + +### singlechecker and multichecker + +skeleton uses `unitchecker` package in `main.go` by default. You can change it to `singlechecker` package or `multichecker` package by specifying the `-checker` flag. + +`singlechecker` package runs a single analyzer and `multichecker` package runs multiple analyzers. These packages does not need `go vet` command to run. + +The following is an example of using `singlechecker` package. + +``` +$ skeleton -checker=single example.com/mylinter +$ cat cmd/mylinter/main.go package main import ( - "pkgname" + "mylinter" "golang.org/x/tools/go/analysis/singlechecker" ) -func main() { singlechecker.Main(pkgname.Analyzer) } +func main() { singlechecker.Main(mylinter.Analyzer) } ``` -## Build as a plugin for golangci-lint +Using `singlechecker` package or `multichecker` package seems easy way. But when you use them, you cannot receive benefit of using `go vet`. If you don't have particular reason of using `singlechecker` package or `multichecker` package, you should use `unitchecker`. It means you should not use `-checker` flag in most cases. -`skeleton` generates plugin directory which has main.go. -The main.go can be built as a plugin for [golangci-lint](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint). +### Kinds of skeleton code -``` -$ skeleton -plugin example.com/pkgname -$ go build -buildmode=plugin -o path_to_plugin_dir example.com/pkgname/plugin/pkgname -``` +skeleton can change kind of skeleton code by using `-kind` flag. + +* `-kind=inspect` (default): using `inspect.Analyzer` +* `-kind=ssa`: using the static single assignment (SSA, Static Single Assignment) form generated by `buildssa.Analyzer` +* `-kind=codegen`: code generator. +* `-kind=packages`: using `x/tools/go/packages` package + +### Create code generator -If you would like to specify flags for your plugin, you can put them via `ldflags` as below. +When you gives `codegen` to `-kind` flag, skeleton generates skeleton code of code generation tool with [gostaticanalysis/codegen](https://pkg.go.dev/github.com/gostaticanalysis/codegen) package. ``` -$ skeleton -plugin example.com/pkgname -$ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/pkgname/plugin/pkgname +$ skeleton -kind=codegen example.com/mycodegen +mycodegen +├── cmd +│   └── mycodegen +│   └── main.go +├── go.mod +├── mycodegen.go +├── mycodegen_test.go +└── testdata + └── src + └── a + ├── a.go + ├── go.mod + └── mycodegen.golden ``` -### Create skeleton codes of codegenerator +`gostaticanalysis/codegen` package is an experimental, please be careful. + +### golangci-lint plugin + +skeleton generates codes that can be used as a plugin of [golangci-lint](https://github.com/golangci/golangci-lint) by specifying `-plugin` flag. ``` -$ skeleton -kind=codegen example.com/pkgname -pkgname +$ skeleton -plugin example.com/mylinter +mylinter ├── cmd -│   └── pkgname +│   └── mylinter │   └── main.go ├── go.mod -├── pkgname.go -├── pkgname_test.go +├── mylinter.go +├── mylinter_test.go +├── plugin +│ └── main.go └── testdata └── src └── a ├── a.go - └── pkgname.golden + └── go.mod ``` -### Change type of skeleton code +You can see [the documentation](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint). -skeleton accepts `-kind` option which indicates kind of skeleton code. +``` +$ skeleton -plugin example.com/mylinter +$ go build -buildmode=plugin -o path_to_plugin_dir example.com/mylinter/plugin/mylinter +``` -* `-kind=inspect`(default): generate skeleton code with `inspect.Analyzer` -* `-kind=ssa`: generate skeleton code with `buildssa.Analyzer` -* `-kind=codegen`: generate skeleton code of a code generator +skeleton provides a way which can specify flags to your plugin with `-ldflags`. If you would like to know the details of it, please read the generated skeleton code. -### Without go.mod file +``` +$ skeleton -plugin example.com/mylinter +$ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/mylinter/plugin/mylinter +``` -If you give `-gomod=false` flag to skeleton, skeleton does not create a go.mod file. +golangci-lint is built with `CGO_ENABLED=0` by default. So you should rebuilt with `CGO_ENABLED=1` because plugin package in the standard library uses CGO. And you should same version of modules with golangci-lint such as `golang.org/x/tools/go` module. The plugin system for golangci-lint is not recommended way. diff --git a/v2/README.md b/v2/README.md index 173af52..a147bef 100644 --- a/v2/README.md +++ b/v2/README.md @@ -1,25 +1,34 @@ [日本語版](./README_ja.md) -# skeleton +# skeleton -skeleton creates skeleton codes for a modularized static analysis tool with [x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package. +skeleton is skeleton codes generator for Go's static analysis tools. skeleton makes easy to develop static analysis tools with [x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package and [x/tools/go/packages](https://golang.org/x/tools/go/packages) package. -## x/tools/go/analysis pacakge +## x/tools/go/analysis package -[x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package provides a type `analysis.Analyzer` which is unit of analyzers in modularized static analysis tool. +[x/tools/go/analysis](https://golang.org/x/tools/go/analysis) package is for modularizing static analysis tools. x/tools/go/analysis package provides [analysis.Analyzer](https://golang.org/x/tools/go/analysis/#Analyzer) type which represents a unit of modularized static analysis tool. -If you want to create new analyzer, you should provide a package variable which type is `*analysis.Analyzer`. -`skeleton` creates skeleton codes of the package and directories including test codes and main.go. +`x/tools/go/analysis` package also provides common works of a static analysis tool. Just run the `skeleton mylinter` command, skeleton generates an `*analyzer.Analyzer` type initialization code, a test code, and a `main.go` for an executable which may be run with the `go vet` command. -## Install +The following blog helps to learn about the skeleton. -### Go version < 1.16 +* [Go static analysis starting with skeleton](https://engineering.mercari.com/blog/entry/20220406-eea588f493/) (Japanese) + +The following slides describes details of Go's static analysis including the `x/tools/go/analysis` package. + +* [A complete introduction of the programming language Go, Chapter 14: Static Analysis and Code Generation](http://tenn.in/analysis) (Japanese) + +## Installation + +There are two diffrent ways to install skeleton by Go versions. + +### Less than Go1.16 ``` $ go get -u github.com/gostaticanalysis/skeleton/v2 ``` -### Go 1.16+ +### Go1.16 or higher ``` $ go install github.com/gostaticanalysis/skeleton/v2@latest @@ -27,17 +36,21 @@ $ go install github.com/gostaticanalysis/skeleton/v2@latest ## How to use -### Create skeleton codes with module path +### Create a skeleton code with a module path + +skeleton receives a module path and generates a skeleton code with the module path. All generated codes are located in a directory which name is the last element of the module path. + +When you run skeleton with `example.com/mylinter` as a module path, skeleton generates the following files. ``` -$ skeleton example.com/pkgname -pkgname +$ skeleton example.com/mylinter +mylinter ├── cmd -│   └── pkgname +│   └── mylinter │   └── main.go ├── go.mod -├── pkgname.go -├── pkgname_test.go +├── mylinter.go +├── mylinter_test.go └── testdata └── src └── a @@ -45,82 +58,280 @@ pkgname └── go.mod ``` +#### Analyzer + +A static analysis tool which developed with `x/tools/go/analysis`, is represented by value of `*analysis.Analyzer` type. In the mylinter case, the value is defined in `mylinter.go` as a variable which name is `Analyzer`. + +The generated code provides toy implement with `inspect.Analyzer`. It finds identifiers which name are `gopher`. + +```go +package mylinter + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +const doc = "mylinter is ..." + +// Analyzer is ... +var Analyzer = &analysis.Analyzer{ + Name: "mylinter", + Doc: doc, + Run: run, + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + }, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.Ident)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + switch n := n.(type) { + case *ast.Ident: + if n.Name == "gopher" { + pass.Reportf(n.Pos(), "identifier is gopher") + } + } + }) + + return nil, nil +} +``` + +#### Test codes + +skeleton also generates test codes. `x/tools/go/analysis` package provides a testing library in `analysistest` sub package. `analysistest.Run` runs tests with source codes in `testdata/src` directory. The second parameter is a path for `testdata` directory. The third parameter is test target analyzer and remains are packages which are used in tests. + +```go +package mylinter_test + +import ( + "testing" + + "github.com/gostaticanalysis/example.com/mylinter" + "github.com/gostaticanalysis/testutil" + "golang.org/x/tools/go/analysis/analysistest" +) + +// TestAnalyzer is a test for Analyzer. +func TestAnalyzer(t *testing.T) { + testdata := testutil.WithModules(t, analysistest.TestData(), nil) + analysistest.Run(t, testdata, mylinter.Analyzer, "a") +} +``` + +In the mylinter case, the test uses `testdata/src/a/a.go` file as a test data. `mylinter.Analyzer` finds `gopher` identifiers in the source code and report them. In the test side, expected reports are described in comments. The comments must be start with `want` and a reporting message follows. The reporting message is represented by a regular expression. When the analyzer reports unexpected diagnostics or does not report expected diagnostics, the test will be failed. + +```go +package a + +func f() { + // The pattern can be written in regular expression. + var gopher int // want "pattern" + print(gopher) // want "identifier is gopher" +} +``` + +When you run `go mod tidy` and `go test`, the test will be failed because the analyzer does not report a diagnostic with "pattern". + +``` +$ go mod tidy +go: finding module for package golang.org/x/tools/go/analysis +go: finding module for package github.com/gostaticanalysis/testutil +go: finding module for package golang.org/x/tools/go/analysis/passes/inspect +go: finding module for package golang.org/x/tools/go/analysis/unitchecker +go: finding module for package golang.org/x/tools/go/ast/inspector +go: finding module for package golang.org/x/tools/go/analysis/analysistest +go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10 +go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0 +go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10 + +$ go test +--- FAIL: TestAnalyzer (0.06s) + analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern` + analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern` +FAIL +exit status 1 +FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s +``` + +#### Executable file + +skeleton generates `main.go` in `cmd` directory. When you build it and generate an executable file, the executable file must be run via `go vet` command such as the following. `-vettool` flag for `go vet` command specifies an absoluted path for an executable file of own static analysis tool. + +``` +$ go vet -vettool=`which mylinter` ./... +``` + +### Overwrite a directory + +If the directory already exists, skeleton gives you with following options. + +``` +$ skeleton example.com/mylinter +mylinter already exists, overwrite? +[1] No (Exit) +[2] Remove and create new directory +[3] Overwrite existing files with confirmation +[4] Create new files only +``` + +### Without cmd directory -### Create skeleton codes without cmd directory +If you don't need `cmd` directory, you can set `false` to `-cmd` flag. ``` -$ skeleton -cmd=false example.com/pkgname -pkgname +$ skeleton -cmd=false example.com/mylinter +mylinter ├── go.mod -├── pkgname.go -├── pkgname_test.go -└── testdata - └── src - └── a - ├── a.go - └── go.mod +mylinter.go +├── mylinter_test.go +└─ testdata + └─ testdata + testdata └── src + Go.mod + go.mod +``` + +### Without go.mod file + +skeleton generates a `go.mod` file by default. When you would like to use skeleton in a directory which is already under Go Modules management, you can set `false` to `-gomod` option as following. + +``` +$ skeleton -gomod=false example.com/mylinter +mylinter +├── cmd +│└── mylinter +└─ main.go +├── mylinter.go +mylinter_test.go +└─ testdata + testdata └── src + testdata └─ a + Go.mod + go.mod ``` -### Change the checker from unitchecker to singlechecker or multichecker +### SKELETON_PREFIX environment variable + +When `SKELETON_PREFIX` environment variable is set, skeleton puts it as a prefix to a module path. + +``` +$ SKELETON_PREFIX=example.com skeleton mylinter +$ head -1 mylinter/go.mod +module example.com/mylinter +``` -You can change the checker from unitchecker to singlechecker or multichecker. +It is useful with [direnv](https://github.com/direnv/direnv) such as following. ``` -$ skeleton -checker=single example.com/pkgname -$ cat cmd/pkgname/main.go +$ cat ~/repos/gostaticanalysis/.envrc +export SKELETON_PREFIX=github.com/gostaticanalysis +``` + +If `SKELETON_PREFIX` environment variable is specified but the `-gomod` flag is `false`, skeleton prioritizes `-gomod` flag. + +### singlechecker and multichecker + +skeleton uses `unitchecker` package in `main.go` by default. You can change it to `singlechecker` package or `multichecker` package by specifying the `-checker` flag. + +`singlechecker` package runs a single analyzer and `multichecker` package runs multiple analyzers. These packages does not need `go vet` command to run. + +The following is an example of using `singlechecker` package. + +``` +$ skeleton -checker=single example.com/mylinter +$ cat cmd/mylinter/main.go package main import ( - "pkgname" + "mylinter" "golang.org/x/tools/go/analysis/singlechecker" ) -func main() { singlechecker.Main(pkgname.Analyzer) } +func main() { singlechecker.Main(mylinter.Analyzer) } ``` -## Build as a plugin for golangci-lint +Using `singlechecker` package or `multichecker` package seems easy way. But when you use them, you cannot receive benefit of using `go vet`. If you don't have particular reason of using `singlechecker` package or `multichecker` package, you should use `unitchecker`. It means you should not use `-checker` flag in most cases. -`skeleton` generates plugin directory which has main.go. -The main.go can be built as a plugin for [golangci-lint](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint). +### Kinds of skeleton code -``` -$ skeleton -plugin example.com/pkgname -$ go build -buildmode=plugin -o path_to_plugin_dir example.com/pkgname/plugin/pkgname -``` +skeleton can change kind of skeleton code by using `-kind` flag. + +* `-kind=inspect` (default): using `inspect.Analyzer` +* `-kind=ssa`: using the static single assignment (SSA, Static Single Assignment) form generated by `buildssa.Analyzer` +* `-kind=codegen`: code generator. +* `-kind=packages`: using `x/tools/go/packages` package + +### Create code generator -If you would like to specify flags for your plugin, you can put them via `ldflags` as below. +When you gives `codegen` to `-kind` flag, skeleton generates skeleton code of code generation tool with [gostaticanalysis/codegen](https://pkg.go.dev/github.com/gostaticanalysis/codegen) package. ``` -$ skeleton -plugin example.com/pkgname -$ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/pkgname/plugin/pkgname +$ skeleton -kind=codegen example.com/mycodegen +mycodegen +├── cmd +│   └── mycodegen +│   └── main.go +├── go.mod +├── mycodegen.go +├── mycodegen_test.go +└── testdata + └── src + └── a + ├── a.go + ├── go.mod + └── mycodegen.golden ``` -### Create skeleton codes of codegenerator +`gostaticanalysis/codegen` package is an experimental, please be careful. + +### golangci-lint plugin + +skeleton generates codes that can be used as a plugin of [golangci-lint](https://github.com/golangci/golangci-lint) by specifying `-plugin` flag. ``` -$ skeleton -kind=codegen example.com/pkgname -pkgname +$ skeleton -plugin example.com/mylinter +mylinter ├── cmd -│   └── pkgname +│   └── mylinter │   └── main.go ├── go.mod -├── pkgname.go -├── pkgname_test.go +├── mylinter.go +├── mylinter_test.go +├── plugin +│ └── main.go └── testdata └── src └── a ├── a.go - └── pkgname.golden + └── go.mod ``` -### Change type of skeleton code +You can see [the documentation](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint). -skeleton accepts `-kind` option which indicates kind of skeleton code. +``` +$ skeleton -plugin example.com/mylinter +$ go build -buildmode=plugin -o path_to_plugin_dir example.com/mylinter/plugin/mylinter +``` -* `-kind=inspect`(default): generate skeleton code with `inspect.Analyzer` -* `-kind=ssa`: generate skeleton code with `buildssa.Analyzer` -* `-kind=codegen`: generate skeleton code of a code generator +skeleton provides a way which can specify flags to your plugin with `-ldflags`. If you would like to know the details of it, please read the generated skeleton code. -### Without go.mod file +``` +$ skeleton -plugin example.com/mylinter +$ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/mylinter/plugin/mylinter +``` -If you give `-gomod=false` flag to skeleton, skeleton does not create a go.mod file. +golangci-lint is built with `CGO_ENABLED=0` by default. So you should rebuilt with `CGO_ENABLED=1` because plugin package in the standard library uses CGO. And you should same version of modules with golangci-lint such as `golang.org/x/tools/go` module. The plugin system for golangci-lint is not recommended way. diff --git a/v2/README_ja.md b/v2/README_ja.md index ada3edb..6e7abb8 100644 --- a/v2/README_ja.md +++ b/v2/README_ja.md @@ -56,11 +56,120 @@ mylinter └── go.mod ``` -`mylinter.go`に静的解析ツールの本体である`*analysis.Analyzer`型の変数が生成されます。`mylinter_test.go`はテストコードですが、基本的には変更する必要はありません。テストは`testdata`ディレクトリ以下に置かれたテスト対象のソースコードを用いて行われます。これは`x/tools/go/analysis`パッケージの仕様に基づいて行われます。詳しくは[ドキュメント](https://pkg.go.dev/golang.org/x/tools/go/analysis)または[プログラミング言語Go完全入門 14章 静的解析とコード生成](http://tenn.in/analysis)をご覧ください。 +#### 解析器 -`cmd`ディレクトリ以下には`go vet`から実行される前提の実行可能ファイルを生成するための`main.go`が配置されています。生成した実行可能ファイルファイルは次のように`go vet`経由で実行します。引数は`go vet`と同じでパッケージなどを指定します。 +`x/tools/go/analysis`パッケージを用いて開発された静的解析ツールは、`*analysis.Analyzer`型の値として表現されます。mylinterの場合、`mylinter.go`に`Analyzer`変数として定義されています。 + +生成されたコードは、`inspect.Analyzer`を用いた簡単な静的解析ツールを実装しています。この静的解析ツールは、`gopher`という名前の識別子を見つけるだけです。 + +```go +package mylinter + +import ( + "go/ast" + + "golang.org/x/tools/go/analysis" + "golang.org/x/tools/go/analysis/passes/inspect" + "golang.org/x/tools/go/ast/inspector" +) + +const doc = "mylinter is ..." + +// Analyzer is ... +var Analyzer = &analysis.Analyzer{ + Name: "mylinter", + Doc: doc, + Run: run, + Requires: []*analysis.Analyzer{ + inspect.Analyzer, + }, +} + +func run(pass *analysis.Pass) (interface{}, error) { + inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) + + nodeFilter := []ast.Node{ + (*ast.Ident)(nil), + } + + inspect.Preorder(nodeFilter, func(n ast.Node) { + switch n := n.(type) { + case *ast.Ident: + if n.Name == "gopher" { + pass.Reportf(n.Pos(), "identifier is gopher") + } + } + }) + + return nil, nil +} +``` + +#### テストコード + +skeletonは、テストコードも生成します。`x/tools/go/analysis`パッケージはサブパッケージの`analysistest`パッケージとして、テストライブラリを提供しています。`analysistest.Run`関数は`testdata/src`ディレクトリ以下にあるソースコードを使ってテストを実行します。この関数の第2引数はテストデータのディレクトリです。第3引数はテスト対象のAnalyzer、第4引数以降はテストデータとして利用するパッケージ名です。 + +```go +package mylinter_test + +import ( + "testing" + + "github.com/gostaticanalysis/example.com/mylinter" + "github.com/gostaticanalysis/testutil" + "golang.org/x/tools/go/analysis/analysistest" +) + +// TestAnalyzer is a test for Analyzer. +func TestAnalyzer(t *testing.T) { + testdata := testutil.WithModules(t, analysistest.TestData(), nil) + analysistest.Run(t, testdata, mylinter.Analyzer, "a") +} +``` + +mylinterの場合、テストは`testdata/src/a/a.go`ファイルをテストデータとして利用します。`mylinter.Analyzer`は`gopher`識別子をソースコードの中から探し報告します。テストでは、期待する報告をコメントで記述します。コメントは`want`で始まり、その後に期待するメッセージが正規表現で記述されます。テストは期待するメッセージで報告がされなかったり、期待していない報告がされた場合に失敗します。 ```go +package a + +func f() { + // The pattern can be written in regular expression. + var gopher int // want "pattern" + print(gopher) // want "identifier is gopher" +} +``` + +デフォルトでは`go mod tidy`コマンドと`go test`コマンドを実行すると、テストは失敗します。これは`pattern`というメッセージで作った静的解析ツールが報告をしないためです。 + +``` +$ go mod tidy +go: finding module for package golang.org/x/tools/go/analysis +go: finding module for package github.com/gostaticanalysis/testutil +go: finding module for package golang.org/x/tools/go/analysis/passes/inspect +go: finding module for package golang.org/x/tools/go/analysis/unitchecker +go: finding module for package golang.org/x/tools/go/ast/inspector +go: finding module for package golang.org/x/tools/go/analysis/analysistest +go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10 +go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10 +go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0 +go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10 + +$ go test +--- FAIL: TestAnalyzer (0.06s) + analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern` + analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern` +FAIL +exit status 1 +FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s +``` + +#### 実行可能ファイル + +skeletonは`cmd`ディレクトリ以下に`main.go`も生成します。この`main.go`をビルドし生成した実行可能ファイルは、`go vet`コマンド経由で実行される必要があります。`go vet`コマンドの`-vettool`オプションは生成した実行可能ファイルへの絶対パスを指定します。 + +``` $ go vet -vettool=`which mylinter` ./... ```