Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,9 @@ Run `constable` on your Go packages,
constable ./...
```

constable skips files that carry the standard generated-code comment, `// Code generated ... DO NOT EDIT.`.
A diagnostic in such a file is not actionable because the next run of the generator overwrites the file.

## Example

This function is reported because it mutates through a pointer parameter,
Expand Down
5 changes: 5 additions & 0 deletions internal/analysis/complexity/complexity.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

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

"github.com/nnutter/constable/internal/generated"
"github.com/nnutter/constable/internal/report"
)

Expand Down Expand Up @@ -41,6 +42,10 @@ func isLogicalOr(op token.Token) bool {

func run(pass *analysis.Pass) (any, error) {
for _, file := range pass.Files {
if generated.IsGenerated(file) {
continue
}

for _, decl := range file.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !isFuncDecl(ok) || isNilBody(funcDecl.Body) {
Expand Down
40 changes: 40 additions & 0 deletions internal/analysis/complexity/testdata/src/c/zz_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

4 changes: 3 additions & 1 deletion internal/analysis/methodical/fix.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ import (
"strings"

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

"github.com/nnutter/constable/internal/generated"
)

type diagnosticReporter struct {
Expand Down Expand Up @@ -82,7 +84,7 @@ func readFixFile(pass *analysis.Pass, file *ast.File) *fixFile {
return nil
}
src, err := pass.ReadFile(tf.Name())
if err != nil || ast.IsGenerated(file) || hasStandaloneComments(pass.Fset, file) {
if err != nil || generated.IsGenerated(file) || hasStandaloneComments(pass.Fset, file) {
return nil
}
base := strings.TrimSuffix(filepath.Base(tf.Name()), ".go")
Expand Down
13 changes: 13 additions & 0 deletions internal/analysis/methodical/methodical.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

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

"github.com/nnutter/constable/internal/generated"
"github.com/nnutter/constable/internal/report"
)

Expand Down Expand Up @@ -63,6 +64,10 @@ func isNilVar(v *types.Var) bool {
func collectTypeFiles(pass *analysis.Pass) map[types.Object]string {
typeFiles := make(map[types.Object]string)
for _, file := range pass.Files {
if generated.IsGenerated(file) {
continue
}

for _, decl := range file.Decls {
genDecl, ok := decl.(*ast.GenDecl)
if !ok {
Expand Down Expand Up @@ -102,6 +107,10 @@ type groupKey struct {
func collectMethods(pass *analysis.Pass, typeFiles map[types.Object]string) []methodInfo {
var methods []methodInfo
for _, file := range pass.Files {
if generated.IsGenerated(file) {
continue
}

for _, decl := range file.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !succeeded(ok) || isNilRecv(funcDecl.Recv) {
Expand Down Expand Up @@ -233,6 +242,10 @@ func checkGrouped(reporter *diagnosticReporter, typeFiles map[types.Object]strin
}

func checkFileGrouped(reporter *diagnosticReporter, file *ast.File, typeFiles map[types.Object]string) {
if generated.IsGenerated(file) {
return
}

states := make(map[types.Object]*groupState)
for _, decl := range file.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
Expand Down
4 changes: 4 additions & 0 deletions internal/analysis/methodical/methodical_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,7 @@ func TestAnalyzer(t *testing.T) {
func TestSuggestedFixes(t *testing.T) {
analysistest.RunWithSuggestedFixes(t, analysistest.TestData(), methodical.Analyzer, "fix", "generic")
}

func TestAnalyzerSkipsGeneratedFiles(t *testing.T) {
analysistest.Run(t, analysistest.TestData(), methodical.Analyzer, "g")
}
6 changes: 6 additions & 0 deletions internal/analysis/methodical/testdata/src/g/g.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
package g

// Hand is a type that a person owns in this file.
type Hand struct{}

func (h Hand) A() {}
7 changes: 7 additions & 0 deletions internal/analysis/methodical/testdata/src/g/handwritten.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package g

// Methods on a generated type cannot move next to the type, so the analyzer
// leaves them alone even though they are in a handwritten file.
func (g Gen) D() {}

func (g Gen) C() {}
18 changes: 18 additions & 0 deletions internal/analysis/methodical/testdata/src/g/zz_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions internal/analysis/nonmutating/nonmutating.go
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ import (

"github.com/nnutter/constable/internal/directive"
"github.com/nnutter/constable/internal/directive/enum"
"github.com/nnutter/constable/internal/generated"
"github.com/nnutter/constable/internal/report"
)

Expand Down Expand Up @@ -59,6 +60,10 @@ func hasNoArgs(call *ast.CallExpr) bool {

func run(pass *analysis.Pass) (any, error) {
for _, file := range pass.Files {
if generated.IsGenerated(file) {
continue
}

for _, decl := range file.Decls {
funcDecl, ok := decl.(*ast.FuncDecl)
if !succeeded(ok) || !hasNonmutatingDirective(funcDecl) || isNilBody(funcDecl.Body) {
Expand Down
11 changes: 11 additions & 0 deletions internal/analysis/nonmutating/testdata/src/a/zz_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions internal/analysis/predicates/predicates.go
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ import (

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

"github.com/nnutter/constable/internal/generated"
"github.com/nnutter/constable/internal/report"
)

Expand Down Expand Up @@ -54,6 +55,10 @@ func isBoolName(ident *ast.Ident) bool {

func run(pass *analysis.Pass) (any, error) {
for _, file := range pass.Files {
if generated.IsGenerated(file) {
continue
}

ast.Inspect(file, func(node ast.Node) bool {
expr, ok := node.(*ast.BinaryExpr)
if !succeeded(ok) || !isLogicalExpr(expr) {
Expand Down
9 changes: 9 additions & 0 deletions internal/analysis/predicates/testdata/src/p/zz_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

11 changes: 11 additions & 0 deletions internal/analysis/testify/testdata/src/t/zz_generated.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

5 changes: 5 additions & 0 deletions internal/analysis/testify/testify.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

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

"github.com/nnutter/constable/internal/generated"
"github.com/nnutter/constable/internal/report"
)

Expand Down Expand Up @@ -46,6 +47,10 @@ var testifyPackages = map[string]string{

func run(pass *analysis.Pass) (any, error) {
for _, file := range pass.Files {
if generated.IsGenerated(file) {
continue
}

ast.Inspect(file, func(node ast.Node) bool {
call, ok := node.(*ast.CallExpr)
if !ok {
Expand Down
15 changes: 15 additions & 0 deletions internal/generated/generated.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
// Package generated recognizes Go files that a tool writes and that a person
// must not edit.
package generated

import "go/ast"

// IsGenerated reports whether file carries the standard generated-code
// comment, "// Code generated <tool> DO NOT EDIT.".
//
// Analyzers skip generated files. A diagnostic there is not actionable
// because the next run of the generating tool overwrites the file. The check
// is the Go convention, so an author controls it with the comment alone.
func IsGenerated(file *ast.File) bool {
return ast.IsGenerated(file)
}
68 changes: 68 additions & 0 deletions internal/generated/generated_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,68 @@
package generated_test

import (
"go/ast"
"go/parser"
"go/token"
"testing"

"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/nnutter/constable/internal/generated"
)

func parse(t *testing.T, src string) *ast.File {
t.Helper()

fset := token.NewFileSet()
file, err := parser.ParseFile(fset, "src.go", src, parser.ParseComments)
require.NoError(t, err)

return file
}

func TestIsGenerated(t *testing.T) {
tests := []struct {
name string
src string
want bool
}{
{
name: "standard comment before package clause",
src: "// Code generated by mockgen. DO NOT EDIT.\n\npackage p\n",
want: true,
},
{
name: "comment in a block with other comments",
src: "// Package doc.\n\n// Code generated by stringer. DO NOT EDIT.\n\npackage p\n",
want: true,
},
{
name: "no comment",
src: "package p\n",
want: false,
},
{
name: "comment without the marker",
src: "// Code generated by hand.\n\npackage p\n",
want: false,
},
{
name: "marker without the trailing period",
src: "// Code generated by hand. DO NOT EDIT\n\npackage p\n",
want: false,
},
{
name: "marker after the package clause",
src: "package p\n\n// Code generated by hand. DO NOT EDIT.\n",
want: false,
},
}

for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
assert.Equal(t, test.want, generated.IsGenerated(parse(t, test.src)))
})
}
}
3 changes: 3 additions & 0 deletions skills/constable/SKILL.md
Original file line number Diff line number Diff line change
Expand Up @@ -152,4 +152,7 @@ It does not by itself guarantee that a function has no I/O, global state changes
Calls to helper functions, interface methods, function values, and other indirect mutation paths may not be modeled.
The analyzer currently recognizes only the `nonmutating` directive.

The analyzers skip files that carry the standard generated-code comment, `// Code generated ... DO NOT EDIT.`.
Edit such a file through its generator, not by hand.

When behavior is uncertain, inspect the diagnostic and the implementation rather than assuming that a clean run proves complete immutability.
Loading