| // Copyright 2023 The Go Authors. All rights reserved. |
| // Use of this source code is governed by a BSD-style |
| // license that can be found in the LICENSE file. |
| |
| // Package directive defines an Analyzer that checks known Go toolchain directives. |
| package directive |
| |
| import ( |
| "go/ast" |
| "go/parser" |
| "go/token" |
| "strings" |
| "unicode" |
| "unicode/utf8" |
| |
| "golang.org/x/tools/go/analysis" |
| "golang.org/x/tools/go/analysis/passes/internal/analysisutil" |
| ) |
| |
| const Doc = `check Go toolchain directives such as //go:debug |
| |
| This analyzer checks for problems with known Go toolchain directives |
| in all Go source files in a package directory, even those excluded by |
| //go:build constraints, and all non-Go source files too. |
| |
| For //go:debug (see https://go.dev/doc/godebug), the analyzer checks |
| that the directives are placed only in Go source files, only above the |
| package comment, and only in package main or *_test.go files. |
| |
| Support for other known directives may be added in the future. |
| |
| This analyzer does not check //go:build, which is handled by the |
| buildtag analyzer. |
| ` |
| |
| var Analyzer = &analysis.Analyzer{ |
| Name: "directive", |
| Doc: Doc, |
| URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/directive", |
| Run: runDirective, |
| } |
| |
| func runDirective(pass *analysis.Pass) (interface{}, error) { |
| for _, f := range pass.Files { |
| checkGoFile(pass, f) |
| } |
| for _, name := range pass.OtherFiles { |
| if err := checkOtherFile(pass, name); err != nil { |
| return nil, err |
| } |
| } |
| for _, name := range pass.IgnoredFiles { |
| if strings.HasSuffix(name, ".go") { |
| f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) |
| if err != nil { |
| // Not valid Go source code - not our job to diagnose, so ignore. |
| continue |
| } |
| checkGoFile(pass, f) |
| } else { |
| if err := checkOtherFile(pass, name); err != nil { |
| return nil, err |
| } |
| } |
| } |
| return nil, nil |
| } |
| |
| func checkGoFile(pass *analysis.Pass, f *ast.File) { |
| check := newChecker(pass, pass.Fset.File(f.Package).Name(), f) |
| |
| for _, group := range f.Comments { |
| // A +build comment is ignored after or adjoining the package declaration. |
| if group.End()+1 >= f.Package { |
| check.inHeader = false |
| } |
| // A //go:build comment is ignored after the package declaration |
| // (but adjoining it is OK, in contrast to +build comments). |
| if group.Pos() >= f.Package { |
| check.inHeader = false |
| } |
| |
| // Check each line of a //-comment. |
| for _, c := range group.List { |
| check.comment(c.Slash, c.Text) |
| } |
| } |
| } |
| |
| func checkOtherFile(pass *analysis.Pass, filename string) error { |
| // We cannot use the Go parser, since is not a Go source file. |
| // Read the raw bytes instead. |
| content, tf, err := analysisutil.ReadFile(pass.Fset, filename) |
| if err != nil { |
| return err |
| } |
| |
| check := newChecker(pass, filename, nil) |
| check.nonGoFile(token.Pos(tf.Base()), string(content)) |
| return nil |
| } |
| |
| type checker struct { |
| pass *analysis.Pass |
| filename string |
| file *ast.File // nil for non-Go file |
| inHeader bool // in file header (before package declaration) |
| inStar bool // currently in a /* */ comment |
| } |
| |
| func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker { |
| return &checker{ |
| pass: pass, |
| filename: filename, |
| file: file, |
| inHeader: true, |
| } |
| } |
| |
| func (check *checker) nonGoFile(pos token.Pos, fullText string) { |
| // Process each line. |
| text := fullText |
| inStar := false |
| for text != "" { |
| offset := len(fullText) - len(text) |
| var line string |
| line, text, _ = stringsCut(text, "\n") |
| |
| if !inStar && strings.HasPrefix(line, "//") { |
| check.comment(pos+token.Pos(offset), line) |
| continue |
| } |
| |
| // Skip over, cut out any /* */ comments, |
| // to avoid being confused by a commented-out // comment. |
| for { |
| line = strings.TrimSpace(line) |
| if inStar { |
| var ok bool |
| _, line, ok = stringsCut(line, "*/") |
| if !ok { |
| break |
| } |
| inStar = false |
| continue |
| } |
| line, inStar = stringsCutPrefix(line, "/*") |
| if !inStar { |
| break |
| } |
| } |
| if line != "" { |
| // Found non-comment non-blank line. |
| // Ends space for valid //go:build comments, |
| // but also ends the fraction of the file we can |
| // reliably parse. From this point on we might |
| // incorrectly flag "comments" inside multiline |
| // string constants or anything else (this might |
| // not even be a Go program). So stop. |
| break |
| } |
| } |
| } |
| |
| func (check *checker) comment(pos token.Pos, line string) { |
| if !strings.HasPrefix(line, "//go:") { |
| return |
| } |
| // testing hack: stop at // ERROR |
| if i := strings.Index(line, " // ERROR "); i >= 0 { |
| line = line[:i] |
| } |
| |
| verb := line |
| if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 { |
| verb = verb[:i] |
| if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' { |
| r, _ := utf8.DecodeRuneInString(line[i:]) |
| check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb) |
| } |
| } |
| |
| switch verb { |
| default: |
| // TODO: Use the go language version for the file. |
| // If that version is not newer than us, then we can |
| // report unknown directives. |
| |
| case "//go:build": |
| // Ignore. The buildtag analyzer reports misplaced comments. |
| |
| case "//go:debug": |
| if check.file == nil { |
| check.pass.Reportf(pos, "//go:debug directive only valid in Go source files") |
| } else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") { |
| check.pass.Reportf(pos, "//go:debug directive only valid in package main or test") |
| } else if !check.inHeader { |
| check.pass.Reportf(pos, "//go:debug directive only valid before package declaration") |
| } |
| } |
| } |
| |
| // Go 1.18 strings.Cut. |
| func stringsCut(s, sep string) (before, after string, found bool) { |
| if i := strings.Index(s, sep); i >= 0 { |
| return s[:i], s[i+len(sep):], true |
| } |
| return s, "", false |
| } |
| |
| // Go 1.20 strings.CutPrefix. |
| func stringsCutPrefix(s, prefix string) (after string, found bool) { |
| if !strings.HasPrefix(s, prefix) { |
| return s, false |
| } |
| return s[len(prefix):], true |
| } |