...

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

Documentation: cmd/vendor/golang.org/x/tools/go/analysis/passes/directive

     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 +build comment is ignored after or adjoining the package declaration.
    74  		if group.End()+1 >= f.Package {
    75  			check.inHeader = false
    76  		}
    77  		// A //go:build comment is ignored after the package declaration
    78  		// (but adjoining it is OK, in contrast to +build comments).
    79  		if group.Pos() >= f.Package {
    80  			check.inHeader = false
    81  		}
    82  
    83  		// Check each line of a //-comment.
    84  		for _, c := range group.List {
    85  			check.comment(c.Slash, c.Text)
    86  		}
    87  	}
    88  }
    89  
    90  func checkOtherFile(pass *analysis.Pass, filename string) error {
    91  	// We cannot use the Go parser, since is not a Go source file.
    92  	// Read the raw bytes instead.
    93  	content, tf, err := analysisutil.ReadFile(pass.Fset, filename)
    94  	if err != nil {
    95  		return err
    96  	}
    97  
    98  	check := newChecker(pass, filename, nil)
    99  	check.nonGoFile(token.Pos(tf.Base()), string(content))
   100  	return nil
   101  }
   102  
   103  type checker struct {
   104  	pass     *analysis.Pass
   105  	filename string
   106  	file     *ast.File // nil for non-Go file
   107  	inHeader bool      // in file header (before package declaration)
   108  	inStar   bool      // currently in a /* */ comment
   109  }
   110  
   111  func newChecker(pass *analysis.Pass, filename string, file *ast.File) *checker {
   112  	return &checker{
   113  		pass:     pass,
   114  		filename: filename,
   115  		file:     file,
   116  		inHeader: true,
   117  	}
   118  }
   119  
   120  func (check *checker) nonGoFile(pos token.Pos, fullText string) {
   121  	// Process each line.
   122  	text := fullText
   123  	inStar := false
   124  	for text != "" {
   125  		offset := len(fullText) - len(text)
   126  		var line string
   127  		line, text, _ = strings.Cut(text, "\n")
   128  
   129  		if !inStar && strings.HasPrefix(line, "//") {
   130  			check.comment(pos+token.Pos(offset), line)
   131  			continue
   132  		}
   133  
   134  		// Skip over, cut out any /* */ comments,
   135  		// to avoid being confused by a commented-out // comment.
   136  		for {
   137  			line = strings.TrimSpace(line)
   138  			if inStar {
   139  				var ok bool
   140  				_, line, ok = strings.Cut(line, "*/")
   141  				if !ok {
   142  					break
   143  				}
   144  				inStar = false
   145  				continue
   146  			}
   147  			line, inStar = stringsCutPrefix(line, "/*")
   148  			if !inStar {
   149  				break
   150  			}
   151  		}
   152  		if line != "" {
   153  			// Found non-comment non-blank line.
   154  			// Ends space for valid //go:build comments,
   155  			// but also ends the fraction of the file we can
   156  			// reliably parse. From this point on we might
   157  			// incorrectly flag "comments" inside multiline
   158  			// string constants or anything else (this might
   159  			// not even be a Go program). So stop.
   160  			break
   161  		}
   162  	}
   163  }
   164  
   165  func (check *checker) comment(pos token.Pos, line string) {
   166  	if !strings.HasPrefix(line, "//go:") {
   167  		return
   168  	}
   169  	// testing hack: stop at // ERROR
   170  	if i := strings.Index(line, " // ERROR "); i >= 0 {
   171  		line = line[:i]
   172  	}
   173  
   174  	verb := line
   175  	if i := strings.IndexFunc(verb, unicode.IsSpace); i >= 0 {
   176  		verb = verb[:i]
   177  		if line[i] != ' ' && line[i] != '\t' && line[i] != '\n' {
   178  			r, _ := utf8.DecodeRuneInString(line[i:])
   179  			check.pass.Reportf(pos, "invalid space %#q in %s directive", r, verb)
   180  		}
   181  	}
   182  
   183  	switch verb {
   184  	default:
   185  		// TODO: Use the go language version for the file.
   186  		// If that version is not newer than us, then we can
   187  		// report unknown directives.
   188  
   189  	case "//go:build":
   190  		// Ignore. The buildtag analyzer reports misplaced comments.
   191  
   192  	case "//go:debug":
   193  		if check.file == nil {
   194  			check.pass.Reportf(pos, "//go:debug directive only valid in Go source files")
   195  		} else if check.file.Name.Name != "main" && !strings.HasSuffix(check.filename, "_test.go") {
   196  			check.pass.Reportf(pos, "//go:debug directive only valid in package main or test")
   197  		} else if !check.inHeader {
   198  			check.pass.Reportf(pos, "//go:debug directive only valid before package declaration")
   199  		}
   200  	}
   201  }
   202  
   203  // Go 1.20 strings.CutPrefix.
   204  func stringsCutPrefix(s, prefix string) (after string, found bool) {
   205  	if !strings.HasPrefix(s, prefix) {
   206  		return s, false
   207  	}
   208  	return s[len(prefix):], true
   209  }
   210  

View as plain text