| /* Copyright 2018 The Bazel Authors. All rights reserved. |
| |
| Licensed under the Apache License, Version 2.0 (the "License"); |
| you may not use this file except in compliance with the License. |
| You may obtain a copy of the License at |
| |
| http://www.apache.org/licenses/LICENSE-2.0 |
| |
| Unless required by applicable law or agreed to in writing, software |
| distributed under the License is distributed on an "AS IS" BASIS, |
| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| See the License for the specific language governing permissions and |
| limitations under the License. |
| */ |
| |
| package golang |
| |
| import ( |
| "bytes" |
| "errors" |
| "fmt" |
| "go/ast" |
| "go/parser" |
| "go/token" |
| "log" |
| "path" |
| "path/filepath" |
| "strconv" |
| "strings" |
| "unicode" |
| "unicode/utf8" |
| |
| "github.com/bazelbuild/bazel-gazelle/config" |
| "github.com/bazelbuild/bazel-gazelle/rule" |
| ) |
| |
| // fileInfo holds information used to decide how to build a file. This |
| // information comes from the file's name, from package and import declarations |
| // (in .go files), and from +build and cgo comments. |
| type fileInfo struct { |
| path string |
| name string |
| |
| // ext is the type of file, based on extension. |
| ext ext |
| |
| // packageName is the Go package name of a .go file, without the |
| // "_test" suffix if it was present. It is empty for non-Go files. |
| packageName string |
| |
| // isTest is true if the file stem (the part before the extension) |
| // ends with "_test.go". This is never true for non-Go files. |
| isTest bool |
| |
| // isExternalTest is true when the file isTest and the original package |
| // name ends with "_test" |
| isExternalTest bool |
| |
| // imports is a list of packages imported by a file. It does not include |
| // "C" or anything from the standard library. |
| imports []string |
| |
| // embeds is a list of //go:embed patterns and their positions. |
| embeds []fileEmbed |
| |
| // isCgo is true for .go files that import "C". |
| isCgo bool |
| |
| // goos and goarch contain the OS and architecture suffixes in the filename, |
| // if they were present. |
| goos, goarch string |
| |
| // tags is a list of build tag lines. Each entry is the trimmed text of |
| // a line after a "+build" prefix. |
| tags *buildTags |
| |
| // cppopts, copts, cxxopts and clinkopts contain flags that are part |
| // of CPPFLAGS, CFLAGS, CXXFLAGS, and LDFLAGS directives in cgo comments. |
| cppopts, copts, cxxopts, clinkopts []*cgoTagsAndOpts |
| |
| // hasServices indicates whether a .proto file has service definitions. |
| hasServices bool |
| } |
| |
| // fileEmbed represents an individual go:embed pattern. |
| // A go:embed directive may contain multiple patterns. A pattern may match |
| // multiple files. |
| type fileEmbed struct { |
| path string |
| pos token.Position |
| } |
| |
| // optSeparator is a special character inserted between options that appeared |
| // together in a #cgo directive. This allows options to be split, modified, |
| // and escaped by other packages. |
| // |
| // It's important to keep options grouped together in the same string. For |
| // example, if we have "-framework IOKit" together in a #cgo directive, |
| // "-framework" shouldn't be treated as a separate string for the purposes of |
| // sorting and de-duplicating. |
| const optSeparator = "\x1D" |
| |
| // ext indicates how a file should be treated, based on extension. |
| type ext int |
| |
| const ( |
| // unknownExt is applied files that aren't buildable with Go. |
| unknownExt ext = iota |
| |
| // goExt is applied to .go files. |
| goExt |
| |
| // cExt is applied to C and C++ files. |
| cExt |
| |
| // hExt is applied to header files. If cgo code is present, these may be |
| // C or C++ headers. If not, they are treated as Go assembly headers. |
| hExt |
| |
| // sExt is applied to Go assembly files, ending with .s. |
| sExt |
| |
| // csExt is applied to other assembly files, ending with .S. These are built |
| // with the C compiler if cgo code is present. |
| csExt |
| |
| // protoExt is applied to .proto files. |
| protoExt |
| ) |
| |
| // fileNameInfo returns information that can be inferred from the name of |
| // a file. It does not read data from the file. |
| func fileNameInfo(path_ string) fileInfo { |
| name := filepath.Base(path_) |
| var ext ext |
| switch path.Ext(name) { |
| case ".go": |
| ext = goExt |
| case ".c", ".cc", ".cpp", ".cxx", ".m", ".mm": |
| ext = cExt |
| case ".h", ".hh", ".hpp", ".hxx": |
| ext = hExt |
| case ".s": |
| ext = sExt |
| case ".S": |
| ext = csExt |
| case ".proto": |
| ext = protoExt |
| default: |
| ext = unknownExt |
| } |
| if strings.HasPrefix(name, ".") || strings.HasPrefix(name, "_") { |
| ext = unknownExt |
| } |
| |
| // Determine test, goos, and goarch. This is intended to match the logic |
| // in goodOSArchFile in go/build. |
| var isTest bool |
| var goos, goarch string |
| l := strings.Split(name[:len(name)-len(path.Ext(name))], "_") |
| if len(l) >= 2 && l[len(l)-1] == "test" { |
| isTest = ext == goExt |
| l = l[:len(l)-1] |
| } |
| switch { |
| case len(l) >= 3 && rule.KnownOSSet[l[len(l)-2]] && rule.KnownArchSet[l[len(l)-1]]: |
| goos = l[len(l)-2] |
| goarch = l[len(l)-1] |
| case len(l) >= 2 && rule.KnownOSSet[l[len(l)-1]]: |
| goos = l[len(l)-1] |
| case len(l) >= 2 && rule.KnownArchSet[l[len(l)-1]]: |
| goarch = l[len(l)-1] |
| } |
| |
| return fileInfo{ |
| path: path_, |
| name: name, |
| ext: ext, |
| isTest: isTest, |
| goos: goos, |
| goarch: goarch, |
| } |
| } |
| |
| // otherFileInfo returns information about a non-.go file. It will parse |
| // part of the file to determine build tags. If the file can't be read, an |
| // error will be logged, and partial information will be returned. |
| func otherFileInfo(path string) fileInfo { |
| info := fileNameInfo(path) |
| if info.ext == unknownExt { |
| return info |
| } |
| |
| tags, err := readTags(info.path) |
| if err != nil { |
| log.Printf("%s: error reading file: %v", info.path, err) |
| return info |
| } |
| info.tags = tags |
| return info |
| } |
| |
| // goFileInfo returns information about a .go file. It will parse part of the |
| // file to determine the package name, imports, and build constraints. |
| // If the file can't be read, an error will be logged, and partial information |
| // will be returned. |
| // This function is intended to match go/build.Context.Import. |
| // TODD(#53): extract canonical import path |
| func goFileInfo(path, rel string) fileInfo { |
| info := fileNameInfo(path) |
| fset := token.NewFileSet() |
| pf, err := parser.ParseFile(fset, info.path, nil, parser.ImportsOnly|parser.ParseComments) |
| if err != nil { |
| log.Printf("%s: error reading go file: %v", info.path, err) |
| return info |
| } |
| |
| info.packageName = pf.Name.Name |
| if info.isTest && strings.HasSuffix(info.packageName, "_test") { |
| info.packageName = info.packageName[:len(info.packageName)-len("_test")] |
| info.isExternalTest = true |
| } |
| |
| importsEmbed := false |
| for _, decl := range pf.Decls { |
| d, ok := decl.(*ast.GenDecl) |
| if !ok { |
| continue |
| } |
| for _, dspec := range d.Specs { |
| spec, ok := dspec.(*ast.ImportSpec) |
| if !ok { |
| continue |
| } |
| quoted := spec.Path.Value |
| path, err := strconv.Unquote(quoted) |
| if err != nil { |
| log.Printf("%s: error reading go file: %v", info.path, err) |
| continue |
| } |
| |
| if path == "C" { |
| if info.isTest { |
| log.Printf("%s: warning: use of cgo in test not supported", info.path) |
| } |
| info.isCgo = true |
| cg := spec.Doc |
| if cg == nil && len(d.Specs) == 1 { |
| cg = d.Doc |
| } |
| if cg != nil { |
| if err := saveCgo(&info, rel, cg); err != nil { |
| log.Printf("%s: error reading go file: %v", info.path, err) |
| } |
| } |
| continue |
| } |
| if path == "embed" { |
| importsEmbed = true |
| } |
| info.imports = append(info.imports, path) |
| } |
| } |
| |
| tags, err := readTags(info.path) |
| if err != nil { |
| log.Printf("%s: error reading go file: %v", info.path, err) |
| return info |
| } |
| info.tags = tags |
| |
| if importsEmbed { |
| pf, err = parser.ParseFile(fset, info.path, nil, parser.ParseComments) |
| if err != nil { |
| log.Printf("%s: error reading go file: %v", info.path, err) |
| return info |
| } |
| for _, cg := range pf.Comments { |
| for _, c := range cg.List { |
| if !strings.HasPrefix(c.Text, "//go:embed") { |
| continue |
| } |
| args := c.Text[len("//go:embed"):] |
| p := c.Pos() |
| for len(args) > 0 && (args[0] == ' ' || args[0] == '\t') { |
| args = args[1:] |
| p++ |
| } |
| args = strings.TrimSpace(args) // trim right side |
| pos := fset.Position(p) |
| embeds, err := parseGoEmbed(args, pos) |
| if err != nil { |
| log.Printf("%v: parsing //go:embed directive: %v", pos, err) |
| continue |
| } |
| info.embeds = append(info.embeds, embeds...) |
| } |
| } |
| } |
| |
| return info |
| } |
| |
| // saveCgo extracts CFLAGS, CPPFLAGS, CXXFLAGS, and LDFLAGS directives |
| // from a comment above a "C" import. This is intended to match logic in |
| // go/build.Context.saveCgo. |
| func saveCgo(info *fileInfo, rel string, cg *ast.CommentGroup) error { |
| text := cg.Text() |
| for _, line := range strings.Split(text, "\n") { |
| orig := line |
| |
| // Line is |
| // #cgo [GOOS/GOARCH...] LDFLAGS: stuff |
| // |
| line = strings.TrimSpace(line) |
| if len(line) < 5 || line[:4] != "#cgo" || (line[4] != ' ' && line[4] != '\t') { |
| continue |
| } |
| |
| // Split at colon. |
| line, argstr, ok := strings.Cut(strings.TrimSpace(line[4:]), ":") |
| if !ok { |
| return fmt.Errorf("%s: invalid #cgo line: %s", info.path, orig) |
| } |
| |
| // Parse tags and verb. |
| f := strings.Fields(line) |
| if len(f) < 1 { |
| return fmt.Errorf("%s: invalid #cgo line: %s", info.path, orig) |
| } |
| |
| cond, verb := f[:len(f)-1], f[len(f)-1] |
| tags, err := matchAuto(cond) |
| if err != nil { |
| return err |
| } |
| |
| // Parse options. |
| opts, err := splitQuoted(argstr) |
| if err != nil { |
| return fmt.Errorf("%s: invalid #cgo line: %s", info.path, orig) |
| } |
| |
| for i, opt := range opts { |
| if opt, ok = expandSrcDir(opt, rel); !ok { |
| return fmt.Errorf("%s: malformed #cgo argument: %s", info.path, orig) |
| } |
| opts[i] = opt |
| } |
| joinedStr := strings.Join(opts, optSeparator) |
| |
| // Add tags to appropriate list. |
| switch verb { |
| case "CPPFLAGS": |
| info.cppopts = append(info.cppopts, &cgoTagsAndOpts{tags, joinedStr}) |
| case "CFLAGS": |
| info.copts = append(info.copts, &cgoTagsAndOpts{tags, joinedStr}) |
| case "CXXFLAGS": |
| info.cxxopts = append(info.cxxopts, &cgoTagsAndOpts{tags, joinedStr}) |
| case "FFLAGS": |
| // TODO: Add support |
| return fmt.Errorf("%s: unsupported #cgo verb: %s", verb, info.path) |
| case "LDFLAGS": |
| info.clinkopts = append(info.clinkopts, &cgoTagsAndOpts{tags, joinedStr}) |
| case "pkg-config": |
| return fmt.Errorf("%s: pkg-config not supported: %s", info.path, orig) |
| default: |
| return fmt.Errorf("%s: invalid #cgo verb: %s", info.path, orig) |
| } |
| } |
| return nil |
| } |
| |
| // splitQuoted splits the string s around each instance of one or more consecutive |
| // white space characters while taking into account quotes and escaping, and |
| // returns an array of substrings of s or an empty list if s contains only white space. |
| // Single quotes and double quotes are recognized to prevent splitting within the |
| // quoted region, and are removed from the resulting substrings. If a quote in s |
| // isn't closed err will be set and r will have the unclosed argument as the |
| // last element. The backslash is used for escaping. |
| // |
| // For example, the following string: |
| // |
| // a b:"c d" 'e''f' "g\"" |
| // |
| // Would be parsed as: |
| // |
| // []string{"a", "b:c d", "ef", `g"`} |
| // |
| // Copied from go/build.splitQuoted |
| func splitQuoted(s string) (r []string, err error) { |
| var args []string |
| arg := make([]rune, len(s)) |
| escaped := false |
| quoted := false |
| quote := '\x00' |
| i := 0 |
| for _, rune := range s { |
| switch { |
| case escaped: |
| escaped = false |
| case rune == '\\': |
| escaped = true |
| continue |
| case quote != '\x00': |
| if rune == quote { |
| quote = '\x00' |
| continue |
| } |
| case rune == '"' || rune == '\'': |
| quoted = true |
| quote = rune |
| continue |
| case unicode.IsSpace(rune): |
| if quoted || i > 0 { |
| quoted = false |
| args = append(args, string(arg[:i])) |
| i = 0 |
| } |
| continue |
| } |
| arg[i] = rune |
| i++ |
| } |
| if quoted || i > 0 { |
| args = append(args, string(arg[:i])) |
| } |
| if quote != 0 { |
| err = errors.New("unclosed quote") |
| } else if escaped { |
| err = errors.New("unfinished escaping") |
| } |
| return args, err |
| } |
| |
| // expandSrcDir expands any occurrence of ${SRCDIR}, making sure |
| // the result is safe for the shell. |
| // |
| // Copied from go/build.expandSrcDir |
| func expandSrcDir(str string, srcdir string) (string, bool) { |
| // "\" delimited paths cause safeCgoName to fail |
| // so convert native paths with a different delimiter |
| // to "/" before starting (eg: on windows). |
| srcdir = filepath.ToSlash(srcdir) |
| if srcdir == "" { |
| srcdir = "." |
| } |
| |
| // Spaces are tolerated in ${SRCDIR}, but not anywhere else. |
| chunks := strings.Split(str, "${SRCDIR}") |
| if len(chunks) < 2 { |
| return str, safeCgoName(str, false) |
| } |
| ok := true |
| for _, chunk := range chunks { |
| ok = ok && (chunk == "" || safeCgoName(chunk, false)) |
| } |
| ok = ok && (srcdir == "" || safeCgoName(srcdir, true)) |
| res := strings.Join(chunks, srcdir) |
| return res, ok && res != "" |
| } |
| |
| // NOTE: $ is not safe for the shell, but it is allowed here because of linker options like -Wl,$ORIGIN. |
| // We never pass these arguments to a shell (just to programs we construct argv for), so this should be okay. |
| // See golang.org/issue/6038. |
| // The @ is for OS X. See golang.org/issue/13720. |
| // The % is for Jenkins. See golang.org/issue/16959. |
| const ( |
| safeString = "+-.,/0123456789=ABCDEFGHIJKLMNOPQRSTUVWXYZ_abcdefghijklmnopqrstuvwxyz:$@%" |
| safeSpaces = " " |
| ) |
| |
| var safeBytes = []byte(safeSpaces + safeString) |
| |
| // Copied from go/build.safeCgoName |
| func safeCgoName(s string, spaces bool) bool { |
| if s == "" { |
| return false |
| } |
| safe := safeBytes |
| if !spaces { |
| safe = safe[len(safeSpaces):] |
| } |
| for i := 0; i < len(s); i++ { |
| if c := s[i]; c < utf8.RuneSelf && bytes.IndexByte(safe, c) < 0 { |
| return false |
| } |
| } |
| return true |
| } |
| |
| func isOSArchSpecific(info fileInfo, cgoTags *cgoTagsAndOpts) (osSpecific, archSpecific bool) { |
| if info.goos != "" { |
| osSpecific = true |
| } |
| if info.goarch != "" { |
| archSpecific = true |
| } |
| |
| checkTags := func(tags []string) { |
| for _, tag := range tags { |
| _, osOk := rule.KnownOSSet[tag] |
| if osOk { |
| osSpecific = true |
| } |
| _, archOk := rule.KnownArchSet[tag] |
| if archOk { |
| archSpecific = true |
| } |
| } |
| } |
| checkTags(info.tags.tags()) |
| if osSpecific && archSpecific { |
| return |
| } |
| |
| checkTags(cgoTags.tags()) |
| return |
| } |
| |
| // matchesOS checks if a value is equal to either an OS value or to any of its |
| // aliases. |
| func matchesOS(os, value string) bool { |
| if os == value { |
| return true |
| } |
| for _, alias := range rule.OSAliases[os] { |
| if alias == value { |
| return true |
| } |
| } |
| return false |
| } |
| |
| // checkConstraints determines whether build constraints are satisfied on |
| // a given platform. |
| // |
| // The first few arguments describe the platform. genericTags is the set |
| // of build tags that are true on all platforms. os and arch are the platform |
| // GOOS and GOARCH strings. If os or arch is empty, checkConstraints will |
| // return false in the presence of OS and architecture constraints, even |
| // if they are negated. |
| // |
| // The remaining arguments describe the file being tested. All of these may |
| // be empty or nil. osSuffix and archSuffix are filename suffixes. tags |
| // is the parsed build tags found near the top of the file. cgoTags |
| // is an extra set of tags in a #cgo directive. |
| func checkConstraints(c *config.Config, os, arch, osSuffix, archSuffix string, tags *buildTags, cgoTags *cgoTagsAndOpts) bool { |
| if osSuffix != "" && !matchesOS(os, osSuffix) || archSuffix != "" && archSuffix != arch { |
| return false |
| } |
| |
| goConf := getGoConfig(c) |
| checker := func(tag string) bool { |
| if isIgnoredTag(tag) { |
| return true |
| } |
| |
| if _, ok := rule.KnownOSSet[tag]; ok { |
| if os == "" { |
| return false |
| } |
| return matchesOS(os, tag) |
| |
| } |
| |
| if _, ok := rule.KnownArchSet[tag]; ok { |
| if arch == "" { |
| return false |
| } |
| return arch == tag |
| |
| } |
| |
| return goConf.genericTags[tag] |
| } |
| |
| return tags.eval(checker) && cgoTags.eval(checker) |
| } |
| |
| // parseGoEmbed parses the text following "//go:embed" to extract the glob patterns. |
| // It accepts unquoted space-separated patterns as well as double-quoted and back-quoted Go strings. |
| // This is based on a similar function in cmd/compile/internal/gc/noder.go; |
| // this version calculates position information as well. |
| // |
| // Copied from go/build.parseGoEmbed. |
| func parseGoEmbed(args string, pos token.Position) ([]fileEmbed, error) { |
| trimBytes := func(n int) { |
| pos.Offset += n |
| pos.Column += utf8.RuneCountInString(args[:n]) |
| args = args[n:] |
| } |
| trimSpace := func() { |
| trim := strings.TrimLeftFunc(args, unicode.IsSpace) |
| trimBytes(len(args) - len(trim)) |
| } |
| |
| var list []fileEmbed |
| for trimSpace(); args != ""; trimSpace() { |
| var path string |
| pathPos := pos |
| Switch: |
| switch args[0] { |
| default: |
| i := len(args) |
| for j, c := range args { |
| if unicode.IsSpace(c) { |
| i = j |
| break |
| } |
| } |
| path = args[:i] |
| trimBytes(i) |
| |
| case '`': |
| i := strings.Index(args[1:], "`") |
| if i < 0 { |
| return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) |
| } |
| path = args[1 : 1+i] |
| trimBytes(1 + i + 1) |
| |
| case '"': |
| i := 1 |
| for ; i < len(args); i++ { |
| if args[i] == '\\' { |
| i++ |
| continue |
| } |
| if args[i] == '"' { |
| q, err := strconv.Unquote(args[:i+1]) |
| if err != nil { |
| return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args[:i+1]) |
| } |
| path = q |
| trimBytes(i + 1) |
| break Switch |
| } |
| } |
| if i >= len(args) { |
| return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) |
| } |
| } |
| |
| if args != "" { |
| r, _ := utf8.DecodeRuneInString(args) |
| if !unicode.IsSpace(r) { |
| return nil, fmt.Errorf("invalid quoted string in //go:embed: %s", args) |
| } |
| } |
| list = append(list, fileEmbed{path, pathPos}) |
| } |
| return list, nil |
| } |