1// Copyright 2023 The Go Authors. All rights reserved. 2// Use of this source code is governed by a BSD-style 3// license that can be found in the LICENSE file. 4 5// Package directive defines an Analyzer that checks known Go toolchain directives. 6package directive 7 8import ( 9 "go/ast" 10 "go/parser" 11 "go/token" 12 "strings" 13 "unicode" 14 "unicode/utf8" 15 16 "golang.org/x/tools/go/analysis" 17 "golang.org/x/tools/go/analysis/passes/internal/analysisutil" 18) 19 20const Doc = `check Go toolchain directives such as //go:debug 21 22This analyzer checks for problems with known Go toolchain directives 23in all Go source files in a package directory, even those excluded by 24//go:build constraints, and all non-Go source files too. 25 26For //go:debug (see https://go.dev/doc/godebug), the analyzer checks 27that the directives are placed only in Go source files, only above the 28package comment, and only in package main or *_test.go files. 29 30Support for other known directives may be added in the future. 31 32This analyzer does not check //go:build, which is handled by the 33buildtag analyzer. 34` 35 36var Analyzer = &analysis.Analyzer{ 37 Name: "directive", 38 Doc: Doc, 39 URL: "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/directive", 40 Run: runDirective, 41} 42 43func runDirective(pass *analysis.Pass) (interface{}, error) { 44 for _, f := range pass.Files { 45 checkGoFile(pass, f) 46 } 47 for _, name := range pass.OtherFiles { 48 if err := checkOtherFile(pass, name); err != nil { 49 return nil, err 50 } 51 } 52 for _, name := range pass.IgnoredFiles { 53 if strings.HasSuffix(name, ".go") { 54 f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) 55 if err != nil { 56 // Not valid Go source code - not our job to diagnose, so ignore. 57 continue 58 } 59 checkGoFile(pass, f) 60 } else { 61 if err := checkOtherFile(pass, name); err != nil { 62 return nil, err 63 } 64 } 65 } 66 return nil, nil 67} 68 69func checkGoFile(pass *analysis.Pass, f *ast.File) { 70 check := newChecker(pass, pass.Fset.File(f.Package).Name(), f) 71 72 for _, group := range f.Comments { 73 // A //go:build or a //go:debug comment is ignored after the package declaration 74 // (but adjoining it is OK, in contrast to +build comments). 75 if group.Pos() >= f.Package { 76 check.inHeader = false 77 } 78 79 // Check each line of a //-comment. 80 for _, c := range group.List { 81 check.comment(c.Slash, c.Text) 82 } 83 } 84} 85 86func checkOtherFile(pass *analysis.Pass, filename string) error { 87 // We cannot use the Go parser, since is not a Go source file. 88 // Read the raw bytes instead. 89 content, tf, err := analysisutil.ReadFile(pass, filename) 90 if err != nil { 91 return err 92 } 93 94 check := newChecker(pass, filename, nil) 95 check.nonGoFile(token.Pos(tf.Base()), string(content)) 96 return nil 97} 98 99type checker struct { 100 pass *analysis.Pass 101 filename string 102 file *ast.File // nil for non-Go file 103 inHeader bool // in file header (before or adjoining package declaration) 104} 105 106func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker { 107 return &checker{ 108 pass: pass, 109 filename: filename, 110 file: file, 111 inHeader: true, 112 } 113} 114 115func (check *checker) nonGoFile(pos token.Pos, fullText string) { 116 // Process each line. 117 text := fullText 118 inStar := false 119 for text != "" { 120 offset := len(fullText) - len(text) 121 var line string 122 line, text, _ = strings.Cut(text, "\n") 123 124 if !inStar && strings.HasPrefix(line, "//") { 125 check.comment(pos+token.Pos(offset), line) 126 continue 127 } 128 129 // Skip over, cut out any /* */ comments, 130 // to avoid being confused by a commented-out // comment. 131 for { 132 line = strings.TrimSpace(line) 133 if inStar { 134 var ok bool 135 _, line, ok = strings.Cut(line, "*/") 136 if !ok { 137 break 138 } 139 inStar = false 140 continue 141 } 142 line, inStar = stringsCutPrefix(line, "/*") 143 if !inStar { 144 break 145 } 146 } 147 if line != "" { 148 // Found non-comment non-blank line. 149 // Ends space for valid //go:build comments, 150 // but also ends the fraction of the file we can 151 // reliably parse. From this point on we might 152 // incorrectly flag "comments" inside multiline 153 // string constants or anything else (this might 154 // not even be a Go program). So stop. 155 break 156 } 157 } 158} 159 160func (check *checker) comment(pos token.Pos, line string) { 161 if !strings.HasPrefix(line, "//go:") { 162 return 163 } 164 // testing hack: stop at // ERROR 165 if i := strings.Index(line, " // ERROR "); i >= 0 { 166 line = line[:i] 167 } 168 169 verb := line 170 if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 { 171 verb = verb[:i] 172 if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' { 173 r, _ := utf8.DecodeRuneInString(line[i:]) 174 check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb) 175 } 176 } 177 178 switch verb { 179 default: 180 // TODO: Use the go language version for the file. 181 // If that version is not newer than us, then we can 182 // report unknown directives. 183 184 case "//go:build": 185 // Ignore. The buildtag analyzer reports misplaced comments. 186 187 case "//go:debug": 188 if check.file == nil { 189 check.pass.Reportf(pos, "//go:debug directive only valid in Go source files") 190 } else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") { 191 check.pass.Reportf(pos, "//go:debug directive only valid in package main or test") 192 } else if !check.inHeader { 193 check.pass.Reportf(pos, "//go:debug directive only valid before package declaration") 194 } 195 } 196} 197 198// Go 1.20 strings.CutPrefix. 199func stringsCutPrefix(s, prefix string) (after string, found bool) { 200 if !strings.HasPrefix(s, prefix) { 201 return s, false 202 } 203 return s[len(prefix):], true 204} 205