st1021.go raw

   1  package st1021
   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:     "ST1021",
  22  		Run:      run,
  23  		Requires: []*analysis.Analyzer{generated.Analyzer, inspect.Analyzer},
  24  	},
  25  	Doc: &lint.RawDocumentation{
  26  		Title: "The documentation of an exported type should start with type'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.TypeSpec:
  65  			if !ast.IsExported(node.Name.Name) {
  66  				return false
  67  			}
  68  
  69  			doc := node.Doc
  70  			text, ok := docText(doc)
  71  			if !ok {
  72  				if len(genDecl.Specs) != 1 {
  73  					// more than one spec in the GenDecl, don't validate the
  74  					// docstring
  75  					return false
  76  				}
  77  				if genDecl.Lparen.IsValid() {
  78  					// 'type ( T )' is weird, don't guess the user's intention
  79  					return false
  80  				}
  81  				doc = genDecl.Doc
  82  				text, ok = docText(doc)
  83  				if !ok {
  84  					return false
  85  				}
  86  			}
  87  
  88  			// Check comment before we strip articles in case the type's name is an article.
  89  			if strings.HasPrefix(text, node.Name.Name+" ") {
  90  				return false
  91  			}
  92  
  93  			s := text
  94  			articles := [...]string{"A", "An", "The"}
  95  			for _, a := range articles {
  96  				if strings.HasPrefix(s, a+" ") {
  97  					s = s[len(a)+1:]
  98  					break
  99  				}
 100  			}
 101  			if !strings.HasPrefix(s, node.Name.Name+" ") {
 102  				report.Report(pass, doc, fmt.Sprintf(`comment on exported type %s should be of the form "%s ..." (with optional leading article)`, node.Name.Name, node.Name.Name), report.FilterGenerated())
 103  			}
 104  			return false
 105  		case *ast.FuncLit, *ast.FuncDecl:
 106  			return false
 107  		default:
 108  			lint.ExhaustiveTypeSwitch(node)
 109  			return false
 110  		}
 111  	}
 112  
 113  	pass.ResultOf[inspect.Analyzer].(*inspector.Inspector).Nodes([]ast.Node{(*ast.GenDecl)(nil), (*ast.TypeSpec)(nil), (*ast.FuncLit)(nil), (*ast.FuncDecl)(nil)}, fn)
 114  	return nil, nil
 115  }
 116  
 117  func docText(doc *ast.CommentGroup) (string, bool) {
 118  	if doc == nil {
 119  		return "", false
 120  	}
 121  	// We trim spaces primarily because of /**/ style comments, which often have leading space.
 122  	text := strings.TrimSpace(doc.Text())
 123  	return text, text != ""
 124  }
 125