Source file src/cmd/vendor/golang.org/x/tools/go/analysis/passes/directive/directive.go

     1  // Copyright 2023 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  // Package directive defines an Analyzer that checks known Go toolchain directives.
     6  package directive
     7  
     8  import (
     9  	"go/ast"
    10  	"go/parser"
    11  	"go/token"
    12  	"strings"
    13  	"unicode"
    14  	"unicode/utf8"
    15  
    16  	"golang.org/x/tools/go/analysis"
    17  	"golang.org/x/tools/go/analysis/passes/internal/analysisutil"
    18  )
    19  
    20  const Doc = `check Go toolchain directives such as //go:debug
    21  
    22  This analyzer checks for problems with known Go toolchain directives
    23  in all Go source files in a package directory, even those excluded by
    24  //go:build constraints, and all non-Go source files too.
    25  
    26  For //go:debug (see https://go.dev/doc/godebug), the analyzer checks
    27  that the directives are placed only in Go source files, only above the
    28  package comment, and only in package main or *_test.go files.
    29  
    30  Support for other known directives may be added in the future.
    31  
    32  This analyzer does not check //go:build, which is handled by the
    33  buildtag analyzer.
    34  `
    35  
    36  var Analyzer = &analysis.Analyzer{
    37  	Name: "directive",
    38  	Doc:  Doc,
    39  	URL:  "https://pkg.go.dev/golang.org/x/tools/go/analysis/passes/directive",
    40  	Run:  runDirective,
    41  }
    42  
    43  func runDirective(pass *analysis.Pass) (interface{}, error) {
    44  	for _, f := range pass.Files {
    45  		checkGoFile(pass, f)
    46  	}
    47  	for _, name := range pass.OtherFiles {
    48  		if err := checkOtherFile(pass, name); err != nil {
    49  			return nil, err
    50  		}
    51  	}
    52  	for _, name := range pass.IgnoredFiles {
    53  		if strings.HasSuffix(name, ".go") {
    54  			f, err := parser.ParseFile(pass.Fset, name, nil, parser.ParseComments)
    55  			if err != nil {
    56  				// Not valid Go source code - not our job to diagnose, so ignore.
    57  				continue
    58  			}
    59  			checkGoFile(pass, f)
    60  		} else {
    61  			if err := checkOtherFile(pass, name); err != nil {
    62  				return nil, err
    63  			}
    64  		}
    65  	}
    66  	return nil, nil
    67  }
    68  
    69  func checkGoFile(pass *analysis.Pass, f *ast.File) {
    70  	check := newChecker(pass, pass.Fset.File(f.Package).Name(), f)
    71  
    72  	for _, group := range f.Comments {
    73  		// A //go:build or a //go:debug comment is ignored after the package declaration
    74  		// (but adjoining it is OK, in contrast to +build comments).
    75  		if group.Pos() >= f.Package {
    76  			check.inHeader = false
    77  		}
    78  
    79  		// Check each line of a //-comment.
    80  		for _, c := range group.List {
    81  			check.comment(c.Slash, c.Text)
    82  		}
    83  	}
    84  }
    85  
    86  func checkOtherFile(pass *analysis.Pass, filename string) error {
    87  	// We cannot use the Go parser, since is not a Go source file.
    88  	// Read the raw bytes instead.
    89  	content, tf, err := analysisutil.ReadFile(pass, filename)
    90  	if err != nil {
    91  		return err
    92  	}
    93  
    94  	check := newChecker(pass, filename, nil)
    95  	check.nonGoFile(token.Pos(tf.Base()), string(content))
    96  	return nil
    97  }
    98  
    99  type checker struct {
   100  	pass     *analysis.Pass
   101  	filename string
   102  	file     *ast.File // nil for non-Go file
   103  	inHeader bool      // in file header (before or adjoining package declaration)
   104  }
   105  
   106  func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker {
   107  	return &checker{
   108  		pass:     pass,
   109  		filename: filename,
   110  		file:     file,
   111  		inHeader: true,
   112  	}
   113  }
   114  
   115  func (check *checker) nonGoFile(pos token.Pos, fullText string) {
   116  	// Process each line.
   117  	text := fullText
   118  	inStar := false
   119  	for text != "" {
   120  		offset := len(fullText) - len(text)
   121  		var line string
   122  		line, text, _ = strings.Cut(text, "\n")
   123  
   124  		if !inStar && strings.HasPrefix(line, "//") {
   125  			check.comment(pos+token.Pos(offset), line)
   126  			continue
   127  		}
   128  
   129  		// Skip over, cut out any /* */ comments,
   130  		// to avoid being confused by a commented-out // comment.
   131  		for {
   132  			line = strings.TrimSpace(line)
   133  			if inStar {
   134  				var ok bool
   135  				_, line, ok = strings.Cut(line, "*/")
   136  				if !ok {
   137  					break
   138  				}
   139  				inStar = false
   140  				continue
   141  			}
   142  			line, inStar = stringsCutPrefix(line, "/*")
   143  			if !inStar {
   144  				break
   145  			}
   146  		}
   147  		if line != "" {
   148  			// Found non-comment non-blank line.
   149  			// Ends space for valid //go:build comments,
   150  			// but also ends the fraction of the file we can
   151  			// reliably parse. From this point on we might
   152  			// incorrectly flag "comments" inside multiline
   153  			// string constants or anything else (this might
   154  			// not even be a Go program). So stop.
   155  			break
   156  		}
   157  	}
   158  }
   159  
   160  func (check *checker) comment(pos token.Pos, line string) {
   161  	if !strings.HasPrefix(line, "//go:") {
   162  		return
   163  	}
   164  	// testing hack: stop at // ERROR
   165  	if i := strings.Index(line, " // ERROR "); i >= 0 {
   166  		line = line[:i]
   167  	}
   168  
   169  	verb := line
   170  	if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 {
   171  		verb = verb[:i]
   172  		if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' {
   173  			r, _ := utf8.DecodeRuneInString(line[i:])
   174  			check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb)
   175  		}
   176  	}
   177  
   178  	switch verb {
   179  	default:
   180  		// TODO: Use the go language version for the file.
   181  		// If that version is not newer than us, then we can
   182  		// report unknown directives.
   183  
   184  	case "//go:build":
   185  		// Ignore. The buildtag analyzer reports misplaced comments.
   186  
   187  	case "//go:debug":
   188  		if check.file == nil {
   189  			check.pass.Reportf(pos, "//go:debug directive only valid in Go source files")
   190  		} else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") {
   191  			check.pass.Reportf(pos, "//go:debug directive only valid in package main or test")
   192  		} else if !check.inHeader {
   193  			check.pass.Reportf(pos, "//go:debug directive only valid before package declaration")
   194  		}
   195  	}
   196  }
   197  
   198  // Go 1.20 strings.CutPrefix.
   199  func stringsCutPrefix(s, prefix string) (after string, found bool) {
   200  	if !strings.HasPrefix(s, prefix) {
   201  		return s, false
   202  	}
   203  	return s[len(prefix):], true
   204  }
   205  

View as plain text