diff --git a/.github/workflows/go.yml b/.github/workflows/go.yml index f03bcba8..4ec29bad 100644 --- a/.github/workflows/go.yml +++ b/.github/workflows/go.yml @@ -21,11 +21,16 @@ jobs: with: go-version: '1.21.5' + - name: install linters + run: | + go install honnef.co/go/tools/cmd/staticcheck@latest + sudo apt install cppcheck + - name: Run go vet run: go vet ./... - name: Run go fmt - run: go fmt ./... + run: go fmt ./... - name: Run go test - run: go test -v ./... + run: go test -v ./... - name: Run go build run: go build ./... \ No newline at end of file diff --git a/Dockerfile b/Dockerfile index 342290f9..add6dfb2 100644 --- a/Dockerfile +++ b/Dockerfile @@ -11,8 +11,6 @@ RUN CGO_ENABLED=0 GOOS=linux go build -o /reviewbot . # install staticcheck lint tools RUN GOPATH=/go go install honnef.co/go/tools/cmd/staticcheck@2023.1.6 -RUN apt-get update && apt-get install -y cppcheck - FROM aslan-spock-register.qiniu.io/library/ubuntu:22.04 as runner RUN apt-get update && apt-get install -y ca-certificates \ @@ -26,6 +24,8 @@ RUN apt-get update && apt-get install -y luarocks \ && luarocks install luacheck \ && rm -rf /var/lib/apt/lists/* +# install cppcheck lint tools +RUN apt-get update && apt-get install -y cppcheck # 设置golang环境 ENV GOLANG_DOWNLOAD_URL https://go.dev/dl/go1.21.5.linux-amd64.tar.gz diff --git a/config/config.yaml b/config/config.yaml index 2c324044..94114717 100644 --- a/config/config.yaml +++ b/config/config.yaml @@ -32,14 +32,8 @@ qbox/3rd-ffmpeg: # github repository name, which will override the settings in q cppcheck: # repo specific settings will override the settings in qbox org enable: true command: cppcheck - args: [] - run_if_changed: [".*\\.c$"] # only run if changed files match the regex jiangpeipei327/reviewbottest: # github repository name, which will override the settings in qbox org cppcheck: # repo specific settings will override the settings in qbox org enable: true - command: cppcheck - args: [] - run_if_changed: ["*.cpp"] # only run if changed files match the regex - staticcheck: # repo specific settings will override the settings in qbox org - enable: false \ No newline at end of file + command: cppcheck \ No newline at end of file diff --git a/internal/linters/c/cppcheck/README.md b/internal/linters/c/cppcheck/README.md new file mode 100644 index 00000000..9d0a92ff --- /dev/null +++ b/internal/linters/c/cppcheck/README.md @@ -0,0 +1,33 @@ +# cppcheck 语法: +cppcheck [选项] [文件或路径]
+如果给出目录而不是文件名,则从给定的目录递归检查 *.cpp、*.cxx、*.cc、*.c++、*.c、*.ipp、*.ixx、*.tpp 和 *.txx 文件。
+Options:
+-q, --quiet 不显示进度报告。
+--template='' 设置错误消息的格式。 可用字段: +  {file}: file name
+  {line}: line number
+  {column}: column number
+  {callstack}: 显示调用堆栈。 示例: [file.c:1] -> [file.c:100]
+  {inconclusive:text}: 如果警告不确定,则写入文本
+  {severity}: severity
+  {message}: warning message
+  {id}: warning id
+  {cwe}: CWE id(常见弱点枚举)
+  {code}: 显示真实代码
+  \t: insert tab
+  \n: insert newline
+  \r: 插入回车符
+--enable= 启用附加检查。 可用的 id 是:
+  * all:启用所有检查。 建议仅在扫描整个程序时使用--enable=all,因为这会启用 unusedFunction。
+  * warning:启用警告消息
+  * style:启用所有编码风格检查。 所有具有“样式”、“警告”、“性能”和“可移植性”严重性的消息均已启用。
+  * performance:启用性能消息
+  * portability:启用可移植性消息
+  * information:Enable information messages
+  * unusedFunction:检查是否有未使用的功能。 建议仅在扫描整个程序时启用此功能。
+  * missingInclude:如果缺少包含内容,则发出警告。
+  如果用逗号分隔,可以给出多个 id。
+Example usage:
+递归检查当前目录,不打印进度,默认启用错误检查,输出格式,文件:行:列: 报错信息
+cppcheck --quiet --template='{file}:{line}:{column}: {message}' ./
+也可以通过设置 --enable 开启其他的检查 diff --git a/internal/linters/c/cppcheck/cppcheck.go b/internal/linters/c/cppcheck/cppcheck.go index d466de09..7aa8425a 100644 --- a/internal/linters/c/cppcheck/cppcheck.go +++ b/internal/linters/c/cppcheck/cppcheck.go @@ -1,85 +1,41 @@ package cppcheck import ( - "bytes" - "os/exec" + "strings" - "github.com/google/go-github/v57/github" - "github.com/qiniu/x/log" + "github.com/qiniu/reviewbot/internal/linters" "github.com/qiniu/x/xlog" - "github.com/reviewbot/config" - "github.com/reviewbot/internal/linters" ) -var lintName = "cppcheck" +// refer to https://cppcheck.sourceforge.io/ +var linterName = "cppcheck" func init() { - linters.RegisterCodeReviewHandler(lintName, cppcheckHandler) + linters.RegisterPullRequestHandler(linterName, cppcheckHandler) } -func cppcheckHandler(log *xlog.Logger, linterConfig config.Linter, agent linters.Agent, event github.PullRequestEvent) (map[string][]linters.LinterOutput, error) { - executor, err := NewCppcheckExecutor(linterConfig.WorkDir) - if err != nil { - log.Errorf("init cppcheck executor failed: %v", err) - return nil, err - } - - if linters.IsEmpty(linterConfig.Args...) { - linterConfig.Args = append([]string{}, "--quiet", "--template='{file}:{line}:{column}: {message}'", "./") - } +func cppcheckHandler(log *xlog.Logger, a linters.Agent) error { - output, err := executor.Run(log, linterConfig.Args...) - if err != nil { - log.Errorf("cppcheck run failed: %v", err) - return nil, err - } - - parsedOutput, err := executor.Parse(log, output) - if err != nil { - log.Errorf("cppcheck parse output failed: %v", err) - return nil, err + if linters.IsEmpty(a.LinterConfig.Args...) { + a.LinterConfig.Args = append([]string{}, "--quiet", "--template='{file}:{line}:{column}: {message}'", "./") } - return parsedOutput, nil -} - -// Cppcheck is an executor that knows how to execute Cppcheck commands. -type Cppcheck struct { - // dir is the location of this repo. - dir string - // cppcheck is the path to the cppcheck binary. - cppcheck string + // return linters.GeneralHandler(log, a, linters.GeneralParse) + return linters.GeneralHandler(log, a, func(l *xlog.Logger, output []byte) (map[string][]linters.LinterOutput, error) { + return linters.Parse(log, output, cppcheckParser) + }) } -// NewCppcheckExecutor returns a new executor that knows how to execute cppcheck commands -// TODO: with config -func NewCppcheckExecutor(dir string) (linters.Linter, error) { - log.Infof("cppcheck executor init") - g, err := exec.LookPath("cppcheck") +func cppcheckParser(line string) (*linters.LinterOutput, error) { + lineResult, err := linters.GeneralLineParser(line) if err != nil { return nil, err - } - return &Cppcheck{ - dir: dir, - cppcheck: g, - }, nil -} -func (e *Cppcheck) Run(log *xlog.Logger, args ...string) ([]byte, error) { - c := exec.Command(e.cppcheck, args...) - c.Dir = e.dir - var out bytes.Buffer - c.Stdout = &out - c.Stderr = &out - err := c.Run() - if err != nil { - log.Errorf("cppcheck run with status: %v, mark and continue", err) - } else { - log.Infof("cppcheck succeeded") } - return out.Bytes(), nil -} - -func (e *Cppcheck) Parse(log *xlog.Logger, output []byte) (map[string][]linters.LinterOutput, error) { - return linters.FormatLinterOutput(log, output, linters.FormatLinterLine) + return &linters.LinterOutput{ + File: strings.TrimLeft(lineResult.File, "'"), + Line: lineResult.Line, + Column: lineResult.Column, + Message: strings.TrimRight(lineResult.Message, "'"), + }, nil } diff --git a/internal/linters/c/cppcheck/cppcheck_test.go b/internal/linters/c/cppcheck/cppcheck_test.go new file mode 100644 index 00000000..6bc9c33e --- /dev/null +++ b/internal/linters/c/cppcheck/cppcheck_test.go @@ -0,0 +1,52 @@ +package cppcheck + +import ( + "testing" + + "github.com/qiniu/reviewbot/internal/linters" +) + +func TestFormatCppCheckLine(t *testing.T) { + tc := []struct { + input string + expected *linters.LinterOutput + }{ + {"'cppcheck_test.c:6:7: Array 'a[10]' accessed at index 10, which is out of bounds.'", &linters.LinterOutput{ + File: "cppcheck_test.c", + Line: 6, + Column: 7, + Message: "Array 'a[10]' accessed at index 10, which is out of bounds.", + }}, + {"'fdk-aac-0.1.4-libMpegTPDec-src/tpdec_asc.cpp:1198:23: Variable 'bitsAvailable' is reassigned a value before the old one has been used.'", &linters.LinterOutput{ + File: "fdk-aac-0.1.4-libMpegTPDec-src/tpdec_asc.cpp", + Line: 1198, + Column: 23, + Message: "Variable 'bitsAvailable' is reassigned a value before the old one has been used.", + }}, + {"Checking test/tpdec_adts.c", nil}, + } + + for _, c := range tc { + + output, err := cppcheckParser(c.input) + + if output == nil { + if c.expected != nil { + t.Errorf("expected: %v, got: %v", c.expected, output) + } + continue + } + + if c.expected == nil && output != nil { + t.Errorf("expected error, got: %v", output) + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + } + + if output.File != c.expected.File || output.Line != c.expected.Line || output.Column != c.expected.Column || output.Message != c.expected.Message { + t.Errorf("expected: %v, got: %v", c.expected, output) + } + } +} diff --git a/internal/linters/linters.go b/internal/linters/linters.go index 9a0420c0..87910ce0 100644 --- a/internal/linters/linters.go +++ b/internal/linters/linters.go @@ -143,7 +143,7 @@ func ExecRun(workDir, command string, args ...string) ([]byte, error) { c := exec.Command(g, args...) c.Dir = workDir - return c.Output() + return c.CombinedOutput() } // GeneralParse parses the output of a linter command. diff --git a/internal/linters/linters_test.go b/internal/linters/linters_test.go index 73676495..c52bbe5e 100644 --- a/internal/linters/linters_test.go +++ b/internal/linters/linters_test.go @@ -48,16 +48,6 @@ func TestFormatStaticcheckLine(t *testing.T) { for _, c := range tc { output, err := GeneralLineParser(c.input) - if c.expected == nil && output != nil { - t.Errorf("expected error, got: %v", output) - } else { - continue - } - - if err != nil { - t.Errorf("unexpected error: %v", err) - } - if output == nil { if c.expected != nil { t.Errorf("expected: %v, got: %v", c.expected, output) @@ -65,6 +55,13 @@ func TestFormatStaticcheckLine(t *testing.T) { continue } + if c.expected == nil && output != nil { + t.Errorf("expected error, got: %v", output) + } + + if err != nil { + t.Errorf("unexpected error: %v", err) + } if output.File != c.expected.File || output.Line != c.expected.Line || output.Column != c.expected.Column || output.Message != c.expected.Message { t.Errorf("expected: %v, got: %v", c.expected, output) } @@ -110,3 +107,21 @@ func TestIsGopAutoGeneratedFile(t *testing.T) { } } } + +func TestExecRun(t *testing.T) { + tcs := []struct { + command string + args []string + expected string + }{ + {"cppcheck", append([]string{}, "--quiet", "--template='{file}:{line}:{column}: {message}'", "./"), "'cppcheck_test.c:6:7: Array 'a[10]' accessed at index 10, which is out of bounds.'\n"}, + {"staticcheck", append([]string{}, "-debug.no-compile-errors=true", "./..."), "staticcheck.go:7:13: Printf format %d has arg #1 of wrong type string (SA5009)\n"}, + } + for _, tc := range tcs { + output, _ := ExecRun("./testdata", tc.command, tc.args...) + sOut := string(output) + if string(output) != tc.expected { + t.Errorf("expected: %v, got: %v", tc.expected, sOut) + } + } +} diff --git a/internal/linters/lua/luacheck/luacheck_test.go b/internal/linters/lua/luacheck/luacheck_test.go index c703cb36..af116a26 100644 --- a/internal/linters/lua/luacheck/luacheck_test.go +++ b/internal/linters/lua/luacheck/luacheck_test.go @@ -28,25 +28,25 @@ func TestFormatLuaCheckLine(t *testing.T) { expected *linters.LinterOutput }{ {" video/mp4/libs/mp4lib.lua:184:11: value assigned to variable mem_data is overwritten on line 202 before use", &linters.LinterOutput{ - File: " video/mp4/libs/mp4lib.lua:184:11", + File: "video/mp4/libs/mp4lib.lua", Line: 184, Column: 11, Message: "value assigned to variable mem_data is overwritten on line 202 before use", }}, {" utils/jsonschema.lua:723:121: line is too long (142 > 120)", &linters.LinterOutput{ - File: " utils/jsonschema.lua", + File: "utils/jsonschema.lua", Line: 723, Column: 121, Message: "line is too long (142 > 120)", }}, {" utils/httpc/http_simple.lua:24:1: setting read-only global variable _VERSION", &linters.LinterOutput{ - File: " utils/httpc/http_simple.lua", + File: "utils/httpc/http_simple.lua", Line: 24, Column: 1, Message: "setting read-only global variable _VERSION", }}, {" test/qtest_access.lua:1220:1: inconsistent indentation (SPACE followed by TAB)", &linters.LinterOutput{ - File: " test/qtest_access.lua", + File: "test/qtest_access.lua", Line: 1220, Column: 1, Message: "inconsistent indentation (SPACE followed by TAB)", @@ -56,23 +56,21 @@ func TestFormatLuaCheckLine(t *testing.T) { for _, c := range tc { output, err := luacheckParser(c.input) + if output == nil { + if c.expected != nil { + t.Errorf("expected: %v, got: %v", c.expected, output) + } + continue + } + if c.expected == nil && output != nil { t.Errorf("expected error, got: %v", output) - } else { - continue } if err != nil { t.Errorf("unexpected error: %v", err) } - if output == nil { - if c.expected != nil { - t.Errorf("expected: %v, got: %v", c.expected, output) - } - continue - } - if output.File != c.expected.File || output.Line != c.expected.Line || output.Column != c.expected.Column || output.Message != c.expected.Message { t.Errorf("expected: %v, got: %v", c.expected, output) } diff --git a/internal/linters/testdata/cppcheck_test.c b/internal/linters/testdata/cppcheck_test.c new file mode 100644 index 00000000..e678e299 --- /dev/null +++ b/internal/linters/testdata/cppcheck_test.c @@ -0,0 +1,8 @@ +// +build ignore go + +int main() +{ + char a[10]; + a[10] = 0; + return 0; +} \ No newline at end of file diff --git a/internal/linters/testdata/staticcheck.go b/internal/linters/testdata/staticcheck.go new file mode 100644 index 00000000..6a726f05 --- /dev/null +++ b/internal/linters/testdata/staticcheck.go @@ -0,0 +1,8 @@ +package main + +import "fmt" + +func main() { + str := "Hello, world!" + fmt.Printf("%d World", str) +} diff --git a/internal/linters/util.go b/internal/linters/util.go deleted file mode 100644 index 6357d797..00000000 --- a/internal/linters/util.go +++ /dev/null @@ -1,108 +0,0 @@ -package linters - -import ( - "fmt" - "path/filepath" - "regexp" - "strconv" - "strings" - - "github.com/qiniu/x/log" - "github.com/qiniu/x/xlog" -) - -func IsEmpty(args ...string) bool { - for _, arg := range args { - if arg != "" { - return false - } - } - - return true -} - -type FormatLinterLineFunc func(line string) (*LinterOutput, error) - -func FormatLinterOutput(log *xlog.Logger, output []byte, FormatLinter FormatLinterLineFunc) (map[string][]LinterOutput, error) { - lines := strings.Split(string(output), "\n") - - var result = make(map[string][]LinterOutput) - for _, line := range lines { - if line == "" { - continue - } - output, err := FormatLinter(line) - if err != nil { - log.Warnf("unexpected linter check output: %v", line) - // 不直接退出 - continue - } - - if output == nil { - continue - } - - if isGopAutoGeneratedFile(output.File) { - log.Infof("skip auto generated file by go+ : %s", output.File) - continue - } - - if outs, ok := result[output.File]; !ok { - result[output.File] = []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 -} - -// common format LinterLine -func FormatLinterLine(line string) (*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 &LinterOutput{ - File: matches[1], - Line: int(lineNumber), - Column: int(columnNumber), - Message: matches[4], - }, nil -} - -var gop_auto_generated_file_pattern = `^.*_autogen.*.go$` -var gopGeneratedFileRegexp = regexp.MustCompile(gop_auto_generated_file_pattern) - -func isGopAutoGeneratedFile(file string) bool { - // TODO: need a more accurate way to determine whether it is a go+ auto generated file - return gopGeneratedFileRegexp.MatchString(filepath.Base(file)) -} diff --git a/main.go b/main.go index d431e008..96cc90ed 100644 --- a/main.go +++ b/main.go @@ -30,12 +30,12 @@ import ( gitv2 "k8s.io/test-infra/prow/git/v2" // linters import + _ "github.com/qiniu/reviewbot/internal/linters/c/cppcheck" _ "github.com/qiniu/reviewbot/internal/linters/git-flow/commit-check" _ "github.com/qiniu/reviewbot/internal/linters/go/gofmt" _ "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/reviewbot/internal/linters/c/cppcheck" ) type options struct {