├── .gitignore ├── v2 ├── version.txt ├── skeleton │ ├── testdata │ │ ├── version.golden │ │ ├── kind-codegen-go-test.golden │ │ ├── kind-packages-go-test.golden │ │ ├── onlypkgname-go-test.golden │ │ ├── nocmd-go-test.golden │ │ ├── nooption-go-test.golden │ │ ├── plugin-go-test.golden │ │ ├── kind-inspect-go-test.golden │ │ ├── overwrite-force-go-test.golden │ │ ├── parent-module-go-test.golden │ │ ├── overwrite-confirm-yes-go-test.golden │ │ ├── parent-module-deep-go-test.golden │ │ ├── kind-inspect-copy-parent-gomod-to-testdata-go-test.golden │ │ ├── kind-ssa-go-test.golden │ │ ├── kind-ssa-copy-parent-gomod-to-testdata-go-test.golden │ │ ├── nocmd.golden │ │ ├── parent-module.golden │ │ ├── overwrite-cancel.golden │ │ ├── overwrite-confirm-no.golden │ │ ├── overwrite-newonly.golden │ │ ├── parent-module-deep.golden │ │ ├── onlypkgname.golden │ │ ├── nooption.golden │ │ ├── kind-inspect.golden │ │ ├── overwrite-force.golden │ │ ├── overwrite-confirm-yes.golden │ │ ├── kind-ssa.golden │ │ ├── kind-inspect-copy-parent-gomod-to-testdata.golden │ │ ├── kind-ssa-copy-parent-gomod-to-testdata.golden │ │ ├── plugin.golden │ │ ├── kind-codegen.golden │ │ └── kind-packages.golden │ ├── _template │ │ ├── packages │ │ │ ├── testdata │ │ │ │ ├── a-stderr.golden │ │ │ │ ├── src │ │ │ │ │ └── a │ │ │ │ │ │ ├── @@gomod@@ │ │ │ │ │ │ └── a.go │ │ │ │ └── a-stdout.golden │ │ │ ├── @@gomod@@ │ │ │ ├── internal │ │ │ │ ├── analyzer.go │ │ │ │ └── buildssa.go │ │ │ ├── cmd │ │ │ │ └── @@.Pkg@@ │ │ │ │ │ └── main.go │ │ │ ├── @@.Pkg@@.go │ │ │ └── @@.Pkg@@_test.go │ │ ├── codegen │ │ │ ├── @@gomod@@ │ │ │ ├── testdata │ │ │ │ └── src │ │ │ │ │ └── a │ │ │ │ │ ├── @@gomod@@ │ │ │ │ │ ├── a.go │ │ │ │ │ └── @@.Pkg@@.golden │ │ │ ├── cmd │ │ │ │ └── @@.Pkg@@ │ │ │ │ │ └── main.go │ │ │ ├── @@.Pkg@@_test.go │ │ │ └── @@.Pkg@@.go │ │ ├── inspect │ │ │ ├── @@gomod@@ │ │ │ ├── testdata │ │ │ │ └── src │ │ │ │ │ └── a │ │ │ │ │ ├── @@gomod@@ │ │ │ │ │ └── a.go │ │ │ ├── cmd │ │ │ │ └── @@.Pkg@@ │ │ │ │ │ └── main.go │ │ │ ├── @@.Pkg@@_test.go │ │ │ ├── @@.Pkg@@.go │ │ │ └── plugin │ │ │ │ └── main.go │ │ └── ssa │ │ │ ├── @@gomod@@ │ │ │ ├── testdata │ │ │ └── src │ │ │ │ └── a │ │ │ │ ├── @@gomod@@ │ │ │ │ └── a.go │ │ │ ├── cmd │ │ │ └── @@.Pkg@@ │ │ │ │ └── main.go │ │ │ ├── @@.Pkg@@_test.go │ │ │ ├── @@.Pkg@@.go │ │ │ └── plugin │ │ │ └── main.go │ ├── info.go │ ├── prompt.go │ ├── generator.go │ ├── checker.go │ ├── template.go │ ├── kind.go │ ├── internal │ │ └── gomod │ │ │ ├── gomod_test.go │ │ │ └── gomod.go │ ├── skeleton.go │ └── skeleton_test.go ├── main.go ├── go.mod ├── go.sum ├── README_ja.md └── README.md ├── .github ├── release.yml └── workflows │ ├── tagpr.yml │ └── testandvet.yml ├── _template ├── go.mod ├── testdata │ └── src │ │ └── a │ │ ├── go.mod │ │ ├── a.go │ │ └── @@.Pkg@@.golden ├── cmd │ └── @@.Pkg@@ │ │ └── main.go ├── @@.Pkg@@_test.go ├── plugin │ └── main.go └── @@.Pkg@@.go ├── go.mod ├── LICENSE ├── .tagpr ├── tools └── txtar │ └── main.go ├── go.sum ├── main.go ├── CHANGELOG.md ├── template.go ├── README_ja.md └── README.md /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | -------------------------------------------------------------------------------- /v2/version.txt: -------------------------------------------------------------------------------- 1 | v2.3.0 2 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/version.golden: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/testdata/a-stderr.golden: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /v2/skeleton/_template/codegen/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit .Path@@ 2 | -------------------------------------------------------------------------------- /v2/skeleton/_template/inspect/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit .Path@@ 2 | -------------------------------------------------------------------------------- /v2/skeleton/_template/ssa/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit .Path@@ 2 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit .Path@@ 2 | -------------------------------------------------------------------------------- /v2/skeleton/_template/ssa/testdata/src/a/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit "a"@@ 2 | -------------------------------------------------------------------------------- /v2/skeleton/_template/codegen/testdata/src/a/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit "a"@@ 2 | -------------------------------------------------------------------------------- /v2/skeleton/_template/inspect/testdata/src/a/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit "a"@@ 2 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/testdata/src/a/@@gomod@@: -------------------------------------------------------------------------------- 1 | @@gomodinit "a"@@ 2 | -------------------------------------------------------------------------------- /.github/release.yml: -------------------------------------------------------------------------------- 1 | changelog: 2 | exclude: 3 | labels: 4 | - tagpr 5 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-codegen-go-test.golden: -------------------------------------------------------------------------------- 1 | PASS 2 | ok example.com/example 0000s 3 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-packages-go-test.golden: -------------------------------------------------------------------------------- 1 | PASS 2 | ok example.com/example 0000s 3 | -------------------------------------------------------------------------------- /_template/go.mod: -------------------------------------------------------------------------------- 1 | @@ if .Mod -@@ 2 | module @@.ImportPath@@ 3 | 4 | go @@.GoVer@@ 5 | @@end@@ 6 | -------------------------------------------------------------------------------- /_template/testdata/src/a/go.mod: -------------------------------------------------------------------------------- 1 | @@ if ne .Type "codegen" -@@ 2 | module a 3 | 4 | go @@.GoVer@@ 5 | @@ end -@@ 6 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gostaticanalysis/skeleton 2 | 3 | go 1.12 4 | 5 | require ( 6 | github.com/pkg/errors v0.8.1 7 | golang.org/x/tools v0.0.0-20200709181711-e327e1019dfe 8 | ) 9 | -------------------------------------------------------------------------------- /v2/skeleton/_template/ssa/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | func f() { 4 | // The pattern can be written in regular expression. 5 | var gopher int // want "pattern" 6 | print(gopher) // want "identifier is gopher" 7 | } 8 | -------------------------------------------------------------------------------- /v2/skeleton/_template/inspect/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | func f() { 4 | // The pattern can be written in regular expression. 5 | var gopher int // want "pattern" 6 | print(gopher) // want "identifier is gopher" 7 | } 8 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | func f() { 4 | // The pattern can be written in regular expression. 5 | var gopher int // want "pattern" 6 | print(gopher) // want "identifier is gopher" 7 | } 8 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/testdata/a-stdout.golden: -------------------------------------------------------------------------------- 1 | a.go:5:6 identifier is gopher 2 | a.go:6:8 identifier is gopher 3 | a.f 4 | Block 0 5 | *ssa.Call print(0:int) 6 | *ssa.Builtin builtin print 7 | *ssa.Const 0:int 8 | *ssa.Return return 9 | -------------------------------------------------------------------------------- /v2/skeleton/_template/inspect/cmd/@@.Pkg@@/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Cmd -@@ 2 | package main 3 | 4 | import ( 5 | "@@.Path@@" 6 | "golang.org/x/tools/go/analysis/@@.Checker@@checker" 7 | ) 8 | 9 | func main() { @@.Checker@@checker.Main(@@.Pkg@@.Analyzer) } 10 | @@end@@ 11 | -------------------------------------------------------------------------------- /v2/skeleton/_template/codegen/cmd/@@.Pkg@@/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Cmd -@@ 2 | package main 3 | 4 | import ( 5 | "@@.Path@@" 6 | "github.com/gostaticanalysis/codegen/@@.Checker@@generator" 7 | ) 8 | 9 | func main() { 10 | @@.Checker@@generator.Main(@@.Pkg@@.Generator) 11 | } 12 | @@end@@ 13 | -------------------------------------------------------------------------------- /v2/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | _ "embed" 5 | "os" 6 | "strings" 7 | 8 | "github.com/gostaticanalysis/skeleton/v2/skeleton" 9 | ) 10 | 11 | //go:embed version.txt 12 | var version string 13 | 14 | func main() { 15 | os.Exit(skeleton.Main(strings.TrimSpace(version), os.Args[1:])) 16 | } 17 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/onlypkgname-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/info.go: -------------------------------------------------------------------------------- 1 | package skeleton 2 | 3 | type Info struct { 4 | Kind Kind 5 | Checker Checker 6 | Pkg string 7 | Path string 8 | Cmd bool 9 | Plugin bool 10 | GoMod bool 11 | GoVersion string 12 | CopyParentGoMod bool 13 | } 14 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/nocmd-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/nooption-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/plugin-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-inspect-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/overwrite-force-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/parent-module-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example/sub 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/overwrite-confirm-yes-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/parent-module-deep-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example/sub/subsub 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-inspect-copy-parent-gomod-to-testdata-go-test.golden: -------------------------------------------------------------------------------- 1 | --- FAIL: TestAnalyzer (0000s) 2 | analysistest.go:550: a/a.go:6: diagnostic "identifier is gopher" does not match pattern `pattern` 3 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 4 | FAIL 5 | exit status 1 6 | FAIL example.com/example 0000s 7 | -------------------------------------------------------------------------------- /v2/skeleton/prompt.go: -------------------------------------------------------------------------------- 1 | package skeleton 2 | 3 | import ( 4 | "os" 5 | 6 | "github.com/gostaticanalysis/skeletonkit" 7 | ) 8 | 9 | // DefaultPrompt is default Prompt. 10 | var DefaultPrompt = &Prompt{ 11 | Input: os.Stdin, 12 | Output: os.Stdout, 13 | ErrOutput: os.Stderr, 14 | } 15 | 16 | // Prompt receive input from a user. 17 | type Prompt = skeletonkit.Prompt 18 | -------------------------------------------------------------------------------- /v2/skeleton/_template/codegen/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | package a 2 | 3 | type DB interface { 4 | Get(id string) int 5 | Set(id string, v int) 6 | } 7 | 8 | type db struct{} 9 | 10 | func (db) Get(id string) int { return 0 } 11 | func (db) Set(id string, v int) {} 12 | 13 | type Logger interface { 14 | Infof(format string, args ...any) 15 | Errorf(format string, args ...any) 16 | } 17 | -------------------------------------------------------------------------------- /.github/workflows/tagpr.yml: -------------------------------------------------------------------------------- 1 | # .github/workflows/tagpr.yml 2 | name: tagpr 3 | on: 4 | push: 5 | branches: ["main"] 6 | jobs: 7 | deploy: 8 | runs-on: ubuntu-24.04 9 | permissions: 10 | contents: write 11 | pull-requests: write 12 | steps: 13 | - uses: actions/checkout@v4.2.2 14 | - uses: Songmu/tagpr@v1.5.0 15 | env: 16 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 17 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-ssa-go-test.golden: -------------------------------------------------------------------------------- 1 | a.f 2 | Block 0 3 | *ssa.Call print(0:int) 4 | *ssa.Builtin builtin print 5 | *ssa.Const 0:int 6 | *ssa.Return return 7 | --- FAIL: TestAnalyzer (0000s) 8 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 9 | analysistest.go:614: a/a.go:7: no diagnostic was reported matching `identifier is gopher` 10 | FAIL 11 | exit status 1 12 | FAIL example.com/example 0000s 13 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-ssa-copy-parent-gomod-to-testdata-go-test.golden: -------------------------------------------------------------------------------- 1 | a.f 2 | Block 0 3 | *ssa.Call print(0:int) 4 | *ssa.Builtin builtin print 5 | *ssa.Const 0:int 6 | *ssa.Return return 7 | --- FAIL: TestAnalyzer (0000s) 8 | analysistest.go:614: a/a.go:6: no diagnostic was reported matching `pattern` 9 | analysistest.go:614: a/a.go:7: no diagnostic was reported matching `identifier is gopher` 10 | FAIL 11 | exit status 1 12 | FAIL example.com/example 0000s 13 | -------------------------------------------------------------------------------- /v2/skeleton/_template/ssa/cmd/@@.Pkg@@/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Cmd -@@ 2 | package main 3 | 4 | import ( 5 | "@@.Path@@" 6 | "golang.org/x/tools/go/analysis/@@.Checker@@checker" 7 | ) 8 | 9 | func main() { @@.Checker@@checker.Main(@@.Pkg@@.Analyzer) } 10 | @@ end -@@ 11 | @@ if eq .Kind "codegen" -@@ 12 | package main 13 | 14 | import ( 15 | "@@.Path@@" 16 | "github.com/gostaticanalysis/codegen/@@.Checker@@generator" 17 | ) 18 | 19 | func main() { 20 | @@.Checker@@generator.Main(@@.Pkg@@.Generator) 21 | } 22 | @@end@@ 23 | -------------------------------------------------------------------------------- /v2/go.mod: -------------------------------------------------------------------------------- 1 | module github.com/gostaticanalysis/skeleton/v2 2 | 3 | go 1.23.3 4 | 5 | require ( 6 | github.com/gostaticanalysis/skeletonkit v0.4.1 7 | github.com/tenntenn/golden v0.2.0 8 | golang.org/x/mod v0.22.0 9 | ) 10 | 11 | require ( 12 | github.com/google/go-cmp v0.6.0 // indirect 13 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6 // indirect 14 | github.com/josharian/txtarfs v0.0.0-20240408113805-5dc76b8fe6bf // indirect 15 | golang.org/x/sync v0.9.0 // indirect 16 | golang.org/x/tools v0.27.0 // indirect 17 | ) 18 | 19 | retract v2.1.1 20 | -------------------------------------------------------------------------------- /v2/skeleton/_template/codegen/@@.Pkg@@_test.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@_test 2 | 3 | import ( 4 | "flag" 5 | "os" 6 | "testing" 7 | 8 | "@@.Path@@" 9 | "github.com/gostaticanalysis/codegen/codegentest" 10 | ) 11 | 12 | var flagUpdate bool 13 | 14 | func TestMain(m *testing.M) { 15 | flag.BoolVar(&flagUpdate, "update", false, "update the golden files") 16 | flag.Parse() 17 | os.Exit(m.Run()) 18 | } 19 | 20 | func TestGenerator(t *testing.T) { 21 | rs := codegentest.Run(t, codegentest.TestData(), @@.Pkg@@.Generator, "a") 22 | codegentest.Golden(t, rs, flagUpdate) 23 | } 24 | -------------------------------------------------------------------------------- /_template/cmd/@@.Pkg@@/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Cmd -@@ 2 | @@ if (or (eq .Type "inspect") (eq .Type "ssa")) -@@ 3 | package main 4 | 5 | import ( 6 | "@@.ImportPath@@" 7 | "golang.org/x/tools/go/analysis/@@.Checker@@checker" 8 | ) 9 | 10 | func main() { @@.Checker@@checker.Main(@@.Pkg@@.Analyzer) } 11 | @@ end -@@ 12 | @@ if eq .Type "codegen" -@@ 13 | package main 14 | 15 | import ( 16 | "@@.ImportPath@@" 17 | "github.com/gostaticanalysis/codegen/@@.Checker@@generator" 18 | ) 19 | 20 | func main() { 21 | @@.Checker@@generator.Main(@@.Pkg@@.Generator) 22 | } 23 | @@ end -@@ 24 | @@end@@ 25 | -------------------------------------------------------------------------------- /v2/skeleton/generator.go: -------------------------------------------------------------------------------- 1 | package skeleton 2 | 3 | import ( 4 | "io/fs" 5 | "text/template" 6 | 7 | "github.com/gostaticanalysis/skeletonkit" 8 | ) 9 | 10 | type Generator struct { 11 | Template *template.Template 12 | } 13 | 14 | func (g *Generator) Run(info *Info) (fs.FS, error) { 15 | tmpl, err := g.template(info) 16 | if err != nil { 17 | return nil, err 18 | } 19 | return skeletonkit.ExecuteTemplate(tmpl, info) 20 | } 21 | 22 | func (g *Generator) template(info *Info) (*template.Template, error) { 23 | if g.Template != nil { 24 | return g.Template, nil 25 | } 26 | return parseTemplate(info) 27 | } 28 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/internal/analyzer.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "flag" 5 | "io" 6 | 7 | "golang.org/x/tools/go/packages" 8 | "golang.org/x/tools/go/ssa" 9 | ) 10 | 11 | type Pass struct { 12 | *packages.Package 13 | SSA *ssa.Program 14 | SrcFuncs []*ssa.Function 15 | Stdin io.Reader 16 | Stdout io.Writer 17 | Stderr io.Writer 18 | } 19 | 20 | type Analyzer struct { 21 | Name string 22 | Doc string 23 | Flags *flag.FlagSet 24 | Config *packages.Config 25 | SSABuilderMode ssa.BuilderMode 26 | Run func(pass *Pass) error 27 | } 28 | -------------------------------------------------------------------------------- /v2/skeleton/_template/ssa/@@.Pkg@@_test.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "@@.Path@@" 7 | "github.com/gostaticanalysis/testutil" 8 | "golang.org/x/tools/go/analysis/analysistest" 9 | ) 10 | 11 | // TestAnalyzer is a test for Analyzer. 12 | func TestAnalyzer(t *testing.T) { 13 | @@if .CopyParentGoMod -@@ 14 | modfile := testutil.ModFile(t, ".", nil) 15 | testdata := testutil.WithModules(t, analysistest.TestData(), modfile) 16 | @@else -@@ 17 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 18 | @@end -@@ 19 | analysistest.Run(t, testdata, @@.Pkg@@.Analyzer, "a") 20 | } 21 | -------------------------------------------------------------------------------- /v2/skeleton/_template/inspect/@@.Pkg@@_test.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@_test 2 | 3 | import ( 4 | "testing" 5 | 6 | "@@.Path@@" 7 | "github.com/gostaticanalysis/testutil" 8 | "golang.org/x/tools/go/analysis/analysistest" 9 | ) 10 | 11 | // TestAnalyzer is a test for Analyzer. 12 | func TestAnalyzer(t *testing.T) { 13 | @@if .CopyParentGoMod -@@ 14 | modfile := testutil.ModFile(t, ".", nil) 15 | testdata := testutil.WithModules(t, analysistest.TestData(), modfile) 16 | @@else -@@ 17 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 18 | @@end -@@ 19 | analysistest.Run(t, testdata, @@.Pkg@@.Analyzer, "a") 20 | } 21 | -------------------------------------------------------------------------------- /_template/testdata/src/a/a.go: -------------------------------------------------------------------------------- 1 | @@ if (or (eq .Type "inspect") (eq .Type "ssa")) -@@ 2 | package a 3 | 4 | func f() { 5 | // The pattern can be written in regular expression. 6 | var gopher int // want "pattern" 7 | print(gopher) // want "identifier is gopher" 8 | } 9 | @@ end -@@ 10 | @@ if eq .Type "codegen" -@@ 11 | package a 12 | 13 | type DB interface { 14 | Get(id string) int 15 | Set(id string, v int) 16 | } 17 | 18 | type db struct{} 19 | 20 | func (db) Get(id string) int { return 0 } 21 | func (db) Set(id string, v int) {} 22 | 23 | type Logger interface { 24 | Infof(format string, args ...interface{}) 25 | Errorf(format string, args ...interface{}) 26 | } 27 | @@ end -@@ 28 | -------------------------------------------------------------------------------- /v2/skeleton/_template/codegen/testdata/src/a/@@.Pkg@@.golden: -------------------------------------------------------------------------------- 1 | // Code generated by @@.Pkg@@; DO NOT EDIT. 2 | package a 3 | 4 | type MockDB struct { 5 | GetFunc func(id string) int 6 | SetFunc func(id string, v int) 7 | } 8 | 9 | func (m *MockDB) Get(id string) int { 10 | return m.GetFunc(id) 11 | } 12 | 13 | func (m *MockDB) Set(id string, v int) { 14 | m.SetFunc(id, v) 15 | } 16 | 17 | type MockLogger struct { 18 | ErrorfFunc func(format string, args ...any) 19 | InfofFunc func(format string, args ...any) 20 | } 21 | 22 | func (m *MockLogger) Errorf(format string, args ...any) { 23 | m.ErrorfFunc(format, args...) 24 | } 25 | 26 | func (m *MockLogger) Infof(format string, args ...any) { 27 | m.InfofFunc(format, args...) 28 | } 29 | -------------------------------------------------------------------------------- /v2/skeleton/checker.go: -------------------------------------------------------------------------------- 1 | package skeleton 2 | 3 | import "flag" 4 | 5 | type Checker string 6 | 7 | const ( 8 | CheckerUnit Checker = "unit" 9 | CheckerSingle Checker = "single" 10 | CheckerMulti Checker = "multi" 11 | ) 12 | 13 | var _ flag.Value = (*Checker)(nil) 14 | 15 | // String returns "single", "multi" or "unit". 16 | func (ch Checker) String() string { 17 | switch ch { 18 | case CheckerSingle: 19 | return "single" 20 | case CheckerMulti: 21 | return "multi" 22 | default: 23 | return "unit" 24 | } 25 | } 26 | 27 | func (ch *Checker) Set(s string) error { 28 | switch s { 29 | case "single": 30 | *ch = CheckerSingle 31 | case "multi": 32 | *ch = CheckerMulti 33 | default: 34 | *ch = CheckerUnit 35 | } 36 | return nil 37 | } 38 | -------------------------------------------------------------------------------- /_template/testdata/src/a/@@.Pkg@@.golden: -------------------------------------------------------------------------------- 1 | @@ if eq .Type "codegen" -@@ 2 | // Code generated by @@.Pkg@@; DO NOT EDIT. 3 | package a 4 | 5 | type MockDB struct { 6 | GetFunc func(id string) int 7 | SetFunc func(id string, v int) 8 | } 9 | 10 | func (m *MockDB) Get(id string) int { 11 | return m.GetFunc(id) 12 | } 13 | 14 | func (m *MockDB) Set(id string, v int) { 15 | m.SetFunc(id, v) 16 | } 17 | 18 | type MockLogger struct { 19 | ErrorfFunc func(format string, args ...interface{}) 20 | InfofFunc func(format string, args ...interface{}) 21 | } 22 | 23 | func (m *MockLogger) Errorf(format string, args ...interface{}) { 24 | m.ErrorfFunc(format, args...) 25 | } 26 | 27 | func (m *MockLogger) Infof(format string, args ...interface{}) { 28 | m.InfofFunc(format, args...) 29 | } 30 | @@ end -@@ 31 | -------------------------------------------------------------------------------- /v2/skeleton/template.go: -------------------------------------------------------------------------------- 1 | package skeleton 2 | 3 | import ( 4 | "embed" 5 | "path" 6 | "text/template" 7 | 8 | "github.com/gostaticanalysis/skeletonkit" 9 | ) 10 | 11 | //go:embed _template/* 12 | var tmplFS embed.FS 13 | 14 | // DefaultTemplate is default template for skeleton. 15 | // Deprecated: should use skeletonkit. 16 | var DefaultTemplate *template.Template 17 | 18 | // DefaultFuncMap is default FuncMap for a template. 19 | // Deprecated: should use skeletonkit.TemplateWithFuncs 20 | var DefaultFuncMap = skeletonkit.DefaultFuncMap 21 | 22 | func init() { 23 | // for backward compatibility 24 | DefaultTemplate = template.Must(skeletonkit.ParseTemplate(tmplFS, "skeleton", "_template/inspect")) 25 | } 26 | 27 | func parseTemplate(info *Info) (*template.Template, error) { 28 | dir := info.Kind.String() 29 | return skeletonkit.ParseTemplate(tmplFS, "skeleton", path.Join("_template", dir)) 30 | } 31 | -------------------------------------------------------------------------------- /v2/skeleton/_template/inspect/@@.Pkg@@.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@ 2 | 3 | import ( 4 | "go/ast" 5 | 6 | "golang.org/x/tools/go/analysis" 7 | "golang.org/x/tools/go/analysis/passes/inspect" 8 | "golang.org/x/tools/go/ast/inspector" 9 | ) 10 | 11 | const doc = "@@.Pkg@@ is ..." 12 | 13 | // Analyzer is ... 14 | var Analyzer = &analysis.Analyzer{ 15 | Name: "@@.Pkg@@", 16 | Doc: doc, 17 | Run: run, 18 | Requires: []*analysis.Analyzer{ 19 | inspect.Analyzer, 20 | }, 21 | } 22 | 23 | func run(pass *analysis.Pass) (any, error) { 24 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 25 | 26 | nodeFilter := []ast.Node{ 27 | (*ast.Ident)(nil), 28 | } 29 | 30 | inspect.Preorder(nodeFilter, func(n ast.Node) { 31 | switch n := n.(type) { 32 | case *ast.Ident: 33 | if n.Name == "gopher" { 34 | pass.Reportf(n.Pos(), "identifier is gopher") 35 | } 36 | } 37 | }) 38 | 39 | return nil, nil 40 | } 41 | -------------------------------------------------------------------------------- /v2/skeleton/_template/ssa/@@.Pkg@@.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@ 2 | 3 | import ( 4 | "fmt" 5 | 6 | "golang.org/x/tools/go/analysis" 7 | "golang.org/x/tools/go/analysis/passes/buildssa" 8 | ) 9 | 10 | const doc = "@@.Pkg@@ is ..." 11 | 12 | // Analyzer is ... 13 | var Analyzer = &analysis.Analyzer{ 14 | Name: "@@.Pkg@@", 15 | Doc: doc, 16 | Run: run, 17 | Requires: []*analysis.Analyzer{ 18 | buildssa.Analyzer, 19 | }, 20 | } 21 | 22 | func run(pass *analysis.Pass) (any, error) { 23 | s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) 24 | for _, f := range s.SrcFuncs { 25 | fmt.Println(f) 26 | for _, b := range f.Blocks { 27 | fmt.Printf("\tBlock %d\n", b.Index) 28 | for _, instr := range b.Instrs { 29 | fmt.Printf("\t\t%[1]T\t%[1]v\n", instr) 30 | for _, v := range instr.Operands(nil) { 31 | if v != nil { 32 | fmt.Printf("\t\t\t%[1]T\t%[1]v\n", *v) 33 | } 34 | } 35 | } 36 | } 37 | } 38 | return nil, nil 39 | } 40 | -------------------------------------------------------------------------------- /v2/skeleton/kind.go: -------------------------------------------------------------------------------- 1 | package skeleton 2 | 3 | import "flag" 4 | 5 | // Kind represents kind of skeleton codes. 6 | // Kind implements flag.Value. 7 | type Kind string 8 | 9 | var _ flag.Value = (*Kind)(nil) 10 | 11 | const ( 12 | KindInspect Kind = "inspect" 13 | KindSSA Kind = "ssa" 14 | KindCodegen Kind = "codegen" 15 | KindPackages Kind = "packages" 16 | ) 17 | 18 | func (k Kind) String() string { 19 | switch k { 20 | case KindSSA: 21 | return "ssa" 22 | case KindCodegen: 23 | return "codegen" 24 | case KindPackages: 25 | return "packages" 26 | default: 27 | return "inspect" 28 | } 29 | } 30 | 31 | // "ssa" -> KindSSA, "codegen" -> KindCodegen, "packages" -> KindPackages otherwise KindInspect. 32 | func (k *Kind) Set(s string) error { 33 | switch s { 34 | case "ssa": 35 | *k = KindSSA 36 | case "codegen": 37 | *k = KindCodegen 38 | case "packages": 39 | *k = KindPackages 40 | default: 41 | *k = KindInspect 42 | } 43 | return nil 44 | } 45 | -------------------------------------------------------------------------------- /_template/@@.Pkg@@_test.go: -------------------------------------------------------------------------------- 1 | @@ if (or (eq .Type "inspect") (eq .Type "ssa")) -@@ 2 | package @@.Pkg@@_test 3 | 4 | import ( 5 | "testing" 6 | 7 | "@@.ImportPath@@" 8 | "github.com/gostaticanalysis/testutil" 9 | "golang.org/x/tools/go/analysis/analysistest" 10 | ) 11 | 12 | // TestAnalyzer is a test for Analyzer. 13 | func TestAnalyzer(t *testing.T) { 14 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 15 | analysistest.Run(t, testdata, @@.Pkg@@.Analyzer, "a") 16 | } 17 | @@ end -@@ 18 | @@ if eq .Type "codegen" -@@ 19 | package @@.Pkg@@_test 20 | 21 | import ( 22 | "flag" 23 | "os" 24 | "testing" 25 | 26 | "@@.ImportPath@@" 27 | "github.com/gostaticanalysis/codegen/codegentest" 28 | ) 29 | 30 | var flagUpdate bool 31 | 32 | func TestMain(m *testing.M) { 33 | flag.BoolVar(&flagUpdate, "update", false, "update the golden files") 34 | flag.Parse() 35 | os.Exit(m.Run()) 36 | } 37 | 38 | func TestGenerator(t *testing.T) { 39 | rs := codegentest.Run(t, codegentest.TestData(), @@.Pkg@@.Generator, "a") 40 | codegentest.Golden(t, rs, flagUpdate) 41 | } 42 | @@ end -@@ 43 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 GoStaticAnalysis 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /v2/skeleton/internal/gomod/gomod_test.go: -------------------------------------------------------------------------------- 1 | package gomod_test 2 | 3 | import ( 4 | "fmt" 5 | "testing" 6 | 7 | "github.com/gostaticanalysis/skeleton/v2/skeleton/internal/gomod" 8 | "github.com/tenntenn/golden" 9 | ) 10 | 11 | func TestParentModule(t *testing.T) { 12 | t.Parallel() 13 | 14 | cases := map[string]struct { 15 | path string 16 | wantErr bool 17 | }{ 18 | "exisit": {"example.com/example", false}, 19 | "noexisit": {"", true}, 20 | } 21 | 22 | for name, tt := range cases { 23 | name, tt := name, tt 24 | t.Run(name, func(t *testing.T) { 25 | t.Parallel() 26 | dir := t.TempDir() 27 | if tt.path != "" { 28 | modfile := fmt.Sprintf("-- go.mod --\nmodule %s\ngo 1.18", tt.path) 29 | golden.DirInit(t, dir, modfile) 30 | } 31 | _, got, err := gomod.ParentModule(dir) 32 | 33 | switch { 34 | case !tt.wantErr && err != nil: 35 | t.Error("unexpected error:", err) 36 | case tt.wantErr && err == nil: 37 | t.Error("expected error did not occur") 38 | } 39 | 40 | if err == nil && got != tt.path { 41 | t.Errorf("want %s but got %s", tt.path, got) 42 | } 43 | }) 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /.github/workflows/testandvet.yml: -------------------------------------------------------------------------------- 1 | name: Test and Vet 2 | 3 | on: 4 | push: 5 | branches: 6 | - main 7 | pull_request: 8 | branches: 9 | - main 10 | - release-v2.*.* 11 | release: 12 | types: 13 | - published 14 | - created 15 | - edited 16 | 17 | defaults: 18 | run: 19 | shell: bash 20 | 21 | jobs: 22 | test: 23 | runs-on: ubuntu-24.04 24 | 25 | steps: 26 | - name: Install Go 27 | uses: actions/setup-go@v5.1.0 28 | with: 29 | go-version: 1.23.3 30 | 31 | - name: Checkout code 32 | uses: actions/checkout@v4.2.2 33 | 34 | - name: Cache Go module and build cache 35 | uses: actions/cache@v4.1.2 36 | with: 37 | key: go-${{ hashFiles('**/go.sum') }} 38 | path: | 39 | ~/go/pkg/mod 40 | restore-keys: | 41 | go- 42 | 43 | - name: Install tennvet 44 | run: | 45 | GOBIN=$(pwd)/v2 go install github.com/tenntenn/tennvet@latest 46 | 47 | - name: Test and vet 48 | run: | 49 | cd v2 50 | go vet ./... 51 | go vet -vettool=$(pwd)/tennvet ./... 52 | go test -v -race ./... 53 | -------------------------------------------------------------------------------- /_template/plugin/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Plugin -@@ 2 | // This file can build as a plugin for golangci-lint by below command. 3 | // go build -buildmode=plugin -o path_to_plugin_dir @@.ImportPath@@/plugin/@@.Pkg@@ 4 | // See: https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint 5 | 6 | package main 7 | 8 | import ( 9 | "strings" 10 | 11 | "@@.ImportPath@@" 12 | "golang.org/x/tools/go/analysis" 13 | ) 14 | 15 | // flags for Analyzer.Flag. 16 | // If you would like to specify flags for your plugin, you can put them via 'ldflags' as below. 17 | // $ go build -buildmode=plugin -ldflags "-X 'main.flags=-opt val'" @@.ImportPath@@/plugin/@@.Pkg@@ 18 | var flags string 19 | 20 | // AnalyzerPlugin provides analyzers as a plugin. 21 | // It follows golangci-lint style plugin. 22 | var AnalyzerPlugin analyzerPlugin 23 | 24 | type analyzerPlugin struct{} 25 | 26 | func (analyzerPlugin) GetAnalyzers() []*analysis.Analyzer { 27 | if flags != "" { 28 | flagset := @@.Pkg@@.Analyzer.Flags 29 | if err := flagset.Parse(strings.Split(flags, " ")); err != nil { 30 | panic("cannot parse flags of @@.Pkg@@: " + err.Error()) 31 | } 32 | } 33 | return []*analysis.Analyzer{ 34 | @@.Pkg@@.Analyzer, 35 | } 36 | } 37 | @@end@@ 38 | -------------------------------------------------------------------------------- /v2/skeleton/_template/inspect/plugin/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Plugin -@@ 2 | // This file can build as a plugin for golangci-lint by below command. 3 | // go build -buildmode=plugin -o path_to_plugin_dir @@.Path@@/plugin/@@.Pkg@@ 4 | // See: https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint 5 | 6 | package main 7 | 8 | import ( 9 | "strings" 10 | 11 | "@@.Path@@" 12 | "golang.org/x/tools/go/analysis" 13 | ) 14 | 15 | // flags for Analyzer.Flag. 16 | // If you would like to specify flags for your plugin, you can put them via 'ldflags' as below. 17 | // $ go build -buildmode=plugin -ldflags "-X 'main.flags=-opt val'" @@.Path@@/plugin/@@.Pkg@@ 18 | var flags string 19 | 20 | // AnalyzerPlugin provides analyzers as a plugin. 21 | // It follows golangci-lint style plugin. 22 | var AnalyzerPlugin analyzerPlugin 23 | 24 | type analyzerPlugin struct{} 25 | 26 | func (analyzerPlugin) GetAnalyzers() []*analysis.Analyzer { 27 | if flags != "" { 28 | flagset := @@.Pkg@@.Analyzer.Flags 29 | if err := flagset.Parse(strings.Split(flags, " ")); err != nil { 30 | panic("cannot parse flags of @@.Pkg@@: " + err.Error()) 31 | } 32 | } 33 | return []*analysis.Analyzer{ 34 | @@.Pkg@@.Analyzer, 35 | } 36 | } 37 | @@ end -@@ 38 | -------------------------------------------------------------------------------- /v2/skeleton/_template/ssa/plugin/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Plugin -@@ 2 | // This file can build as a plugin for golangci-lint by below command. 3 | // go build -buildmode=plugin -o path_to_plugin_dir @@.Path@@/plugin/@@.Pkg@@ 4 | // See: https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint 5 | 6 | package main 7 | 8 | import ( 9 | "strings" 10 | 11 | "@@.Path@@" 12 | "golang.org/x/tools/go/analysis" 13 | ) 14 | 15 | // flags for Analyzer.Flag. 16 | // If you would like to specify flags for your plugin, you can put them via 'ldflags' as below. 17 | // $ go build -buildmode=plugin -ldflags "-X 'main.flags=-opt val'" @@.Path@@/plugin/@@.Pkg@@ 18 | var flags string 19 | 20 | // AnalyzerPlugin provides analyzers as a plugin. 21 | // It follows golangci-lint style plugin. 22 | var AnalyzerPlugin analyzerPlugin 23 | 24 | type analyzerPlugin struct{} 25 | 26 | func (analyzerPlugin) GetAnalyzers() []*analysis.Analyzer { 27 | if flags != "" { 28 | flagset := @@.Pkg@@.Analyzer.Flags 29 | if err := flagset.Parse(strings.Split(flags, " ")); err != nil { 30 | panic("cannot parse flags of @@.Pkg@@: " + err.Error()) 31 | } 32 | } 33 | return []*analysis.Analyzer{ 34 | @@.Pkg@@.Analyzer, 35 | } 36 | } 37 | @@ end -@@ 38 | -------------------------------------------------------------------------------- /v2/skeleton/internal/gomod/gomod.go: -------------------------------------------------------------------------------- 1 | package gomod 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "os" 7 | "os/exec" 8 | "path/filepath" 9 | "strings" 10 | 11 | "golang.org/x/mod/modfile" 12 | ) 13 | 14 | func ModFile(dir string) (string, error) { 15 | var stdout bytes.Buffer 16 | cmd := exec.Command("go", "list", "-m", "-f", "{{.GoMod}}") 17 | cmd.Dir = dir 18 | cmd.Stdout = &stdout 19 | if err := cmd.Run(); err != nil { 20 | return "", fmt.Errorf("cannot get the parent module with %s: %w", dir, err) 21 | } 22 | 23 | gomod := strings.TrimSpace(stdout.String()) 24 | if gomod == "" { 25 | return "", fmt.Errorf("cannot find go.mod, %s may not managed with Go Modules", dir) 26 | } 27 | 28 | return gomod, nil 29 | } 30 | 31 | func ParentModule(dir string) (moddir, modpath string, _ error) { 32 | gomodfile, err := ModFile(dir) 33 | if err != nil { 34 | return "", "", err 35 | } 36 | 37 | moddata, err := os.ReadFile(gomodfile) 38 | if err != nil { 39 | return "", "", fmt.Errorf("cat not read the go.mod of the parent module: %w", err) 40 | } 41 | 42 | gomod, err := modfile.Parse(gomodfile, moddata, nil) 43 | if err != nil { 44 | return "", "", fmt.Errorf("cat parse the go.mod of the parent module: %w", err) 45 | } 46 | 47 | return filepath.Dir(gomodfile), gomod.Module.Mod.Path, nil 48 | } 49 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/cmd/@@.Pkg@@/main.go: -------------------------------------------------------------------------------- 1 | @@ if .Cmd -@@ 2 | package main 3 | 4 | import ( 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "@@.Path@@" 11 | "@@.Path@@/internal" 12 | "golang.org/x/tools/go/packages" 13 | ) 14 | 15 | func main() { 16 | if err := run(); err != nil { 17 | fmt.Fprintln(os.Stderr, err) 18 | os.Exit(1) 19 | } 20 | } 21 | 22 | func run() error { 23 | @@.Pkg@@.Analyzer.Flags = flag.NewFlagSet(@@.Pkg@@.Analyzer.Name, flag.ExitOnError) 24 | @@.Pkg@@.Analyzer.Flags.Parse(os.Args[1:]) 25 | 26 | if @@.Pkg@@.Analyzer.Flags.NArg() < 1 { 27 | return errors.New("patterns of packages must be specified") 28 | } 29 | 30 | pkgs, err := packages.Load(@@.Pkg@@.Analyzer.Config, @@.Pkg@@.Analyzer.Flags.Args()...) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | for _, pkg := range pkgs { 36 | prog, srcFuncs, err := internal.BuildSSA(pkg, @@.Pkg@@.Analyzer.SSABuilderMode) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | pass := &internal.Pass{ 42 | Package: pkg, 43 | SSA: prog, 44 | SrcFuncs: srcFuncs, 45 | Stdin: os.Stdin, 46 | Stdout: os.Stdout, 47 | Stderr: os.Stderr, 48 | } 49 | 50 | if err := @@.Pkg@@.Analyzer.Run(pass); err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | @@end@@ 58 | -------------------------------------------------------------------------------- /.tagpr: -------------------------------------------------------------------------------- 1 | # config file for the tagpr in git config format 2 | # The tagpr generates the initial configuration, which you can rewrite to suit your environment. 3 | # CONFIGURATIONS: 4 | # tagpr.releaseBranch 5 | # Generally, it is "main." It is the branch for releases. The pcpr tracks this branch, 6 | # creates or updates a pull request as a release candidate, or tags when they are merged. 7 | # 8 | # tagpr.versionFile 9 | # Versioning file containing the semantic version needed to be updated at release. 10 | # It will be synchronized with the "git tag". 11 | # Often this is a meta-information file such as gemspec, setup.cfg, package.json, etc. 12 | # Sometimes the source code file, such as version.go or Bar.pm, is used. 13 | # If you do not want to use versioning files but only git tags, specify the "-" string here. 14 | # You can specify multiple version files by comma separated strings. 15 | # 16 | # tagpr.vPrefix 17 | # Flag whether or not v-prefix is added to semver when git tagging. (e.g. v1.2.3 if true) 18 | # This is only a tagging convention, not how it is described in the version file. 19 | # 20 | # tagpr.changelog (Optional) 21 | # Flag whether or not changelog is added or changed during the release. 22 | # 23 | # tagpr.command (Optional) 24 | # Command to change files just before release. 25 | # 26 | # tagpr.tmplate (Optional) 27 | # Pull request template in go template format 28 | # 29 | # tagpr.release (Optional) 30 | # GitHub Release creation behavior after tagging [true, draft, false] 31 | # If this value is not set, the release is to be created. 32 | [tagpr] 33 | vPrefix = true 34 | releaseBranch = main 35 | versionFile = v2/version.txt 36 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/@@.Pkg@@.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@ 2 | 3 | import ( 4 | "fmt" 5 | "go/ast" 6 | "path/filepath" 7 | 8 | "@@.Path@@/internal" 9 | "golang.org/x/tools/go/ast/inspector" 10 | "golang.org/x/tools/go/packages" 11 | ) 12 | 13 | var Analyzer = &internal.Analyzer{ 14 | Name: "@@.Pkg@@", 15 | Doc: "@@.Pkg@@ is ...", 16 | Config: &packages.Config{ 17 | Mode: packages.NeedName | packages.NeedTypes | 18 | packages.NeedSyntax | packages.NeedTypesInfo | 19 | packages.NeedModule, 20 | }, 21 | SSABuilderMode: 0, 22 | Run: run, 23 | } 24 | 25 | func run(pass *internal.Pass) error { 26 | inspect := inspector.New(pass.Syntax) 27 | 28 | nodeFilter := []ast.Node{ 29 | (*ast.Ident)(nil), 30 | } 31 | 32 | inspect.Preorder(nodeFilter, func(n ast.Node) { 33 | switch n := n.(type) { 34 | case *ast.Ident: 35 | if n.Name == "gopher" { 36 | pos := pass.Fset.Position(n.Pos()) 37 | fname := pos.Filename 38 | if pass.Module != nil { 39 | var err error 40 | fname, err = filepath.Rel(pass.Module.Dir, fname) 41 | if err != nil { 42 | return 43 | } 44 | } 45 | fmt.Fprintf(pass.Stdout, "%s:%d:%d identifier is gopher\n", fname, pos.Line, pos.Column) 46 | } 47 | } 48 | }) 49 | 50 | // See: golang.org/x/tools/go/ssa 51 | for _, f := range pass.SrcFuncs { 52 | fmt.Fprintln(pass.Stdout, f) 53 | for _, b := range f.Blocks { 54 | fmt.Fprintf(pass.Stdout, "\tBlock %d\n", b.Index) 55 | for _, instr := range b.Instrs { 56 | fmt.Fprintf(pass.Stdout, "\t\t%[1]T\t%[1]v\n", instr) 57 | for _, v := range instr.Operands(nil) { 58 | if v != nil { 59 | fmt.Fprintf(pass.Stdout, "\t\t\t%[1]T\t%[1]v\n", *v) 60 | } 61 | } 62 | } 63 | } 64 | } 65 | 66 | return nil 67 | } 68 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/nocmd.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | 3 | -- example/example.go -- 4 | package example 5 | 6 | import ( 7 | "go/ast" 8 | 9 | "golang.org/x/tools/go/analysis" 10 | "golang.org/x/tools/go/analysis/passes/inspect" 11 | "golang.org/x/tools/go/ast/inspector" 12 | ) 13 | 14 | const doc = "example is ..." 15 | 16 | // Analyzer is ... 17 | var Analyzer = &analysis.Analyzer{ 18 | Name: "example", 19 | Doc: doc, 20 | Run: run, 21 | Requires: []*analysis.Analyzer{ 22 | inspect.Analyzer, 23 | }, 24 | } 25 | 26 | func run(pass *analysis.Pass) (any, error) { 27 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 28 | 29 | nodeFilter := []ast.Node{ 30 | (*ast.Ident)(nil), 31 | } 32 | 33 | inspect.Preorder(nodeFilter, func(n ast.Node) { 34 | switch n := n.(type) { 35 | case *ast.Ident: 36 | if n.Name == "gopher" { 37 | pass.Reportf(n.Pos(), "identifier is gopher") 38 | } 39 | } 40 | }) 41 | 42 | return nil, nil 43 | } 44 | -- example/example_test.go -- 45 | package example_test 46 | 47 | import ( 48 | "testing" 49 | 50 | "example.com/example" 51 | "github.com/gostaticanalysis/testutil" 52 | "golang.org/x/tools/go/analysis/analysistest" 53 | ) 54 | 55 | // TestAnalyzer is a test for Analyzer. 56 | func TestAnalyzer(t *testing.T) { 57 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 58 | analysistest.Run(t, testdata, example.Analyzer, "a") 59 | } 60 | -- example/go.mod -- 61 | module example.com/example 62 | 63 | go 1.23.3 64 | 65 | -- example/testdata/src/a/a.go -- 66 | package a 67 | 68 | func f() { 69 | // The pattern can be written in regular expression. 70 | var gopher int // want "pattern" 71 | print(gopher) // want "identifier is gopher" 72 | } 73 | -- example/testdata/src/a/go.mod -- 74 | module a 75 | 76 | go 1.23.3 77 | 78 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/parent-module.golden: -------------------------------------------------------------------------------- 1 | -- go.mod -- 2 | module example.com/example 3 | -- sub/cmd/sub/main.go -- 4 | package main 5 | 6 | import ( 7 | "example.com/example/sub" 8 | "golang.org/x/tools/go/analysis/unitchecker" 9 | ) 10 | 11 | func main() { unitchecker.Main(sub.Analyzer) } 12 | -- sub/sub.go -- 13 | package sub 14 | 15 | import ( 16 | "go/ast" 17 | 18 | "golang.org/x/tools/go/analysis" 19 | "golang.org/x/tools/go/analysis/passes/inspect" 20 | "golang.org/x/tools/go/ast/inspector" 21 | ) 22 | 23 | const doc = "sub is ..." 24 | 25 | // Analyzer is ... 26 | var Analyzer = &analysis.Analyzer{ 27 | Name: "sub", 28 | Doc: doc, 29 | Run: run, 30 | Requires: []*analysis.Analyzer{ 31 | inspect.Analyzer, 32 | }, 33 | } 34 | 35 | func run(pass *analysis.Pass) (any, error) { 36 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 37 | 38 | nodeFilter := []ast.Node{ 39 | (*ast.Ident)(nil), 40 | } 41 | 42 | inspect.Preorder(nodeFilter, func(n ast.Node) { 43 | switch n := n.(type) { 44 | case *ast.Ident: 45 | if n.Name == "gopher" { 46 | pass.Reportf(n.Pos(), "identifier is gopher") 47 | } 48 | } 49 | }) 50 | 51 | return nil, nil 52 | } 53 | -- sub/sub_test.go -- 54 | package sub_test 55 | 56 | import ( 57 | "testing" 58 | 59 | "example.com/example/sub" 60 | "github.com/gostaticanalysis/testutil" 61 | "golang.org/x/tools/go/analysis/analysistest" 62 | ) 63 | 64 | // TestAnalyzer is a test for Analyzer. 65 | func TestAnalyzer(t *testing.T) { 66 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 67 | analysistest.Run(t, testdata, sub.Analyzer, "a") 68 | } 69 | -- sub/testdata/src/a/a.go -- 70 | package a 71 | 72 | func f() { 73 | // The pattern can be written in regular expression. 74 | var gopher int // want "pattern" 75 | print(gopher) // want "identifier is gopher" 76 | } 77 | -- sub/testdata/src/a/go.mod -- 78 | module a 79 | 80 | go 1.23.3 81 | 82 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/overwrite-cancel.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | // empty 69 | -- example/testdata/src/a/a.go -- 70 | package a 71 | 72 | func f() { 73 | // The pattern can be written in regular expression. 74 | var gopher int // want "pattern" 75 | print(gopher) // want "identifier is gopher" 76 | } 77 | -- example/testdata/src/a/go.mod -- 78 | module a 79 | 80 | go 1.23.3 81 | 82 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/overwrite-confirm-no.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | // empty 69 | -- example/testdata/src/a/a.go -- 70 | package a 71 | 72 | func f() { 73 | // The pattern can be written in regular expression. 74 | var gopher int // want "pattern" 75 | print(gopher) // want "identifier is gopher" 76 | } 77 | -- example/testdata/src/a/go.mod -- 78 | module a 79 | 80 | go 1.23.3 81 | 82 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/overwrite-newonly.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | // empty 69 | -- example/testdata/src/a/a.go -- 70 | package a 71 | 72 | func f() { 73 | // The pattern can be written in regular expression. 74 | var gopher int // want "pattern" 75 | print(gopher) // want "identifier is gopher" 76 | } 77 | -- example/testdata/src/a/go.mod -- 78 | module a 79 | 80 | go 1.23.3 81 | 82 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/parent-module-deep.golden: -------------------------------------------------------------------------------- 1 | -- sub.go -- 2 | package sub 3 | -- subsub/cmd/subsub/main.go -- 4 | package main 5 | 6 | import ( 7 | "example.com/example/sub/subsub" 8 | "golang.org/x/tools/go/analysis/unitchecker" 9 | ) 10 | 11 | func main() { unitchecker.Main(subsub.Analyzer) } 12 | -- subsub/subsub.go -- 13 | package subsub 14 | 15 | import ( 16 | "go/ast" 17 | 18 | "golang.org/x/tools/go/analysis" 19 | "golang.org/x/tools/go/analysis/passes/inspect" 20 | "golang.org/x/tools/go/ast/inspector" 21 | ) 22 | 23 | const doc = "subsub is ..." 24 | 25 | // Analyzer is ... 26 | var Analyzer = &analysis.Analyzer{ 27 | Name: "subsub", 28 | Doc: doc, 29 | Run: run, 30 | Requires: []*analysis.Analyzer{ 31 | inspect.Analyzer, 32 | }, 33 | } 34 | 35 | func run(pass *analysis.Pass) (any, error) { 36 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 37 | 38 | nodeFilter := []ast.Node{ 39 | (*ast.Ident)(nil), 40 | } 41 | 42 | inspect.Preorder(nodeFilter, func(n ast.Node) { 43 | switch n := n.(type) { 44 | case *ast.Ident: 45 | if n.Name == "gopher" { 46 | pass.Reportf(n.Pos(), "identifier is gopher") 47 | } 48 | } 49 | }) 50 | 51 | return nil, nil 52 | } 53 | -- subsub/subsub_test.go -- 54 | package subsub_test 55 | 56 | import ( 57 | "testing" 58 | 59 | "example.com/example/sub/subsub" 60 | "github.com/gostaticanalysis/testutil" 61 | "golang.org/x/tools/go/analysis/analysistest" 62 | ) 63 | 64 | // TestAnalyzer is a test for Analyzer. 65 | func TestAnalyzer(t *testing.T) { 66 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 67 | analysistest.Run(t, testdata, subsub.Analyzer, "a") 68 | } 69 | -- subsub/testdata/src/a/a.go -- 70 | package a 71 | 72 | func f() { 73 | // The pattern can be written in regular expression. 74 | var gopher int // want "pattern" 75 | print(gopher) // want "identifier is gopher" 76 | } 77 | -- subsub/testdata/src/a/go.mod -- 78 | module a 79 | 80 | go 1.23.3 81 | 82 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/onlypkgname.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example" 6 | 7 | "golang.org/x/tools/go/analysis/unitchecker" 8 | ) 9 | 10 | func main() { unitchecker.Main(example.Analyzer) } 11 | -- example/example.go -- 12 | package example 13 | 14 | import ( 15 | "go/ast" 16 | 17 | "golang.org/x/tools/go/analysis" 18 | "golang.org/x/tools/go/analysis/passes/inspect" 19 | "golang.org/x/tools/go/ast/inspector" 20 | ) 21 | 22 | const doc = "example is ..." 23 | 24 | // Analyzer is ... 25 | var Analyzer = &analysis.Analyzer{ 26 | Name: "example", 27 | Doc: doc, 28 | Run: run, 29 | Requires: []*analysis.Analyzer{ 30 | inspect.Analyzer, 31 | }, 32 | } 33 | 34 | func run(pass *analysis.Pass) (any, error) { 35 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 36 | 37 | nodeFilter := []ast.Node{ 38 | (*ast.Ident)(nil), 39 | } 40 | 41 | inspect.Preorder(nodeFilter, func(n ast.Node) { 42 | switch n := n.(type) { 43 | case *ast.Ident: 44 | if n.Name == "gopher" { 45 | pass.Reportf(n.Pos(), "identifier is gopher") 46 | } 47 | } 48 | }) 49 | 50 | return nil, nil 51 | } 52 | -- example/example_test.go -- 53 | package example_test 54 | 55 | import ( 56 | "testing" 57 | 58 | "example" 59 | 60 | "github.com/gostaticanalysis/testutil" 61 | "golang.org/x/tools/go/analysis/analysistest" 62 | ) 63 | 64 | // TestAnalyzer is a test for Analyzer. 65 | func TestAnalyzer(t *testing.T) { 66 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 67 | analysistest.Run(t, testdata, example.Analyzer, "a") 68 | } 69 | -- example/go.mod -- 70 | module example 71 | 72 | go 1.23.3 73 | 74 | -- example/testdata/src/a/a.go -- 75 | package a 76 | 77 | func f() { 78 | // The pattern can be written in regular expression. 79 | var gopher int // want "pattern" 80 | print(gopher) // want "identifier is gopher" 81 | } 82 | -- example/testdata/src/a/go.mod -- 83 | module a 84 | 85 | go 1.23.3 86 | 87 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/nooption.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | module example.com/example 69 | 70 | go 1.23.3 71 | 72 | -- example/testdata/src/a/a.go -- 73 | package a 74 | 75 | func f() { 76 | // The pattern can be written in regular expression. 77 | var gopher int // want "pattern" 78 | print(gopher) // want "identifier is gopher" 79 | } 80 | -- example/testdata/src/a/go.mod -- 81 | module a 82 | 83 | go 1.23.3 84 | 85 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-inspect.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | module example.com/example 69 | 70 | go 1.23.3 71 | 72 | -- example/testdata/src/a/a.go -- 73 | package a 74 | 75 | func f() { 76 | // The pattern can be written in regular expression. 77 | var gopher int // want "pattern" 78 | print(gopher) // want "identifier is gopher" 79 | } 80 | -- example/testdata/src/a/go.mod -- 81 | module a 82 | 83 | go 1.23.3 84 | 85 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/overwrite-force.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | module example.com/example 69 | 70 | go 1.23.3 71 | 72 | -- example/testdata/src/a/a.go -- 73 | package a 74 | 75 | func f() { 76 | // The pattern can be written in regular expression. 77 | var gopher int // want "pattern" 78 | print(gopher) // want "identifier is gopher" 79 | } 80 | -- example/testdata/src/a/go.mod -- 81 | module a 82 | 83 | go 1.23.3 84 | 85 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/overwrite-confirm-yes.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | module example.com/example 69 | 70 | go 1.23.3 71 | 72 | -- example/testdata/src/a/a.go -- 73 | package a 74 | 75 | func f() { 76 | // The pattern can be written in regular expression. 77 | var gopher int // want "pattern" 78 | print(gopher) // want "identifier is gopher" 79 | } 80 | -- example/testdata/src/a/go.mod -- 81 | module a 82 | 83 | go 1.23.3 84 | 85 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-ssa.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "fmt" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/buildssa" 18 | ) 19 | 20 | const doc = "example is ..." 21 | 22 | // Analyzer is ... 23 | var Analyzer = &analysis.Analyzer{ 24 | Name: "example", 25 | Doc: doc, 26 | Run: run, 27 | Requires: []*analysis.Analyzer{ 28 | buildssa.Analyzer, 29 | }, 30 | } 31 | 32 | func run(pass *analysis.Pass) (any, error) { 33 | s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) 34 | for _, f := range s.SrcFuncs { 35 | fmt.Println(f) 36 | for _, b := range f.Blocks { 37 | fmt.Printf("\tBlock %d\n", b.Index) 38 | for _, instr := range b.Instrs { 39 | fmt.Printf("\t\t%[1]T\t%[1]v\n", instr) 40 | for _, v := range instr.Operands(nil) { 41 | if v != nil { 42 | fmt.Printf("\t\t\t%[1]T\t%[1]v\n", *v) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | return nil, nil 49 | } 50 | -- example/example_test.go -- 51 | package example_test 52 | 53 | import ( 54 | "testing" 55 | 56 | "example.com/example" 57 | "github.com/gostaticanalysis/testutil" 58 | "golang.org/x/tools/go/analysis/analysistest" 59 | ) 60 | 61 | // TestAnalyzer is a test for Analyzer. 62 | func TestAnalyzer(t *testing.T) { 63 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 64 | analysistest.Run(t, testdata, example.Analyzer, "a") 65 | } 66 | -- example/go.mod -- 67 | module example.com/example 68 | 69 | go 1.23.3 70 | 71 | -- example/testdata/src/a/a.go -- 72 | package a 73 | 74 | func f() { 75 | // The pattern can be written in regular expression. 76 | var gopher int // want "pattern" 77 | print(gopher) // want "identifier is gopher" 78 | } 79 | -- example/testdata/src/a/go.mod -- 80 | module a 81 | 82 | go 1.23.3 83 | 84 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-inspect-copy-parent-gomod-to-testdata.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | modfile := testutil.ModFile(t, ".", nil) 65 | testdata := testutil.WithModules(t, analysistest.TestData(), modfile) 66 | analysistest.Run(t, testdata, example.Analyzer, "a") 67 | } 68 | -- example/go.mod -- 69 | module example.com/example 70 | 71 | go 1.23.3 72 | 73 | -- example/testdata/src/a/a.go -- 74 | package a 75 | 76 | func f() { 77 | // The pattern can be written in regular expression. 78 | var gopher int // want "pattern" 79 | print(gopher) // want "identifier is gopher" 80 | } 81 | -- example/testdata/src/a/go.mod -- 82 | module a 83 | 84 | go 1.23.3 85 | 86 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-ssa-copy-parent-gomod-to-testdata.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "fmt" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/buildssa" 18 | ) 19 | 20 | const doc = "example is ..." 21 | 22 | // Analyzer is ... 23 | var Analyzer = &analysis.Analyzer{ 24 | Name: "example", 25 | Doc: doc, 26 | Run: run, 27 | Requires: []*analysis.Analyzer{ 28 | buildssa.Analyzer, 29 | }, 30 | } 31 | 32 | func run(pass *analysis.Pass) (any, error) { 33 | s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) 34 | for _, f := range s.SrcFuncs { 35 | fmt.Println(f) 36 | for _, b := range f.Blocks { 37 | fmt.Printf("\tBlock %d\n", b.Index) 38 | for _, instr := range b.Instrs { 39 | fmt.Printf("\t\t%[1]T\t%[1]v\n", instr) 40 | for _, v := range instr.Operands(nil) { 41 | if v != nil { 42 | fmt.Printf("\t\t\t%[1]T\t%[1]v\n", *v) 43 | } 44 | } 45 | } 46 | } 47 | } 48 | return nil, nil 49 | } 50 | -- example/example_test.go -- 51 | package example_test 52 | 53 | import ( 54 | "testing" 55 | 56 | "example.com/example" 57 | "github.com/gostaticanalysis/testutil" 58 | "golang.org/x/tools/go/analysis/analysistest" 59 | ) 60 | 61 | // TestAnalyzer is a test for Analyzer. 62 | func TestAnalyzer(t *testing.T) { 63 | modfile := testutil.ModFile(t, ".", nil) 64 | testdata := testutil.WithModules(t, analysistest.TestData(), modfile) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | module example.com/example 69 | 70 | go 1.23.3 71 | 72 | -- example/testdata/src/a/a.go -- 73 | package a 74 | 75 | func f() { 76 | // The pattern can be written in regular expression. 77 | var gopher int // want "pattern" 78 | print(gopher) // want "identifier is gopher" 79 | } 80 | -- example/testdata/src/a/go.mod -- 81 | module a 82 | 83 | go 1.23.3 84 | 85 | -------------------------------------------------------------------------------- /tools/txtar/main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "flag" 5 | "fmt" 6 | "io/ioutil" 7 | "os" 8 | "path/filepath" 9 | "strings" 10 | 11 | "golang.org/x/tools/txtar" 12 | ) 13 | 14 | func main() { 15 | 16 | var ( 17 | flagStripPrefix = flag.String("strip", "", "string which remove from head of path") 18 | ) 19 | flag.Parse() 20 | 21 | dir := flag.Arg(0) 22 | if dir == "" { 23 | fmt.Fprintln(os.Stderr, "target directory must be specified") 24 | os.Exit(1) 25 | } 26 | 27 | output := flag.Arg(1) 28 | if output == "" { 29 | fmt.Fprintln(os.Stderr, "output path must be specified") 30 | os.Exit(1) 31 | } 32 | 33 | var ar txtar.Archive 34 | 35 | err := filepath.Walk(dir, func(path string, info os.FileInfo, err error) error { 36 | if err != nil { 37 | return err 38 | } 39 | 40 | base := filepath.Base(path) 41 | 42 | if info.IsDir() { 43 | if len(base) > 0 && base[0] == '.' { 44 | return filepath.SkipDir 45 | } 46 | return nil 47 | } 48 | 49 | if len(base) > 0 && base[0] == '.' { 50 | return nil 51 | } 52 | 53 | data, err := ioutil.ReadFile(path) 54 | if err != nil { 55 | return err 56 | } 57 | 58 | p := filepath.ToSlash(path) 59 | ar.Files = append(ar.Files, txtar.File{ 60 | Name: strings.TrimPrefix(p, *flagStripPrefix), 61 | Data: data, 62 | }) 63 | 64 | return nil 65 | }) 66 | 67 | if err != nil { 68 | fmt.Fprintln(os.Stderr, "Error:", err) 69 | os.Exit(1) 70 | } 71 | 72 | w, err := os.Create(output) 73 | if err != nil { 74 | fmt.Fprintln(os.Stderr, "Error:", err) 75 | os.Exit(1) 76 | } 77 | 78 | archived := string(txtar.Format(&ar)) 79 | if archived != "" { 80 | fmt.Fprintln(w, "// Code generated by _tools/txtar/main.go; DO NOT EDIT.") 81 | fmt.Fprintln(w, "") 82 | fmt.Fprintln(w, "package main") 83 | fmt.Fprintln(w) 84 | fmt.Fprintln(w, `import "text/template"`) 85 | fmt.Fprintln(w) 86 | fmt.Fprintf(w, "var tmpl = template.Must(template.New"+ 87 | "(\"template\").Delims(`@@`, `@@`).Parse(%q))\n", archived) 88 | } 89 | 90 | if err := w.Close(); err != nil { 91 | fmt.Fprintln(os.Stderr, "Error:", err) 92 | os.Exit(1) 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/internal/buildssa.go: -------------------------------------------------------------------------------- 1 | package internal 2 | 3 | import ( 4 | "go/ast" 5 | "go/types" 6 | 7 | "golang.org/x/tools/go/packages" 8 | "golang.org/x/tools/go/ssa" 9 | ) 10 | 11 | // copy from golang.org/x/tools/analysis/passes/buildssa 12 | func BuildSSA(pkg *packages.Package, mode ssa.BuilderMode) (*ssa.Program, []*ssa.Function, error) { 13 | 14 | prog := ssa.NewProgram(pkg.Fset, mode) 15 | 16 | // Create SSA packages for all imports. 17 | // Order is not significant. 18 | created := make(map[*types.Package]bool) 19 | var createAll func(pkgs []*types.Package) 20 | createAll = func(pkgs []*types.Package) { 21 | for _, p := range pkgs { 22 | if !created[p] { 23 | created[p] = true 24 | prog.CreatePackage(p, nil, nil, true) 25 | createAll(p.Imports()) 26 | } 27 | } 28 | } 29 | createAll(pkg.Types.Imports()) 30 | 31 | // Create and build the primary package. 32 | ssapkg := prog.CreatePackage(pkg.Types, pkg.Syntax, pkg.TypesInfo, false) 33 | ssapkg.Build() 34 | 35 | // Compute list of source functions, including literals, 36 | // in source order. 37 | var funcs []*ssa.Function 38 | for _, f := range pkg.Syntax { 39 | for _, decl := range f.Decls { 40 | if fdecl, ok := decl.(*ast.FuncDecl); ok { 41 | 42 | // SSA will not build a Function 43 | // for a FuncDecl named blank. 44 | // That's arguably too strict but 45 | // relaxing it would break uniqueness of 46 | // names of package members. 47 | if fdecl.Name.Name == "_" { 48 | continue 49 | } 50 | 51 | // (init functions have distinct Func 52 | // objects named "init" and distinct 53 | // ssa.Functions named "init#1", ...) 54 | 55 | fn := pkg.TypesInfo.Defs[fdecl.Name].(*types.Func) 56 | if fn == nil { 57 | panic(fn) 58 | } 59 | 60 | f := ssapkg.Prog.FuncValue(fn) 61 | if f == nil { 62 | panic(fn) 63 | } 64 | 65 | var addAnons func(f *ssa.Function) 66 | addAnons = func(f *ssa.Function) { 67 | funcs = append(funcs, f) 68 | for _, anon := range f.AnonFuncs { 69 | addAnons(anon) 70 | } 71 | } 72 | addAnons(f) 73 | } 74 | } 75 | } 76 | 77 | return prog, funcs, nil 78 | } 79 | -------------------------------------------------------------------------------- /go.sum: -------------------------------------------------------------------------------- 1 | github.com/pkg/errors v0.8.1 h1:iURUrRGxPUNPdy5/HRSm+Yj6okJ6UtLINN0Q9M4+h3I= 2 | github.com/pkg/errors v0.8.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= 3 | github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 4 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 5 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 6 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 7 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 8 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 9 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 10 | golang.org/x/net v0.0.0-20200625001655-4c5254603344/go.mod h1:/O7V0waA8r7cgGh81Ro3o1hOxt32SMVPicZroKQ2sZA= 11 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 12 | golang.org/x/sync v0.0.0-20200625203802-6e8e738ad208/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 13 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 14 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 15 | golang.org/x/sys v0.0.0-20200323222414-85ca7c5b95cd/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 16 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 17 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 18 | golang.org/x/tools v0.0.0-20200709181711-e327e1019dfe h1:BT/vSbkiKG3rURL2PMNvsoVnPUqYiSv4HYQL/gVKAZw= 19 | golang.org/x/tools v0.0.0-20200709181711-e327e1019dfe/go.mod h1:njjCfa9FT2d7l9Bc6FUM5FLjQPp3cFF28FI3qnDFljA= 20 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 21 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 22 | golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 23 | -------------------------------------------------------------------------------- /v2/skeleton/_template/packages/@@.Pkg@@_test.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@_test 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "path/filepath" 7 | "strings" 8 | "testing" 9 | 10 | "@@.Path@@" 11 | "@@.Path@@/internal" 12 | "github.com/tenntenn/golden" 13 | "golang.org/x/tools/go/packages" 14 | "golang.org/x/tools/go/ssa" 15 | ) 16 | 17 | var ( 18 | flagUpdate bool 19 | ) 20 | 21 | func init() { 22 | flag.BoolVar(&flagUpdate, "update", false, "update golden files") 23 | } 24 | 25 | func Test(t *testing.T) { 26 | pkgs := load(t, testdata(t), "a") 27 | for _, pkg := range pkgs { 28 | prog, funcs := buildssa(t, pkg) 29 | run(t, pkg, prog, funcs) 30 | } 31 | } 32 | 33 | func load(t *testing.T, testdata string, pkgname string) []*packages.Package { 34 | t.Helper() 35 | @@.Pkg@@.Analyzer.Config.Dir = filepath.Join(testdata, "src", pkgname) 36 | pkgs, err := packages.Load(@@.Pkg@@.Analyzer.Config, pkgname) 37 | if err != nil { 38 | t.Fatal("unexpected error:", err) 39 | } 40 | return pkgs 41 | } 42 | 43 | func buildssa(t *testing.T, pkg *packages.Package) (*ssa.Program, []*ssa.Function) { 44 | t.Helper() 45 | program, funcs, err := internal.BuildSSA(pkg, @@.Pkg@@.Analyzer.SSABuilderMode) 46 | if err != nil { 47 | t.Fatal("unexpected error:", err) 48 | } 49 | return program, funcs 50 | } 51 | 52 | func testdata(t *testing.T) string { 53 | t.Helper() 54 | dir, err := filepath.Abs("testdata") 55 | if err != nil { 56 | t.Fatal("unexpected error:", err) 57 | } 58 | return dir 59 | } 60 | 61 | func run(t *testing.T, pkg *packages.Package, prog *ssa.Program, funcs []*ssa.Function) { 62 | var stdin, stdout, stderr bytes.Buffer 63 | pass := &internal.Pass{ 64 | Stdin: &stdin, 65 | Stdout: &stdout, 66 | Stderr: &stderr, 67 | Package: pkg, 68 | SSA: prog, 69 | SrcFuncs: funcs, 70 | } 71 | 72 | if err := @@.Pkg@@.Analyzer.Run(pass); err != nil { 73 | t.Error("unexpected error:", err) 74 | } 75 | 76 | pkgname := pkgname(pkg) 77 | 78 | if flagUpdate { 79 | golden.Update(t, testdata(t), pkgname+"-stdout", &stdout) 80 | golden.Update(t, testdata(t), pkgname+"-stderr", &stderr) 81 | return 82 | } 83 | 84 | if diff := golden.Diff(t, testdata(t), pkgname+"-stdout", &stdout); diff != "" { 85 | t.Errorf("stdout of analyzing %s:\n%s", pkgname, diff) 86 | } 87 | 88 | if diff := golden.Diff(t, testdata(t), pkgname+"-stderr", &stderr); diff != "" { 89 | t.Errorf("stderr of analyzing %s:\n%s", pkgname, diff) 90 | } 91 | } 92 | 93 | func pkgname(pkg *packages.Package) string { 94 | switch { 95 | case pkg.PkgPath != "": 96 | return strings.ReplaceAll(pkg.PkgPath, "/", "-") 97 | case pkg.Name != "": 98 | return pkg.Name 99 | default: 100 | return pkg.ID 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /v2/skeleton/_template/codegen/@@.Pkg@@.go: -------------------------------------------------------------------------------- 1 | package @@.Pkg@@ 2 | 3 | import ( 4 | "bytes" 5 | "fmt" 6 | "go/format" 7 | "go/types" 8 | "os" 9 | 10 | "github.com/gostaticanalysis/analysisutil" 11 | "github.com/gostaticanalysis/codegen" 12 | "github.com/gostaticanalysis/knife" 13 | ) 14 | 15 | const doc = "@@.Pkg@@ is ..." 16 | 17 | var ( 18 | flagOutput string 19 | ) 20 | 21 | func init() { 22 | Generator.Flags.StringVar(&flagOutput, "o", "", "output file name") 23 | } 24 | 25 | var Generator = &codegen.Generator{ 26 | Name: "@@.Pkg@@", 27 | Doc: doc, 28 | Run: run, 29 | } 30 | 31 | func run(pass *codegen.Pass) error { 32 | ifaces := map[string]*knife.Interface{} 33 | 34 | s := pass.Pkg.Scope() 35 | for _, name := range s.Names() { 36 | obj := s.Lookup(name) 37 | if !obj.Exported() { 38 | continue 39 | } 40 | iface, _ := analysisutil.Under(obj.Type()).(*types.Interface) 41 | if iface != nil { 42 | ifaces[name] = knife.NewInterface(iface) 43 | } 44 | } 45 | 46 | td := &knife.TempalteData{ 47 | Fset: pass.Fset, 48 | Files: pass.Files, 49 | TypesInfo: pass.TypesInfo, 50 | Pkg: pass.Pkg, 51 | } 52 | t, err := knife.NewTemplate(td).Parse(tmpl) 53 | if err != nil { 54 | return err 55 | } 56 | 57 | var buf bytes.Buffer 58 | if err := t.Execute(&buf, ifaces); err != nil { 59 | return err 60 | } 61 | 62 | src, err := format.Source(buf.Bytes()) 63 | if err != nil { 64 | return err 65 | } 66 | 67 | if flagOutput == "" { 68 | pass.Print(string(src)) 69 | return nil 70 | } 71 | 72 | f, err := os.Create(flagOutput) 73 | if err != nil { 74 | return err 75 | } 76 | 77 | fmt.Fprint(f, string(src)) 78 | 79 | if err := f.Close(); err != nil { 80 | return err 81 | } 82 | 83 | return nil 84 | } 85 | 86 | var tmpl = `// Code generated by @@.Pkg@@; DO NOT EDIT. 87 | package {{(pkg).Name}} 88 | {{range $tn, $t := .}} 89 | type Mock{{$tn}} struct { 90 | {{- range $n, $f := $t.Methods}} 91 | {{$n}}Func {{$f.Signature}} 92 | {{- end}} 93 | } 94 | {{range $n, $f := $t.Methods}} 95 | func (m *Mock{{$tn}}) {{$n}}({{range $f.Signature.Params}} 96 | {{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}} 97 | {{- .Name}} ...{{(slice .Type).Elem}}, 98 | {{- else}} 99 | {{- .Name}} {{.Type}}, 100 | {{- end}} 101 | {{- end}}) ({{range $f.Signature.Results}} 102 | {{- .Name}} {{.Type}}, 103 | {{- end}}) { 104 | {{if $f.Signature.Results}}return {{end}}m.{{$n}}Func({{range $f.Signature.Params}} 105 | {{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}} 106 | {{- .Name}}..., 107 | {{- else}} 108 | {{- .Name}}, 109 | {{- end}} 110 | {{- end}}) 111 | } 112 | {{end}} 113 | {{end}} 114 | ` 115 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/plugin.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "golang.org/x/tools/go/analysis/unitchecker" 7 | ) 8 | 9 | func main() { unitchecker.Main(example.Analyzer) } 10 | -- example/example.go -- 11 | package example 12 | 13 | import ( 14 | "go/ast" 15 | 16 | "golang.org/x/tools/go/analysis" 17 | "golang.org/x/tools/go/analysis/passes/inspect" 18 | "golang.org/x/tools/go/ast/inspector" 19 | ) 20 | 21 | const doc = "example is ..." 22 | 23 | // Analyzer is ... 24 | var Analyzer = &analysis.Analyzer{ 25 | Name: "example", 26 | Doc: doc, 27 | Run: run, 28 | Requires: []*analysis.Analyzer{ 29 | inspect.Analyzer, 30 | }, 31 | } 32 | 33 | func run(pass *analysis.Pass) (any, error) { 34 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 35 | 36 | nodeFilter := []ast.Node{ 37 | (*ast.Ident)(nil), 38 | } 39 | 40 | inspect.Preorder(nodeFilter, func(n ast.Node) { 41 | switch n := n.(type) { 42 | case *ast.Ident: 43 | if n.Name == "gopher" { 44 | pass.Reportf(n.Pos(), "identifier is gopher") 45 | } 46 | } 47 | }) 48 | 49 | return nil, nil 50 | } 51 | -- example/example_test.go -- 52 | package example_test 53 | 54 | import ( 55 | "testing" 56 | 57 | "example.com/example" 58 | "github.com/gostaticanalysis/testutil" 59 | "golang.org/x/tools/go/analysis/analysistest" 60 | ) 61 | 62 | // TestAnalyzer is a test for Analyzer. 63 | func TestAnalyzer(t *testing.T) { 64 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 65 | analysistest.Run(t, testdata, example.Analyzer, "a") 66 | } 67 | -- example/go.mod -- 68 | module example.com/example 69 | 70 | go 1.23.3 71 | 72 | -- example/plugin/main.go -- 73 | // This file can build as a plugin for golangci-lint by below command. 74 | // go build -buildmode=plugin -o path_to_plugin_dir example.com/example/plugin/example 75 | // See: https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint 76 | 77 | package main 78 | 79 | import ( 80 | "strings" 81 | 82 | "example.com/example" 83 | "golang.org/x/tools/go/analysis" 84 | ) 85 | 86 | // flags for Analyzer.Flag. 87 | // If you would like to specify flags for your plugin, you can put them via 'ldflags' as below. 88 | // 89 | // $ go build -buildmode=plugin -ldflags "-X 'main.flags=-opt val'" example.com/example/plugin/example 90 | var flags string 91 | 92 | // AnalyzerPlugin provides analyzers as a plugin. 93 | // It follows golangci-lint style plugin. 94 | var AnalyzerPlugin analyzerPlugin 95 | 96 | type analyzerPlugin struct{} 97 | 98 | func (analyzerPlugin) GetAnalyzers() []*analysis.Analyzer { 99 | if flags != "" { 100 | flagset := example.Analyzer.Flags 101 | if err := flagset.Parse(strings.Split(flags, " ")); err != nil { 102 | panic("cannot parse flags of example: " + err.Error()) 103 | } 104 | } 105 | return []*analysis.Analyzer{ 106 | example.Analyzer, 107 | } 108 | } 109 | -- example/testdata/src/a/a.go -- 110 | package a 111 | 112 | func f() { 113 | // The pattern can be written in regular expression. 114 | var gopher int // want "pattern" 115 | print(gopher) // want "identifier is gopher" 116 | } 117 | -- example/testdata/src/a/go.mod -- 118 | module a 119 | 120 | go 1.23.3 121 | 122 | -------------------------------------------------------------------------------- /v2/go.sum: -------------------------------------------------------------------------------- 1 | github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 2 | github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 | github.com/gostaticanalysis/skeletonkit v0.4.1 h1:22ImX8zqtjDGfvQZYwJvtVR6ff6JwG4jQLIigZGpNto= 4 | github.com/gostaticanalysis/skeletonkit v0.4.1/go.mod h1:ep5WlFfwr3pi3erF3rcpyobHmyle9aLLbKCAacBRGc4= 5 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6 h1:c+ctPFdISggaSNCfU1IueNBAsqetJSvMcpQlT+0OVdY= 6 | github.com/josharian/mapfs v0.0.0-20210615234106-095c008854e6/go.mod h1:Rv/momJI8DgrWnBZip+SgagpcgORIZQE5SERlxNb8LY= 7 | github.com/josharian/txtarfs v0.0.0-20240408113805-5dc76b8fe6bf h1:ZWuoyLMwZvLJ6OHUhPq1sZHa37Pikt6DXkZPhhOBzEE= 8 | github.com/josharian/txtarfs v0.0.0-20240408113805-5dc76b8fe6bf/go.mod h1:UbC32ft9G/jG+sZI8wLbIBNIrYr7vp/yqMDa9SxVBNA= 9 | github.com/tenntenn/golden v0.2.0 h1:ENbHNS5P2Bcnh2QWQcwtNPDYnIvFGuK4lKVDkCq4AHs= 10 | github.com/tenntenn/golden v0.2.0/go.mod h1:OB8A7xwUZ9xE19KXoOMPl223hhcH4uD8oeQS9fLTiEE= 11 | github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= 12 | golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= 13 | golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= 14 | golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 15 | golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= 16 | golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4= 17 | golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY= 18 | golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= 19 | golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= 20 | golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= 21 | golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 22 | golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= 23 | golang.org/x/sync v0.9.0 h1:fEo0HyrW1GIgZdpbhCRO0PkJajUS5H9IFUztCgEo2jQ= 24 | golang.org/x/sync v0.9.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= 25 | golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= 26 | golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 27 | golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 28 | golang.org/x/sys v0.0.0-20210119212857-b64e53b001e4/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= 29 | golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= 30 | golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= 31 | golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= 32 | golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= 33 | golang.org/x/tools v0.1.0/go.mod h1:xkSsbof2nBLbhDlRMhhhyNLN/zl3eTqcnHD5viDpcZ0= 34 | golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= 35 | golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q= 36 | golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 37 | golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 38 | golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 39 | -------------------------------------------------------------------------------- /_template/@@.Pkg@@.go: -------------------------------------------------------------------------------- 1 | @@ if eq .Type "inspect" -@@ 2 | package @@.Pkg@@ 3 | 4 | import ( 5 | "go/ast" 6 | 7 | "golang.org/x/tools/go/analysis" 8 | "golang.org/x/tools/go/analysis/passes/inspect" 9 | "golang.org/x/tools/go/ast/inspector" 10 | ) 11 | 12 | const doc = "@@.Pkg@@ is ..." 13 | 14 | // Analyzer is ... 15 | var Analyzer = &analysis.Analyzer{ 16 | Name: "@@.Pkg@@", 17 | Doc: doc, 18 | Run: run, 19 | Requires: []*analysis.Analyzer{ 20 | inspect.Analyzer, 21 | }, 22 | } 23 | 24 | func run(pass *analysis.Pass) (interface{}, error) { 25 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 26 | 27 | nodeFilter := []ast.Node{ 28 | (*ast.Ident)(nil), 29 | } 30 | 31 | inspect.Preorder(nodeFilter, func(n ast.Node) { 32 | switch n := n.(type) { 33 | case *ast.Ident: 34 | if n.Name == "gopher" { 35 | pass.Reportf(n.Pos(), "identifier is gopher") 36 | } 37 | } 38 | }) 39 | 40 | return nil, nil 41 | } 42 | @@ end -@@ 43 | @@ if eq .Type "ssa" -@@ 44 | package @@.Pkg@@ 45 | 46 | import ( 47 | "fmt" 48 | 49 | "golang.org/x/tools/go/analysis" 50 | "golang.org/x/tools/go/analysis/passes/buildssa" 51 | ) 52 | 53 | const doc = "@@.Pkg@@ is ..." 54 | 55 | // Analyzer is ... 56 | var Analyzer = &analysis.Analyzer{ 57 | Name: "@@.Pkg@@", 58 | Doc: doc, 59 | Run: run, 60 | Requires: []*analysis.Analyzer{ 61 | buildssa.Analyzer, 62 | }, 63 | } 64 | 65 | func run(pass *analysis.Pass) (interface{}, error) { 66 | s := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA) 67 | for _, f := range s.SrcFuncs { 68 | fmt.Println(f) 69 | for _, b := range f.Blocks { 70 | fmt.Printf("\tBlock %d\n", b.Index) 71 | for _, instr := range b.Instrs { 72 | fmt.Printf("\t\t%[1]T\t%[1]v(%[1]p)\n", instr) 73 | for _, v := range instr.Operands(nil) { 74 | if v != nil { 75 | fmt.Printf("\t\t\t%[1]T\t%[1]v(%[1]p)\n", *v) 76 | } 77 | } 78 | } 79 | } 80 | } 81 | return nil, nil 82 | } 83 | @@ end -@@ 84 | @@ if eq .Type "codegen" -@@ 85 | package @@.Pkg@@ 86 | 87 | import ( 88 | "bytes" 89 | "fmt" 90 | "go/format" 91 | "go/types" 92 | "os" 93 | 94 | "github.com/gostaticanalysis/analysisutil" 95 | "github.com/gostaticanalysis/codegen" 96 | "github.com/gostaticanalysis/knife" 97 | ) 98 | 99 | const doc = "@@.Pkg@@ is ..." 100 | 101 | var ( 102 | flagOutput string 103 | ) 104 | 105 | func init() { 106 | Generator.Flags.StringVar(&flagOutput, "o", "", "output file name") 107 | } 108 | 109 | var Generator = &codegen.Generator{ 110 | Name: "@@.Pkg@@", 111 | Doc: doc, 112 | Run: run, 113 | } 114 | 115 | func run(pass *codegen.Pass) error { 116 | ifaces := map[string]*knife.Interface{} 117 | 118 | s := pass.Pkg.Scope() 119 | for _, name := range s.Names() { 120 | obj := s.Lookup(name) 121 | if !obj.Exported() { 122 | continue 123 | } 124 | iface, _ := analysisutil.Under(obj.Type()).(*types.Interface) 125 | if iface != nil { 126 | ifaces[name] = knife.NewInterface(iface) 127 | } 128 | } 129 | 130 | td := &knife.TempalteData{ 131 | Fset: pass.Fset, 132 | Files: pass.Files, 133 | TypesInfo: pass.TypesInfo, 134 | Pkg: pass.Pkg, 135 | } 136 | t, err := knife.NewTemplate(td).Parse(tmpl) 137 | if err != nil { 138 | return err 139 | } 140 | 141 | var buf bytes.Buffer 142 | if err := t.Execute(&buf, ifaces); err != nil { 143 | return err 144 | } 145 | 146 | src, err := format.Source(buf.Bytes()) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | if flagOutput == "" { 152 | pass.Print(string(src)) 153 | return nil 154 | } 155 | 156 | f, err := os.Create(flagOutput) 157 | if err != nil { 158 | return err 159 | } 160 | 161 | fmt.Fprint(f, string(src)) 162 | 163 | if err := f.Close(); err != nil { 164 | return err 165 | } 166 | 167 | return nil 168 | } 169 | 170 | var tmpl = `// Code generated by @@.Pkg@@; DO NOT EDIT. 171 | package {{(pkg).Name}} 172 | {{range $tn, $t := .}} 173 | type Mock{{$tn}} struct { 174 | {{- range $n, $f := $t.Methods}} 175 | {{$n}}Func {{$f.Signature}} 176 | {{- end}} 177 | } 178 | {{range $n, $f := $t.Methods}} 179 | func (m *Mock{{$tn}}) {{$n}}({{range $f.Signature.Params}} 180 | {{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}} 181 | {{- .Name}} ...{{(slice .Type).Elem}}, 182 | {{- else}} 183 | {{- .Name}} {{.Type}}, 184 | {{- end}} 185 | {{- end}}) ({{range $f.Signature.Results}} 186 | {{- .Name}} {{.Type}}, 187 | {{- end}}) { 188 | {{if $f.Signature.Results}}return {{end}}m.{{$n}}Func({{range $f.Signature.Params}} 189 | {{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}} 190 | {{- .Name}}..., 191 | {{- else}} 192 | {{- .Name}}, 193 | {{- end}} 194 | {{- end}}) 195 | } 196 | {{end}} 197 | {{end}} 198 | ` 199 | @@ end -@@ 200 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-codegen.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "example.com/example" 6 | "github.com/gostaticanalysis/codegen/singlegenerator" 7 | ) 8 | 9 | func main() { 10 | singlegenerator.Main(example.Generator) 11 | } 12 | -- example/example.go -- 13 | package example 14 | 15 | import ( 16 | "bytes" 17 | "fmt" 18 | "go/format" 19 | "go/types" 20 | "os" 21 | 22 | "github.com/gostaticanalysis/analysisutil" 23 | "github.com/gostaticanalysis/codegen" 24 | "github.com/gostaticanalysis/knife" 25 | ) 26 | 27 | const doc = "example is ..." 28 | 29 | var ( 30 | flagOutput string 31 | ) 32 | 33 | func init() { 34 | Generator.Flags.StringVar(&flagOutput, "o", "", "output file name") 35 | } 36 | 37 | var Generator = &codegen.Generator{ 38 | Name: "example", 39 | Doc: doc, 40 | Run: run, 41 | } 42 | 43 | func run(pass *codegen.Pass) error { 44 | ifaces := map[string]*knife.Interface{} 45 | 46 | s := pass.Pkg.Scope() 47 | for _, name := range s.Names() { 48 | obj := s.Lookup(name) 49 | if !obj.Exported() { 50 | continue 51 | } 52 | iface, _ := analysisutil.Under(obj.Type()).(*types.Interface) 53 | if iface != nil { 54 | ifaces[name] = knife.NewInterface(iface) 55 | } 56 | } 57 | 58 | td := &knife.TempalteData{ 59 | Fset: pass.Fset, 60 | Files: pass.Files, 61 | TypesInfo: pass.TypesInfo, 62 | Pkg: pass.Pkg, 63 | } 64 | t, err := knife.NewTemplate(td).Parse(tmpl) 65 | if err != nil { 66 | return err 67 | } 68 | 69 | var buf bytes.Buffer 70 | if err := t.Execute(&buf, ifaces); err != nil { 71 | return err 72 | } 73 | 74 | src, err := format.Source(buf.Bytes()) 75 | if err != nil { 76 | return err 77 | } 78 | 79 | if flagOutput == "" { 80 | pass.Print(string(src)) 81 | return nil 82 | } 83 | 84 | f, err := os.Create(flagOutput) 85 | if err != nil { 86 | return err 87 | } 88 | 89 | fmt.Fprint(f, string(src)) 90 | 91 | if err := f.Close(); err != nil { 92 | return err 93 | } 94 | 95 | return nil 96 | } 97 | 98 | var tmpl = `// Code generated by example; DO NOT EDIT. 99 | package {{(pkg).Name}} 100 | {{range $tn, $t := .}} 101 | type Mock{{$tn}} struct { 102 | {{- range $n, $f := $t.Methods}} 103 | {{$n}}Func {{$f.Signature}} 104 | {{- end}} 105 | } 106 | {{range $n, $f := $t.Methods}} 107 | func (m *Mock{{$tn}}) {{$n}}({{range $f.Signature.Params}} 108 | {{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}} 109 | {{- .Name}} ...{{(slice .Type).Elem}}, 110 | {{- else}} 111 | {{- .Name}} {{.Type}}, 112 | {{- end}} 113 | {{- end}}) ({{range $f.Signature.Results}} 114 | {{- .Name}} {{.Type}}, 115 | {{- end}}) { 116 | {{if $f.Signature.Results}}return {{end}}m.{{$n}}Func({{range $f.Signature.Params}} 117 | {{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}} 118 | {{- .Name}}..., 119 | {{- else}} 120 | {{- .Name}}, 121 | {{- end}} 122 | {{- end}}) 123 | } 124 | {{end}} 125 | {{end}} 126 | ` 127 | -- example/example_test.go -- 128 | package example_test 129 | 130 | import ( 131 | "flag" 132 | "os" 133 | "testing" 134 | 135 | "example.com/example" 136 | "github.com/gostaticanalysis/codegen/codegentest" 137 | ) 138 | 139 | var flagUpdate bool 140 | 141 | func TestMain(m *testing.M) { 142 | flag.BoolVar(&flagUpdate, "update", false, "update the golden files") 143 | flag.Parse() 144 | os.Exit(m.Run()) 145 | } 146 | 147 | func TestGenerator(t *testing.T) { 148 | rs := codegentest.Run(t, codegentest.TestData(), example.Generator, "a") 149 | codegentest.Golden(t, rs, flagUpdate) 150 | } 151 | -- example/go.mod -- 152 | module example.com/example 153 | 154 | go 1.23.3 155 | 156 | -- example/testdata/src/a/a.go -- 157 | package a 158 | 159 | type DB interface { 160 | Get(id string) int 161 | Set(id string, v int) 162 | } 163 | 164 | type db struct{} 165 | 166 | func (db) Get(id string) int { return 0 } 167 | func (db) Set(id string, v int) {} 168 | 169 | type Logger interface { 170 | Infof(format string, args ...any) 171 | Errorf(format string, args ...any) 172 | } 173 | -- example/testdata/src/a/example.golden -- 174 | // Code generated by example; DO NOT EDIT. 175 | package a 176 | 177 | type MockDB struct { 178 | GetFunc func(id string) int 179 | SetFunc func(id string, v int) 180 | } 181 | 182 | func (m *MockDB) Get(id string) int { 183 | return m.GetFunc(id) 184 | } 185 | 186 | func (m *MockDB) Set(id string, v int) { 187 | m.SetFunc(id, v) 188 | } 189 | 190 | type MockLogger struct { 191 | ErrorfFunc func(format string, args ...any) 192 | InfofFunc func(format string, args ...any) 193 | } 194 | 195 | func (m *MockLogger) Errorf(format string, args ...any) { 196 | m.ErrorfFunc(format, args...) 197 | } 198 | 199 | func (m *MockLogger) Infof(format string, args ...any) { 200 | m.InfofFunc(format, args...) 201 | } 202 | -- example/testdata/src/a/go.mod -- 203 | module a 204 | 205 | go 1.23.3 206 | 207 | -------------------------------------------------------------------------------- /v2/skeleton/skeleton.go: -------------------------------------------------------------------------------- 1 | package skeleton 2 | 3 | import ( 4 | "bytes" 5 | "flag" 6 | "fmt" 7 | "io" 8 | "io/fs" 9 | "os" 10 | "os/exec" 11 | "path" 12 | "path/filepath" 13 | "strings" 14 | 15 | "github.com/gostaticanalysis/skeletonkit" 16 | "golang.org/x/mod/module" 17 | 18 | "github.com/gostaticanalysis/skeleton/v2/skeleton/internal/gomod" 19 | ) 20 | 21 | const ( 22 | ExitSuccess = 0 23 | ExitError = 1 24 | ) 25 | 26 | type Skeleton struct { 27 | Dir string 28 | Output io.Writer 29 | ErrOutput io.Writer 30 | Input io.Reader 31 | GoVersion string 32 | } 33 | 34 | func Main(version string, args []string) int { 35 | s := &Skeleton{ 36 | Dir: ".", 37 | Output: os.Stdout, 38 | ErrOutput: os.Stderr, 39 | Input: os.Stdin, 40 | } 41 | 42 | gover, err := goVersion(s.Dir) 43 | if err != nil { 44 | fmt.Fprintln(s.ErrOutput, "Error:", err) 45 | return ExitError 46 | } 47 | 48 | if strings.HasPrefix(gover, "devel ") { 49 | // The devel format is like following. 50 | // devel go1.24-xxxxxxxxxx Day Date Mon Time Arch" 51 | ver, _, ok := strings.Cut(strings.TrimPrefix(gover, "go"), "-") 52 | if !ok { 53 | return ExitError 54 | } 55 | gover = ver 56 | } 57 | 58 | s.GoVersion = gover 59 | 60 | return s.Run(version, args) 61 | } 62 | 63 | func (s *Skeleton) Run(version string, args []string) int { 64 | if len(args) > 0 && args[0] == "-v" { 65 | fmt.Fprintln(s.Output, "skeleton", version) 66 | return ExitSuccess 67 | } 68 | 69 | var info Info 70 | flags, err := s.parseFlag(args, &info) 71 | if err != nil { 72 | fmt.Fprintln(s.ErrOutput, "Error:", err) 73 | return ExitError 74 | } 75 | 76 | info.GoVersion = s.GoVersion 77 | 78 | info.Path = flags.Arg(0) 79 | if !info.GoMod { 80 | importpath, err := s.withoutGoMod(info.Path) 81 | if err != nil { 82 | fmt.Fprintln(s.ErrOutput, "Error:", err) 83 | return ExitError 84 | } 85 | info.Path = importpath 86 | } else if prefix := os.Getenv("SKELETON_PREFIX"); prefix != "" { 87 | info.Path = path.Join(prefix, info.Path) 88 | } 89 | 90 | // allow package name only 91 | if module.CheckImportPath(info.Path) != nil { 92 | flags.Usage() 93 | return ExitError 94 | } 95 | 96 | if info.Pkg == "" { 97 | info.Pkg = path.Base(info.Path) 98 | } 99 | 100 | if err := s.run(&info); err != nil { 101 | fmt.Fprintln(s.ErrOutput, "Error:", err) 102 | return ExitError 103 | } 104 | 105 | return ExitSuccess 106 | } 107 | 108 | func (s *Skeleton) parseFlag(args []string, info *Info) (*flag.FlagSet, error) { 109 | flags := flag.NewFlagSet("skeleton", flag.ContinueOnError) 110 | flags.SetOutput(s.ErrOutput) 111 | flags.Usage = func() { 112 | fmt.Fprintln(s.ErrOutput, "skeleton [-checker,-kind,-cmd,-plugin] example.com/path") 113 | flags.PrintDefaults() 114 | } 115 | flags.Var(&info.Checker, "checker", "[unit,single,multi]") 116 | 117 | flags.Var(&info.Kind, "kind", "[inspect,ssa,codegen,packages]") 118 | 119 | flags.BoolVar(&info.Cmd, "cmd", true, "create main file") 120 | flags.BoolVar(&info.Plugin, "plugin", false, "create golangci-lint plugin") 121 | flags.StringVar(&info.Pkg, "pkg", "", "package name") 122 | flags.BoolVar(&info.GoMod, "gomod", true, "create a go.mod file") 123 | flags.BoolVar(&info.CopyParentGoMod, "copy-parent-gomod", false, "copy parent go.mod file to testdata") 124 | 125 | if err := flags.Parse(args); err != nil { 126 | return nil, err 127 | } 128 | 129 | if info.Kind == "" { 130 | info.Kind = KindInspect 131 | } 132 | 133 | if info.Checker == "" { 134 | switch info.Kind { 135 | case KindCodegen: 136 | info.Checker = CheckerSingle 137 | default: 138 | info.Checker = CheckerUnit 139 | } 140 | } 141 | 142 | return flags, nil 143 | } 144 | 145 | func (s *Skeleton) run(info *Info) error { 146 | fsys, err := new(Generator).Run(info) 147 | if err != nil { 148 | return err 149 | } 150 | 151 | prompt := &Prompt{ 152 | Output: s.Output, 153 | ErrOutput: s.ErrOutput, 154 | Input: s.Input, 155 | } 156 | 157 | dst := filepath.Join(s.Dir, info.Pkg) 158 | opts := []skeletonkit.CreatorOption{ 159 | skeletonkit.CreatorWithEmpty(true), 160 | skeletonkit.CreatorWithSkipFunc(func(p string, d fs.DirEntry) bool { 161 | switch { 162 | case !info.Plugin && path.Base(p) == "plugin": 163 | return true 164 | case !info.GoMod && isGoMod(p): 165 | return true 166 | } 167 | return false // no skip 168 | }), 169 | } 170 | if err := skeletonkit.CreateDir(prompt, dst, fsys, opts...); err != nil { 171 | return err 172 | } 173 | return nil 174 | } 175 | 176 | func (s *Skeleton) withoutGoMod(p string) (string, error) { 177 | moddir, modpath, err := gomod.ParentModule(s.Dir) 178 | if err != nil { 179 | return "", err 180 | } 181 | 182 | wd, err := filepath.EvalSymlinks(s.Dir) 183 | if err != nil { 184 | return "", err 185 | } 186 | 187 | wd, err = filepath.Abs(wd) 188 | if err != nil { 189 | return "", err 190 | } 191 | 192 | moddir, err = filepath.EvalSymlinks(moddir) 193 | if err != nil { 194 | return "", err 195 | } 196 | 197 | moddir, err = filepath.Abs(moddir) 198 | if err != nil { 199 | return "", err 200 | } 201 | 202 | rel, err := filepath.Rel(moddir, wd) 203 | if err != nil { 204 | return "", err 205 | } 206 | 207 | return path.Join(modpath, filepath.ToSlash(rel), p), nil 208 | } 209 | 210 | func isGoMod(p string) bool { 211 | return path.Base(p) == "go.mod" && 212 | !strings.Contains(p, "testdata/") 213 | } 214 | 215 | func goVersion(dir string) (string, error) { 216 | var stdout bytes.Buffer 217 | cmd := exec.Command("go", "env", "GOVERSION") 218 | cmd.Dir = dir 219 | cmd.Stdout = &stdout 220 | 221 | if err := cmd.Run(); err != nil { 222 | return "", err 223 | } 224 | 225 | return strings.TrimSpace(stdout.String()), nil 226 | } 227 | -------------------------------------------------------------------------------- /v2/skeleton/skeleton_test.go: -------------------------------------------------------------------------------- 1 | package skeleton_test 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "io" 8 | "os" 9 | "os/exec" 10 | "path" 11 | "path/filepath" 12 | "regexp" 13 | "strings" 14 | "testing" 15 | 16 | "github.com/tenntenn/golden" 17 | 18 | "github.com/gostaticanalysis/skeleton/v2/skeleton" 19 | "github.com/gostaticanalysis/skeleton/v2/skeleton/internal/gomod" 20 | ) 21 | 22 | var ( 23 | flagUpdate bool 24 | ) 25 | 26 | func init() { 27 | flag.BoolVar(&flagUpdate, "update", false, "update golden files") 28 | os.Setenv("SKELETON_PREFIX", "") 29 | } 30 | 31 | func TestSkeletonRun(t *testing.T) { 32 | t.Parallel() 33 | F := golden.TxtarWith 34 | const noflags = "" 35 | cases := map[string]struct { 36 | goVersion string 37 | dir string 38 | dirinit string 39 | flags string 40 | path string 41 | input string 42 | 43 | wantExitCode int 44 | wantOutput string 45 | wantGoTest bool 46 | }{ 47 | "nooption": {"", "", "", "", "example.com/example", noflags, skeleton.ExitSuccess, "", true}, 48 | "overwrite-cancel": {"", "", F(t, "example/go.mod", "// empty"), noflags, "example.com/example", "1\n", skeleton.ExitSuccess, "", false}, 49 | "overwrite-force": {"", "", F(t, "example/go.mod", "// empty"), noflags, "example.com/example", "2\n", skeleton.ExitSuccess, "", true}, 50 | "overwrite-confirm-yes": {"", "", F(t, "example/go.mod", "// empty"), noflags, "example.com/example", "3\ny\n", skeleton.ExitSuccess, "", true}, 51 | "overwrite-confirm-no": {"", "", F(t, "example/go.mod", "// empty"), noflags, "example.com/example", "3\nn\n", skeleton.ExitSuccess, "", false}, 52 | "overwrite-newonly": {"", "", F(t, "example/go.mod", "// empty"), noflags, "example.com/example", "4\n", skeleton.ExitSuccess, "", false}, 53 | "plugin": {"", "", "", "-plugin", "example.com/example", "", skeleton.ExitSuccess, "", true}, 54 | "nocmd": {"", "", "", "-cmd=false", "example.com/example", "", skeleton.ExitSuccess, "", true}, 55 | "onlypkgname": {"", "", "", noflags, "example", "", skeleton.ExitSuccess, "", true}, 56 | "version": {"", "", "", "-v", "", "", skeleton.ExitSuccess, "skeleton version\n", false}, 57 | "kind-inspect": {"", "", "", "-kind inspect", "example.com/example", "", skeleton.ExitSuccess, "", true}, 58 | "kind-ssa": {"", "", "", "-kind ssa", "example.com/example", "", skeleton.ExitSuccess, "", true}, 59 | "kind-codegen": {"", "", "", "-kind codegen", "example.com/example", "", skeleton.ExitSuccess, "", true}, 60 | "kind-packages": {"", "", "", "-kind packages", "example.com/example", "", skeleton.ExitSuccess, "", true}, 61 | "parent-module": {"", "", F(t, "go.mod", "module example.com/example"), "-gomod=false", "sub", "", skeleton.ExitSuccess, "", true}, 62 | "parent-module-deep": {"", "sub", F(t, "go.mod", "module example.com/example", "sub/sub.go", "package sub"), "-gomod=false", "subsub", "", skeleton.ExitSuccess, "", true}, 63 | "kind-inspect-copy-parent-gomod-to-testdata": {"", "", "", "-kind inspect -copy-parent-gomod", "example.com/example", "", skeleton.ExitSuccess, "", true}, 64 | "kind-ssa-copy-parent-gomod-to-testdata": {"", "", "", "-kind ssa -copy-parent-gomod", "example.com/example", "", skeleton.ExitSuccess, "", true}, 65 | } 66 | 67 | if flagUpdate { 68 | golden.RemoveAll(t, "testdata") 69 | } 70 | 71 | for name, tt := range cases { 72 | name, tt := name, tt 73 | t.Run(name, func(t *testing.T) { 74 | t.Parallel() 75 | 76 | tmpdir := t.TempDir() 77 | if tt.dirinit != "" { 78 | golden.DirInit(t, tmpdir, tt.dirinit) 79 | } 80 | 81 | var out, errout bytes.Buffer 82 | s := &skeleton.Skeleton{ 83 | Dir: filepath.Join(tmpdir, tt.dir), 84 | Output: &out, 85 | ErrOutput: &errout, 86 | Input: strings.NewReader(tt.input), 87 | GoVersion: "1.17", // do not change it even if your go version is over 1.18 88 | } 89 | 90 | if tt.goVersion != "" { 91 | s.GoVersion = tt.goVersion 92 | } 93 | 94 | var args []string 95 | if tt.flags != "" { 96 | args = strings.Split(tt.flags, " ") 97 | } 98 | if tt.path != "" { 99 | args = append(args, tt.path) 100 | } 101 | gotExitCode := s.Run(name, args) 102 | 103 | if gotExitCode != tt.wantExitCode { 104 | t.Errorf("exit code want %d got %d", tt.wantExitCode, gotExitCode) 105 | } 106 | 107 | if tt.wantExitCode == 0 && errout.String() != "" { 108 | t.Error("exit code want 0 but error messages are outputed", errout.String()) 109 | } 110 | 111 | if tt.wantOutput != "" && out.String() != tt.wantOutput { 112 | t.Errorf("output want %s got %s", tt.wantOutput, out.String()) 113 | } 114 | 115 | got := golden.Txtar(t, s.Dir) 116 | 117 | if tt.wantGoTest && tt.path != "" { 118 | skeletondir := filepath.Join(s.Dir, path.Base(tt.path)) 119 | modroot := modroot(t, skeletondir) 120 | execCmd(t, modroot, "go", "mod", "tidy") 121 | gotest(t, name, skeletondir) 122 | } 123 | 124 | if flagUpdate { 125 | golden.Update(t, "testdata", name, got) 126 | return 127 | } 128 | 129 | if diff := golden.Diff(t, "testdata", name, got); diff != "" { 130 | t.Error(diff) 131 | } 132 | }) 133 | } 134 | } 135 | 136 | func modroot(t *testing.T, dir string) string { 137 | t.Helper() 138 | modfile, err := gomod.ModFile(dir) 139 | if err != nil { 140 | t.Fatal("unexpected error:", err) 141 | } 142 | 143 | return filepath.Dir(modfile) 144 | } 145 | 146 | var ( 147 | timeRegexp = regexp.MustCompile(`([\(\t])([0-9.]+s)(\)?)`) 148 | ) 149 | 150 | func gotest(t *testing.T, name, dir string) { 151 | t.Helper() 152 | 153 | var stdout, stderr bytes.Buffer 154 | cmd := exec.Command("go", "test") 155 | cmd.Dir = dir 156 | cmd.Stdout = &stdout 157 | cmd.Stderr = &stderr 158 | if err := cmd.Run(); err != nil && !errors.As(err, new(*exec.ExitError)) { 159 | t.Fatal("unexpected error:", err) 160 | } 161 | 162 | got := stdout.String() + stderr.String() 163 | got = timeRegexp.ReplaceAllString(got, "${1}0000s${3}") 164 | 165 | goldenname := name + "-go-test" 166 | if flagUpdate { 167 | golden.Update(t, "testdata", goldenname, got) 168 | return 169 | } 170 | 171 | if diff := golden.Diff(t, "testdata", goldenname, got); diff != "" { 172 | t.Error(diff) 173 | } 174 | } 175 | 176 | func execCmd(t *testing.T, dir, cmd string, args ...string) io.Reader { 177 | t.Helper() 178 | var stdout, stderr bytes.Buffer 179 | _cmd := exec.Command(cmd, args...) 180 | _cmd.Stdout = &stdout 181 | _cmd.Stderr = &stderr 182 | _cmd.Dir = dir 183 | t.Log("exec", cmd, strings.Join(args, " ")) 184 | if err := _cmd.Run(); err != nil { 185 | t.Fatal(err, "\n", &stderr) 186 | } 187 | return &stdout 188 | } 189 | -------------------------------------------------------------------------------- /main.go: -------------------------------------------------------------------------------- 1 | package main 2 | 3 | import ( 4 | "bytes" 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "go/build" 9 | "go/format" 10 | "io" 11 | "os" 12 | "path" 13 | "path/filepath" 14 | "runtime" 15 | "strings" 16 | 17 | "golang.org/x/tools/txtar" 18 | ) 19 | 20 | //go:generate go run tools/txtar/main.go -strip "_template/" _template template.go 21 | 22 | func main() { 23 | var s Skeleton 24 | flag.BoolVar(&s.OverWrite, "overwrite", false, "overwrite all file") 25 | flag.BoolVar(&s.Cmd, "cmd", true, "create cmd directory") 26 | flag.StringVar(&s.Checker, "checker", "unit", "checker which is used in main.go (unit,single,multi)") 27 | flag.BoolVar(&s.Plugin, "plugin", true, "create plugin directory") 28 | flag.StringVar(&s.Type, "type", "inspect", "type of skeleton code (inspect|ssa|codegen)") 29 | flag.BoolVar(&s.Mod, "mod", true, "generate empty go.mod") 30 | flag.StringVar(&s.ImportPath, "path", "", "import path") 31 | flag.Parse() 32 | s.ExeName = os.Args[0] 33 | s.Args = flag.Args() 34 | 35 | switch s.Checker { 36 | case "unit", "single", "multi": 37 | // noop 38 | default: 39 | s.Checker = "unit" 40 | } 41 | 42 | if s.Type == "codegen" { 43 | if s.Checker != "single" { 44 | s.Checker = "single" 45 | } 46 | s.Plugin = false 47 | } 48 | 49 | if err := s.Run(); err != nil { 50 | fmt.Fprintln(os.Stderr, "Error:", err) 51 | os.Exit(1) 52 | } 53 | } 54 | 55 | type Skeleton struct { 56 | ExeName string 57 | Args []string 58 | Dir string 59 | ImportPath string 60 | OverWrite bool 61 | Cmd bool 62 | Checker string 63 | Plugin bool 64 | Mode Mode 65 | Type string 66 | Mod bool 67 | } 68 | 69 | type Mode int 70 | 71 | const ( 72 | ModeRemoveAndCreateNew Mode = iota 73 | ModeConfirm 74 | ModeCreateNewFile 75 | ) 76 | 77 | type TemplateData struct { 78 | Pkg string 79 | ImportPath string 80 | Cmd bool 81 | Plugin bool 82 | Checker string 83 | Type string 84 | Mod bool 85 | GoVer string 86 | } 87 | 88 | func (s *Skeleton) Run() error { 89 | 90 | // go1.xx.yy -> 1.xx 91 | // go1.xx -> 1.xx 92 | gover := strings.Join(strings.Split(runtime.Version(), ".")[:2], ".")[2:] 93 | 94 | td := &TemplateData{ 95 | Cmd: s.Cmd, 96 | Plugin: s.Plugin, 97 | Checker: s.Checker, 98 | Type: s.Type, 99 | Mod: s.Mod, 100 | GoVer: gover, 101 | } 102 | 103 | if len(s.Args) < 1 { 104 | if s.ImportPath != "" { 105 | s.Dir = s.ImportPath 106 | td.Pkg = path.Base(s.ImportPath) 107 | } else { 108 | return errors.New("package must be specified") 109 | } 110 | } else { 111 | s.Dir = s.Args[0] 112 | td.Pkg = path.Base(s.Args[0]) 113 | } 114 | 115 | switch s.Type { 116 | case "inspect", "ssa", "codegen": 117 | default: 118 | return fmt.Errorf("unexpected type: %s", s.Type) 119 | } 120 | 121 | cwd, err := os.Getwd() 122 | if err != nil { 123 | return err 124 | } 125 | 126 | td.ImportPath = s.importPath(cwd) 127 | 128 | if td.ImportPath == "" { 129 | const format = "%s must be executed in GOPATH or -path option must be specified" 130 | return fmt.Errorf(format, s.ExeName) 131 | } 132 | 133 | exist, err := isExist(s.Dir) 134 | if err != nil { 135 | return err 136 | } 137 | if exist && !s.OverWrite { 138 | if exit := s.selectMode(s.Dir); exit { 139 | return nil 140 | } 141 | } 142 | 143 | if err := s.createAll(td); err != nil { 144 | return err 145 | } 146 | 147 | return nil 148 | } 149 | 150 | func (s *Skeleton) importPath(cwd string) string { 151 | 152 | if s.ImportPath != "" { 153 | return s.ImportPath 154 | } 155 | 156 | for _, gopath := range filepath.SplitList(build.Default.GOPATH) { 157 | if gopath == "" { 158 | continue 159 | } 160 | 161 | src := filepath.Join(gopath, "src") 162 | if strings.HasPrefix(cwd, src) { 163 | rel, err := filepath.Rel(src, cwd) 164 | if err != nil { 165 | return "" 166 | } 167 | return path.Join(filepath.ToSlash(rel), filepath.ToSlash(s.Dir)) 168 | } 169 | } 170 | 171 | return "" 172 | } 173 | 174 | func (s *Skeleton) selectMode(dir string) bool { 175 | fmt.Printf("%s already exist, remove?\n", dir) 176 | fmt.Println("[1] No(Exit)") 177 | fmt.Println("[2] Remove and create new directory") 178 | fmt.Println("[3] Overwrite existing files with confirmation") 179 | fmt.Println("[4] Create new files only") 180 | fmt.Print("(default is 1) >") 181 | var m string 182 | fmt.Scanln(&m) 183 | switch m { 184 | case "2": 185 | s.Mode = ModeRemoveAndCreateNew 186 | case "3": 187 | s.Mode = ModeConfirm 188 | case "4": 189 | s.Mode = ModeCreateNewFile 190 | default: 191 | // exit 192 | return true 193 | } 194 | return false 195 | } 196 | 197 | func (s *Skeleton) createAll(td *TemplateData) error { 198 | 199 | if s.Mode == ModeRemoveAndCreateNew { 200 | if err := os.RemoveAll(s.Dir); err != nil { 201 | return err 202 | } 203 | } 204 | 205 | var buf bytes.Buffer 206 | if err := tmpl.Execute(&buf, td); err != nil { 207 | return err 208 | } 209 | 210 | ar := txtar.Parse(buf.Bytes()) 211 | for _, f := range ar.Files { 212 | if err := s.createFile(f); err != nil { 213 | return err 214 | } 215 | } 216 | 217 | return nil 218 | } 219 | 220 | func (s *Skeleton) createFile(f txtar.File) (rerr error) { 221 | if len(bytes.TrimSpace(f.Data)) == 0 { 222 | return nil 223 | } 224 | 225 | path := filepath.Join(s.Dir, filepath.FromSlash(f.Name)) 226 | 227 | exist, err := isExist(path) 228 | if err != nil { 229 | return err 230 | } 231 | 232 | if exist { 233 | switch s.Mode { 234 | case ModeConfirm: 235 | fmt.Printf("%s already exit, replace? [y/N] >", path) 236 | var yn string 237 | fmt.Scanln(&yn) 238 | switch strings.ToLower(yn) { 239 | case "y", "yes": 240 | // continue 241 | default: 242 | // skip 243 | fmt.Println("skip", path) 244 | return nil 245 | } 246 | case ModeCreateNewFile: 247 | // skip 248 | fmt.Println("skip", path) 249 | return nil 250 | } 251 | } 252 | 253 | if err := os.MkdirAll(filepath.Dir(path), 0777); err != nil { 254 | return err 255 | } 256 | 257 | w, err := os.Create(path) 258 | if err != nil { 259 | return err 260 | } 261 | defer func() { 262 | if err := w.Close(); err != nil && rerr == nil { 263 | rerr = err 264 | } 265 | }() 266 | 267 | // format a go file 268 | data := f.Data 269 | if filepath.Ext(path) == ".go" { 270 | data, err = format.Source(data) 271 | if err != nil { 272 | return err 273 | } 274 | } 275 | 276 | r := bytes.NewReader(data) 277 | if _, err := io.Copy(w, r); err != nil { 278 | return err 279 | } 280 | 281 | fmt.Println("create", path) 282 | 283 | return nil 284 | } 285 | 286 | func isExist(path string) (bool, error) { 287 | _, err := os.Stat(path) 288 | if err == nil { 289 | return true, nil 290 | } 291 | 292 | if os.IsNotExist(err) { 293 | return false, nil 294 | } 295 | 296 | return false, err 297 | } 298 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # Changelog 2 | 3 | ## [v2.3.0](https://github.com/gostaticanalysis/skeleton/compare/v2.2.3...v2.3.0) - 2024-11-17 4 | - Fix compare version by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/80 5 | - support development Go version by @sivchari in https://github.com/gostaticanalysis/skeleton/pull/79 6 | - Remove pre-go1.18 support by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/82 7 | - Add -copy-parent-gomod flag by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/83 8 | 9 | ## [v2.2.3](https://github.com/gostaticanalysis/skeleton/compare/v2.2.2...v2.2.3) - 2024-11-13 10 | - fix -checker option description in README_ja.md by @ikura-hamu in https://github.com/gostaticanalysis/skeleton/pull/68 11 | - Fix test and vet workflow by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/69 12 | - Add tagpr by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/74 13 | - Move .tagpr by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/76 14 | - Fix go version logic by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/77 15 | 16 | ## [v2.2.2](https://github.com/gostaticanalysis/skeleton/compare/v2.2.1...v2.2.2) - 2022-06-09 17 | - Copy README_ja.md from v2 to top level by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/63 18 | - Fix for rel path by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/65 19 | - Release v2.2.2 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/66 20 | 21 | ## [v2.2.1](https://github.com/gostaticanalysis/skeleton/compare/v2.2.0...v2.2.1) - 2022-05-27 22 | - Fix for go118 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/60 23 | - Fix gomod false by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/59 24 | - Update documents by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/61 25 | - Release v2.2.1 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/62 26 | 27 | ## [v2.2.0](https://github.com/gostaticanalysis/skeleton/compare/v2.1.2...v2.2.0) - 2022-05-22 28 | - Add mod less by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/55 29 | - Release v2.2.0 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/56 30 | 31 | ## [v2.1.2](https://github.com/gostaticanalysis/skeleton/compare/v2.1.1...v2.1.2) - 2022-03-25 32 | - Fix main.go of packages kind tempalte by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/52 33 | 34 | ## [v2.1.1](https://github.com/gostaticanalysis/skeleton/compare/v2.1.0...v2.1.1) - 2022-03-25 35 | - Add execution go test to each test cases by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/49 36 | - Fix packages template by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/50 37 | - Release v2.1.1 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/51 38 | 39 | ## [v2.1.0](https://github.com/gostaticanalysis/skeleton/compare/v2.0.5...v2.1.0) - 2022-03-22 40 | - Add packages template by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/48 41 | 42 | ## [v2.0.5](https://github.com/gostaticanalysis/skeleton/compare/v2.0.4...v2.0.5) - 2021-11-12 43 | - Use skeletonkit by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/46 44 | - Use tenntenn/golden by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/47 45 | - Update version v2.0.5 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/45 46 | - Add description of SKELETON_PREFIX usage by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/44 47 | 48 | ## [v2.0.4](https://github.com/gostaticanalysis/skeleton/compare/v2.0.3...v2.0.4) - 2021-07-12 49 | - Trim spaces from version by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/42 50 | - Release v2.0.4 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/43 51 | 52 | ## [v2.0.3](https://github.com/gostaticanalysis/skeleton/compare/v2.0.2...v2.0.3) - 2021-07-12 53 | - Fix typo in README.md by @nobishino in https://github.com/gostaticanalysis/skeleton/pull/36 54 | - Execute goimports by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/38 55 | - Fix checker of codegen by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/37 56 | - Add prefix to module path by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/39 57 | - Create testandvet.yml by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/40 58 | - Release v2.0.3 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/41 59 | 60 | ## [v2.0.2](https://github.com/gostaticanalysis/skeleton/compare/v2.0.1...v2.0.2) - 2021-06-07 61 | - Remove newline by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/35 62 | 63 | ## [v2.0.1](https://github.com/gostaticanalysis/skeleton/compare/v2.0.0...v2.0.1) - 2021-06-04 64 | - Onlypkgname by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/34 65 | 66 | ## [v2.0.0](https://github.com/gostaticanalysis/skeleton/compare/v1.6.0...v2.0.0) - 2021-06-04 67 | - Fix: typo and add install with `go install` by @sanposhiho in https://github.com/gostaticanalysis/skeleton/pull/31 68 | - v2 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/33 69 | 70 | ## [v1.6.0](https://github.com/gostaticanalysis/skeleton/compare/v1.5.1...v1.6.0) - 2020-10-18 71 | - Add codegen by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/29 72 | - Release v1.6.0 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/30 73 | 74 | ## [v1.5.1](https://github.com/gostaticanalysis/skeleton/compare/v1.4.0...v1.5.1) - 2020-10-17 75 | - Add type flag by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/23 76 | - Add mod flag by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/24 77 | - Fix README by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/25 78 | - Fix go version bug by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/27 79 | - Release v1.5.1 by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/28 80 | 81 | ## [v1.4.0](https://github.com/gostaticanalysis/skeleton/compare/v1.3.2...v1.4.0) - 2020-09-09 82 | - Add testdata mod by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/22 83 | 84 | ## [v1.3.2](https://github.com/gostaticanalysis/skeleton/compare/v1.3.1...v1.3.2) - 2020-07-10 85 | - Fix template delims by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/18 86 | - Fix nested directory by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/19 87 | 88 | ## [v1.3.1](https://github.com/gostaticanalysis/skeleton/compare/v1.3.0...v1.3.1) - 2020-07-10 89 | - Fix pkg base by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/16 90 | 91 | ## [v1.3.0](https://github.com/gostaticanalysis/skeleton/compare/v1.2.2...v1.3.0) - 2020-07-10 92 | - Add txtartemplate by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/13 93 | - Add checker flag by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/14 94 | - Fix typo identifier by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/15 95 | 96 | ## [v1.2.2](https://github.com/gostaticanalysis/skeleton/compare/v1.2.1...v1.2.2) - 2020-06-29 97 | - gofmt by @sinmetal in https://github.com/gostaticanalysis/skeleton/pull/12 98 | 99 | ## [v1.2.1](https://github.com/gostaticanalysis/skeleton/compare/v1.2.0...v1.2.1) - 2020-06-15 100 | - Change back tick to single quote by @ymotongpoo in https://github.com/gostaticanalysis/skeleton/pull/10 101 | 102 | ## [v1.2.0](https://github.com/gostaticanalysis/skeleton/compare/v1.1.1...v1.2.0) - 2020-06-07 103 | - Add flags for plugin by @tenntenn in https://github.com/gostaticanalysis/skeleton/pull/8 104 | -------------------------------------------------------------------------------- /template.go: -------------------------------------------------------------------------------- 1 | // Code generated by _tools/txtar/main.go; DO NOT EDIT. 2 | 3 | package main 4 | 5 | import "text/template" 6 | 7 | var tmpl = template.Must(template.New("template").Delims(`@@`, `@@`).Parse("-- @@.Pkg@@.go --\n@@ if eq .Type \"inspect\" -@@\npackage @@.Pkg@@\n\nimport (\n\t\"go/ast\"\n\n\t\"golang.org/x/tools/go/analysis\"\n\t\"golang.org/x/tools/go/analysis/passes/inspect\"\n\t\"golang.org/x/tools/go/ast/inspector\"\n)\n\nconst doc = \"@@.Pkg@@ is ...\"\n\n// Analyzer is ...\nvar Analyzer = &analysis.Analyzer{\n\tName: \"@@.Pkg@@\",\n\tDoc: doc,\n\tRun: run,\n\tRequires: []*analysis.Analyzer{\n\t\tinspect.Analyzer,\n\t},\n}\n\nfunc run(pass *analysis.Pass) (interface{}, error) {\n\tinspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector)\n\n\tnodeFilter := []ast.Node{\n\t\t(*ast.Ident)(nil),\n\t}\n\n\tinspect.Preorder(nodeFilter, func(n ast.Node) {\n\t\tswitch n := n.(type) {\n\t\tcase *ast.Ident:\n\t\t\tif n.Name == \"gopher\" {\n\t\t\t\tpass.Reportf(n.Pos(), \"identifier is gopher\")\n\t\t\t}\n\t\t}\n\t})\n\n\treturn nil, nil\n}\n@@ end -@@\n@@ if eq .Type \"ssa\" -@@\npackage @@.Pkg@@\n\nimport (\n\t\"fmt\"\n\n\t\"golang.org/x/tools/go/analysis\"\n\t\"golang.org/x/tools/go/analysis/passes/buildssa\"\n)\n\nconst doc = \"@@.Pkg@@ is ...\"\n\n// Analyzer is ...\nvar Analyzer = &analysis.Analyzer{\n\tName: \"@@.Pkg@@\",\n\tDoc: doc,\n\tRun: run,\n\tRequires: []*analysis.Analyzer{\n\t\tbuildssa.Analyzer,\n\t},\n}\n\nfunc run(pass *analysis.Pass) (interface{}, error) {\n\ts := pass.ResultOf[buildssa.Analyzer].(*buildssa.SSA)\n\tfor _, f := range s.SrcFuncs {\n\t\tfmt.Println(f)\n\t\tfor _, b := range f.Blocks {\n\t\t\tfmt.Printf(\"\\tBlock %d\\n\", b.Index)\n\t\t\tfor _, instr := range b.Instrs {\n\t\t\t\tfmt.Printf(\"\\t\\t%[1]T\\t%[1]v(%[1]p)\\n\", instr)\n\t\t\t\tfor _, v := range instr.Operands(nil) {\n\t\t\t\t\tif v != nil {\n\t\t\t\t\t\tfmt.Printf(\"\\t\\t\\t%[1]T\\t%[1]v(%[1]p)\\n\", *v)\n\t\t\t\t\t}\n \t\t\t}\n\t\t\t}\n\t\t}\n\t}\n\treturn nil, nil\n}\n@@ end -@@\n@@ if eq .Type \"codegen\" -@@\npackage @@.Pkg@@\n\nimport (\n\t\"bytes\"\n\t\"fmt\"\n\t\"go/format\"\n\t\"go/types\"\n\t\"os\"\n\n\t\"github.com/gostaticanalysis/analysisutil\"\n\t\"github.com/gostaticanalysis/codegen\"\n\t\"github.com/gostaticanalysis/knife\"\n)\n\nconst doc = \"@@.Pkg@@ is ...\"\n\nvar (\n\tflagOutput string\n)\n\nfunc init() {\n\tGenerator.Flags.StringVar(&flagOutput, \"o\", \"\", \"output file name\")\n}\n\nvar Generator = &codegen.Generator{\n\tName: \"@@.Pkg@@\",\n\tDoc: doc,\n\tRun: run,\n}\n\nfunc run(pass *codegen.Pass) error {\n\tifaces := map[string]*knife.Interface{}\n\n\ts := pass.Pkg.Scope()\n\tfor _, name := range s.Names() {\n\t\tobj := s.Lookup(name)\n\t\tif !obj.Exported() {\n\t\t\tcontinue\n\t\t}\n\t\tiface, _ := analysisutil.Under(obj.Type()).(*types.Interface)\n\t\tif iface != nil {\n\t\t\tifaces[name] = knife.NewInterface(iface)\n\t\t}\n\t}\n\n\ttd := &knife.TempalteData{\n\t\tFset: pass.Fset,\n\t\tFiles: pass.Files,\n\t\tTypesInfo: pass.TypesInfo,\n\t\tPkg: pass.Pkg,\n\t}\n\tt, err := knife.NewTemplate(td).Parse(tmpl)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tvar buf bytes.Buffer\n\tif err := t.Execute(&buf, ifaces); err != nil {\n\t\treturn err\n\t}\n\n\tsrc, err := format.Source(buf.Bytes())\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tif flagOutput == \"\" {\n\t\tpass.Print(string(src))\n\t\treturn nil\n\t}\n\n\tf, err := os.Create(flagOutput)\n\tif err != nil {\n\t\treturn err\n\t}\n\n\tfmt.Fprint(f, string(src))\n\n\tif err := f.Close(); err != nil {\n\t\treturn err\n\t}\n\n\treturn nil\n}\n\nvar tmpl = `// Code generated by @@.Pkg@@; DO NOT EDIT.\npackage {{(pkg).Name}}\n{{range $tn, $t := .}}\ntype Mock{{$tn}} struct {\n{{- range $n, $f := $t.Methods}}\n {{$n}}Func {{$f.Signature}}\n{{- end}}\n}\n{{range $n, $f := $t.Methods}}\nfunc (m *Mock{{$tn}}) {{$n}}({{range $f.Signature.Params}}\n\t{{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}}\n \t{{- .Name}} ...{{(slice .Type).Elem}},\n\t{{- else}}\n \t{{- .Name}} {{.Type}},\n\t{{- end}}\n{{- end}}) ({{range $f.Signature.Results}}\n {{- .Name}} {{.Type}},\n{{- end}}) {\n {{if $f.Signature.Results}}return {{end}}m.{{$n}}Func({{range $f.Signature.Params}}\n\t\t{{- if (and $f.Signature.Variadic (eq . (last $f.Signature.Params)))}}\n \t\t{{- .Name}}...,\n\t\t{{- else}}\n \t\t{{- .Name}},\n\t\t{{- end}}\n {{- end}})\n}\n{{end}}\n{{end}}\n`\n@@ end -@@\n-- @@.Pkg@@_test.go --\n@@ if (or (eq .Type \"inspect\") (eq .Type \"ssa\")) -@@\npackage @@.Pkg@@_test\n\nimport (\n\t\"testing\"\n\n\t\"@@.ImportPath@@\"\n\t\"github.com/gostaticanalysis/testutil\"\n\t\"golang.org/x/tools/go/analysis/analysistest\"\n)\n\n// TestAnalyzer is a test for Analyzer.\nfunc TestAnalyzer(t *testing.T) {\n\ttestdata := testutil.WithModules(t, analysistest.TestData(), nil)\n\tanalysistest.Run(t, testdata, @@.Pkg@@.Analyzer, \"a\")\n}\n@@ end -@@\n@@ if eq .Type \"codegen\" -@@\npackage @@.Pkg@@_test\n\nimport (\n\t\"flag\"\n\t\"os\"\n\t\"testing\"\n\n\t\"@@.ImportPath@@\"\n\t\"github.com/gostaticanalysis/codegen/codegentest\"\n)\n\nvar flagUpdate bool\n\nfunc TestMain(m *testing.M) {\n\tflag.BoolVar(&flagUpdate, \"update\", false, \"update the golden files\")\n\tflag.Parse()\n\tos.Exit(m.Run())\n}\n\nfunc TestGenerator(t *testing.T) {\n\trs := codegentest.Run(t, codegentest.TestData(), @@.Pkg@@.Generator, \"a\")\n\tcodegentest.Golden(t, rs, flagUpdate)\n}\n@@ end -@@\n-- cmd/@@.Pkg@@/main.go --\n@@ if .Cmd -@@\n@@ if (or (eq .Type \"inspect\") (eq .Type \"ssa\")) -@@\npackage main\n\nimport (\n\t\"@@.ImportPath@@\"\n\t\"golang.org/x/tools/go/analysis/@@.Checker@@checker\"\n)\n\nfunc main() { @@.Checker@@checker.Main(@@.Pkg@@.Analyzer) }\n@@ end -@@\n@@ if eq .Type \"codegen\" -@@\npackage main\n\nimport (\n\t\"@@.ImportPath@@\"\n\t\"github.com/gostaticanalysis/codegen/@@.Checker@@generator\"\n)\n\nfunc main() {\n\t@@.Checker@@generator.Main(@@.Pkg@@.Generator)\n}\n@@ end -@@\n@@end@@\n-- go.mod --\n@@ if .Mod -@@\nmodule @@.ImportPath@@\n\ngo @@.GoVer@@\n@@end@@\n-- plugin/main.go --\n@@ if .Plugin -@@\n// This file can build as a plugin for golangci-lint by below command.\n// go build -buildmode=plugin -o path_to_plugin_dir @@.ImportPath@@/plugin/@@.Pkg@@\n// See: https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint\n\npackage main\n\nimport (\n\t\"strings\"\n\n\t\"@@.ImportPath@@\"\n\t\"golang.org/x/tools/go/analysis\"\n)\n\n// flags for Analyzer.Flag.\n// If you would like to specify flags for your plugin, you can put them via 'ldflags' as below.\n// $ go build -buildmode=plugin -ldflags \"-X 'main.flags=-opt val'\" @@.ImportPath@@/plugin/@@.Pkg@@\nvar flags string\n\n// AnalyzerPlugin provides analyzers as a plugin.\n// It follows golangci-lint style plugin.\nvar AnalyzerPlugin analyzerPlugin\n\ntype analyzerPlugin struct{}\n\nfunc (analyzerPlugin) GetAnalyzers() []*analysis.Analyzer {\n\tif flags != \"\" {\n\t\tflagset := @@.Pkg@@.Analyzer.Flags\n\t\tif err := flagset.Parse(strings.Split(flags, \" \")); err != nil {\n\t\t\tpanic(\"cannot parse flags of @@.Pkg@@: \" + err.Error())\n\t\t}\n\t}\n\treturn []*analysis.Analyzer{\n\t\t@@.Pkg@@.Analyzer,\n\t}\n}\n@@end@@\n-- testdata/src/a/@@.Pkg@@.golden --\n@@ if eq .Type \"codegen\" -@@\n// Code generated by @@.Pkg@@; DO NOT EDIT.\npackage a\n\ntype MockDB struct {\n\tGetFunc func(id string) int\n\tSetFunc func(id string, v int)\n}\n\nfunc (m *MockDB) Get(id string) int {\n\treturn m.GetFunc(id)\n}\n\nfunc (m *MockDB) Set(id string, v int) {\n\tm.SetFunc(id, v)\n}\n\ntype MockLogger struct {\n\tErrorfFunc func(format string, args ...interface{})\n\tInfofFunc func(format string, args ...interface{})\n}\n\nfunc (m *MockLogger) Errorf(format string, args ...interface{}) {\n\tm.ErrorfFunc(format, args...)\n}\n\nfunc (m *MockLogger) Infof(format string, args ...interface{}) {\n\tm.InfofFunc(format, args...)\n}\n@@ end -@@\n-- testdata/src/a/a.go --\n@@ if (or (eq .Type \"inspect\") (eq .Type \"ssa\")) -@@\npackage a\n\nfunc f() {\n\t// The pattern can be written in regular expression.\n\tvar gopher int // want \"pattern\"\n\tprint(gopher) // want \"identifier is gopher\"\n}\n@@ end -@@\n@@ if eq .Type \"codegen\" -@@\npackage a\n\ntype DB interface {\n\tGet(id string) int\n\tSet(id string, v int)\n}\n\ntype db struct{}\n\nfunc (db) Get(id string) int { return 0 }\nfunc (db) Set(id string, v int) {}\n\ntype Logger interface {\n\tInfof(format string, args ...interface{})\n\tErrorf(format string, args ...interface{})\n}\n@@ end -@@\n-- testdata/src/a/go.mod --\n@@ if ne .Type \"codegen\" -@@\nmodule a\n\ngo @@.GoVer@@\n@@ end -@@\n")) 8 | -------------------------------------------------------------------------------- /v2/skeleton/testdata/kind-packages.golden: -------------------------------------------------------------------------------- 1 | -- example/cmd/example/main.go -- 2 | package main 3 | 4 | import ( 5 | "errors" 6 | "flag" 7 | "fmt" 8 | "os" 9 | 10 | "example.com/example" 11 | "example.com/example/internal" 12 | "golang.org/x/tools/go/packages" 13 | ) 14 | 15 | func main() { 16 | if err := run(); err != nil { 17 | fmt.Fprintln(os.Stderr, err) 18 | os.Exit(1) 19 | } 20 | } 21 | 22 | func run() error { 23 | example.Analyzer.Flags = flag.NewFlagSet(example.Analyzer.Name, flag.ExitOnError) 24 | example.Analyzer.Flags.Parse(os.Args[1:]) 25 | 26 | if example.Analyzer.Flags.NArg() < 1 { 27 | return errors.New("patterns of packages must be specified") 28 | } 29 | 30 | pkgs, err := packages.Load(example.Analyzer.Config, example.Analyzer.Flags.Args()...) 31 | if err != nil { 32 | return err 33 | } 34 | 35 | for _, pkg := range pkgs { 36 | prog, srcFuncs, err := internal.BuildSSA(pkg, example.Analyzer.SSABuilderMode) 37 | if err != nil { 38 | return err 39 | } 40 | 41 | pass := &internal.Pass{ 42 | Package: pkg, 43 | SSA: prog, 44 | SrcFuncs: srcFuncs, 45 | Stdin: os.Stdin, 46 | Stdout: os.Stdout, 47 | Stderr: os.Stderr, 48 | } 49 | 50 | if err := example.Analyzer.Run(pass); err != nil { 51 | return err 52 | } 53 | } 54 | 55 | return nil 56 | } 57 | -- example/example.go -- 58 | package example 59 | 60 | import ( 61 | "fmt" 62 | "go/ast" 63 | "path/filepath" 64 | 65 | "example.com/example/internal" 66 | "golang.org/x/tools/go/ast/inspector" 67 | "golang.org/x/tools/go/packages" 68 | ) 69 | 70 | var Analyzer = &internal.Analyzer{ 71 | Name: "example", 72 | Doc: "example is ...", 73 | Config: &packages.Config{ 74 | Mode: packages.NeedName | packages.NeedTypes | 75 | packages.NeedSyntax | packages.NeedTypesInfo | 76 | packages.NeedModule, 77 | }, 78 | SSABuilderMode: 0, 79 | Run: run, 80 | } 81 | 82 | func run(pass *internal.Pass) error { 83 | inspect := inspector.New(pass.Syntax) 84 | 85 | nodeFilter := []ast.Node{ 86 | (*ast.Ident)(nil), 87 | } 88 | 89 | inspect.Preorder(nodeFilter, func(n ast.Node) { 90 | switch n := n.(type) { 91 | case *ast.Ident: 92 | if n.Name == "gopher" { 93 | pos := pass.Fset.Position(n.Pos()) 94 | fname := pos.Filename 95 | if pass.Module != nil { 96 | var err error 97 | fname, err = filepath.Rel(pass.Module.Dir, fname) 98 | if err != nil { 99 | return 100 | } 101 | } 102 | fmt.Fprintf(pass.Stdout, "%s:%d:%d identifier is gopher\n", fname, pos.Line, pos.Column) 103 | } 104 | } 105 | }) 106 | 107 | // See: golang.org/x/tools/go/ssa 108 | for _, f := range pass.SrcFuncs { 109 | fmt.Fprintln(pass.Stdout, f) 110 | for _, b := range f.Blocks { 111 | fmt.Fprintf(pass.Stdout, "\tBlock %d\n", b.Index) 112 | for _, instr := range b.Instrs { 113 | fmt.Fprintf(pass.Stdout, "\t\t%[1]T\t%[1]v\n", instr) 114 | for _, v := range instr.Operands(nil) { 115 | if v != nil { 116 | fmt.Fprintf(pass.Stdout, "\t\t\t%[1]T\t%[1]v\n", *v) 117 | } 118 | } 119 | } 120 | } 121 | } 122 | 123 | return nil 124 | } 125 | -- example/example_test.go -- 126 | package example_test 127 | 128 | import ( 129 | "bytes" 130 | "flag" 131 | "path/filepath" 132 | "strings" 133 | "testing" 134 | 135 | "example.com/example" 136 | "example.com/example/internal" 137 | "github.com/tenntenn/golden" 138 | "golang.org/x/tools/go/packages" 139 | "golang.org/x/tools/go/ssa" 140 | ) 141 | 142 | var ( 143 | flagUpdate bool 144 | ) 145 | 146 | func init() { 147 | flag.BoolVar(&flagUpdate, "update", false, "update golden files") 148 | } 149 | 150 | func Test(t *testing.T) { 151 | pkgs := load(t, testdata(t), "a") 152 | for _, pkg := range pkgs { 153 | prog, funcs := buildssa(t, pkg) 154 | run(t, pkg, prog, funcs) 155 | } 156 | } 157 | 158 | func load(t *testing.T, testdata string, pkgname string) []*packages.Package { 159 | t.Helper() 160 | example.Analyzer.Config.Dir = filepath.Join(testdata, "src", pkgname) 161 | pkgs, err := packages.Load(example.Analyzer.Config, pkgname) 162 | if err != nil { 163 | t.Fatal("unexpected error:", err) 164 | } 165 | return pkgs 166 | } 167 | 168 | func buildssa(t *testing.T, pkg *packages.Package) (*ssa.Program, []*ssa.Function) { 169 | t.Helper() 170 | program, funcs, err := internal.BuildSSA(pkg, example.Analyzer.SSABuilderMode) 171 | if err != nil { 172 | t.Fatal("unexpected error:", err) 173 | } 174 | return program, funcs 175 | } 176 | 177 | func testdata(t *testing.T) string { 178 | t.Helper() 179 | dir, err := filepath.Abs("testdata") 180 | if err != nil { 181 | t.Fatal("unexpected error:", err) 182 | } 183 | return dir 184 | } 185 | 186 | func run(t *testing.T, pkg *packages.Package, prog *ssa.Program, funcs []*ssa.Function) { 187 | var stdin, stdout, stderr bytes.Buffer 188 | pass := &internal.Pass{ 189 | Stdin: &stdin, 190 | Stdout: &stdout, 191 | Stderr: &stderr, 192 | Package: pkg, 193 | SSA: prog, 194 | SrcFuncs: funcs, 195 | } 196 | 197 | if err := example.Analyzer.Run(pass); err != nil { 198 | t.Error("unexpected error:", err) 199 | } 200 | 201 | pkgname := pkgname(pkg) 202 | 203 | if flagUpdate { 204 | golden.Update(t, testdata(t), pkgname+"-stdout", &stdout) 205 | golden.Update(t, testdata(t), pkgname+"-stderr", &stderr) 206 | return 207 | } 208 | 209 | if diff := golden.Diff(t, testdata(t), pkgname+"-stdout", &stdout); diff != "" { 210 | t.Errorf("stdout of analyzing %s:\n%s", pkgname, diff) 211 | } 212 | 213 | if diff := golden.Diff(t, testdata(t), pkgname+"-stderr", &stderr); diff != "" { 214 | t.Errorf("stderr of analyzing %s:\n%s", pkgname, diff) 215 | } 216 | } 217 | 218 | func pkgname(pkg *packages.Package) string { 219 | switch { 220 | case pkg.PkgPath != "": 221 | return strings.ReplaceAll(pkg.PkgPath, "/", "-") 222 | case pkg.Name != "": 223 | return pkg.Name 224 | default: 225 | return pkg.ID 226 | } 227 | } 228 | -- example/go.mod -- 229 | module example.com/example 230 | 231 | go 1.23.3 232 | 233 | -- example/internal/analyzer.go -- 234 | package internal 235 | 236 | import ( 237 | "flag" 238 | "io" 239 | 240 | "golang.org/x/tools/go/packages" 241 | "golang.org/x/tools/go/ssa" 242 | ) 243 | 244 | type Pass struct { 245 | *packages.Package 246 | SSA *ssa.Program 247 | SrcFuncs []*ssa.Function 248 | Stdin io.Reader 249 | Stdout io.Writer 250 | Stderr io.Writer 251 | } 252 | 253 | type Analyzer struct { 254 | Name string 255 | Doc string 256 | Flags *flag.FlagSet 257 | Config *packages.Config 258 | SSABuilderMode ssa.BuilderMode 259 | Run func(pass *Pass) error 260 | } 261 | -- example/internal/buildssa.go -- 262 | package internal 263 | 264 | import ( 265 | "go/ast" 266 | "go/types" 267 | 268 | "golang.org/x/tools/go/packages" 269 | "golang.org/x/tools/go/ssa" 270 | ) 271 | 272 | // copy from golang.org/x/tools/analysis/passes/buildssa 273 | func BuildSSA(pkg *packages.Package, mode ssa.BuilderMode) (*ssa.Program, []*ssa.Function, error) { 274 | 275 | prog := ssa.NewProgram(pkg.Fset, mode) 276 | 277 | // Create SSA packages for all imports. 278 | // Order is not significant. 279 | created := make(map[*types.Package]bool) 280 | var createAll func(pkgs []*types.Package) 281 | createAll = func(pkgs []*types.Package) { 282 | for _, p := range pkgs { 283 | if !created[p] { 284 | created[p] = true 285 | prog.CreatePackage(p, nil, nil, true) 286 | createAll(p.Imports()) 287 | } 288 | } 289 | } 290 | createAll(pkg.Types.Imports()) 291 | 292 | // Create and build the primary package. 293 | ssapkg := prog.CreatePackage(pkg.Types, pkg.Syntax, pkg.TypesInfo, false) 294 | ssapkg.Build() 295 | 296 | // Compute list of source functions, including literals, 297 | // in source order. 298 | var funcs []*ssa.Function 299 | for _, f := range pkg.Syntax { 300 | for _, decl := range f.Decls { 301 | if fdecl, ok := decl.(*ast.FuncDecl); ok { 302 | 303 | // SSA will not build a Function 304 | // for a FuncDecl named blank. 305 | // That's arguably too strict but 306 | // relaxing it would break uniqueness of 307 | // names of package members. 308 | if fdecl.Name.Name == "_" { 309 | continue 310 | } 311 | 312 | // (init functions have distinct Func 313 | // objects named "init" and distinct 314 | // ssa.Functions named "init#1", ...) 315 | 316 | fn := pkg.TypesInfo.Defs[fdecl.Name].(*types.Func) 317 | if fn == nil { 318 | panic(fn) 319 | } 320 | 321 | f := ssapkg.Prog.FuncValue(fn) 322 | if f == nil { 323 | panic(fn) 324 | } 325 | 326 | var addAnons func(f *ssa.Function) 327 | addAnons = func(f *ssa.Function) { 328 | funcs = append(funcs, f) 329 | for _, anon := range f.AnonFuncs { 330 | addAnons(anon) 331 | } 332 | } 333 | addAnons(f) 334 | } 335 | } 336 | } 337 | 338 | return prog, funcs, nil 339 | } 340 | -- example/testdata/a-stderr.golden -- 341 | -- example/testdata/a-stdout.golden -- 342 | a.go:5:6 identifier is gopher 343 | a.go:6:8 identifier is gopher 344 | a.f 345 | Block 0 346 | *ssa.Call print(0:int) 347 | *ssa.Builtin builtin print 348 | *ssa.Const 0:int 349 | *ssa.Return return 350 | -- example/testdata/src/a/a.go -- 351 | package a 352 | 353 | func f() { 354 | // The pattern can be written in regular expression. 355 | var gopher int // want "pattern" 356 | print(gopher) // want "identifier is gopher" 357 | } 358 | -- example/testdata/src/a/go.mod -- 359 | module a 360 | 361 | go 1.23.3 362 | 363 | -------------------------------------------------------------------------------- /README_ja.md: -------------------------------------------------------------------------------- 1 | [English Version](./README.md) 2 | 3 | # skeleton 4 | 5 | skeletonはGoの静的解析ツールのためのスケルトンコードジェネレータです。[x/tools/go/analysis](https://golang.org/x/tools/go/analysis)パッケージや[x/tools/go/packages](https://golang.org/x/tools/go/packages)パッケージを用いた静的解析ツールの開発を簡単にします。 6 | 7 | ## x/tools/go/analysisパッケージ 8 | 9 | [x/tools/go/analysis](https://golang.org/x/tools/go/analysis)パッケージは静的解析ツールをモジュール化するためのパッケージです。[analysis.Analyzer](https://golang.org/x/tools/go/analysis)型を1つの単位として扱います。 10 | 11 | `x/tools/go/analysis`パッケージは、静的解析ツールの共通部分を定型化しています。skeletonは定型化されているコードの大部分をスケルトンコードとして生成します。`skeleton mylinter`コマンドを実行するだけで`*analyzer.Analyzer`型の初期化コードやテストコード、`go vet`から実行できる実行可能ファイルを作るための`main.go`を生成してくれます。 12 | 13 | skeletonについて詳しく知りたい場合は、次のブログも参考になります。 14 | 15 | * [skeletonで始めるGoの静的解析](https://engineering.mercari.com/blog/entry/20220406-eea588f493/) 16 | 17 | `x/tools/go/analysis`パッケージやGoの静的解析自体を知りたい場合は、次の資料が参考になります。 18 | 19 | * [プログラミング言語Go完全入門 14章 静的解析とコード生成](http://tenn.in/analysis) 20 | 21 | ## インストール 22 | 23 | ``` 24 | $ go install github.com/gostaticanalysis/skeleton/v2@latest 25 | ``` 26 | 27 | ## 使用方法 28 | 29 | ### モジュールパスを指定して作成 30 | 31 | skeletonの引数にモジュールパスを指定するとそのパスでモジュールを生成します。ディレクトリ名はモジュールパスの最後の要素になります。`example.com/mylinter`と指定すると次のようになります。 32 | 33 | ``` 34 | $ skeleton example.com/mylinter 35 | mylinter 36 | ├── cmd 37 | │   └── mylinter 38 | │   └── main.go 39 | ├── go.mod 40 | ├── mylinter.go 41 | ├── mylinter_test.go 42 | └── testdata 43 | └── src 44 | └── a 45 | ├── a.go 46 | └── go.mod 47 | ``` 48 | 49 | #### 解析器 50 | 51 | `x/tools/go/analysis`パッケージを用いて開発された静的解析ツールは、`*analysis.Analyzer`型の値として表現されます。mylinterの場合、`mylinter.go`に`Analyzer`変数として定義されています。 52 | 53 | 生成されたコードは、`inspect.Analyzer`を用いた簡単な静的解析ツールを実装しています。この静的解析ツールは、`gopher`という名前の識別子を見つけるだけです。 54 | 55 | ```go 56 | package mylinter 57 | 58 | import ( 59 | "go/ast" 60 | 61 | "golang.org/x/tools/go/analysis" 62 | "golang.org/x/tools/go/analysis/passes/inspect" 63 | "golang.org/x/tools/go/ast/inspector" 64 | ) 65 | 66 | const doc = "mylinter is ..." 67 | 68 | // Analyzer is ... 69 | var Analyzer = &analysis.Analyzer{ 70 | Name: "mylinter", 71 | Doc: doc, 72 | Run: run, 73 | Requires: []*analysis.Analyzer{ 74 | inspect.Analyzer, 75 | }, 76 | } 77 | 78 | func run(pass *analysis.Pass) (interface{}, error) { 79 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 80 | 81 | nodeFilter := []ast.Node{ 82 | (*ast.Ident)(nil), 83 | } 84 | 85 | inspect.Preorder(nodeFilter, func(n ast.Node) { 86 | switch n := n.(type) { 87 | case *ast.Ident: 88 | if n.Name == "gopher" { 89 | pass.Reportf(n.Pos(), "identifier is gopher") 90 | } 91 | } 92 | }) 93 | 94 | return nil, nil 95 | } 96 | ``` 97 | 98 | #### テストコード 99 | 100 | skeletonは、テストコードも生成します。`x/tools/go/analysis`パッケージはサブパッケージの`analysistest`パッケージとして、テストライブラリを提供しています。`analysistest.Run`関数は`testdata/src`ディレクトリ以下にあるソースコードを使ってテストを実行します。この関数の第2引数はテストデータのディレクトリです。第3引数はテスト対象のAnalyzer、第4引数以降はテストデータとして利用するパッケージ名です。 101 | 102 | ```go 103 | package mylinter_test 104 | 105 | import ( 106 | "testing" 107 | 108 | "github.com/gostaticanalysis/example.com/mylinter" 109 | "github.com/gostaticanalysis/testutil" 110 | "golang.org/x/tools/go/analysis/analysistest" 111 | ) 112 | 113 | // TestAnalyzer is a test for Analyzer. 114 | func TestAnalyzer(t *testing.T) { 115 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 116 | analysistest.Run(t, testdata, mylinter.Analyzer, "a") 117 | } 118 | ``` 119 | 120 | mylinterの場合、テストは`testdata/src/a/a.go`ファイルをテストデータとして利用します。`mylinter.Analyzer`は`gopher`識別子をソースコードの中から探し報告します。テストでは、期待する報告をコメントで記述します。コメントは`want`で始まり、その後に期待するメッセージが正規表現で記述されます。テストは期待するメッセージで報告がされなかったり、期待していない報告がされた場合に失敗します。 121 | 122 | ```go 123 | package a 124 | 125 | func f() { 126 | // The pattern can be written in regular expression. 127 | var gopher int // want "pattern" 128 | print(gopher) // want "identifier is gopher" 129 | } 130 | ``` 131 | 132 | デフォルトでは`go mod tidy`コマンドと`go test`コマンドを実行すると、テストは失敗します。これは`pattern`というメッセージで作った静的解析ツールが報告をしないためです。 133 | 134 | ``` 135 | $ go mod tidy 136 | go: finding module for package golang.org/x/tools/go/analysis 137 | go: finding module for package github.com/gostaticanalysis/testutil 138 | go: finding module for package golang.org/x/tools/go/analysis/passes/inspect 139 | go: finding module for package golang.org/x/tools/go/analysis/unitchecker 140 | go: finding module for package golang.org/x/tools/go/ast/inspector 141 | go: finding module for package golang.org/x/tools/go/analysis/analysistest 142 | go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10 143 | go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10 144 | go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10 145 | go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10 146 | go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0 147 | go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10 148 | 149 | $ go test 150 | --- FAIL: TestAnalyzer (0.06s) 151 | analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern` 152 | analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern` 153 | FAIL 154 | exit status 1 155 | FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s 156 | ``` 157 | 158 | #### 実行可能ファイル 159 | 160 | skeletonは`cmd`ディレクトリ以下に`main.go`も生成します。この`main.go`をビルドし生成した実行可能ファイルは、`go vet`コマンド経由で実行される必要があります。`go vet`コマンドの`-vettool`オプションは生成した実行可能ファイルへの絶対パスを指定します。 161 | 162 | ``` 163 | $ go vet -vettool=`which mylinter` ./... 164 | ``` 165 | 166 | ### ディレクトリの上書き 167 | 168 | すでにディレクトリが存在する場合は上書きするか聞かれます。 169 | 170 | ``` 171 | $ skeleton example.com/mylinter 172 | mylinter is already exist, overwrite? 173 | [1] No (Exit) 174 | [2] Remove and create new directory 175 | [3] Overwrite existing files with confirmation 176 | [4] Create new files only 177 | ``` 178 | 179 | 選んだ選択肢によって処理が変わります。 180 | 181 | * [1] 上書きしない(終了) 182 | * [2] 削除して新しいディレクトリを作成 183 | * [3] すでにあるファイルを上書きするか都度確認 184 | * [4] 新しいファイルのみ生成する 185 | 186 | ### cmdディレクトリを生成しない 187 | 188 | `-cmd`オプションを`false`にすると`cmd`ディレクトリは生成されません。 189 | 190 | ``` 191 | $ skeleton -cmd=false example.com/mylinter 192 | mylinter 193 | ├── go.mod 194 | ├── mylinter.go 195 | ├── mylinter_test.go 196 | └── testdata 197 | └── src 198 | └── a 199 | ├── a.go 200 | └── go.mod 201 | ``` 202 | 203 | ### go.modファイルを生成しない 204 | 205 | skeletonはデフォルトでは`go.mod`ファイルを生成します。すでにGo Modules管理下にあるディレクトリでスケルトンコードを生成したい場合は、次のように`-gomod`オプションに`false`を指定します。 206 | 207 | ``` 208 | $ skeleton -gomod=false example.com/mylinter 209 | mylinter 210 | ├── cmd 211 | │   └── mylinter 212 | │   └── main.go 213 | ├── mylinter.go 214 | ├── mylinter_test.go 215 | └── testdata 216 | └── src 217 | └── a 218 | ├── a.go 219 | └── go.mod 220 | ``` 221 | 222 | ### SKELETON_PREFIX環境変数 223 | 224 | 次のように`SKELETON_PREFIX`環境変数を指定するとモジュールパスの前にプリフィックスを付与します。 225 | 226 | ``` 227 | $ SKELETON_PREFIX=example.com skeleton mylinter 228 | $ head -1 mylinter/go.mod 229 | module example.com/mylinter 230 | ``` 231 | 232 | 次のように[direnv](https://github.com/direnv/direnv)などを用いて特定のディレクトリ以下でプリフィックスをつけるようにすると便利です。 233 | 234 | ``` 235 | $ cat ~/repos/gostaticanalysis/.envrc 236 | export SKELETON_PREFIX=github.com/gostaticanalysis 237 | ``` 238 | 239 | `SKELETON_PREFIX`環境変数を指定していても、`-gomod`オプションを`false`にした場合は親のモジュールのモジュールパスが使用されます。 240 | 241 | ### singlecheckerまたはmulticheckerの使用 242 | 243 | デフォルトでは`main.go`では`go vet`から実行することを前提とした`unitchecker`パッケージが使われています。`-checker`オプションを指定することで、`singlechecker`パッケージや`multichecker`パッケージに変更できます。 244 | 245 | `singlechecker`パッケージは、単一のAnalyzerを実行するためのパッケージで`go vet`は必要としません。利用するには`-checker=single`を指定します。 246 | 247 | `multichecker`パッケージは、複数のAnalyzerを実行するためのパッケージで`go vet`は必要としません。利用するには`-checker=multi`を指定します。 248 | 249 | 次に`singlechecker`パッケージを利用した例を示します。 250 | 251 | ``` 252 | $ skeleton -checker=single example.com/mylinter 253 | $ cat cmd/mylinter/main.go 254 | package main 255 | 256 | import ( 257 | "mylinter" 258 | "golang.org/x/tools/go/analysis/singlechecker" 259 | ) 260 | 261 | func main() { singlechecker.Main(mylinter.Analyzer) } 262 | ``` 263 | 264 | `singlechecker`パッケージや`multichecker`パッケージを利用した方が簡単そうに見えますが、`go vet`を使った恩恵を受けられないため、特にこだわりがない場合は`unitchecker`(デフォルト)を使用すると良いでしょう。 265 | 266 | ### スケルトンコードの種類を変更 267 | 268 | skeletonは`-kind`オプションを指定することで生成するスケルトンコードを変更できます。 269 | 270 | * `-kind=inspect`(デフォルト): `inspect.Analyzer`を用いたコードを作成 271 | * `-kind=ssa`: `buildssa.Analyzer`で生成した静的単一代入(SSA, Static Single Assignment)形式を用いたコードを作成 272 | * `-kind=codegen`: コード生成器を作成 273 | * `-kind=packages`: `x/tools/go/packages`パッケージを用いたコードを作成 274 | 275 | ### コード生成器の作成 276 | 277 | skeletonは`-kind`オプションに`codegen`を指定すると[gostaticanalysis/codegen](https://pkg.go.dev/github.com/gostaticanalysis/codegen)パッケージを用いたコード生成器のスケルトンコードも生成できます。 278 | 279 | ``` 280 | $ skeleton -kind=codegen example.com/mycodegen 281 | mycodegen 282 | ├── cmd 283 | │   └── mycodegen 284 | │   └── main.go 285 | ├── go.mod 286 | ├── mycodegen.go 287 | ├── mycodegen_test.go 288 | └── testdata 289 | └── src 290 | └── a 291 | ├── a.go 292 | ├── go.mod 293 | └── mycodegen.golden 294 | ``` 295 | 296 | `gostaticanalysis/codegen`パッケージは実験的なパッケージです。ご注意ください。 297 | 298 | ### golangci-lintのプラグインを生成する 299 | 300 | skeletonは`-plugin`パッケージを指定すると[golangci-lint](https://github.com/golangci/golangci-lint)からプラグインとして利用できるコードを生成します。 301 | 302 | ``` 303 | $ skeleton -plugin example.com/mylinter 304 | mylinter 305 | ├── cmd 306 | │   └── mylinter 307 | │   └── main.go 308 | ├── go.mod 309 | ├── mylinter.go 310 | ├── mylinter_test.go 311 | ├── plugin 312 | │ └── main.go 313 | └── testdata 314 | └── src 315 | └── a 316 | ├── a.go 317 | └── go.mod 318 | ``` 319 | 320 | ビルド方法は[golangci-lintのドキュメント](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint)にも記載がありますが、生成されたコードの先頭にコメントとして記述されています。 321 | 322 | ``` 323 | $ skeleton -plugin example.com/mylinter 324 | $ go build -buildmode=plugin -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 325 | ``` 326 | 327 | もし、プラグインで特定のフラグを指定したい場合は、ビルドする際に`-ldflags`オプションを指定して設定します。この機能はskeletonで生成したコードのみに提供されます。詳しくは生成されたスケルトンコードをご覧ください。 328 | 329 | ``` 330 | $ skeleton -plugin example.com/mylinter 331 | $ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 332 | ``` 333 | 334 | なお、プラグインは標準の`plugin`パッケージを使用するため、golangci-lintを`CGO_ENABLED=1`でビルドし直す必要があります。また、golangci-lintと生成した静的解析ツールで使用しているモジュールのバージョンを揃えないといけないため、あまりおすすめはしません。 335 | -------------------------------------------------------------------------------- /v2/README_ja.md: -------------------------------------------------------------------------------- 1 | [English Version](./README.md) 2 | 3 | # skeleton 4 | 5 | skeletonはGoの静的解析ツールのためのスケルトンコードジェネレータです。[x/tools/go/analysis](https://golang.org/x/tools/go/analysis)パッケージや[x/tools/go/packages](https://golang.org/x/tools/go/packages)パッケージを用いた静的解析ツールの開発を簡単にします。 6 | 7 | ## x/tools/go/analysisパッケージ 8 | 9 | [x/tools/go/analysis](https://golang.org/x/tools/go/analysis)パッケージは静的解析ツールをモジュール化するためのパッケージです。[analysis.Analyzer](https://golang.org/x/tools/go/analysis)型を1つの単位として扱います。 10 | 11 | `x/tools/go/analysis`パッケージは、静的解析ツールの共通部分を定型化しています。skeletonは定型化されているコードの大部分をスケルトンコードとして生成します。`skeleton mylinter`コマンドを実行するだけで`*analyzer.Analyzer`型の初期化コードやテストコード、`go vet`から実行できる実行可能ファイルを作るための`main.go`を生成してくれます。 12 | 13 | skeletonについて詳しく知りたい場合は、次のブログも参考になります。 14 | 15 | * [skeletonで始めるGoの静的解析](https://engineering.mercari.com/blog/entry/20220406-eea588f493/) 16 | 17 | `x/tools/go/analysis`パッケージやGoの静的解析自体を知りたい場合は、次の資料が参考になります。 18 | 19 | * [プログラミング言語Go完全入門 14章 静的解析とコード生成](http://tenn.in/analysis) 20 | 21 | ## インストール 22 | 23 | ``` 24 | $ go install github.com/gostaticanalysis/skeleton/v2@latest 25 | ``` 26 | 27 | ## 使用方法 28 | 29 | ### モジュールパスを指定して作成 30 | 31 | skeletonの引数にモジュールパスを指定するとそのパスでモジュールを生成します。ディレクトリ名はモジュールパスの最後の要素になります。`example.com/mylinter`と指定すると次のようになります。 32 | 33 | ``` 34 | $ skeleton example.com/mylinter 35 | mylinter 36 | ├── cmd 37 | │   └── mylinter 38 | │   └── main.go 39 | ├── go.mod 40 | ├── mylinter.go 41 | ├── mylinter_test.go 42 | └── testdata 43 | └── src 44 | └── a 45 | ├── a.go 46 | └── go.mod 47 | ``` 48 | 49 | #### 解析器 50 | 51 | `x/tools/go/analysis`パッケージを用いて開発された静的解析ツールは、`*analysis.Analyzer`型の値として表現されます。mylinterの場合、`mylinter.go`に`Analyzer`変数として定義されています。 52 | 53 | 生成されたコードは、`inspect.Analyzer`を用いた簡単な静的解析ツールを実装しています。この静的解析ツールは、`gopher`という名前の識別子を見つけるだけです。 54 | 55 | ```go 56 | package mylinter 57 | 58 | import ( 59 | "go/ast" 60 | 61 | "golang.org/x/tools/go/analysis" 62 | "golang.org/x/tools/go/analysis/passes/inspect" 63 | "golang.org/x/tools/go/ast/inspector" 64 | ) 65 | 66 | const doc = "mylinter is ..." 67 | 68 | // Analyzer is ... 69 | var Analyzer = &analysis.Analyzer{ 70 | Name: "mylinter", 71 | Doc: doc, 72 | Run: run, 73 | Requires: []*analysis.Analyzer{ 74 | inspect.Analyzer, 75 | }, 76 | } 77 | 78 | func run(pass *analysis.Pass) (interface{}, error) { 79 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 80 | 81 | nodeFilter := []ast.Node{ 82 | (*ast.Ident)(nil), 83 | } 84 | 85 | inspect.Preorder(nodeFilter, func(n ast.Node) { 86 | switch n := n.(type) { 87 | case *ast.Ident: 88 | if n.Name == "gopher" { 89 | pass.Reportf(n.Pos(), "identifier is gopher") 90 | } 91 | } 92 | }) 93 | 94 | return nil, nil 95 | } 96 | ``` 97 | 98 | #### テストコード 99 | 100 | skeletonは、テストコードも生成します。`x/tools/go/analysis`パッケージはサブパッケージの`analysistest`パッケージとして、テストライブラリを提供しています。`analysistest.Run`関数は`testdata/src`ディレクトリ以下にあるソースコードを使ってテストを実行します。この関数の第2引数はテストデータのディレクトリです。第3引数はテスト対象のAnalyzer、第4引数以降はテストデータとして利用するパッケージ名です。 101 | 102 | ```go 103 | package mylinter_test 104 | 105 | import ( 106 | "testing" 107 | 108 | "github.com/gostaticanalysis/example.com/mylinter" 109 | "github.com/gostaticanalysis/testutil" 110 | "golang.org/x/tools/go/analysis/analysistest" 111 | ) 112 | 113 | // TestAnalyzer is a test for Analyzer. 114 | func TestAnalyzer(t *testing.T) { 115 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 116 | analysistest.Run(t, testdata, mylinter.Analyzer, "a") 117 | } 118 | ``` 119 | 120 | mylinterの場合、テストは`testdata/src/a/a.go`ファイルをテストデータとして利用します。`mylinter.Analyzer`は`gopher`識別子をソースコードの中から探し報告します。テストでは、期待する報告をコメントで記述します。コメントは`want`で始まり、その後に期待するメッセージが正規表現で記述されます。テストは期待するメッセージで報告がされなかったり、期待していない報告がされた場合に失敗します。 121 | 122 | ```go 123 | package a 124 | 125 | func f() { 126 | // The pattern can be written in regular expression. 127 | var gopher int // want "pattern" 128 | print(gopher) // want "identifier is gopher" 129 | } 130 | ``` 131 | 132 | デフォルトでは`go mod tidy`コマンドと`go test`コマンドを実行すると、テストは失敗します。これは`pattern`というメッセージで作った静的解析ツールが報告をしないためです。 133 | 134 | ``` 135 | $ go mod tidy 136 | go: finding module for package golang.org/x/tools/go/analysis 137 | go: finding module for package github.com/gostaticanalysis/testutil 138 | go: finding module for package golang.org/x/tools/go/analysis/passes/inspect 139 | go: finding module for package golang.org/x/tools/go/analysis/unitchecker 140 | go: finding module for package golang.org/x/tools/go/ast/inspector 141 | go: finding module for package golang.org/x/tools/go/analysis/analysistest 142 | go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10 143 | go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10 144 | go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10 145 | go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10 146 | go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0 147 | go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10 148 | 149 | $ go test 150 | --- FAIL: TestAnalyzer (0.06s) 151 | analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern` 152 | analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern` 153 | FAIL 154 | exit status 1 155 | FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s 156 | ``` 157 | 158 | #### 実行可能ファイル 159 | 160 | skeletonは`cmd`ディレクトリ以下に`main.go`も生成します。この`main.go`をビルドし生成した実行可能ファイルは、`go vet`コマンド経由で実行される必要があります。`go vet`コマンドの`-vettool`オプションは生成した実行可能ファイルへの絶対パスを指定します。 161 | 162 | ``` 163 | $ go vet -vettool=`which mylinter` ./... 164 | ``` 165 | 166 | ### ディレクトリの上書き 167 | 168 | すでにディレクトリが存在する場合は上書きするか聞かれます。 169 | 170 | ``` 171 | $ skeleton example.com/mylinter 172 | mylinter is already exist, overwrite? 173 | [1] No (Exit) 174 | [2] Remove and create new directory 175 | [3] Overwrite existing files with confirmation 176 | [4] Create new files only 177 | ``` 178 | 179 | 選んだ選択肢によって処理が変わります。 180 | 181 | * [1] 上書きしない(終了) 182 | * [2] 削除して新しいディレクトリを作成 183 | * [3] すでにあるファイルを上書きするか都度確認 184 | * [4] 新しいファイルのみ生成する 185 | 186 | ### cmdディレクトリを生成しない 187 | 188 | `-cmd`オプションを`false`にすると`cmd`ディレクトリは生成されません。 189 | 190 | ``` 191 | $ skeleton -cmd=false example.com/mylinter 192 | mylinter 193 | ├── go.mod 194 | ├── mylinter.go 195 | ├── mylinter_test.go 196 | └── testdata 197 | └── src 198 | └── a 199 | ├── a.go 200 | └── go.mod 201 | ``` 202 | 203 | ### go.modファイルを生成しない 204 | 205 | skeletonはデフォルトでは`go.mod`ファイルを生成します。すでにGo Modules管理下にあるディレクトリでスケルトンコードを生成したい場合は、次のように`-gomod`オプションに`false`を指定します。 206 | 207 | ``` 208 | $ skeleton -gomod=false example.com/mylinter 209 | mylinter 210 | ├── cmd 211 | │   └── mylinter 212 | │   └── main.go 213 | ├── mylinter.go 214 | ├── mylinter_test.go 215 | └── testdata 216 | └── src 217 | └── a 218 | ├── a.go 219 | └── go.mod 220 | ``` 221 | 222 | ### SKELETON_PREFIX環境変数 223 | 224 | 次のように`SKELETON_PREFIX`環境変数を指定するとモジュールパスの前にプリフィックスを付与します。 225 | 226 | ``` 227 | $ SKELETON_PREFIX=example.com skeleton mylinter 228 | $ head -1 mylinter/go.mod 229 | module example.com/mylinter 230 | ``` 231 | 232 | 次のように[direnv](https://github.com/direnv/direnv)などを用いて特定のディレクトリ以下でプリフィックスをつけるようにすると便利です。 233 | 234 | ``` 235 | $ cat ~/repos/gostaticanalysis/.envrc 236 | export SKELETON_PREFIX=github.com/gostaticanalysis 237 | ``` 238 | 239 | `SKELETON_PREFIX`環境変数を指定していても、`-gomod`オプションを`false`にした場合は親のモジュールのモジュールパスが使用されます。 240 | 241 | ### singlecheckerまたはmulticheckerの使用 242 | 243 | デフォルトでは`main.go`では`go vet`から実行することを前提とした`unitchecker`パッケージが使われています。`-checker`オプションを指定することで、`singlechecker`パッケージや`multichecker`パッケージに変更できます。 244 | 245 | `singlechecker`パッケージは、単一のAnalyzerを実行するためのパッケージで`go vet`は必要としません。利用するには`-checker=single`を指定します。 246 | 247 | `multichecker`パッケージは、複数のAnalyzerを実行するためのパッケージで`go vet`は必要としません。利用するには`-checker=multi`を指定します。 248 | 249 | 次に`singlechecker`パッケージを利用した例を示します。 250 | 251 | ``` 252 | $ skeleton -checker=single example.com/mylinter 253 | $ cat cmd/mylinter/main.go 254 | package main 255 | 256 | import ( 257 | "mylinter" 258 | "golang.org/x/tools/go/analysis/singlechecker" 259 | ) 260 | 261 | func main() { singlechecker.Main(mylinter.Analyzer) } 262 | ``` 263 | 264 | `singlechecker`パッケージや`multichecker`パッケージを利用した方が簡単そうに見えますが、`go vet`を使った恩恵を受けられないため、特にこだわりがない場合は`unitchecker`(デフォルト)を使用すると良いでしょう。 265 | 266 | ### スケルトンコードの種類を変更 267 | 268 | skeletonは`-kind`オプションを指定することで生成するスケルトンコードを変更できます。 269 | 270 | * `-kind=inspect`(デフォルト): `inspect.Analyzer`を用いたコードを作成 271 | * `-kind=ssa`: `buildssa.Analyzer`で生成した静的単一代入(SSA, Static Single Assignment)形式を用いたコードを作成 272 | * `-kind=codegen`: コード生成器を作成 273 | * `-kind=packages`: `x/tools/go/packages`パッケージを用いたコードを作成 274 | 275 | ### コード生成器の作成 276 | 277 | skeletonは`-kind`オプションに`codegen`を指定すると[gostaticanalysis/codegen](https://pkg.go.dev/github.com/gostaticanalysis/codegen)パッケージを用いたコード生成器のスケルトンコードも生成できます。 278 | 279 | ``` 280 | $ skeleton -kind=codegen example.com/mycodegen 281 | mycodegen 282 | ├── cmd 283 | │   └── mycodegen 284 | │   └── main.go 285 | ├── go.mod 286 | ├── mycodegen.go 287 | ├── mycodegen_test.go 288 | └── testdata 289 | └── src 290 | └── a 291 | ├── a.go 292 | ├── go.mod 293 | └── mycodegen.golden 294 | ``` 295 | 296 | `gostaticanalysis/codegen`パッケージは実験的なパッケージです。ご注意ください。 297 | 298 | ### golangci-lintのプラグインを生成する 299 | 300 | skeletonは`-plugin`パッケージを指定すると[golangci-lint](https://github.com/golangci/golangci-lint)からプラグインとして利用できるコードを生成します。 301 | 302 | ``` 303 | $ skeleton -plugin example.com/mylinter 304 | mylinter 305 | ├── cmd 306 | │   └── mylinter 307 | │   └── main.go 308 | ├── go.mod 309 | ├── mylinter.go 310 | ├── mylinter_test.go 311 | ├── plugin 312 | │ └── main.go 313 | └── testdata 314 | └── src 315 | └── a 316 | ├── a.go 317 | └── go.mod 318 | ``` 319 | 320 | ビルド方法は[golangci-lintのドキュメント](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint)にも記載がありますが、生成されたコードの先頭にコメントとして記述されています。 321 | 322 | ``` 323 | $ skeleton -plugin example.com/mylinter 324 | $ go build -buildmode=plugin -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 325 | ``` 326 | 327 | もし、プラグインで特定のフラグを指定したい場合は、ビルドする際に`-ldflags`オプションを指定して設定します。この機能はskeletonで生成したコードのみに提供されます。詳しくは生成されたスケルトンコードをご覧ください。 328 | 329 | ``` 330 | $ skeleton -plugin example.com/mylinter 331 | $ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 332 | ``` 333 | 334 | なお、プラグインは標準の`plugin`パッケージを使用するため、golangci-lintを`CGO_ENABLED=1`でビルドし直す必要があります。また、golangci-lintと生成した静的解析ツールで使用しているモジュールのバージョンを揃えないといけないため、あまりおすすめはしません。 335 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | [日本語版](./README_ja.md) 2 | 3 | # skeleton 4 | 5 | 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. 6 | 7 | ## x/tools/go/analysis package 8 | 9 | [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. 10 | 11 | `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. 12 | 13 | The following blog helps to learn about the skeleton. 14 | 15 | * [Go static analysis starting with skeleton](https://engineering.mercari.com/blog/entry/20220406-eea588f493/) (Japanese) 16 | 17 | The following slides describes details of Go's static analysis including the `x/tools/go/analysis` package. 18 | 19 | * [A complete introduction of the programming language Go, Chapter 14: Static Analysis and Code Generation](http://tenn.in/analysis) (Japanese) 20 | 21 | ## Installation 22 | 23 | ``` 24 | $ go install github.com/gostaticanalysis/skeleton/v2@latest 25 | ``` 26 | 27 | ## How to use 28 | 29 | ### Create a skeleton code with a module path 30 | 31 | 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. 32 | 33 | When you run skeleton with `example.com/mylinter` as a module path, skeleton generates the following files. 34 | 35 | ``` 36 | $ skeleton example.com/mylinter 37 | mylinter 38 | ├── cmd 39 | │   └── mylinter 40 | │   └── main.go 41 | ├── go.mod 42 | ├── mylinter.go 43 | ├── mylinter_test.go 44 | └── testdata 45 | └── src 46 | └── a 47 | ├── a.go 48 | └── go.mod 49 | ``` 50 | 51 | #### Analyzer 52 | 53 | 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`. 54 | 55 | The generated code provides toy implement with `inspect.Analyzer`. It finds identifiers which name are `gopher`. 56 | 57 | ```go 58 | package mylinter 59 | 60 | import ( 61 | "go/ast" 62 | 63 | "golang.org/x/tools/go/analysis" 64 | "golang.org/x/tools/go/analysis/passes/inspect" 65 | "golang.org/x/tools/go/ast/inspector" 66 | ) 67 | 68 | const doc = "mylinter is ..." 69 | 70 | // Analyzer is ... 71 | var Analyzer = &analysis.Analyzer{ 72 | Name: "mylinter", 73 | Doc: doc, 74 | Run: run, 75 | Requires: []*analysis.Analyzer{ 76 | inspect.Analyzer, 77 | }, 78 | } 79 | 80 | func run(pass *analysis.Pass) (interface{}, error) { 81 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 82 | 83 | nodeFilter := []ast.Node{ 84 | (*ast.Ident)(nil), 85 | } 86 | 87 | inspect.Preorder(nodeFilter, func(n ast.Node) { 88 | switch n := n.(type) { 89 | case *ast.Ident: 90 | if n.Name == "gopher" { 91 | pass.Reportf(n.Pos(), "identifier is gopher") 92 | } 93 | } 94 | }) 95 | 96 | return nil, nil 97 | } 98 | ``` 99 | 100 | #### Test codes 101 | 102 | 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. 103 | 104 | ```go 105 | package mylinter_test 106 | 107 | import ( 108 | "testing" 109 | 110 | "github.com/gostaticanalysis/example.com/mylinter" 111 | "github.com/gostaticanalysis/testutil" 112 | "golang.org/x/tools/go/analysis/analysistest" 113 | ) 114 | 115 | // TestAnalyzer is a test for Analyzer. 116 | func TestAnalyzer(t *testing.T) { 117 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 118 | analysistest.Run(t, testdata, mylinter.Analyzer, "a") 119 | } 120 | ``` 121 | 122 | 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. 123 | 124 | ```go 125 | package a 126 | 127 | func f() { 128 | // The pattern can be written in regular expression. 129 | var gopher int // want "pattern" 130 | print(gopher) // want "identifier is gopher" 131 | } 132 | ``` 133 | 134 | When you run `go mod tidy` and `go test`, the test will be failed because the analyzer does not report a diagnostic with "pattern". 135 | 136 | ``` 137 | $ go mod tidy 138 | go: finding module for package golang.org/x/tools/go/analysis 139 | go: finding module for package github.com/gostaticanalysis/testutil 140 | go: finding module for package golang.org/x/tools/go/analysis/passes/inspect 141 | go: finding module for package golang.org/x/tools/go/analysis/unitchecker 142 | go: finding module for package golang.org/x/tools/go/ast/inspector 143 | go: finding module for package golang.org/x/tools/go/analysis/analysistest 144 | go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10 145 | go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10 146 | go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10 147 | go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10 148 | go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0 149 | go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10 150 | 151 | $ go test 152 | --- FAIL: TestAnalyzer (0.06s) 153 | analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern` 154 | analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern` 155 | FAIL 156 | exit status 1 157 | FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s 158 | ``` 159 | 160 | #### Executable file 161 | 162 | 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. 163 | 164 | ``` 165 | $ go vet -vettool=`which mylinter` ./... 166 | ``` 167 | 168 | ### Overwrite a directory 169 | 170 | If the directory already exists, skeleton gives you with following options. 171 | 172 | ``` 173 | $ skeleton example.com/mylinter 174 | mylinter already exists, overwrite? 175 | [1] No (Exit) 176 | [2] Remove and create new directory 177 | [3] Overwrite existing files with confirmation 178 | [4] Create new files only 179 | ``` 180 | 181 | ### Without cmd directory 182 | 183 | If you don't need `cmd` directory, you can set `false` to `-cmd` flag. 184 | 185 | ``` 186 | $ skeleton -cmd=false example.com/mylinter 187 | mylinter 188 | ├── go.mod 189 | mylinter.go 190 | ├── mylinter_test.go 191 | └─ testdata 192 | └─ testdata 193 | testdata └── src 194 | Go.mod 195 | go.mod 196 | ``` 197 | 198 | ### Without go.mod file 199 | 200 | 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. 201 | 202 | ``` 203 | $ skeleton -gomod=false example.com/mylinter 204 | mylinter 205 | ├── cmd 206 | │└── mylinter 207 | └─ main.go 208 | ├── mylinter.go 209 | mylinter_test.go 210 | └─ testdata 211 | testdata └── src 212 | testdata └─ a 213 | Go.mod 214 | go.mod 215 | ``` 216 | 217 | ### SKELETON_PREFIX environment variable 218 | 219 | When `SKELETON_PREFIX` environment variable is set, skeleton puts it as a prefix to a module path. 220 | 221 | ``` 222 | $ SKELETON_PREFIX=example.com skeleton mylinter 223 | $ head -1 mylinter/go.mod 224 | module example.com/mylinter 225 | ``` 226 | 227 | It is useful with [direnv](https://github.com/direnv/direnv) such as following. 228 | 229 | ``` 230 | $ cat ~/repos/gostaticanalysis/.envrc 231 | export SKELETON_PREFIX=github.com/gostaticanalysis 232 | ``` 233 | 234 | If `SKELETON_PREFIX` environment variable is specified but the `-gomod` flag is `false`, skeleton prioritizes `-gomod` flag. 235 | 236 | ### singlechecker and multichecker 237 | 238 | skeleton uses `unitchecker` package in `main.go` by default. You can change it to `singlechecker` package or `multichecker` package by specifying the `-checker` flag. 239 | 240 | `singlechecker` package runs a single analyzer and `multichecker` package runs multiple analyzers. These packages does not need `go vet` command to run. 241 | 242 | The following is an example of using `singlechecker` package. 243 | 244 | ``` 245 | $ skeleton -checker=single example.com/mylinter 246 | $ cat cmd/mylinter/main.go 247 | package main 248 | 249 | import ( 250 | "mylinter" 251 | "golang.org/x/tools/go/analysis/singlechecker" 252 | ) 253 | 254 | func main() { singlechecker.Main(mylinter.Analyzer) } 255 | ``` 256 | 257 | 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. 258 | 259 | ### Kinds of skeleton code 260 | 261 | skeleton can change kind of skeleton code by using `-kind` flag. 262 | 263 | * `-kind=inspect` (default): using `inspect.Analyzer` 264 | * `-kind=ssa`: using the static single assignment (SSA, Static Single Assignment) form generated by `buildssa.Analyzer` 265 | * `-kind=codegen`: code generator. 266 | * `-kind=packages`: using `x/tools/go/packages` package 267 | 268 | ### Create code generator 269 | 270 | 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. 271 | 272 | ``` 273 | $ skeleton -kind=codegen example.com/mycodegen 274 | mycodegen 275 | ├── cmd 276 | │   └── mycodegen 277 | │   └── main.go 278 | ├── go.mod 279 | ├── mycodegen.go 280 | ├── mycodegen_test.go 281 | └── testdata 282 | └── src 283 | └── a 284 | ├── a.go 285 | ├── go.mod 286 | └── mycodegen.golden 287 | ``` 288 | 289 | `gostaticanalysis/codegen` package is an experimental, please be careful. 290 | 291 | ### golangci-lint plugin 292 | 293 | skeleton generates codes that can be used as a plugin of [golangci-lint](https://github.com/golangci/golangci-lint) by specifying `-plugin` flag. 294 | 295 | ``` 296 | $ skeleton -plugin example.com/mylinter 297 | mylinter 298 | ├── cmd 299 | │   └── mylinter 300 | │   └── main.go 301 | ├── go.mod 302 | ├── mylinter.go 303 | ├── mylinter_test.go 304 | ├── plugin 305 | │ └── main.go 306 | └── testdata 307 | └── src 308 | └── a 309 | ├── a.go 310 | └── go.mod 311 | ``` 312 | 313 | You can see [the documentation](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint). 314 | 315 | ``` 316 | $ skeleton -plugin example.com/mylinter 317 | $ go build -buildmode=plugin -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 318 | ``` 319 | 320 | 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. 321 | 322 | ``` 323 | $ skeleton -plugin example.com/mylinter 324 | $ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 325 | ``` 326 | 327 | 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. 328 | -------------------------------------------------------------------------------- /v2/README.md: -------------------------------------------------------------------------------- 1 | [日本語版](./README_ja.md) 2 | 3 | # skeleton 4 | 5 | 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. 6 | 7 | ## x/tools/go/analysis package 8 | 9 | [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. 10 | 11 | `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. 12 | 13 | The following blog helps to learn about the skeleton. 14 | 15 | * [Go static analysis starting with skeleton](https://engineering.mercari.com/blog/entry/20220406-eea588f493/) (Japanese) 16 | 17 | The following slides describes details of Go's static analysis including the `x/tools/go/analysis` package. 18 | 19 | * [A complete introduction of the programming language Go, Chapter 14: Static Analysis and Code Generation](http://tenn.in/analysis) (Japanese) 20 | 21 | ## Installation 22 | 23 | ``` 24 | $ go install github.com/gostaticanalysis/skeleton/v2@latest 25 | ``` 26 | 27 | ## How to use 28 | 29 | ### Create a skeleton code with a module path 30 | 31 | 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. 32 | 33 | When you run skeleton with `example.com/mylinter` as a module path, skeleton generates the following files. 34 | 35 | ``` 36 | $ skeleton example.com/mylinter 37 | mylinter 38 | ├── cmd 39 | │   └── mylinter 40 | │   └── main.go 41 | ├── go.mod 42 | ├── mylinter.go 43 | ├── mylinter_test.go 44 | └── testdata 45 | └── src 46 | └── a 47 | ├── a.go 48 | └── go.mod 49 | ``` 50 | 51 | #### Analyzer 52 | 53 | 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`. 54 | 55 | The generated code provides toy implement with `inspect.Analyzer`. It finds identifiers which name are `gopher`. 56 | 57 | ```go 58 | package mylinter 59 | 60 | import ( 61 | "go/ast" 62 | 63 | "golang.org/x/tools/go/analysis" 64 | "golang.org/x/tools/go/analysis/passes/inspect" 65 | "golang.org/x/tools/go/ast/inspector" 66 | ) 67 | 68 | const doc = "mylinter is ..." 69 | 70 | // Analyzer is ... 71 | var Analyzer = &analysis.Analyzer{ 72 | Name: "mylinter", 73 | Doc: doc, 74 | Run: run, 75 | Requires: []*analysis.Analyzer{ 76 | inspect.Analyzer, 77 | }, 78 | } 79 | 80 | func run(pass *analysis.Pass) (interface{}, error) { 81 | inspect := pass.ResultOf[inspect.Analyzer].(*inspector.Inspector) 82 | 83 | nodeFilter := []ast.Node{ 84 | (*ast.Ident)(nil), 85 | } 86 | 87 | inspect.Preorder(nodeFilter, func(n ast.Node) { 88 | switch n := n.(type) { 89 | case *ast.Ident: 90 | if n.Name == "gopher" { 91 | pass.Reportf(n.Pos(), "identifier is gopher") 92 | } 93 | } 94 | }) 95 | 96 | return nil, nil 97 | } 98 | ``` 99 | 100 | #### Test codes 101 | 102 | 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. 103 | 104 | ```go 105 | package mylinter_test 106 | 107 | import ( 108 | "testing" 109 | 110 | "github.com/gostaticanalysis/example.com/mylinter" 111 | "github.com/gostaticanalysis/testutil" 112 | "golang.org/x/tools/go/analysis/analysistest" 113 | ) 114 | 115 | // TestAnalyzer is a test for Analyzer. 116 | func TestAnalyzer(t *testing.T) { 117 | testdata := testutil.WithModules(t, analysistest.TestData(), nil) 118 | analysistest.Run(t, testdata, mylinter.Analyzer, "a") 119 | } 120 | ``` 121 | 122 | 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. 123 | 124 | ```go 125 | package a 126 | 127 | func f() { 128 | // The pattern can be written in regular expression. 129 | var gopher int // want "pattern" 130 | print(gopher) // want "identifier is gopher" 131 | } 132 | ``` 133 | 134 | When you run `go mod tidy` and `go test`, the test will be failed because the analyzer does not report a diagnostic with "pattern". 135 | 136 | ``` 137 | $ go mod tidy 138 | go: finding module for package golang.org/x/tools/go/analysis 139 | go: finding module for package github.com/gostaticanalysis/testutil 140 | go: finding module for package golang.org/x/tools/go/analysis/passes/inspect 141 | go: finding module for package golang.org/x/tools/go/analysis/unitchecker 142 | go: finding module for package golang.org/x/tools/go/ast/inspector 143 | go: finding module for package golang.org/x/tools/go/analysis/analysistest 144 | go: found golang.org/x/tools/go/analysis in golang.org/x/tools v0.1.10 145 | go: found golang.org/x/tools/go/analysis/passes/inspect in golang.org/x/tools v0.1.10 146 | go: found golang.org/x/tools/go/ast/inspector in golang.org/x/tools v0.1.10 147 | go: found golang.org/x/tools/go/analysis/unitchecker in golang.org/x/tools v0.1.10 148 | go: found github.com/gostaticanalysis/testutil in github.com/gostaticanalysis/testutil v0.4.0 149 | go: found golang.org/x/tools/go/analysis/analysistest in golang.org/x/tools v0.1.10 150 | 151 | $ go test 152 | --- FAIL: TestAnalyzer (0.06s) 153 | analysistest.go:454: a/a.go:5:6: diagnostic "identifier is gopher" does not match pattern `pattern` 154 | analysistest.go:518: a/a.go:5: no diagnostic was reported matching `pattern` 155 | FAIL 156 | exit status 1 157 | FAIL github.com/gostaticanalysis/example.com/mylinter 1.270s 158 | ``` 159 | 160 | #### Executable file 161 | 162 | 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. 163 | 164 | ``` 165 | $ go vet -vettool=`which mylinter` ./... 166 | ``` 167 | 168 | ### Overwrite a directory 169 | 170 | If the directory already exists, skeleton gives you with following options. 171 | 172 | ``` 173 | $ skeleton example.com/mylinter 174 | mylinter already exists, overwrite? 175 | [1] No (Exit) 176 | [2] Remove and create new directory 177 | [3] Overwrite existing files with confirmation 178 | [4] Create new files only 179 | ``` 180 | 181 | ### Without cmd directory 182 | 183 | If you don't need `cmd` directory, you can set `false` to `-cmd` flag. 184 | 185 | ``` 186 | $ skeleton -cmd=false example.com/mylinter 187 | mylinter 188 | ├── go.mod 189 | mylinter.go 190 | ├── mylinter_test.go 191 | └─ testdata 192 | └─ testdata 193 | testdata └── src 194 | Go.mod 195 | go.mod 196 | ``` 197 | 198 | ### Without go.mod file 199 | 200 | 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. 201 | 202 | ``` 203 | $ skeleton -gomod=false example.com/mylinter 204 | mylinter 205 | ├── cmd 206 | │└── mylinter 207 | └─ main.go 208 | ├── mylinter.go 209 | mylinter_test.go 210 | └─ testdata 211 | testdata └── src 212 | testdata └─ a 213 | Go.mod 214 | go.mod 215 | ``` 216 | 217 | ### SKELETON_PREFIX environment variable 218 | 219 | When `SKELETON_PREFIX` environment variable is set, skeleton puts it as a prefix to a module path. 220 | 221 | ``` 222 | $ SKELETON_PREFIX=example.com skeleton mylinter 223 | $ head -1 mylinter/go.mod 224 | module example.com/mylinter 225 | ``` 226 | 227 | It is useful with [direnv](https://github.com/direnv/direnv) such as following. 228 | 229 | ``` 230 | $ cat ~/repos/gostaticanalysis/.envrc 231 | export SKELETON_PREFIX=github.com/gostaticanalysis 232 | ``` 233 | 234 | If `SKELETON_PREFIX` environment variable is specified but the `-gomod` flag is `false`, skeleton prioritizes `-gomod` flag. 235 | 236 | ### singlechecker and multichecker 237 | 238 | skeleton uses `unitchecker` package in `main.go` by default. You can change it to `singlechecker` package or `multichecker` package by specifying the `-checker` flag. 239 | 240 | `singlechecker` package runs a single analyzer and `multichecker` package runs multiple analyzers. These packages does not need `go vet` command to run. 241 | 242 | The following is an example of using `singlechecker` package. 243 | 244 | ``` 245 | $ skeleton -checker=single example.com/mylinter 246 | $ cat cmd/mylinter/main.go 247 | package main 248 | 249 | import ( 250 | "mylinter" 251 | "golang.org/x/tools/go/analysis/singlechecker" 252 | ) 253 | 254 | func main() { singlechecker.Main(mylinter.Analyzer) } 255 | ``` 256 | 257 | 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. 258 | 259 | ### Kinds of skeleton code 260 | 261 | skeleton can change kind of skeleton code by using `-kind` flag. 262 | 263 | * `-kind=inspect` (default): using `inspect.Analyzer` 264 | * `-kind=ssa`: using the static single assignment (SSA, Static Single Assignment) form generated by `buildssa.Analyzer` 265 | * `-kind=codegen`: code generator. 266 | * `-kind=packages`: using `x/tools/go/packages` package 267 | 268 | ### Create code generator 269 | 270 | 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. 271 | 272 | ``` 273 | $ skeleton -kind=codegen example.com/mycodegen 274 | mycodegen 275 | ├── cmd 276 | │   └── mycodegen 277 | │   └── main.go 278 | ├── go.mod 279 | ├── mycodegen.go 280 | ├── mycodegen_test.go 281 | └── testdata 282 | └── src 283 | └── a 284 | ├── a.go 285 | ├── go.mod 286 | └── mycodegen.golden 287 | ``` 288 | 289 | `gostaticanalysis/codegen` package is an experimental, please be careful. 290 | 291 | ### golangci-lint plugin 292 | 293 | skeleton generates codes that can be used as a plugin of [golangci-lint](https://github.com/golangci/golangci-lint) by specifying `-plugin` flag. 294 | 295 | ``` 296 | $ skeleton -plugin example.com/mylinter 297 | mylinter 298 | ├── cmd 299 | │   └── mylinter 300 | │   └── main.go 301 | ├── go.mod 302 | ├── mylinter.go 303 | ├── mylinter_test.go 304 | ├── plugin 305 | │ └── main.go 306 | └── testdata 307 | └── src 308 | └── a 309 | ├── a.go 310 | └── go.mod 311 | ``` 312 | 313 | You can see [the documentation](https://golangci-lint.run/contributing/new-linters/#how-to-add-a-private-linter-to-golangci-lint). 314 | 315 | ``` 316 | $ skeleton -plugin example.com/mylinter 317 | $ go build -buildmode=plugin -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 318 | ``` 319 | 320 | 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. 321 | 322 | ``` 323 | $ skeleton -plugin example.com/mylinter 324 | $ go build -buildmode=plugin -ldflags "-X 'main.flags=-funcs log.Fatal'" -o path_to_plugin_dir example.com/mylinter/plugin/mylinter 325 | ``` 326 | 327 | 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. 328 | --------------------------------------------------------------------------------