st1022.go raw

   1  package st1022
   2  
   3  import (
   4  	"fmt"
   5  	"go/ast"
   6  	"go/token"
   7  	"strings"
   8  
   9  	"honnef.co/go/tools/analysis/code"
  10  	"honnef.co/go/tools/analysis/facts/generated"
  11  	"honnef.co/go/tools/analysis/lint"
  12  	"honnef.co/go/tools/analysis/report"
  13  
  14  	"golang.org/x/tools/go/analysis"
  15  	"golang.org/x/tools/go/analysis/passes/inspect"
  16  	"golang.org/x/tools/go/ast/inspector"
  17  )
  18  
  19  var SCAnalyzer = lint.InitializeAnalyzer(&lint.Analyzer{
  20  	Analyzer: &analysis.Analyzer{
  21  		Name:     "ST1022",
  22  		Run:      run,
  23  		Requires: []*analysis.Analyzer{generated.Analyzer, inspect.Analyzer},
  24  	},
  25  	Doc: &lint.RawDocumentation{
  26  		Title: "The documentation of an exported variable or constant should start with variable's name",
  27  		Text: `Doc comments work best as complete sentences, which
  28  allow a wide variety of automated presentations. The first sentence
  29  should be a one-sentence summary that starts with the name being
  30  declared.
  31  
  32  If every doc comment begins with the name of the item it describes,
  33  you can use the \'doc\' subcommand of the \'go\' tool and run the output
  34  through grep.
  35  
  36  See https://go.dev/doc/effective_go#commentary for more
  37  information on how to write good documentation.`,
  38  		Since:      "2020.1",
  39  		NonDefault: true,
  40  		MergeIf:    lint.MergeIfAny,
  41  	},
  42  })
  43  
  44  var Analyzer = SCAnalyzer.Analyzer
  45  
  46  func run(pass *analysis.Pass) (interface{}, error) {
  47  	var genDecl *ast.GenDecl
  48  	fn := func(node ast.Node, push bool) bool {
  49  		if !push {
  50  			genDecl = nil
  51  			return false
  52  		}
  53  		if code.IsInTest(pass, node) {
  54  			return false
  55  		}
  56  
  57  		switch node := node.(type) {
  58  		case *ast.GenDecl:
  59  			if node.Tok == token.IMPORT {
  60  				return false
  61  			}
  62  			genDecl = node
  63  			return true
  64  		case *ast.ValueSpec:
  65  			if genDecl.Lparen.IsValid() || len(node.Names) > 1 {
  66  				// Don't try to guess the user's intention
  67  				return false
  68  			}
  69  			name := node.Names[0].Name
  70  			if !ast.IsExported(name) {
  71  				return false
  72  			}
  73  			text, ok := docText(genDecl.Doc)
  74  			if !ok {
  75  				return false
  76  			}
  77  			prefix := name + " "
  78  			if !strings.HasPrefix(text, prefix) {
  79  				kind := "var"
  80  				if genDecl.Tok == token.CONST {
  81  					kind = "const"
  82  				}
  83  				report.Report(pass, genDecl.Doc, fmt.Sprintf(`comment on exported %s %s should be of the form "%s..."`, kind, name, prefix), report.FilterGenerated())
  84  			}
  85  			return false
  86  		case *ast.FuncLit, *ast.FuncDecl:
  87  			return false
  88  		default:
  89  			lint.ExhaustiveTypeSwitch(node)
  90  			return false
  91  		}
  92  	}
  93  
  94  	pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.ValueSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
  95  	return nil, nil
  96  }
  97  
  98  func docText(doc *ast.CommentGroup) (string, bool) {
  99  	if doc == nil {
 100  		return "", false
 101  	}
 102  	// We trim spaces primarily because of /**/ style comments, which often have leading space.
 103  	text := strings.TrimSpace(doc.Text())
 104  	return text, text != ""
 105  }
 106