Skip to content

Commit

Permalink
feat: new shellcheck linter
Browse files Browse the repository at this point in the history
  • Loading branch information
wwcchh0123 committed Mar 14, 2024
1 parent 28845b9 commit b448349
Show file tree
Hide file tree
Showing 4 changed files with 237 additions and 0 deletions.
167 changes: 167 additions & 0 deletions internal/linters/shell/shellcheck/shellcheck.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,167 @@
package shellcheck

import (
"os/exec"
"regexp"
"strconv"
"strings"

"github.com/qiniu/reviewbot/internal/linters"
"github.com/qiniu/x/log"
"github.com/qiniu/x/xlog"
)

var lintName = "shellcheck"

func init() {
linters.RegisterPullRequestHandler(lintName, shellcheckHandler)
}

func shellcheckHandler(log *xlog.Logger, a linters.Agent) error {
/*
* Shellcheck can not support check directories recursively, it is usually used in conjunction with the "find" command
* Info:
* https://github.com/koalaman/shellcheck/issues/143
* https://github.com/koalaman/shellcheck/wiki/Recursiveness
*/
if linters.IsEmpty(a.LinterConfig.Args...) {
paths := FindShellFilesPath(a.LinterConfig.WorkDir)
a.LinterConfig.Args = append(a.LinterConfig.Args, paths...)
}

executor, err := NewShellcheckExecutor(a.LinterConfig.WorkDir)
if err != nil {
log.Errorf("init shellcheck executor failed: %v", err)
return err
}

output, err := executor.Run(log, a.LinterConfig.Args...)
if err != nil {
log.Errorf("shellcheck run failed: %v", err)
return err
}
parsedOutput, err := executor.Parse(log, output)
if err != nil {
log.Errorf("shellcheck parse output failed: %v", err)
return err
}

return linters.Report(log, a, parsedOutput)
}

func FindShellFilesPath(dir string) []string {
c := exec.Command("find", ".", "-type", "f", "-name", "*.sh")
c.Dir = dir
out, err := c.Output()
if err != nil {
log.Errorf("find the path of shell files failed, err: %v", err)
}
fps := strings.Split(string(out), "\n")
for i := 0; i < len(fps); i++ {
fps[i] = strings.TrimPrefix(fps[i], "./")
}
return fps
}

type Shellcheck struct {
dir string
shellcheck string
execute func(dir, command string, args ...string) ([]byte, error)
}

func NewShellcheckExecutor(dir string) (linters.Linter, error) {
log.Infof("shellcheck executor init")
g, err := exec.LookPath("shellcheck")
if err != nil {
return nil, err
}
return &Shellcheck{
dir: dir,
shellcheck: g,
execute: func(dir, command string, args ...string) ([]byte, error) {
c := exec.Command(command, args...)
c.Dir = dir
log.Printf("final command: %v \n", c)
return c.Output()
},
}, nil
}

func (g *Shellcheck) Run(log *xlog.Logger, args ...string) ([]byte, error) {
b, err := g.execute(g.dir, g.shellcheck, args...)
if err != nil {
log.Errorf("shellcheck run with status: %v, mark and continue", err)
} else {
log.Infof("shellcheck running succeeded")
}
return b, nil
}

func (g *Shellcheck) Parse(log *xlog.Logger, output []byte) (map[string][]linters.LinterOutput, error) {
log.Infof("shellcheck output is being parsed")
return formatShellcheckOutput(output)
}

func formatShellcheckOutput(output []byte) (map[string][]linters.LinterOutput, error) {
//format for tty format output
var result = make(map[string][]linters.LinterOutput)
lines := strings.Split(string(output), "\n")
var filename string
var location int
fileErr := make(map[string]map[int][]string)
re := regexp.MustCompile(`In (\S+) line (\d+):`)
for _, line := range lines {
if _, ok := fileErr[filename]; !ok {
fileErr[filename] = make(map[int][]string)
}
if strings.HasPrefix(line, "In") {
match := re.FindStringSubmatch(line)
if len(match) >= 3 {
filename = strings.TrimPrefix(match[1], "./")
location, _ = strconv.Atoi(match[2])
}

} else if strings.HasPrefix(line, "For more information:") {
break
} else {
fileErr[filename][location] = append(fileErr[filename][location], line)
}
}

for filename, errs := range fileErr {
for locationLine, msgs := range errs {
sendMsg := strings.Join(msgs, "\n")
sendMsg = "Is there some potential issue with your shell code?" + "\n```\n" + sendMsg + "\n```\n"
addShellcheckOutput(result, filename, locationLine, sendMsg)
}
}

return result, nil
}

func addShellcheckOutput(result map[string][]linters.LinterOutput, filename string, line int, message string) {
output := &linters.LinterOutput{
File: filename,
Line: int(line),
Column: int(line),
Message: message,
}

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)
}
}

}
62 changes: 62 additions & 0 deletions internal/linters/shell/shellcheck/shellcheck_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,62 @@
package shellcheck

import (
"os"
"testing"

"github.com/qiniu/reviewbot/internal/linters"
)

func TestShell(t *testing.T) {
content, err := os.ReadFile("./test.txt")
if err != nil {
t.Errorf("open file failed ,the err is : %v", err)
return
}
expect := []linters.LinterOutput{
{
File: "lua-ut.sh",
Line: 22,
Column: 22,
Message: "",
},
{
File: "util.sh",
Line: 13,
Column: 13,
Message: "",
},
}
tc := []struct {
input []byte
expected []linters.LinterOutput
}{
{
content,
expect,
},
}
for _, c := range tc {
outputMap, err := formatShellcheckOutput([]byte(c.input))
for _, outputs := range outputMap {
for _, output := range outputs {

if err != nil {
t.Errorf("unexpected error: %v", err)
}

if output.File == "util.sh" {
if output.File != c.expected[1].File || output.Line != c.expected[1].Line || output.Column != c.expected[1].Column {
t.Errorf("expected: %v, got: %v", c.expected[1], output)
}
} else {
if output.File != c.expected[0].File || output.Line != c.expected[0].Line || output.Column != c.expected[0].Column {
t.Errorf("expected: %v, got: %v", c.expected[0], output)
}
}

}

}
}
}
7 changes: 7 additions & 0 deletions internal/linters/shell/shellcheck/testdata/test.txt
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
In lua-ut.sh line 22:
for svc in ${svcs[@]}
^--------^ SC2068 (error): Double quote array expansions to avoid re-splitting elements.

In util.sh line 13:
echo -e "[$(date +'%Y-%m-%dT%H:%M:%S.%N%z')] WARN: $@" >&2
^-- SC2145 (error): Argument mixes string and array. Use * or separate argument.
1 change: 1 addition & 0 deletions main.go
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ import (
_ "github.com/qiniu/reviewbot/internal/linters/go/golangci_lint"
_ "github.com/qiniu/reviewbot/internal/linters/go/staticcheck"
_ "github.com/qiniu/reviewbot/internal/linters/lua/luacheck"
_ "github.com/qiniu/reviewbot/internal/linters/shell/shellcheck"
)

type options struct {
Expand Down

0 comments on commit b448349

Please sign in to comment.