lint.go raw

   1  // Package lint provides abstractions on top of go/analysis.
   2  // These abstractions add extra information to analyzes, such as structured documentation and severities.
   3  package lint
   4  
   5  import (
   6  	"fmt"
   7  	"go/ast"
   8  	"go/token"
   9  	"strings"
  10  
  11  	"golang.org/x/tools/go/analysis"
  12  	"honnef.co/go/tools/analysis/facts/tokenfile"
  13  )
  14  
  15  // Analyzer wraps a go/analysis.Analyzer and provides structured documentation.
  16  type Analyzer struct {
  17  	// The analyzer's documentation. Unlike go/analysis.Analyzer.Doc,
  18  	// this field is structured, providing access to severity, options
  19  	// etc.
  20  	Doc      *RawDocumentation
  21  	Analyzer *analysis.Analyzer
  22  }
  23  
  24  func InitializeAnalyzer(a *Analyzer) *Analyzer {
  25  	a.Analyzer.Doc = a.Doc.Compile().String()
  26  	a.Analyzer.URL = "https://staticcheck.dev/docs/checks/#" + a.Analyzer.Name
  27  	a.Analyzer.Requires = append(a.Analyzer.Requires, tokenfile.Analyzer)
  28  	return a
  29  }
  30  
  31  // Severity describes the severity of diagnostics reported by an analyzer.
  32  type Severity int
  33  
  34  const (
  35  	SeverityNone Severity = iota
  36  	SeverityError
  37  	SeverityDeprecated
  38  	SeverityWarning
  39  	SeverityInfo
  40  	SeverityHint
  41  )
  42  
  43  // MergeStrategy sets how merge mode should behave for diagnostics of an analyzer.
  44  type MergeStrategy int
  45  
  46  const (
  47  	MergeIfAny MergeStrategy = iota
  48  	MergeIfAll
  49  )
  50  
  51  type RawDocumentation struct {
  52  	Title      string
  53  	Text       string
  54  	Before     string
  55  	After      string
  56  	Since      string
  57  	NonDefault bool
  58  	Options    []string
  59  	Severity   Severity
  60  	MergeIf    MergeStrategy
  61  }
  62  
  63  type Documentation struct {
  64  	Title string
  65  	Text  string
  66  
  67  	TitleMarkdown string
  68  	TextMarkdown  string
  69  
  70  	Before     string
  71  	After      string
  72  	Since      string
  73  	NonDefault bool
  74  	Options    []string
  75  	Severity   Severity
  76  	MergeIf    MergeStrategy
  77  }
  78  
  79  func (doc RawDocumentation) Compile() *Documentation {
  80  	return &Documentation{
  81  		Title: strings.TrimSpace(stripMarkdown(doc.Title)),
  82  		Text:  strings.TrimSpace(stripMarkdown(doc.Text)),
  83  
  84  		TitleMarkdown: strings.TrimSpace(toMarkdown(doc.Title)),
  85  		TextMarkdown:  strings.TrimSpace(toMarkdown(doc.Text)),
  86  
  87  		Before:     strings.TrimSpace(doc.Before),
  88  		After:      strings.TrimSpace(doc.After),
  89  		Since:      doc.Since,
  90  		NonDefault: doc.NonDefault,
  91  		Options:    doc.Options,
  92  		Severity:   doc.Severity,
  93  		MergeIf:    doc.MergeIf,
  94  	}
  95  }
  96  
  97  func toMarkdown(s string) string {
  98  	return strings.NewReplacer(`\'`, "`", `\"`, "`").Replace(s)
  99  }
 100  
 101  func stripMarkdown(s string) string {
 102  	return strings.NewReplacer(`\'`, "", `\"`, "'").Replace(s)
 103  }
 104  
 105  func (doc *Documentation) Format(metadata bool) string {
 106  	return doc.format(false, metadata)
 107  }
 108  
 109  func (doc *Documentation) FormatMarkdown(metadata bool) string {
 110  	return doc.format(true, metadata)
 111  }
 112  
 113  func (doc *Documentation) format(markdown bool, metadata bool) string {
 114  	b := &strings.Builder{}
 115  	if markdown {
 116  		fmt.Fprintf(b, "%s\n\n", doc.TitleMarkdown)
 117  		if doc.Text != "" {
 118  			fmt.Fprintf(b, "%s\n\n", doc.TextMarkdown)
 119  		}
 120  	} else {
 121  		fmt.Fprintf(b, "%s\n\n", doc.Title)
 122  		if doc.Text != "" {
 123  			fmt.Fprintf(b, "%s\n\n", doc.Text)
 124  		}
 125  	}
 126  
 127  	if doc.Before != "" {
 128  		fmt.Fprintln(b, "Before:")
 129  		fmt.Fprintln(b, "")
 130  		for _, line := range strings.Split(doc.Before, "\n") {
 131  			fmt.Fprint(b, "    ", line, "\n")
 132  		}
 133  		fmt.Fprintln(b, "")
 134  		fmt.Fprintln(b, "After:")
 135  		fmt.Fprintln(b, "")
 136  		for _, line := range strings.Split(doc.After, "\n") {
 137  			fmt.Fprint(b, "    ", line, "\n")
 138  		}
 139  		fmt.Fprintln(b, "")
 140  	}
 141  
 142  	if metadata {
 143  		fmt.Fprint(b, "Available since\n    ")
 144  		if doc.Since == "" {
 145  			fmt.Fprint(b, "unreleased")
 146  		} else {
 147  			fmt.Fprintf(b, "%s", doc.Since)
 148  		}
 149  		if doc.NonDefault {
 150  			fmt.Fprint(b, ", non-default")
 151  		}
 152  		fmt.Fprint(b, "\n")
 153  		if len(doc.Options) > 0 {
 154  			fmt.Fprintf(b, "\nOptions\n")
 155  			for _, opt := range doc.Options {
 156  				fmt.Fprintf(b, "    %s", opt)
 157  			}
 158  			fmt.Fprint(b, "\n")
 159  		}
 160  	}
 161  
 162  	return b.String()
 163  }
 164  
 165  func (doc *Documentation) String() string {
 166  	return doc.Format(true)
 167  }
 168  
 169  // ExhaustiveTypeSwitch panics when called. It can be used to ensure
 170  // that type switches are exhaustive.
 171  func ExhaustiveTypeSwitch(v interface{}) {
 172  	panic(fmt.Sprintf("internal error: unhandled case %T", v))
 173  }
 174  
 175  // A directive is a comment of the form '//lint:<command>
 176  // [arguments...]'. It represents instructions to the static analysis
 177  // tool.
 178  type Directive struct {
 179  	Command   string
 180  	Arguments []string
 181  	Directive *ast.Comment
 182  	Node      ast.Node
 183  }
 184  
 185  func parseDirective(s string) (cmd string, args []string) {
 186  	if !strings.HasPrefix(s, "//lint:") {
 187  		return "", nil
 188  	}
 189  	s = strings.TrimPrefix(s, "//lint:")
 190  	fields := strings.Split(s, " ")
 191  	return fields[0], fields[1:]
 192  }
 193  
 194  // ParseDirectives extracts all directives from a list of Go files.
 195  func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
 196  	var dirs []Directive
 197  	for _, f := range files {
 198  		// OPT(dh): in our old code, we skip all the comment map work if we
 199  		// couldn't find any directives, benchmark if that's actually
 200  		// worth doing
 201  		cm := ast.NewCommentMap(fset, f, f.Comments)
 202  		for node, cgs := range cm {
 203  			for _, cg := range cgs {
 204  				for _, c := range cg.List {
 205  					if !strings.HasPrefix(c.Text, "//lint:") {
 206  						continue
 207  					}
 208  					cmd, args := parseDirective(c.Text)
 209  					d := Directive{
 210  						Command:   cmd,
 211  						Arguments: args,
 212  						Directive: c,
 213  						Node:      node,
 214  					}
 215  					dirs = append(dirs, d)
 216  				}
 217  			}
 218  		}
 219  	}
 220  	return dirs
 221  }
 222