github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/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 Run: runDirective, 40 } 41 42 func runDirective(pass *analysis.Pass) (interface{}, error) { 43 for _, f := range pass.Files { 44 checkGoFile(pass, f) 45 } 46 for _, name := range pass.OtherFiles { 47 if err := checkOtherFile(pass, name); err != nil { 48 return nil, err 49 } 50 } 51 for _, name := range pass.IgnoredFiles { 52 if strings.HasSuffix(name, ".go") { 53 f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) 54 if err != nil { 55 // Not valid Go source code - not our job to diagnose, so ignore. 56 continue 57 } 58 checkGoFile(pass, f) 59 } else { 60 if err := checkOtherFile(pass, name); err != nil { 61 return nil, err 62 } 63 } 64 } 65 return nil, nil 66 } 67 68 func checkGoFile(pass *analysis.Pass, f *ast.File) { 69 check := newChecker(pass, pass.Fset.File(f.Package).Name(), f) 70 71 for _, group := range f.Comments { 72 // A +build comment is ignored after or adjoining the package declaration. 73 if group.End()+1 >= f.Package { 74 check.inHeader = false 75 } 76 // A //go:build comment is ignored after the package declaration 77 // (but adjoining it is OK, in contrast to +build comments). 78 if group.Pos() >= f.Package { 79 check.inHeader = false 80 } 81 82 // Check each line of a //-comment. 83 for _, c := range group.List { 84 check.comment(c.Slash, c.Text) 85 } 86 } 87 } 88 89 func checkOtherFile(pass *analysis.Pass, filename string) error { 90 // We cannot use the Go parser, since is not a Go source file. 91 // Read the raw bytes instead. 92 content, tf, err := analysisutil.ReadFile(pass.Fset, filename) 93 if err != nil { 94 return err 95 } 96 97 check := newChecker(pass, filename, nil) 98 check.nonGoFile(token.Pos(tf.Base()), string(content)) 99 return nil 100 } 101 102 type checker struct { 103 pass *analysis.Pass 104 filename string 105 file *ast.File // nil for non-Go file 106 inHeader bool // in file header (before package declaration) 107 inStar bool // currently in a /* */ comment 108 } 109 110 func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker { 111 return &checker{ 112 pass: pass, 113 filename: filename, 114 file: file, 115 inHeader: true, 116 } 117 } 118 119 func (check *checker) nonGoFile(pos token.Pos, fullText string) { 120 // Process each line. 121 text := fullText 122 inStar := false 123 for text != "" { 124 offset := len(fullText) - len(text) 125 var line string 126 line, text, _ = stringsCut(text, "\n") 127 128 if !inStar && strings.HasPrefix(line, "//") { 129 check.comment(pos+token.Pos(offset), line) 130 continue 131 } 132 133 // Skip over, cut out any /* */ comments, 134 // to avoid being confused by a commented-out // comment. 135 for { 136 line = strings.TrimSpace(line) 137 if inStar { 138 var ok bool 139 _, line, ok = stringsCut(line, "*/") 140 if !ok { 141 break 142 } 143 inStar = false 144 continue 145 } 146 line, inStar = stringsCutPrefix(line, "/*") 147 if !inStar { 148 break 149 } 150 } 151 if line != "" { 152 // Found non-comment non-blank line. 153 // Ends space for valid //go:build comments, 154 // but also ends the fraction of the file we can 155 // reliably parse. From this point on we might 156 // incorrectly flag "comments" inside multiline 157 // string constants or anything else (this might 158 // not even be a Go program). So stop. 159 break 160 } 161 } 162 } 163 164 func (check *checker) comment(pos token.Pos, line string) { 165 if !strings.HasPrefix(line, "//go:") { 166 return 167 } 168 // testing hack: stop at // ERROR 169 if i := strings.Index(line, " // ERROR "); i >= 0 { 170 line = line[:i] 171 } 172 173 verb := line 174 if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 { 175 verb = verb[:i] 176 if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' { 177 r, _ := utf8.DecodeRuneInString(line[i:]) 178 check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb) 179 } 180 } 181 182 switch verb { 183 default: 184 // TODO: Use the go language version for the file. 185 // If that version is not newer than us, then we can 186 // report unknown directives. 187 188 case "//go:build": 189 // Ignore. The buildtag analyzer reports misplaced comments. 190 191 case "//go:debug": 192 if check.file == nil { 193 check.pass.Reportf(pos, "//go:debug directive only valid in Go source files") 194 } else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") { 195 check.pass.Reportf(pos, "//go:debug directive only valid in package main or test") 196 } else if !check.inHeader { 197 check.pass.Reportf(pos, "//go:debug directive only valid before package declaration") 198 } 199 } 200 } 201 202 // Go 1.18 strings.Cut. 203 func stringsCut(s, sep string) (before, after string, found bool) { 204 if i := strings.Index(s, sep); i >= 0 { 205 return s[:i], s[i+len(sep):], true 206 } 207 return s, "", false 208 } 209 210 // Go 1.20 strings.CutPrefix. 211 func stringsCutPrefix(s, prefix string) (after string, found bool) { 212 if !strings.HasPrefix(s, prefix) { 213 return s, false 214 } 215 return s[len(prefix):], true 216 }