mirror of
https://github.com/woodpecker-ci/woodpecker.git
synced 2024-12-04 15:46:30 +00:00
c28f7cb29f
Initial part of #435
198 lines
4.2 KiB
Go
198 lines
4.2 KiB
Go
// Package lint provides abstractions on top of go/analysis.
|
|
package lint
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"go/ast"
|
|
"go/build"
|
|
"go/token"
|
|
"strconv"
|
|
"strings"
|
|
|
|
"golang.org/x/tools/go/analysis"
|
|
)
|
|
|
|
type Analyzer struct {
|
|
// The analyzer's documentation. Unlike go/analysis.Analyzer.Doc,
|
|
// this field is structured, providing access to severity, options
|
|
// etc.
|
|
Doc *Documentation
|
|
Analyzer *analysis.Analyzer
|
|
}
|
|
|
|
func (a *Analyzer) initialize() {
|
|
a.Analyzer.Doc = a.Doc.String()
|
|
if a.Analyzer.Flags.Usage == nil {
|
|
fs := flag.NewFlagSet("", flag.PanicOnError)
|
|
fs.Var(newVersionFlag(), "go", "Target Go version")
|
|
a.Analyzer.Flags = *fs
|
|
}
|
|
}
|
|
|
|
func InitializeAnalyzers(docs map[string]*Documentation, analyzers map[string]*analysis.Analyzer) []*Analyzer {
|
|
out := make([]*Analyzer, 0, len(analyzers))
|
|
for k, v := range analyzers {
|
|
v.Name = k
|
|
a := &Analyzer{
|
|
Doc: docs[k],
|
|
Analyzer: v,
|
|
}
|
|
a.initialize()
|
|
out = append(out, a)
|
|
}
|
|
return out
|
|
}
|
|
|
|
type Severity int
|
|
|
|
const (
|
|
SeverityNone Severity = iota
|
|
SeverityError
|
|
SeverityDeprecated
|
|
SeverityWarning
|
|
SeverityInfo
|
|
SeverityHint
|
|
)
|
|
|
|
type Documentation struct {
|
|
Title string
|
|
Text string
|
|
Since string
|
|
NonDefault bool
|
|
Options []string
|
|
Severity Severity
|
|
}
|
|
|
|
func Markdownify(m map[string]*Documentation) map[string]*Documentation {
|
|
for _, v := range m {
|
|
v.Title = toMarkdown(v.Title)
|
|
v.Text = toMarkdown(v.Text)
|
|
}
|
|
return m
|
|
}
|
|
|
|
func toMarkdown(s string) string {
|
|
return strings.ReplaceAll(s, `\'`, "`")
|
|
}
|
|
|
|
func (doc *Documentation) String() string {
|
|
if doc == nil {
|
|
return "Error: No documentation."
|
|
}
|
|
|
|
b := &strings.Builder{}
|
|
fmt.Fprintf(b, "%s\n\n", doc.Title)
|
|
if doc.Text != "" {
|
|
fmt.Fprintf(b, "%s\n\n", doc.Text)
|
|
}
|
|
fmt.Fprint(b, "Available since\n ")
|
|
if doc.Since == "" {
|
|
fmt.Fprint(b, "unreleased")
|
|
} else {
|
|
fmt.Fprintf(b, "%s", doc.Since)
|
|
}
|
|
if doc.NonDefault {
|
|
fmt.Fprint(b, ", non-default")
|
|
}
|
|
fmt.Fprint(b, "\n")
|
|
if len(doc.Options) > 0 {
|
|
fmt.Fprintf(b, "\nOptions\n")
|
|
for _, opt := range doc.Options {
|
|
fmt.Fprintf(b, " %s", opt)
|
|
}
|
|
fmt.Fprint(b, "\n")
|
|
}
|
|
return b.String()
|
|
}
|
|
|
|
func newVersionFlag() flag.Getter {
|
|
tags := build.Default.ReleaseTags
|
|
v := tags[len(tags)-1][2:]
|
|
version := new(VersionFlag)
|
|
if err := version.Set(v); err != nil {
|
|
panic(fmt.Sprintf("internal error: %s", err))
|
|
}
|
|
return version
|
|
}
|
|
|
|
type VersionFlag int
|
|
|
|
func (v *VersionFlag) String() string {
|
|
return fmt.Sprintf("1.%d", *v)
|
|
}
|
|
|
|
func (v *VersionFlag) Set(s string) error {
|
|
if len(s) < 3 {
|
|
return fmt.Errorf("invalid Go version: %q", s)
|
|
}
|
|
if s[0] != '1' {
|
|
return fmt.Errorf("invalid Go version: %q", s)
|
|
}
|
|
if s[1] != '.' {
|
|
return fmt.Errorf("invalid Go version: %q", s)
|
|
}
|
|
i, err := strconv.Atoi(s[2:])
|
|
if err != nil {
|
|
return fmt.Errorf("invalid Go version: %q", s)
|
|
}
|
|
*v = VersionFlag(i)
|
|
return nil
|
|
}
|
|
|
|
func (v *VersionFlag) Get() interface{} {
|
|
return int(*v)
|
|
}
|
|
|
|
// ExhaustiveTypeSwitch panics when called. It can be used to ensure
|
|
// that type switches are exhaustive.
|
|
func ExhaustiveTypeSwitch(v interface{}) {
|
|
panic(fmt.Sprintf("internal error: unhandled case %T", v))
|
|
}
|
|
|
|
// A directive is a comment of the form '//lint:<command>
|
|
// [arguments...]'. It represents instructions to the static analysis
|
|
// tool.
|
|
type Directive struct {
|
|
Command string
|
|
Arguments []string
|
|
Directive *ast.Comment
|
|
Node ast.Node
|
|
}
|
|
|
|
func parseDirective(s string) (cmd string, args []string) {
|
|
if !strings.HasPrefix(s, "//lint:") {
|
|
return "", nil
|
|
}
|
|
s = strings.TrimPrefix(s, "//lint:")
|
|
fields := strings.Split(s, " ")
|
|
return fields[0], fields[1:]
|
|
}
|
|
|
|
func ParseDirectives(files []*ast.File, fset *token.FileSet) []Directive {
|
|
var dirs []Directive
|
|
for _, f := range files {
|
|
// OPT(dh): in our old code, we skip all the comment map work if we
|
|
// couldn't find any directives, benchmark if that's actually
|
|
// worth doing
|
|
cm := ast.NewCommentMap(fset, f, f.Comments)
|
|
for node, cgs := range cm {
|
|
for _, cg := range cgs {
|
|
for _, c := range cg.List {
|
|
if !strings.HasPrefix(c.Text, "//lint:") {
|
|
continue
|
|
}
|
|
cmd, args := parseDirective(c.Text)
|
|
d := Directive{
|
|
Command: cmd,
|
|
Arguments: args,
|
|
Directive: c,
|
|
Node: node,
|
|
}
|
|
dirs = append(dirs, d)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return dirs
|
|
}
|