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 | // TODO(rsc): Delete this file once Go 1.17 comes out and we can retire Go 1.15 support. |
6 | |
7 | //go:build !go1.16 |
8 | // +build !go1.16 |
9 | |
10 | // Package buildtag defines an Analyzer that checks build tags. |
11 | package buildtag |
12 | |
13 | import ( |
14 | "bytes" |
15 | "fmt" |
16 | "go/ast" |
17 | "go/parser" |
18 | "strings" |
19 | "unicode" |
20 | |
21 | "golang.org/x/tools/go/analysis" |
22 | "golang.org/x/tools/go/analysis/passes/internal/analysisutil" |
23 | ) |
24 | |
25 | const Doc = "check that +build tags are well-formed and correctly located" |
26 | |
27 | var Analyzer = &analysis.Analyzer{ |
28 | Name: "buildtag", |
29 | Doc: Doc, |
30 | Run: runBuildTag, |
31 | } |
32 | |
33 | func runBuildTag(pass *analysis.Pass) (interface{}, error) { |
34 | for _, f := range pass.Files { |
35 | checkGoFile(pass, f) |
36 | } |
37 | for _, name := range pass.OtherFiles { |
38 | if err := checkOtherFile(pass, name); err != nil { |
39 | return nil, err |
40 | } |
41 | } |
42 | for _, name := range pass.IgnoredFiles { |
43 | if strings.HasSuffix(name, ".go") { |
44 | f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments) |
45 | if err != nil { |
46 | // Not valid Go source code - not our job to diagnose, so ignore. |
47 | return nil, nil |
48 | } |
49 | checkGoFile(pass, f) |
50 | } else { |
51 | if err := checkOtherFile(pass, name); err != nil { |
52 | return nil, err |
53 | } |
54 | } |
55 | } |
56 | return nil, nil |
57 | } |
58 | |
59 | func checkGoFile(pass *analysis.Pass, f *ast.File) { |
60 | pastCutoff := false |
61 | for _, group := range f.Comments { |
62 | // A +build comment is ignored after or adjoining the package declaration. |
63 | if group.End()+1 >= f.Package { |
64 | pastCutoff = true |
65 | } |
66 | |
67 | // "+build" is ignored within or after a /*...*/ comment. |
68 | if !strings.HasPrefix(group.List[0].Text, "//") { |
69 | pastCutoff = true |
70 | continue |
71 | } |
72 | |
73 | // Check each line of a //-comment. |
74 | for _, c := range group.List { |
75 | if !strings.Contains(c.Text, "+build") { |
76 | continue |
77 | } |
78 | if err := checkLine(c.Text, pastCutoff); err != nil { |
79 | pass.Reportf(c.Pos(), "%s", err) |
80 | } |
81 | } |
82 | } |
83 | } |
84 | |
85 | func checkOtherFile(pass *analysis.Pass, filename string) error { |
86 | content, tf, err := analysisutil.ReadFile(pass.Fset, filename) |
87 | if err != nil { |
88 | return err |
89 | } |
90 | |
91 | // We must look at the raw lines, as build tags may appear in non-Go |
92 | // files such as assembly files. |
93 | lines := bytes.SplitAfter(content, nl) |
94 | |
95 | // Determine cutpoint where +build comments are no longer valid. |
96 | // They are valid in leading // comments in the file followed by |
97 | // a blank line. |
98 | // |
99 | // This must be done as a separate pass because of the |
100 | // requirement that the comment be followed by a blank line. |
101 | var cutoff int |
102 | for i, line := range lines { |
103 | line = bytes.TrimSpace(line) |
104 | if !bytes.HasPrefix(line, slashSlash) { |
105 | if len(line) > 0 { |
106 | break |
107 | } |
108 | cutoff = i |
109 | } |
110 | } |
111 | |
112 | for i, line := range lines { |
113 | line = bytes.TrimSpace(line) |
114 | if !bytes.HasPrefix(line, slashSlash) { |
115 | continue |
116 | } |
117 | if !bytes.Contains(line, []byte("+build")) { |
118 | continue |
119 | } |
120 | if err := checkLine(string(line), i >= cutoff); err != nil { |
121 | pass.Reportf(analysisutil.LineStart(tf, i+1), "%s", err) |
122 | continue |
123 | } |
124 | } |
125 | return nil |
126 | } |
127 | |
128 | // checkLine checks a line that starts with "//" and contains "+build". |
129 | func checkLine(line string, pastCutoff bool) error { |
130 | line = strings.TrimPrefix(line, "//") |
131 | line = strings.TrimSpace(line) |
132 | |
133 | if strings.HasPrefix(line, "+build") { |
134 | fields := strings.Fields(line) |
135 | if fields[0] != "+build" { |
136 | // Comment is something like +buildasdf not +build. |
137 | return fmt.Errorf("possible malformed +build comment") |
138 | } |
139 | if pastCutoff { |
140 | return fmt.Errorf("+build comment must appear before package clause and be followed by a blank line") |
141 | } |
142 | if err := checkArguments(fields); err != nil { |
143 | return err |
144 | } |
145 | } else { |
146 | // Comment with +build but not at beginning. |
147 | if !pastCutoff { |
148 | return fmt.Errorf("possible malformed +build comment") |
149 | } |
150 | } |
151 | return nil |
152 | } |
153 | |
154 | func checkArguments(fields []string) error { |
155 | for _, arg := range fields[1:] { |
156 | for _, elem := range strings.Split(arg, ",") { |
157 | if strings.HasPrefix(elem, "!!") { |
158 | return fmt.Errorf("invalid double negative in build constraint: %s", arg) |
159 | } |
160 | elem = strings.TrimPrefix(elem, "!") |
161 | for _, c := range elem { |
162 | if !unicode.IsLetter(c) && !unicode.IsDigit(c) && c != '_' && c != '.' { |
163 | return fmt.Errorf("invalid non-alphanumeric build constraint: %s", arg) |
164 | } |
165 | } |
166 | } |
167 | } |
168 | return nil |
169 | } |
170 | |
171 | var ( |
172 | nl = []byte("\n") |
173 | slashSlash = []byte("//") |
174 | ) |
175 |
Members