github.com/afking/bazel-gazelle@v0.0.0-20180301150245-c02bc0f529e8/internal/packages/fileinfo_go.go (about) 1 /* Copyright 2017 The Bazel Authors. All rights reserved. 2 3 Licensed under the Apache License, Version 2.0 (the "License"); 4 you may not use this file except in compliance with the License. 5 You may obtain a copy of the License at 6 7 http://www.apache.org/licenses/LICENSE-2.0 8 9 Unless required by applicable law or agreed to in writing, software 10 distributed under the License is distributed on an "AS IS" BASIS, 11 WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 See the License for the specific language governing permissions and 13 limitations under the License. 14 */ 15 16 package packages 17 18 import ( 19 "bytes" 20 "errors" 21 "fmt" 22 "go/ast" 23 "go/parser" 24 "go/token" 25 "log" 26 "path/filepath" 27 "strconv" 28 "strings" 29 "unicode" 30 "unicode/utf8" 31 32 "github.com/bazelbuild/bazel-gazelle/internal/config" 33 ) 34 35 // goFileInfo returns information about a .go file. It will parse part of the 36 // file to determine the package name, imports, and build constraints. 37 // If the file can't be read, an error will be logged, and partial information 38 // will be returned. 39 // This function is intended to match go/build.Context.Import. 40 // TODD(#53): extract canonical import path 41 func goFileInfo(c *config.Config, dir, rel, name string) fileInfo { 42 info := fileNameInfo(dir, rel, name) 43 fset := token.NewFileSet() 44 pf, err := parser.ParseFile(fset, info.path, nil, parser.ImportsOnly|parser.ParseComments) 45 if err != nil { 46 log.Printf("%s: error reading go file: %v", info.path, err) 47 return info 48 } 49 50 info.packageName = pf.Name.Name 51 if info.isTest && strings.HasSuffix(info.packageName, "_test") { 52 info.isXTest = true 53 info.packageName = info.packageName[:len(info.packageName)-len("_test")] 54 } 55 56 for _, decl := range pf.Decls { 57 d, ok := decl.(*ast.GenDecl) 58 if !ok { 59 continue 60 } 61 for _, dspec := range d.Specs { 62 spec, ok := dspec.(*ast.ImportSpec) 63 if !ok { 64 continue 65 } 66 quoted := spec.Path.Value 67 path, err := strconv.Unquote(quoted) 68 if err != nil { 69 log.Printf("%s: error reading go file: %v", info.path, err) 70 continue 71 } 72 73 if path == "C" { 74 if info.isTest { 75 log.Printf("%s: warning: use of cgo in test not supported", info.path) 76 } 77 info.isCgo = true 78 cg := spec.Doc 79 if cg == nil && len(d.Specs) == 1 { 80 cg = d.Doc 81 } 82 if cg != nil { 83 if err := saveCgo(&info, cg); err != nil { 84 log.Printf("%s: error reading go file: %v", info.path, err) 85 } 86 } 87 continue 88 } 89 info.imports = append(info.imports, path) 90 } 91 } 92 93 tags, err := readTags(info.path) 94 if err != nil { 95 log.Printf("%s: error reading go file: %v", info.path, err) 96 return info 97 } 98 info.tags = tags 99 100 return info 101 } 102 103 // saveCgo extracts CFLAGS, CPPFLAGS, CXXFLAGS, and LDFLAGS directives 104 // from a comment above a "C" import. This is intended to match logic in 105 // go/build.Context.saveCgo. 106 func saveCgo(info *fileInfo, cg *ast.CommentGroup) error { 107 text := cg.Text() 108 for _, line := range strings.Split(text, "\n") { 109 orig := line 110 111 // Line is 112 // #cgo [GOOS/GOARCH...] LDFLAGS: stuff 113 // 114 line = strings.TrimSpace(line) 115 if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') { 116 continue 117 } 118 119 // Split at colon. 120 line = strings.TrimSpace(line[4:]) 121 i := strings.Index(line, ":") 122 if i < 0 { 123 return fmt.Errorf("%s: invalid #cgo line: %s", info.path, orig) 124 } 125 line, optstr := strings.TrimSpace(line[:i]), strings.TrimSpace(line[i+1:]) 126 127 // Parse tags and verb. 128 f := strings.Fields(line) 129 if len(f) < 1 { 130 return fmt.Errorf("%s: invalid #cgo line: %s", info.path, orig) 131 } 132 verb := f[len(f)-1] 133 tags := parseTagsInGroups(f[:len(f)-1]) 134 135 // Parse options. 136 opts, err := splitQuoted(optstr) 137 if err != nil { 138 return fmt.Errorf("%s: invalid #cgo line: %s", info.path, orig) 139 } 140 var ok bool 141 for i, opt := range opts { 142 if opt, ok = expandSrcDir(opt, info.rel); !ok { 143 return fmt.Errorf("%s: malformed #cgo argument: %s", info.path, orig) 144 } 145 opts[i] = opt 146 } 147 joinedStr := strings.Join(opts, OptSeparator) 148 149 // Add tags to appropriate list. 150 switch verb { 151 case "CFLAGS", "CPPFLAGS", "CXXFLAGS": 152 info.copts = append(info.copts, taggedOpts{tags, joinedStr}) 153 case "LDFLAGS": 154 info.clinkopts = append(info.clinkopts, taggedOpts{tags, joinedStr}) 155 case "pkg-config": 156 return fmt.Errorf("%s: pkg-config not supported: %s", info.path, orig) 157 default: 158 return fmt.Errorf("%s: invalid #cgo verb: %s", info.path, orig) 159 } 160 } 161 return nil 162 } 163 164 // splitQuoted splits the string s around each instance of one or more consecutive 165 // white space characters while taking into account quotes and escaping, and 166 // returns an array of substrings of s or an empty list if s contains only white space. 167 // Single quotes and double quotes are recognized to prevent splitting within the 168 // quoted region, and are removed from the resulting substrings. If a quote in s 169 // isn't closed err will be set and r will have the unclosed argument as the 170 // last element. The backslash is used for escaping. 171 // 172 // For example, the following string: 173 // 174 // a b:"c d" 'e''f' "g\"" 175 // 176 // Would be parsed as: 177 // 178 // []string{"a", "b:c d", "ef", `g"`} 179 // 180 // Copied from go/build.splitQuoted 181 func splitQuoted(s string) (r []string, err error) { 182 var args []string 183 arg := make([]rune, len(s)) 184 escaped := false 185 quoted := false 186 quote := '\x00' 187 i := 0 188 for _, rune := range s { 189 switch { 190 case escaped: 191 escaped = false 192 case rune == '\\': 193 escaped = true 194 continue 195 case quote != '\x00': 196 if rune == quote { 197 quote = '\x00' 198 continue 199 } 200 case rune == '"' || rune == '\'': 201 quoted = true 202 quote = rune 203 continue 204 case unicode.IsSpace(rune): 205 if quoted || i > 0 { 206 quoted = false 207 args = append(args, string(arg[:i])) 208 i = 0 209 } 210 continue 211 } 212 arg[i] = rune 213 i++ 214 } 215 if quoted || i > 0 { 216 args = append(args, string(arg[:i])) 217 } 218 if quote != 0 { 219 err = errors.New("unclosed quote") 220 } else if escaped { 221 err = errors.New("unfinished escaping") 222 } 223 return args, err 224 } 225 226 // expandSrcDir expands any occurrence of ${SRCDIR}, making sure 227 // the result is safe for the shell. 228 // 229 // Copied from go/build.expandSrcDir 230 func expandSrcDir(str string, srcdir string) (string, bool) { 231 // "\" delimited paths cause safeCgoName to fail 232 // so convert native paths with a different delimiter 233 // to "/" before starting (eg: on windows). 234 srcdir = filepath.ToSlash(srcdir) 235 236 // Spaces are tolerated in ${SRCDIR}, but not anywhere else. 237 chunks := strings.Split(str, "${SRCDIR}") 238 if len(chunks) < 2 { 239 return str, safeCgoName(str, false) 240 } 241 ok := true 242 for _, chunk := range chunks { 243 ok = ok && (chunk == "" || safeCgoName(chunk, false)) 244 } 245 ok = ok && (srcdir == "" || safeCgoName(srcdir, true)) 246 res := strings.Join(chunks, srcdir) 247 return res, ok && res != "" 248 } 249 250 // NOTE: $ is not safe for the shell, but it is allowed here because of linker options like -Wl,$ORIGIN. 251 // We never pass these arguments to a shell (just to programs we construct argv for), so this should be okay. 252 // See golang.org/issue/6038. 253 // The @ is for OS X. See golang.org/issue/13720. 254 // The % is for Jenkins. See golang.org/issue/16959. 255 const safeString = "+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz:$@%" 256 const safeSpaces = " " 257 258 var safeBytes = []byte(safeSpaces + safeString) 259 260 // Copied from go/build.safeCgoName 261 func safeCgoName(s string, spaces bool) bool { 262 if s == "" { 263 return false 264 } 265 safe := safeBytes 266 if !spaces { 267 safe = safe[len(safeSpaces):] 268 } 269 for i := 0; i < len(s); i++ { 270 if c := s[i]; c < utf8.RuneSelf && bytes.IndexByte(safe, c) < 0 { 271 return false 272 } 273 } 274 return true 275 }