github.com/amarpal/go-tools@v0.0.0-20240422043104-40142f59f616/stylecheck/st1021/st1021.go (about)

     1  package st1021
     2  
     3  import (
     4  	"fmt"
     5  	"go/ast"
     6  	"go/token"
     7  	"strings"
     8  
     9  	"github.com/amarpal/go-tools/analysis/code"
    10  	"github.com/amarpal/go-tools/analysis/facts/generated"
    11  	"github.com/amarpal/go-tools/analysis/lint"
    12  	"github.com/amarpal/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.Documentation{
    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://golang.org/doc/effective_go.html#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  }