diff --git a/analysis/facts/generated/generated.go b/analysis/facts/generated/generated.go index 301ad0e3..b665582a 100644 --- a/analysis/facts/generated/generated.go +++ b/analysis/facts/generated/generated.go @@ -1,10 +1,9 @@ package generated import ( - "bufio" "bytes" - "io" - "os" + "go/parser" + "go/token" "reflect" "strings" @@ -27,52 +26,45 @@ var ( oldCgo = []byte("// Created by cgo - DO NOT EDIT") prefix = []byte("// Code generated ") suffix = []byte(" DO NOT EDIT.") - nl = []byte("\n") - crnl = []byte("\r\n") ) func isGenerated(path string) (Generator, bool) { - f, err := os.Open(path) + f, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.ParseComments|parser.PackageClauseOnly|parser.SkipObjectResolution) if err != nil { return 0, false } - defer f.Close() - br := bufio.NewReader(f) - for { - s, err := br.ReadBytes('\n') - if err != nil && err != io.EOF { - return 0, false - } - s = bytes.TrimSuffix(s, crnl) - s = bytes.TrimSuffix(s, nl) - if bytes.HasPrefix(s, prefix) && bytes.HasSuffix(s, suffix) { - if len(s)-len(suffix) < len(prefix) { - return Unknown, true + for _, group := range f.Comments { + for _, comment := range group.List { + if comment.Pos() >= f.Package { + continue } + s := []byte(comment.Text) + if bytes.HasPrefix(s, prefix) && bytes.HasSuffix(s, suffix) { + if len(s)-len(suffix) < len(prefix) { + return Unknown, true + } - text := string(s[len(prefix) : len(s)-len(suffix)]) - switch text { - case "by goyacc.": - return Goyacc, true - case "by cmd/cgo;": - return Cgo, true - case "by protoc-gen-go.": - return ProtocGenGo, true - } - if strings.HasPrefix(text, `by "stringer `) { - return Stringer, true + text := string(s[len(prefix) : len(s)-len(suffix)]) + switch text { + case "by goyacc.": + return Goyacc, true + case "by cmd/cgo;": + return Cgo, true + case "by protoc-gen-go.": + return ProtocGenGo, true + } + if strings.HasPrefix(text, `by "stringer `) { + return Stringer, true + } + if strings.HasPrefix(text, `by goyacc `) { + return Goyacc, true + } + + return Unknown, true } - if strings.HasPrefix(text, `by goyacc `) { - return Goyacc, true + if bytes.Equal(s, oldCgo) { + return Cgo, true } - - return Unknown, true - } - if bytes.Equal(s, oldCgo) { - return Cgo, true - } - if err == io.EOF { - break } } return 0, false diff --git a/analysis/facts/generated/generated_test.go b/analysis/facts/generated/generated_test.go new file mode 100644 index 00000000..d7adfa36 --- /dev/null +++ b/analysis/facts/generated/generated_test.go @@ -0,0 +1,65 @@ +package generated + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +func TestIsGeneratedOnlyChecksFileHeader(t *testing.T) { + tests := []struct { + name string + text string + want bool + }{ + { + name: "long license header", + text: strings.Repeat("// license\n", 20) + "// Code generated by protoc-gen-go. DO NOT EDIT.\npackage p\n", + want: true, + }, + { + name: "indented package clause", + text: " package p\n// Code generated by protoc-gen-go. DO NOT EDIT.\n", + want: false, + }, + { + name: "marker inside block comment", + text: "/*\n// Code generated by protoc-gen-go. DO NOT EDIT.\n*/\npackage p\n", + want: false, + }, + { + name: "old cgo header", + text: "// Created by cgo - DO NOT EDIT\npackage p\n", + want: true, + }, + { + name: "header", + text: "// Code generated by protoc-gen-go. DO NOT EDIT.\n\npackage p\n", + want: true, + }, + { + name: "body comment", + text: "package p\n\nfunc f() {\n\t// Code generated by protoc-gen-go. DO NOT EDIT.\n}\n", + want: false, + }, + { + name: "late comment", + text: "package p\n\n// Code generated by protoc-gen-go. DO NOT EDIT.\n", + want: false, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + path := filepath.Join(t.TempDir(), "generated.go") + if err := os.WriteFile(path, []byte(tt.text), 0o600); err != nil { + t.Fatal(err) + } + _, got := isGenerated(path) + if got != tt.want { + t.Fatalf("isGenerated() = %v, want %v", got, tt.want) + } + }) + } +}