diff --git a/cmd/joj3/main_test.go b/cmd/joj3/main_test.go index 73b387d..33f16a0 100644 --- a/cmd/joj3/main_test.go +++ b/cmd/joj3/main_test.go @@ -53,6 +53,11 @@ func TestMain(t *testing.T) { {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{ {Name: "compile", Results: []stage.ParserResult{ {Score: 0, Comment: "Unexpected executor status: Nonzero Exit Status\\."}, diff --git a/internal/executors/clang_tidy/executor.go b/internal/executors/clang_tidy/executor.go index 7e227bf..045fd4f 100644 --- a/internal/executors/clang_tidy/executor.go +++ b/internal/executors/clang_tidy/executor.go @@ -1,6 +1,10 @@ package clang_tidy import ( + "fmt" + "io" + "os/exec" + "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" "github.com/criyle/go-judge/envexec" ) @@ -9,17 +13,49 @@ type ClangTidy struct{} func (e *ClangTidy) Run(cmds []stage.Cmd) ([]stage.ExecutorResult, error) { var res []stage.ExecutorResult - for range cmds { - res = append(res, stage.ExecutorResult{ - Status: stage.Status(envexec.StatusInvalid), + + for _, cmd := range cmds { + 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, - Error: "I'm a dummy", + Error: "", Time: 0, Memory: 0, RunTime: 0, Files: 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 } diff --git a/internal/parsers/clang_tidy/convert.go b/internal/parsers/clang_tidy/convert.go new file mode 100644 index 0000000..ab63b86 --- /dev/null +++ b/internal/parsers/clang_tidy/convert.go @@ -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.+):(?P\d+):(?P\d+): (?P\S+): (?P.*?)(?: \[(?P.*)\])?\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) +} diff --git a/internal/parsers/clang_tidy/formatter.go b/internal/parsers/clang_tidy/formatter.go new file mode 100644 index 0000000..058d4a1 --- /dev/null +++ b/internal/parsers/clang_tidy/formatter.go @@ -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)) +// } diff --git a/internal/parsers/clang_tidy/parser.go b/internal/parsers/clang_tidy/parser.go index db3300e..2ce969e 100644 --- a/internal/parsers/clang_tidy/parser.go +++ b/internal/parsers/clang_tidy/parser.go @@ -3,15 +3,21 @@ package clang_tidy import ( "encoding/json" "fmt" + "os" + "strings" "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" ) +type Match struct { + Keyword []string + Score int +} + type Conf struct { Score int - Comment string + Matches []Match } type ClangTidy struct{} @@ -19,6 +25,20 @@ type ClangTidy struct{} func Parse(executorResult stage.ExecutorResult, conf Conf) stage.ParserResult { stdout := executorResult.Files["stdout"] 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) { return stage.ParserResult{ 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{ - Score: 0, - Comment: fmt.Sprintf("Failed to parse result: %s", err), - } - } return stage.ParserResult{ - Score: dummyResult.Score + conf.Score, - Comment: dummyResult.Comment + conf.Comment, + Score: get_score(formatted_messages, conf), + Comment: "", } } diff --git a/internal/parsers/clang_tidy/score.go b/internal/parsers/clang_tidy/score.go new file mode 100644 index 0000000..1c204eb --- /dev/null +++ b/internal/parsers/clang_tidy/score.go @@ -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 +}