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  }