Skip to content

Commit

Permalink
go vet linter
Browse files Browse the repository at this point in the history
  • Loading branch information
xwen-winnie committed Feb 1, 2024
1 parent 45b1725 commit b911883
Show file tree
Hide file tree
Showing 4 changed files with 356 additions and 7 deletions.
17 changes: 13 additions & 4 deletions config/config.yaml
Original file line number Diff line number Diff line change
@@ -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:
Expand Down
168 changes: 168 additions & 0 deletions internal/linters/go/govet/govet.go
Original file line number Diff line number Diff line change
@@ -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
}
170 changes: 170 additions & 0 deletions internal/linters/lua/luacheck/luacheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
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], "\x1b[0m", ""), "\x1b[0m", ""),
Message: strings.ReplaceAll(strings.ReplaceAll(matches[4], "\x1b[0m\x1b[1m", ""), "\x1b[0m", ""),
}, nil
}
8 changes: 5 additions & 3 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -85,6 +86,7 @@ func gatherOptions() options {
func main() {
o := gatherOptions()
if err := o.Validate(); err != nil {

log.Fatalf("invalid options: %v", err)
}

Expand Down

0 comments on commit b911883

Please sign in to comment.