github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/go/analysis/passes/buildtag/buildtag.go (about) 1 // Copyright 2013 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 //go:build go1.16 6 // +build go1.16 7 8 // Package buildtag defines an Analyzer that checks build tags. 9 package buildtag 10 11 import ( 12 "go/ast" 13 "go/build/constraint" 14 "go/parser" 15 "go/token" 16 "strings" 17 "unicode" 18 19 "golang.org/x/tools/go/analysis" 20 "golang.org/x/tools/go/analysis/passes/internal/analysisutil" 21 ) 22 23 const Doc = "check //go:build and // +build directives" 24 25 var Analyzer = &analysis.Analyzer{ 26 Name: "buildtag", 27 Doc: Doc, 28 Run: runBuildTag, 29 } 30 31 func runBuildTag(pass *analysis.Pass) (interface{}, error) { 32 for _, f := range pass.Files { 33 checkGoFile(pass, f) 34 } 35 for _, name := range pass.OtherFiles { 36 if err := checkOtherFile(pass, name); err != nil { 37 return nil, err 38 } 39 } 40 for _, name := range pass.IgnoredFiles { 41 if strings.HasSuffix(name, ".go") { 42 f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) 43 if err != nil { 44 // Not valid Go source code - not our job to diagnose, so ignore. 45 return nil, nil 46 } 47 checkGoFile(pass, f) 48 } else { 49 if err := checkOtherFile(pass, name); err != nil { 50 return nil, err 51 } 52 } 53 } 54 return nil, nil 55 } 56 57 func checkGoFile(pass *analysis.Pass, f *ast.File) { 58 var check checker 59 check.init(pass) 60 defer check.finish() 61 62 for _, group := range f.Comments { 63 // A +build comment is ignored after or adjoining the package declaration. 64 if group.End()+1 >= f.Package { 65 check.plusBuildOK = false 66 } 67 // A //go:build comment is ignored after the package declaration 68 // (but adjoining it is OK, in contrast to +build comments). 69 if group.Pos() >= f.Package { 70 check.goBuildOK = false 71 } 72 73 // Check each line of a //-comment. 74 for _, c := range group.List { 75 // "+build" is ignored within or after a /*...*/ comment. 76 if !strings.HasPrefix(c.Text, "//") { 77 check.plusBuildOK = false 78 } 79 check.comment(c.Slash, c.Text) 80 } 81 } 82 } 83 84 func checkOtherFile(pass *analysis.Pass, filename string) error { 85 var check checker 86 check.init(pass) 87 defer check.finish() 88 89 // We cannot use the Go parser, since this may not be a Go source file. 90 // Read the raw bytes instead. 91 content, tf, err := analysisutil.ReadFile(pass.Fset, filename) 92 if err != nil { 93 return err 94 } 95 96 check.file(token.Pos(tf.Base()), string(content)) 97 return nil 98 } 99 100 type checker struct { 101 pass *analysis.Pass 102 plusBuildOK bool // "+build" lines still OK 103 goBuildOK bool // "go:build" lines still OK 104 crossCheck bool // cross-check go:build and +build lines when done reading file 105 inStar bool // currently in a /* */ comment 106 goBuildPos token.Pos // position of first go:build line found 107 plusBuildPos token.Pos // position of first "+build" line found 108 goBuild constraint.Expr // go:build constraint found 109 plusBuild constraint.Expr // AND of +build constraints found 110 } 111 112 func (check *checker) init(pass *analysis.Pass) { 113 check.pass = pass 114 check.goBuildOK = true 115 check.plusBuildOK = true 116 check.crossCheck = true 117 } 118 119 func (check *checker) file(pos token.Pos, text string) { 120 // Determine cutpoint where +build comments are no longer valid. 121 // They are valid in leading // comments in the file followed by 122 // a blank line. 123 // 124 // This must be done as a separate pass because of the 125 // requirement that the comment be followed by a blank line. 126 var plusBuildCutoff int 127 fullText := text 128 for text != "" { 129 i := strings.Index(text, "\n") 130 if i < 0 { 131 i = len(text) 132 } else { 133 i++ 134 } 135 offset := len(fullText) - len(text) 136 line := text[:i] 137 text = text[i:] 138 line = strings.TrimSpace(line) 139 if !strings.HasPrefix(line, "//") && line != "" { 140 break 141 } 142 if line == "" { 143 plusBuildCutoff = offset 144 } 145 } 146 147 // Process each line. 148 // Must stop once we hit goBuildOK == false 149 text = fullText 150 check.inStar = false 151 for text != "" { 152 i := strings.Index(text, "\n") 153 if i < 0 { 154 i = len(text) 155 } else { 156 i++ 157 } 158 offset := len(fullText) - len(text) 159 line := text[:i] 160 text = text[i:] 161 check.plusBuildOK = offset < plusBuildCutoff 162 163 if strings.HasPrefix(line, "//") { 164 check.comment(pos+token.Pos(offset), line) 165 continue 166 } 167 168 // Keep looking for the point at which //go:build comments 169 // stop being allowed. Skip over, cut out any /* */ comments. 170 for { 171 line = strings.TrimSpace(line) 172 if check.inStar { 173 i := strings.Index(line, "*/") 174 if i < 0 { 175 line = "" 176 break 177 } 178 line = line[i+len("*/"):] 179 check.inStar = false 180 continue 181 } 182 if strings.HasPrefix(line, "/*") { 183 check.inStar = true 184 line = line[len("/*"):] 185 continue 186 } 187 break 188 } 189 if line != "" { 190 // Found non-comment non-blank line. 191 // Ends space for valid //go:build comments, 192 // but also ends the fraction of the file we can 193 // reliably parse. From this point on we might 194 // incorrectly flag "comments" inside multiline 195 // string constants or anything else (this might 196 // not even be a Go program). So stop. 197 break 198 } 199 } 200 } 201 202 func (check *checker) comment(pos token.Pos, text string) { 203 if strings.HasPrefix(text, "//") { 204 if strings.Contains(text, "+build") { 205 check.plusBuildLine(pos, text) 206 } 207 if strings.Contains(text, "//go:build") { 208 check.goBuildLine(pos, text) 209 } 210 } 211 if strings.HasPrefix(text, "/*") { 212 if i := strings.Index(text, "\n"); i >= 0 { 213 // multiline /* */ comment - process interior lines 214 check.inStar = true 215 i++ 216 pos += token.Pos(i) 217 text = text[i:] 218 for text != "" { 219 i := strings.Index(text, "\n") 220 if i < 0 { 221 i = len(text) 222 } else { 223 i++ 224 } 225 line := text[:i] 226 if strings.HasPrefix(line, "//") { 227 check.comment(pos, line) 228 } 229 pos += token.Pos(i) 230 text = text[i:] 231 } 232 check.inStar = false 233 } 234 } 235 } 236 237 func (check *checker) goBuildLine(pos token.Pos, line string) { 238 if !constraint.IsGoBuild(line) { 239 if !strings.HasPrefix(line, "//go:build") && constraint.IsGoBuild("//"+strings.TrimSpace(line[len("//"):])) { 240 check.pass.Reportf(pos, "malformed //go:build line (space between // and go:build)") 241 } 242 return 243 } 244 if !check.goBuildOK || check.inStar { 245 check.pass.Reportf(pos, "misplaced //go:build comment") 246 check.crossCheck = false 247 return 248 } 249 250 if check.goBuildPos == token.NoPos { 251 check.goBuildPos = pos 252 } else { 253 check.pass.Reportf(pos, "unexpected extra //go:build line") 254 check.crossCheck = false 255 } 256 257 // testing hack: stop at // ERROR 258 if i := strings.Index(line, " // ERROR "); i >= 0 { 259 line = line[:i] 260 } 261 262 x, err := constraint.Parse(line) 263 if err != nil { 264 check.pass.Reportf(pos, "%v", err) 265 check.crossCheck = false 266 return 267 } 268 269 if check.goBuild == nil { 270 check.goBuild = x 271 } 272 } 273 274 func (check *checker) plusBuildLine(pos token.Pos, line string) { 275 line = strings.TrimSpace(line) 276 if !constraint.IsPlusBuild(line) { 277 // Comment with +build but not at beginning. 278 // Only report early in file. 279 if check.plusBuildOK && !strings.HasPrefix(line, "// want") { 280 check.pass.Reportf(pos, "possible malformed +build comment") 281 } 282 return 283 } 284 if !check.plusBuildOK { // inStar implies !plusBuildOK 285 check.pass.Reportf(pos, "misplaced +build comment") 286 check.crossCheck = false 287 } 288 289 if check.plusBuildPos == token.NoPos { 290 check.plusBuildPos = pos 291 } 292 293 // testing hack: stop at // ERROR 294 if i := strings.Index(line, " // ERROR "); i >= 0 { 295 line = line[:i] 296 } 297 298 fields := strings.Fields(line[len("//"):]) 299 // IsPlusBuildConstraint check above implies fields[0] == "+build" 300 for _, arg := range fields[1:] { 301 for _, elem := range strings.Split(arg, ",") { 302 if strings.HasPrefix(elem, "!!") { 303 check.pass.Reportf(pos, "invalid double negative in build constraint: %s", arg) 304 check.crossCheck = false 305 continue 306 } 307 elem = strings.TrimPrefix(elem, "!") 308 for _, c := range elem { 309 if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { 310 check.pass.Reportf(pos, "invalid non-alphanumeric build constraint: %s", arg) 311 check.crossCheck = false 312 break 313 } 314 } 315 } 316 } 317 318 if check.crossCheck { 319 y, err := constraint.Parse(line) 320 if err != nil { 321 // Should never happen - constraint.Parse never rejects a // +build line. 322 // Also, we just checked the syntax above. 323 // Even so, report. 324 check.pass.Reportf(pos, "%v", err) 325 check.crossCheck = false 326 return 327 } 328 if check.plusBuild == nil { 329 check.plusBuild = y 330 } else { 331 check.plusBuild = &constraint.AndExpr{X: check.plusBuild, Y: y} 332 } 333 } 334 } 335 336 func (check *checker) finish() { 337 if !check.crossCheck || check.plusBuildPos == token.NoPos || check.goBuildPos == token.NoPos { 338 return 339 } 340 341 // Have both //go:build and // +build, 342 // with no errors found (crossCheck still true). 343 // Check they match. 344 var want constraint.Expr 345 lines, err := constraint.PlusBuildLines(check.goBuild) 346 if err != nil { 347 check.pass.Reportf(check.goBuildPos, "%v", err) 348 return 349 } 350 for _, line := range lines { 351 y, err := constraint.Parse(line) 352 if err != nil { 353 // Definitely should not happen, but not the user's fault. 354 // Do not report. 355 return 356 } 357 if want == nil { 358 want = y 359 } else { 360 want = &constraint.AndExpr{X: want, Y: y} 361 } 362 } 363 if want.String() != check.plusBuild.String() { 364 check.pass.Reportf(check.plusBuildPos, "+build lines do not match //go:build condition") 365 return 366 } 367 }