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

     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  	"flag"
     7  	"fmt"
     8  	"go/ast"
     9  	"go/build"
    10  	"go/token"
    11  	"regexp"
    12  	"strconv"
    13  	"strings"
    14  
    15  	"github.com/amarpal/go-tools/analysis/facts/tokenfile"
    16  	"golang.org/x/tools/go/analysis"
    17  )
    18  
    19  // Analyzer wraps a go/analysis.Analyzer and provides structured documentation.
    20  type Analyzer struct {
    21  	// The analyzer's documentation. Unlike go/analysis.Analyzer.Doc,
    22  	// this field is structured, providing access to severity, options
    23  	// etc.
    24  	Doc      *Documentation
    25  	Analyzer *analysis.Analyzer
    26  }
    27  
    28  func (a *Analyzer) initialize() {
    29  	a.Analyzer.Doc = a.Doc.String()
    30  	if a.Analyzer.Flags.Usage == nil {
    31  		fs := flag.NewFlagSet("", flag.PanicOnError)
    32  		fs.Var(newVersionFlag(), "go", "Target Go version")
    33  		a.Analyzer.Flags = *fs
    34  	}
    35  	a.Analyzer.Requires = append(a.Analyzer.Requires, tokenfile.Analyzer)
    36  }
    37  
    38  func InitializeAnalyzer(a *Analyzer) *Analyzer {
    39  	a.Analyzer.Doc = a.Doc.String()
    40  	a.Analyzer.URL = "https://staticcheck.dev/docs/checks/#" + a.Analyzer.Name
    41  	if a.Analyzer.Flags.Usage == nil {
    42  		fs := flag.NewFlagSet("", flag.PanicOnError)
    43  		fs.Var(newVersionFlag(), "go", "Target Go version")
    44  		a.Analyzer.Flags = *fs
    45  	}
    46  	a.Analyzer.Requires = append(a.Analyzer.Requires, tokenfile.Analyzer)
    47  	return a
    48  }
    49  
    50  // InitializeAnalyzers takes a map of documentation and a map of go/analysis.Analyzers and returns a slice of Analyzers.
    51  // The map keys are the analyzer names.
    52  func InitializeAnalyzers(docs map[string]*Documentation, analyzers map[string]*analysis.Analyzer) []*Analyzer {
    53  	out := make([]*Analyzer, 0, len(analyzers))
    54  	for k, v := range analyzers {
    55  		v.Name = k
    56  		v.URL = "https://staticcheck.dev/docs/checks/#" + k
    57  		a := &Analyzer{
    58  			Doc:      docs[k],
    59  			Analyzer: v,
    60  		}
    61  		a.initialize()
    62  		out = append(out, a)
    63  	}
    64  	return out
    65  }
    66  
    67  // Severity describes the severity of diagnostics reported by an analyzer.
    68  type Severity int
    69  
    70  const (
    71  	SeverityNone Severity = iota
    72  	SeverityError
    73  	SeverityDeprecated
    74  	SeverityWarning
    75  	SeverityInfo
    76  	SeverityHint
    77  )
    78  
    79  // MergeStrategy sets how merge mode should behave for diagnostics of an analyzer.
    80  type MergeStrategy int
    81  
    82  const (
    83  	MergeIfAny MergeStrategy = iota
    84  	MergeIfAll
    85  )
    86  
    87  type RawDocumentation struct {
    88  	Title      string
    89  	Text       string
    90  	Before     string
    91  	After      string
    92  	Since      string
    93  	NonDefault bool
    94  	Options    []string
    95  	Severity   Severity
    96  	MergeIf    MergeStrategy
    97  }
    98  
    99  type Documentation struct {
   100  	Title string
   101  	Text  string
   102  
   103  	TitleMarkdown string
   104  	TextMarkdown  string
   105  
   106  	Before     string
   107  	After      string
   108  	Since      string
   109  	NonDefault bool
   110  	Options    []string
   111  	Severity   Severity
   112  	MergeIf    MergeStrategy
   113  }
   114  
   115  func Markdownify(m map[string]*RawDocumentation) map[string]*Documentation {
   116  	out := make(map[string]*Documentation, len(m))
   117  	for k, v := range m {
   118  		out[k] = &Documentation{
   119  			Title: strings.TrimSpace(stripMarkdown(v.Title)),
   120  			Text:  strings.TrimSpace(stripMarkdown(v.Text)),
   121  
   122  			TitleMarkdown: strings.TrimSpace(toMarkdown(v.Title)),
   123  			TextMarkdown:  strings.TrimSpace(toMarkdown(v.Text)),
   124  
   125  			Before:     strings.TrimSpace(v.Before),
   126  			After:      strings.TrimSpace(v.After),
   127  			Since:      v.Since,
   128  			NonDefault: v.NonDefault,
   129  			Options:    v.Options,
   130  			Severity:   v.Severity,
   131  			MergeIf:    v.MergeIf,
   132  		}
   133  	}
   134  	return out
   135  }
   136  
   137  func toMarkdown(s string) string {
   138  	return strings.NewReplacer(`\'`, "`", `\"`, "`").Replace(s)
   139  }
   140  
   141  func stripMarkdown(s string) string {
   142  	return strings.NewReplacer(`\'`, "", `\"`, "'").Replace(s)
   143  }
   144  
   145  func (doc *Documentation) Format(metadata bool) string {
   146  	return doc.format(false, metadata)
   147  }
   148  
   149  func (doc *Documentation) FormatMarkdown(metadata bool) string {
   150  	return doc.format(true, metadata)
   151  }
   152  
   153  func (doc *Documentation) format(markdown bool, metadata bool) string {
   154  	b := &strings.Builder{}
   155  	if markdown {
   156  		fmt.Fprintf(b, "%s\n\n", doc.TitleMarkdown)
   157  		if doc.Text != "" {
   158  			fmt.Fprintf(b, "%s\n\n", doc.TextMarkdown)
   159  		}
   160  	} else {
   161  		fmt.Fprintf(b, "%s\n\n", doc.Title)
   162  		if doc.Text != "" {
   163  			fmt.Fprintf(b, "%s\n\n", doc.Text)
   164  		}
   165  	}
   166  
   167  	if doc.Before != "" {
   168  		fmt.Fprintln(b, "Before:")
   169  		fmt.Fprintln(b, "")
   170  		for _, line := range strings.Split(doc.Before, "\n") {
   171  			fmt.Fprint(b, "    ", line, "\n")
   172  		}
   173  		fmt.Fprintln(b, "")
   174  		fmt.Fprintln(b, "After:")
   175  		fmt.Fprintln(b, "")
   176  		for _, line := range strings.Split(doc.After, "\n") {
   177  			fmt.Fprint(b, "    ", line, "\n")
   178  		}
   179  		fmt.Fprintln(b, "")
   180  	}
   181  
   182  	if metadata {
   183  		fmt.Fprint(b, "Available since\n    ")
   184  		if doc.Since == "" {
   185  			fmt.Fprint(b, "unreleased")
   186  		} else {
   187  			fmt.Fprintf(b, "%s", doc.Since)
   188  		}
   189  		if doc.NonDefault {
   190  			fmt.Fprint(b, ", non-default")
   191  		}
   192  		fmt.Fprint(b, "\n")
   193  		if len(doc.Options) > 0 {
   194  			fmt.Fprintf(b, "\nOptions\n")
   195  			for _, opt := range doc.Options {
   196  				fmt.Fprintf(b, "    %s", opt)
   197  			}
   198  			fmt.Fprint(b, "\n")
   199  		}
   200  	}
   201  
   202  	return b.String()
   203  }
   204  
   205  func (doc *Documentation) String() string {
   206  	return doc.Format(true)
   207  }
   208  
   209  func newVersionFlag() flag.Getter {
   210  	tags := build.Default.ReleaseTags
   211  	v := tags[len(tags)-1][2:]
   212  	version := new(VersionFlag)
   213  	if err := version.Set(v); err != nil {
   214  		panic(fmt.Sprintf("internal error: %s", err))
   215  	}
   216  	return version
   217  }
   218  
   219  type VersionFlag int
   220  
   221  func (v *VersionFlag) String() string {
   222  	return fmt.Sprintf("1.%d", *v)
   223  }
   224  
   225  var goVersionRE = regexp.MustCompile(`^(?:go)?1.(\d+).*$`)
   226  
   227  // ParseGoVersion parses Go versions of the form 1.M, 1.M.N, or 1.M.NrcR, with an optional "go" prefix. It assumes that
   228  // versions have already been verified and are valid.
   229  func ParseGoVersion(s string) (int, bool) {
   230  	m := goVersionRE.FindStringSubmatch(s)
   231  	if m == nil {
   232  		return 0, false
   233  	}
   234  	n, err := strconv.Atoi(m[1])
   235  	if err != nil {
   236  		return 0, false
   237  	}
   238  	return n, true
   239  }
   240  
   241  func (v *VersionFlag) Set(s string) error {
   242  	n, ok := ParseGoVersion(s)
   243  	if !ok {
   244  		return fmt.Errorf("invalid Go version: %q", s)
   245  	}
   246  	*v = VersionFlag(n)
   247  	return nil
   248  }
   249  
   250  func (v *VersionFlag) Get() interface{} {
   251  	return int(*v)
   252  }
   253  
   254  // ExhaustiveTypeSwitch panics when called. It can be used to ensure
   255  // that type switches are exhaustive.
   256  func ExhaustiveTypeSwitch(v interface{}) {
   257  	panic(fmt.Sprintf("internal error: unhandled case %T", v))
   258  }
   259  
   260  // A directive is a comment of the form '//lint:<command>
   261  // [arguments...]'. It represents instructions to the static analysis
   262  // tool.
   263  type Directive struct {
   264  	Command   string
   265  	Arguments []string
   266  	Directive *ast.Comment
   267  	Node      ast.Node
   268  }
   269  
   270  func parseDirective(s string) (cmd string, args []string) {
   271  	if !strings.HasPrefix(s, "//lint:") {
   272  		return "", nil
   273  	}
   274  	s = strings.TrimPrefix(s, "//lint:")
   275  	fields := strings.Split(s, " ")
   276  	return fields[0], fields[1:]
   277  }
   278  
   279  // ParseDirectives extracts all directives from a list of Go files.
   280  func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
   281  	var dirs []Directive
   282  	for _, f := range files {
   283  		// OPT(dh): in our old code, we skip all the comment map work if we
   284  		// couldn't find any directives, benchmark if that's actually
   285  		// worth doing
   286  		cm := ast.NewCommentMap(fset, f, f.Comments)
   287  		for node, cgs := range cm {
   288  			for _, cg := range cgs {
   289  				for _, c := range cg.List {
   290  					if !strings.HasPrefix(c.Text, "//lint:") {
   291  						continue
   292  					}
   293  					cmd, args := parseDirective(c.Text)
   294  					d := Directive{
   295  						Command:   cmd,
   296  						Arguments: args,
   297  						Directive: c,
   298  						Node:      node,
   299  					}
   300  					dirs = append(dirs, d)
   301  				}
   302  			}
   303  		}
   304  	}
   305  	return dirs
   306  }