src

Go monorepo.
git clone git://code.dwrz.net/src
Log | Files | Refs

lint.go (5308B)


      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.SplitSeq(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.SplitSeq(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 any) {
    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 }