diff --git a/config/config.yaml b/config/config.yaml index 6fa6f4c9..98388a82 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -1,10 +1,19 @@ # This is the default config file, you can override it by creating a config.yaml in the same directory # by default, all linters are enabled if you don't specify. You can disable them by setting enable to false # example1: disable staticcheck for org -# qbox: -# staticcheck: -# enable: false -# + qbox: + staticcheck: + enable: false + govet: + enable: true + command: go + args: [ "vet", "./..." ] + luacheck: + enable: true + workDir: "nginx/Lua" + command: luacheck + +# # example2: disable staticcheck for repo # qbox/kodo: # staticcheck: diff --git a/internal/linters/go/govet/govet.go b/internal/linters/go/govet/govet.go new file mode 100644 index 00000000..a63e8825 --- /dev/null +++ b/internal/linters/go/govet/govet.go @@ -0,0 +1,168 @@ +package govet + +import ( + "bytes" + "fmt" + "os/exec" + "regexp" + "strconv" + "strings" + + "github.com/google/go-github/v57/github" + "github.com/qiniu/x/log" + "github.com/qiniu/x/xlog" + "github.com/reviewbot/config" + "github.com/reviewbot/internal/linters" +) + +var lintName = "govet" + +func init() { + linters.RegisterCodeReviewHandler(lintName, goVetHandler) +} + +func goVetHandler(log *xlog.Logger, linterConfig config.Linter, _ linters.Agent, _ github.PullRequestEvent) (map[string][]linters.LinterOutput, error) { + executor, err := NewGoVetExecutor(linterConfig.WorkDir) + if err != nil { + log.Errorf("init govet executor failed: %v", err) + return nil, err + } + + if isEmpty(linterConfig.Args...) { + linterConfig.Args = append([]string{}, "vet", "./...") + } + + output, err := executor.Run(log, linterConfig.Args...) + if err != nil { + log.Errorf("govet run failed: %v", err) + return nil, err + } + + parsedOutput, err := executor.Parse(log, output) + if err != nil { + log.Errorf("govet parse output failed: %v", err) + return nil, err + } + return parsedOutput, nil +} + +func isEmpty(args ...string) bool { + for _, arg := range args { + if arg != "" { + return false + } + } + + return true +} + +// govet is an executor that knows how to execute govet commands. +type govet struct { + // dir is the location of this repo. + dir string + // govet is the path to the govet binary. + govet string +} + +func NewGoVetExecutor(dir string) (linters.Linter, error) { + g, err := exec.LookPath("go") + if err != nil { + return nil, err + } + return &govet{ + dir: dir, + govet: g, + }, nil +} + +func (e *govet) Run(log *xlog.Logger, args ...string) ([]byte, error) { + c := exec.Command(e.govet, args...) + c.Dir = e.dir + var out bytes.Buffer + c.Stdout = &out + c.Stderr = &out + err := c.Run() + if err != nil { + log.Errorf("go vet run with status: %v, mark and continue", err) + } else { + log.Infof("go vet succeeded") + } + + return out.Bytes(), nil +} + +func (e *govet) Parse(log *xlog.Logger, output []byte) (map[string][]linters.LinterOutput, error) { + return formatGoVetOutput(log, output) +} + +func formatGoVetOutput(log *xlog.Logger, out []byte) (map[string][]linters.LinterOutput, error) { + lines := strings.Split(string(out), "\n") + + var result = make(map[string][]linters.LinterOutput) + for _, line := range lines { + if line == "" { + continue + } + output, err := formatGoVetLine(line) + + if err != nil { + log.Warnf("unexpected govet output: %v", line) + // 不直接退出 + continue + } + + if output == nil { + continue + } + + if outs, ok := result[output.File]; !ok { + result[output.File] = []linters.LinterOutput{*output} + } else { + // remove duplicate + var existed bool + for _, v := range outs { + if v.File == output.File && v.Line == output.Line && + v.Column == output.Column && v.Message == output.Message { + existed = true + break + } + } + + if !existed { + result[output.File] = append(result[output.File], *output) + } + } + } + + return result, nil +} + +func formatGoVetLine(line string) (*linters.LinterOutput, error) { + pattern := `^(.*):(\d+):(\d+): (.*)$` + regex, err := regexp.Compile(pattern) + if err != nil { + log.Errorf("compile regex failed: %v", err) + return nil, err + } + matches := regex.FindStringSubmatch(line) + if len(matches) != 5 { + return nil, fmt.Errorf("unexpected format, original: %s", line) + } + + lineNumber, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return nil, err + } + + columnNumber, err := strconv.ParseInt(matches[3], 10, 64) + if err != nil { + return nil, err + } + + return &linters.LinterOutput{ + File: matches[1], + Line: int(lineNumber), + Column: int(columnNumber), + Message: matches[4], + }, nil +} diff --git a/internal/linters/lua/luacheck/luacheck.go b/internal/linters/lua/luacheck/luacheck.go new file mode 100644 index 00000000..f1617e2a --- /dev/null +++ b/internal/linters/lua/luacheck/luacheck.go @@ -0,0 +1,169 @@ +package luacheck + +import ( + "bytes" + "fmt" + "github.com/google/go-github/v57/github" + "github.com/qiniu/x/log" + "github.com/qiniu/x/xlog" + "github.com/reviewbot/config" + "github.com/reviewbot/internal/linters" + "os/exec" + "regexp" + "strconv" + "strings" +) + +var lintName = "luacheck" + +func init() { + linters.RegisterCodeReviewHandler(lintName, luaCheckHandler) +} + +func luaCheckHandler(log *xlog.Logger, linterConfig config.Linter, _ linters.Agent, _ github.PullRequestEvent) (map[string][]linters.LinterOutput, error) { + executor, err := NewGoVetExecutor(linterConfig.WorkDir) + if err != nil { + log.Errorf("init luacheck executor failed: %v", err) + return nil, err + } + + if isEmpty(linterConfig.Args...) { + linterConfig.Args = append([]string{}, ".") + } + + output, err := executor.Run(log, linterConfig.Args...) + if err != nil { + log.Errorf("luacheck run failed: %v", err) + return nil, err + } + + parsedOutput, err := executor.Parse(log, output) + if err != nil { + log.Errorf("luacheck parse output failed: %v", err) + return nil, err + } + + return parsedOutput, nil +} + +func isEmpty(args ...string) bool { + for _, arg := range args { + if arg != "" { + return false + } + } + + return true +} + +// luacheck is an executor that knows how to execute luacheck commands. +type luacheck struct { + // dir is the location of this repo. + dir string + // luacheck is the path to the luacheck binary. + luacheck string +} + +func NewGoVetExecutor(dir string) (linters.Linter, error) { + g, err := exec.LookPath("luacheck") + if err != nil { + return nil, err + } + return &luacheck{ + dir: dir, + luacheck: g, + }, nil +} + +func (e *luacheck) Run(log *xlog.Logger, args ...string) ([]byte, error) { + c := exec.Command(e.luacheck, args...) + c.Dir = e.dir + var out bytes.Buffer + c.Stdout = &out + c.Stderr = &out + err := c.Run() + if err != nil { + log.Errorf("luacheck run with status: %v, mark and continue", err) + } else { + log.Infof("luacheck succeeded") + } + + return out.Bytes(), nil +} + +func (e *luacheck) Parse(log *xlog.Logger, output []byte) (map[string][]linters.LinterOutput, error) { + return formatLuaCheckOutput(log, output) +} + +func formatLuaCheckOutput(log *xlog.Logger, out []byte) (map[string][]linters.LinterOutput, error) { + lines := strings.Split(string(out), "\n") + + var result = make(map[string][]linters.LinterOutput) + for _, line := range lines { + // " access/access.lua:1:14: accessing undefined variable ngx" + if line == "" { + continue + } + output, err := formatLuaCheckLine(line) + + if err != nil { + log.Warnf("unexpected luacheck output: %v", line) + // 不直接退出 + continue + } + + if output == nil { + continue + } + + if outs, ok := result[output.File]; !ok { + result[output.File] = []linters.LinterOutput{*output} + } else { + // remove duplicate + var existed bool + for _, v := range outs { + if v.File == output.File && v.Line == output.Line && + v.Column == output.Column && v.Message == output.Message { + existed = true + break + } + } + + if !existed { + result[output.File] = append(result[output.File], *output) + } + } + } + + return result, nil +} + +func formatLuaCheckLine(line string) (*linters.LinterOutput, error) { + pattern := `^(.*):(\d+):(\d+): (.*)$` + regex, err := regexp.Compile(pattern) + if err != nil { + log.Errorf("compile regex failed: %v", err) + return nil, err + } + matches := regex.FindStringSubmatch(line) + if len(matches) != 5 { + return nil, fmt.Errorf("unexpected format, original: %s", line) + } + + lineNumber, err := strconv.ParseInt(matches[2], 10, 64) + if err != nil { + return nil, err + } + + columnNumber, err := strconv.ParseInt(matches[3], 10, 64) + if err != nil { + return nil, err + } + + return &linters.LinterOutput{ + File: strings.TrimLeft(matches[1], " "), + Line: int(lineNumber), + Column: int(columnNumber), + Message: strings.ReplaceAll(strings.ReplaceAll(matches[4], "[0m", ""), "[0m", ""), + }, nil +} diff --git a/main.go b/main.go index 1679a0b8..8474b08f 100644 --- a/main.go +++ b/main.go @@ -20,18 +20,19 @@ import ( "errors" "flag" "fmt" - "net/http" - "os" - "github.com/google/go-github/v57/github" "github.com/qiniu/x/log" "github.com/reviewbot/config" "github.com/sirupsen/logrus" gitv2 "k8s.io/test-infra/prow/git/v2" + "net/http" + "os" // linters import _ "github.com/reviewbot/internal/linters/git-flow/rebase-suggestion" + _ "github.com/reviewbot/internal/linters/go/govet" _ "github.com/reviewbot/internal/linters/go/staticcheck" + _ "github.com/reviewbot/internal/linters/lua/luacheck" ) type options struct { @@ -85,6 +86,7 @@ func gatherOptions() options { func main() { o := gatherOptions() if err := o.Validate(); err != nil { + log.Fatalf("invalid options: %v", err) }