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