...

Source file src/golang.org/x/tools/go/analysis/passes/buildtag/buildtag.go

Documentation: golang.org/x/tools/go/analysis/passes/buildtag

     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 that +build tags are well-formed and correctly located"
    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  }
   368  

View as plain text