github.com/cockroachdb/tools@v0.0.0-20230222021103-a6d27438930d/go/analysis/passes/directive/directive.go (about)

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