feat(internal/executors/clang_tidy,-internal/parsers/clang_tidy,-cmd/joj3/main_test.go): Parsers and executors for clang-tidy
Some checks failed
continuous-integration/drone/pr Build is failing
continuous-integration/drone/push Build is failing

This commit is contained in:
张佳澈520370910044 2024-05-01 00:18:09 +08:00
parent eb4815f10d
commit e133b13a84
6 changed files with 437 additions and 17 deletions

View File

@ -53,6 +53,11 @@ func TestMain(t *testing.T) {
{Score: 100, Comment: "executor status: run time: \\d+ ns, memory: \\d+ bytes"}, {Score: 100, Comment: "executor status: run time: \\d+ ns, memory: \\d+ bytes"},
}}, }},
}}, }},
{"clang_tidy", []stage.StageResult{
{Name: "clang-tidy", Results: []stage.ParserResult{
{Score: -200, Comment: ""},
}},
}},
{"compile_error", []stage.StageResult{ {"compile_error", []stage.StageResult{
{Name: "compile", Results: []stage.ParserResult{ {Name: "compile", Results: []stage.ParserResult{
{Score: 0, Comment: "Unexpected executor status: Nonzero Exit Status\\."}, {Score: 0, Comment: "Unexpected executor status: Nonzero Exit Status\\."},

View File

@ -1,6 +1,10 @@
package clang_tidy package clang_tidy
import ( import (
"fmt"
"io"
"os/exec"
"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
"github.com/criyle/go-judge/envexec" "github.com/criyle/go-judge/envexec"
) )
@ -9,17 +13,49 @@ type ClangTidy struct{}
func (e *ClangTidy) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) { func (e *ClangTidy) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) {
var res []stage.ExecutorResult var res []stage.ExecutorResult
for range cmds {
res = append(res, stage.ExecutorResult{ for _, cmd := range cmds {
Status: stage.Status(envexec.StatusInvalid), args := ""
for _, arg := range cmd.Args {
args += fmt.Sprint(arg)
args += " "
}
clang_tidy_Cmd := exec.Command("bash", "-c", args)
clang_tidy_stdout, err1 := clang_tidy_Cmd.StdoutPipe()
clang_tidy_stderr, err2 := clang_tidy_Cmd.StderrPipe()
if err1 != nil {
return nil, err1
}
if err2 != nil {
return nil, err2
}
_ = clang_tidy_Cmd.Start()
clang_tidy_Out, err1 := io.ReadAll(clang_tidy_stdout)
clang_tidy_Err, err2 := io.ReadAll(clang_tidy_stderr)
if err1 != nil {
return nil, err1
}
if err2 != nil {
return nil, err2
}
_ = clang_tidy_Cmd.Wait()
r := stage.ExecutorResult{
Status: stage.Status(envexec.StatusAccepted),
ExitStatus: 0, ExitStatus: 0,
Error: "I'm a dummy", Error: "",
Time: 0, Time: 0,
Memory: 0, Memory: 0,
RunTime: 0, RunTime: 0,
Files: map[string]string{}, Files: map[string]string{},
FileIDs: map[string]string{}, FileIDs: map[string]string{},
}) }
r.Files["stdout"] = string(clang_tidy_Out)
// TODO: We may don't want stderr
r.Files["stderr"] = string(clang_tidy_Err)
res = append(res, r)
} }
return res, nil return res, nil
} }

View File

@ -0,0 +1,135 @@
// Referenced from https://github.com/yuriisk/clang-tidy-converter/blob/master/clang_tidy_converter/parser/clang_tidy_parser.py
package clang_tidy
import (
"os"
"path/filepath"
"regexp"
"strconv"
)
type Level int
const (
UNKNOWN Level = iota
NOTE
REMARK
WARNING
ERROR
FATAL
)
type ClangMessage struct {
filepath string
line int
column int
level Level
message string
diagnosticName string
detailsLines []string
children []ClangMessage
}
func newClangMessage(filepath string, line int, column int, level Level, message string, diagnosticName string, detailsLines []string, children []ClangMessage) *ClangMessage {
if detailsLines == nil {
detailsLines = make([]string, 0)
}
if children == nil {
children = make([]ClangMessage, 0)
}
return &ClangMessage{
filepath: filepath,
line: line,
column: column,
level: level,
message: message,
diagnosticName: diagnosticName,
detailsLines: detailsLines,
children: children,
}
}
func LevelFromString(levelString string) Level {
switch levelString {
case "note":
return NOTE
case "remark":
return REMARK
case "warning":
return WARNING
case "error":
return ERROR
case "fatal":
return FATAL
default:
return UNKNOWN
}
}
func is_ignored(line string) bool {
IGNORE_REGEX := regexp.MustCompile("^error:.*$")
return IGNORE_REGEX.MatchString(line)
}
func parse_message(line string) ClangMessage {
MESSAGE_REGEX := regexp.MustCompile(`^(?P<filepath>.+):(?P<line>\d+):(?P<column>\d+): (?P<level>\S+): (?P<message>.*?)(?: \[(?P<diagnostic_name>.*)\])?\n$`)
regex_res := MESSAGE_REGEX.FindStringSubmatch(line)
if len(regex_res) == 0 {
return *newClangMessage("", 0, 0, UNKNOWN, "", "", nil, nil)
} else {
filepath := regex_res[1]
line, _ := strconv.Atoi(regex_res[2])
column, _ := strconv.Atoi(regex_res[3])
level := LevelFromString(regex_res[4])
message := regex_res[5]
diagnostic_name := regex_res[6]
return ClangMessage{
filepath: filepath,
line: line,
column: column,
level: level,
message: message,
diagnosticName: diagnostic_name,
detailsLines: make([]string, 0),
children: make([]ClangMessage, 0),
}
}
}
func group_messages(messages []ClangMessage) []ClangMessage {
grouped_messages := make([]ClangMessage, 0)
for _, message := range messages {
if message.level == NOTE {
grouped_messages[len(grouped_messages)-1].children = append(grouped_messages[len(grouped_messages)-1].children, message)
} else {
grouped_messages = append(grouped_messages, message)
}
}
return grouped_messages
}
func convert_paths_to_relative(messages *[]ClangMessage) {
currentDir, _ := os.Getwd()
for i := range *messages {
(*messages)[i].filepath, _ = filepath.Rel(currentDir, (*messages)[i].filepath)
}
}
func parse_lines(lines []string) []ClangMessage {
messages := make([]ClangMessage, 0)
for _, line := range lines {
if is_ignored(string(line)) {
continue
}
message := parse_message(string(line))
if message.level == UNKNOWN && len(messages) > 0 {
messages[len(messages)-1].detailsLines = append(messages[len(messages)-1].detailsLines, string(line))
} else {
messages = append(messages, message)
}
}
convert_paths_to_relative(&messages)
return group_messages(messages)
}

View File

@ -0,0 +1,201 @@
// Referenced from https://github.com/yuriisk/clang-tidy-converter/blob/master/clang_tidy_converter/parser/clang_tidy_parser.py
package clang_tidy
import (
"fmt"
"strings"
)
type json_message struct {
Type string `json:"type"`
Check_name string `json:"check_name"`
Description string `json:"description"`
Content map[string]interface{} `json:"content"`
Categories []string `json:"categories"`
Location map[string]interface{} `json:"location"`
Trace map[string]interface{} `json:"trace"`
Severity string `json:"severity"`
Fingerprint string `json:"fingerprint"`
}
func format(messages []ClangMessage) []json_message {
formatted_messages := make([]json_message, len(messages))
for i, message := range messages {
formatted_messages[i] = format_message(message)
}
return formatted_messages
}
func format_message(message ClangMessage) json_message {
result := json_message{
Type: "issue",
Check_name: message.diagnosticName,
Description: message.message,
Content: extract_content(message),
Categories: extract_categories(message),
Location: extract_location(message),
Trace: extract_trace(message),
Severity: extract_severity(message),
Fingerprint: "",
// Fingerprint: generate_fingerprint(message),
}
return result
}
func messages_to_text(messages []ClangMessage) []string {
text_lines := []string{}
for _, message := range messages {
text_lines = append(text_lines, fmt.Sprintf("%s:%d:%d: %s", message.filepath, message.line, message.column, message.message))
text_lines = append(text_lines, message.detailsLines...)
text_lines = append(text_lines, messages_to_text(message.children)...)
}
return text_lines
}
func extract_content(message ClangMessage) map[string]interface{} {
detailLines := ""
for _, line := range message.detailsLines {
if line == "" {
continue
}
detailLines += (line + "\n")
}
for _, line := range messages_to_text(message.children) {
if line == "" {
continue
}
detailLines += (line + "\n")
}
result := map[string]interface{}{
"body": "```\n" + detailLines + "```",
}
return result
}
func remove_duplicates(list []string) []string {
uniqueMap := make(map[string]bool)
for _, v := range list {
uniqueMap[v] = true
}
result := []string{}
for k := range uniqueMap {
result = append(result, k)
}
return result
}
func extract_categories(message ClangMessage) []string {
BUGRISC_CATEGORY := "Bug Risk"
CLARITY_CATEGORY := "Clarity"
COMPATIBILITY_CATEGORY := "Compatibility"
COMPLEXITY_CATEGORY := "Complexity"
DUPLICATION_CATEGORY := "Duplication"
PERFORMANCE_CATEGORY := "Performance"
SECURITY_CATEGORY := "Security"
STYLE_CATEGORY := "Style"
categories := []string{}
if strings.Contains(message.diagnosticName, "bugprone") {
categories = append(categories, BUGRISC_CATEGORY)
}
if strings.Contains(message.diagnosticName, "modernize") {
categories = append(categories, COMPATIBILITY_CATEGORY)
}
if strings.Contains(message.diagnosticName, "portability") {
categories = append(categories, COMPATIBILITY_CATEGORY)
}
if strings.Contains(message.diagnosticName, "performance") {
categories = append(categories, PERFORMANCE_CATEGORY)
}
if strings.Contains(message.diagnosticName, "readability") {
categories = append(categories, CLARITY_CATEGORY)
}
if strings.Contains(message.diagnosticName, "cloexec") {
categories = append(categories, SECURITY_CATEGORY)
}
if strings.Contains(message.diagnosticName, "security") {
categories = append(categories, SECURITY_CATEGORY)
}
if strings.Contains(message.diagnosticName, "naming") {
categories = append(categories, STYLE_CATEGORY)
}
if strings.Contains(message.diagnosticName, "misc") {
categories = append(categories, STYLE_CATEGORY)
}
if strings.Contains(message.diagnosticName, "cppcoreguidelines") {
categories = append(categories, STYLE_CATEGORY)
}
if strings.Contains(message.diagnosticName, "hicpp") {
categories = append(categories, STYLE_CATEGORY)
}
if strings.Contains(message.diagnosticName, "simplify") {
categories = append(categories, COMPLEXITY_CATEGORY)
}
if strings.Contains(message.diagnosticName, "redundant") {
categories = append(categories, DUPLICATION_CATEGORY)
}
if strings.HasPrefix(message.diagnosticName, "boost-use-to-string") {
categories = append(categories, COMPATIBILITY_CATEGORY)
}
if len(categories) == 0 {
categories = append(categories, BUGRISC_CATEGORY)
}
return remove_duplicates(categories)
}
func extract_location(message ClangMessage) map[string]interface{} {
location := map[string]interface{}{
"path": message.filepath,
"lines": map[string]interface{}{
"begin": message.line,
},
}
return location
}
func extract_other_locations(message ClangMessage) []map[string]interface{} {
location_list := []map[string]interface{}{}
for _, child := range message.children {
location_list = append(location_list, extract_location(child))
location_list = append(location_list, extract_other_locations(child)...)
}
return location_list
}
func extract_trace(message ClangMessage) map[string]interface{} {
result := map[string]interface{}{
"locations": extract_other_locations(message),
}
return result
}
func extract_severity(message ClangMessage) string {
switch message.level {
case NOTE:
return "info"
case REMARK:
return "minor"
case WARNING:
return "major"
case ERROR:
return "critical"
case FATAL:
return "blocker"
default:
return "unknown"
}
}
// func generate_fingerprint(message ClangMessage) string {
// h := md5.New()
// h.Write([]byte(message.filepath))
// h.Write([]byte(fmt.Sprintf("%d", message.line)))
// h.Write([]byte(fmt.Sprintf("%d", message.column)))
// h.Write([]byte(message.message))
// h.Write([]byte(message.diagnosticName))
// for _, child := range message.children {
// childFingerprint := generate_fingerprint(child)
// h.Write([]byte(childFingerprint))
// }
// return hex.EncodeToString(h.Sum(nil))
// }

View File

@ -3,15 +3,21 @@ package clang_tidy
import ( import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"os"
"strings"
"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage"
"focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/pkg/dummy"
"github.com/criyle/go-judge/envexec" "github.com/criyle/go-judge/envexec"
) )
type Match struct {
Keyword []string
Score int
}
type Conf struct { type Conf struct {
Score int Score int
Comment string Matches []Match
} }
type ClangTidy struct{} type ClangTidy struct{}
@ -19,6 +25,20 @@ type ClangTidy struct{}
func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult { func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
stdout := executorResult.Files["stdout"] stdout := executorResult.Files["stdout"]
stderr := executorResult.Files["stderr"] stderr := executorResult.Files["stderr"]
lines := strings.SplitAfter(stdout, "\n")
messages := parse_lines(lines)
formatted_messages := format(messages)
// TODO: Handle the json file (parse into markdown and delete it?)
json_file, _ := os.Create("./clangtidy_result.json")
defer json_file.Close()
encoder := json.NewEncoder(json_file)
encoder.SetEscapeHTML(false)
encoder.SetIndent("", " ")
_ = encoder.Encode(formatted_messages)
if executorResult.Status != stage.Status(envexec.StatusAccepted) { if executorResult.Status != stage.Status(envexec.StatusAccepted) {
return stage.ParserResult{ return stage.ParserResult{
Score: 0, Score: 0,
@ -28,17 +48,9 @@ func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult {
), ),
} }
} }
var dummyResult dummy.Result
err := json.Unmarshal([]byte(stdout), &dummyResult)
if err != nil {
return stage.ParserResult{ return stage.ParserResult{
Score: 0, Score: get_score(formatted_messages, conf),
Comment: fmt.Sprintf("Failed to parse result: %s", err), Comment: "",
}
}
return stage.ParserResult{
Score: dummyResult.Score + conf.Score,
Comment: dummyResult.Comment + conf.Comment,
} }
} }

View File

@ -0,0 +1,31 @@
package clang_tidy
func Contains[T comparable](arr []T, element T) bool {
for i := range arr {
// TODO: The keyword in json report might also be an array, need to split it
// TODO: Might use string.Contains() rather than ==
if element == arr[i] {
return true
}
}
return false
}
func get_score(json_messages []json_message, conf Conf) int {
fullmark := conf.Score
for _, json_message := range json_messages {
keyword := json_message.Check_name
flag := false
for _, match := range conf.Matches {
if Contains(match.Keyword, keyword) {
fullmark -= match.Score
flag = true
break
}
}
if !flag {
fullmark -= 1
}
}
return fullmark
}