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 }