From a042ac01eae590ebec3d2172f082f63dc8aec7d6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E5=BC=A0=E4=BD=B3=E6=BE=88520370910044?= Date: Sat, 18 May 2024 02:50:13 +0800 Subject: [PATCH] feat: clang-tidy parser (#26) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: zjc_he Reviewed-on: https://focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/pulls/26 Reviewed-by: 张泊明518370910136 Co-authored-by: 张佳澈520370910044 Co-committed-by: 张佳澈520370910044 --- .gitmodules | 10 +- examples/clangtidy/sillycode | 1 + .../{clang-tidy => clangtidy}/sillycode | 0 internal/parsers/all.go | 1 + internal/parsers/clangtidy/convert.go | 134 +++++++++++++ internal/parsers/clangtidy/formatter.go | 184 ++++++++++++++++++ internal/parsers/clangtidy/meta.go | 9 + internal/parsers/clangtidy/parser.go | 62 ++++++ internal/parsers/clangtidy/score.go | 77 ++++++++ 9 files changed, 475 insertions(+), 3 deletions(-) create mode 160000 examples/clangtidy/sillycode rename examples/keyword/{clang-tidy => clangtidy}/sillycode (100%) create mode 100644 internal/parsers/clangtidy/convert.go create mode 100644 internal/parsers/clangtidy/formatter.go create mode 100644 internal/parsers/clangtidy/meta.go create mode 100644 internal/parsers/clangtidy/parser.go create mode 100644 internal/parsers/clangtidy/score.go diff --git a/.gitmodules b/.gitmodules index 13637d8..71c5721 100644 --- a/.gitmodules +++ b/.gitmodules @@ -22,7 +22,11 @@ path = examples/keyword/cpplint/sillycode url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git branch = keyword/cpplint/sillycode -[submodule "examples/keyword/clang-tidy/sillycode"] - path = examples/keyword/clang-tidy/sillycode +[submodule "examples/clangtidy/sillycode"] + path = examples/clangtidy/sillycode url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git - branch = keyword/clang-tidy/sillycode + branch = clangtidy/sillycode +[submodule "examples/keyword/clangtidy/sillycode"] + path = examples/keyword/clangtidy/sillycode + url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git + branch = keyword/clangtidy/sillycode diff --git a/examples/clangtidy/sillycode b/examples/clangtidy/sillycode new file mode 160000 index 0000000..a69a7e8 --- /dev/null +++ b/examples/clangtidy/sillycode @@ -0,0 +1 @@ +Subproject commit a69a7e87fddaddf21fc4f9cd6774e310fa7137c1 diff --git a/examples/keyword/clang-tidy/sillycode b/examples/keyword/clangtidy/sillycode similarity index 100% rename from examples/keyword/clang-tidy/sillycode rename to examples/keyword/clangtidy/sillycode diff --git a/internal/parsers/all.go b/internal/parsers/all.go index c687743..4decc13 100644 --- a/internal/parsers/all.go +++ b/internal/parsers/all.go @@ -1,6 +1,7 @@ package parsers import ( + _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/clangtidy" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/cpplint" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/diff" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/dummy" diff --git a/internal/parsers/clangtidy/convert.go b/internal/parsers/clangtidy/convert.go new file mode 100644 index 0000000..ea5aa1f --- /dev/null +++ b/internal/parsers/clangtidy/convert.go @@ -0,0 +1,134 @@ +// Referenced from https://github.com/yuriisk/clang-tidy-converter/blob/master/clang_tidy_converter/parser/clang_tidy_parser.py +package clangtidy + +import ( + "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 isIgnored(line string) bool { + ignoreRegex := regexp.MustCompile("^error:.*$") + return ignoreRegex.MatchString(line) +} + +func parseMessage(line string) ClangMessage { + messageRegex := regexp.MustCompile(`^(?P.+):(?P\d+):(?P\d+): (?P\S+): (?P.*?)(?: \[(?P.*)\])?\n$`) + regexRes := messageRegex.FindStringSubmatch(line) + if len(regexRes) == 0 { + return *newClangMessage("", 0, 0, UNKNOWN, "", "", nil, nil) + } else { + filepath := regexRes[1] + line, _ := strconv.Atoi(regexRes[2]) + column, _ := strconv.Atoi(regexRes[3]) + level := levelFromString(regexRes[4]) + message := regexRes[5] + diagnosticName := regexRes[6] + + return ClangMessage{ + filepath: filepath, + line: line, + column: column, + level: level, + message: message, + diagnosticName: diagnosticName, + detailsLines: make([]string, 0), + children: make([]ClangMessage, 0), + } + } +} + +func groupMessages(messages []ClangMessage) []ClangMessage { + groupedMessages := make([]ClangMessage, 0) + for _, message := range messages { + if message.level == NOTE { + groupedMessages[len(groupedMessages)-1].children = append(groupedMessages[len(groupedMessages)-1].children, message) + } else { + groupedMessages = append(groupedMessages, message) + } + } + return groupedMessages +} + +func convertPathsToRelative(messages *[]ClangMessage, conf Conf) { + currentDir := conf.RootDir + for i := range *messages { + (*messages)[i].filepath, _ = filepath.Rel(currentDir, (*messages)[i].filepath) + } +} + +func ParseLines(lines []string, conf Conf) []ClangMessage { + messages := make([]ClangMessage, 0) + for _, line := range lines { + if isIgnored(string(line)) { + continue + } + message := parseMessage(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) + } + } + convertPathsToRelative(&messages, conf) + return groupMessages(messages) +} diff --git a/internal/parsers/clangtidy/formatter.go b/internal/parsers/clangtidy/formatter.go new file mode 100644 index 0000000..21bda2a --- /dev/null +++ b/internal/parsers/clangtidy/formatter.go @@ -0,0 +1,184 @@ +// Referenced from https://github.com/yuriisk/clang-tidy-converter/blob/master/clang_tidy_converter/parser/clang_tidy_parser.py +package clangtidy + +import ( + "fmt" + "strings" +) + +type JsonMessage struct { + Type string `json:"type"` + CheckName string `json:"checkname"` + 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"` +} + +func Format(messages []ClangMessage) []JsonMessage { + formattedMessages := make([]JsonMessage, len(messages)) + for i, message := range messages { + formattedMessages[i] = formatMessage(message) + } + return formattedMessages +} + +func formatMessage(message ClangMessage) JsonMessage { + result := JsonMessage{ + Type: "issue", + CheckName: message.diagnosticName, + Description: message.message, + Content: extractContent(message), + Categories: extractCategories(message), + Location: extractLocation(message), + Trace: extractTrace(message), + Severity: extractSeverity(message), + } + return result +} + +func messagesToText(messages []ClangMessage) []string { + textLines := []string{} + for _, message := range messages { + textLines = append(textLines, fmt.Sprintf("%s:%d:%d: %s", message.filepath, message.line, message.column, message.message)) + textLines = append(textLines, message.detailsLines...) + textLines = append(textLines, messagesToText(message.children)...) + } + return textLines +} + +func extractContent(message ClangMessage) map[string]interface{} { + detailLines := "" + for _, line := range message.detailsLines { + if line == "" { + continue + } + detailLines += (line + "\n") + } + for _, line := range messagesToText(message.children) { + if line == "" { + continue + } + detailLines += (line + "\n") + } + result := map[string]interface{}{ + "body": "```\n" + detailLines + "```", + } + return result +} + +func removeDuplicates(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 extractCategories(message ClangMessage) []string { + bugriskCategory := "Bug Risk" + clarityCategory := "Clarity" + compatibilityCategory := "Compatibility" + complexityCategory := "Complexity" + duplicationCategory := "Duplication" + performanceCategory := "Performance" + securityCategory := "Security" + styleCategory := "Style" + + categories := []string{} + if strings.Contains(message.diagnosticName, "bugprone") { + categories = append(categories, bugriskCategory) + } + if strings.Contains(message.diagnosticName, "modernize") { + categories = append(categories, compatibilityCategory) + } + if strings.Contains(message.diagnosticName, "portability") { + categories = append(categories, compatibilityCategory) + } + if strings.Contains(message.diagnosticName, "performance") { + categories = append(categories, performanceCategory) + } + if strings.Contains(message.diagnosticName, "readability") { + categories = append(categories, clarityCategory) + } + if strings.Contains(message.diagnosticName, "cloexec") { + categories = append(categories, securityCategory) + } + if strings.Contains(message.diagnosticName, "security") { + categories = append(categories, securityCategory) + } + if strings.Contains(message.diagnosticName, "naming") { + categories = append(categories, styleCategory) + } + if strings.Contains(message.diagnosticName, "misc") { + categories = append(categories, styleCategory) + } + if strings.Contains(message.diagnosticName, "cppcoreguidelines") { + categories = append(categories, styleCategory) + } + if strings.Contains(message.diagnosticName, "hicpp") { + categories = append(categories, styleCategory) + } + if strings.Contains(message.diagnosticName, "simplify") { + categories = append(categories, complexityCategory) + } + if strings.Contains(message.diagnosticName, "redundant") { + categories = append(categories, duplicationCategory) + } + if strings.HasPrefix(message.diagnosticName, "boost-use-to-string") { + categories = append(categories, compatibilityCategory) + } + if len(categories) == 0 { + categories = append(categories, bugriskCategory) + } + return removeDuplicates(categories) +} + +func extractLocation(message ClangMessage) map[string]interface{} { + location := map[string]interface{}{ + "path": message.filepath, + "lines": map[string]interface{}{ + "begin": message.line, + }, + } + return location +} + +func extractOtherLocations(message ClangMessage) []map[string]interface{} { + locationList := []map[string]interface{}{} + for _, child := range message.children { + locationList = append(locationList, extractLocation(child)) + locationList = append(locationList, extractOtherLocations(child)...) + } + return locationList +} + +func extractTrace(message ClangMessage) map[string]interface{} { + result := map[string]interface{}{ + "locations": extractOtherLocations(message), + } + return result +} + +func extractSeverity(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" + } +} diff --git a/internal/parsers/clangtidy/meta.go b/internal/parsers/clangtidy/meta.go new file mode 100644 index 0000000..dcf7406 --- /dev/null +++ b/internal/parsers/clangtidy/meta.go @@ -0,0 +1,9 @@ +package clangtidy + +import "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" + +var name = "clangtidy" + +func init() { + stage.RegisterParser(name, &ClangTidy{}) +} diff --git a/internal/parsers/clangtidy/parser.go b/internal/parsers/clangtidy/parser.go new file mode 100644 index 0000000..38940f0 --- /dev/null +++ b/internal/parsers/clangtidy/parser.go @@ -0,0 +1,62 @@ +package clangtidy + +import ( + "fmt" + "strings" + + "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/stage" + "github.com/criyle/go-judge/envexec" +) + +type Match struct { + Keyword []string + Score int +} + +type Conf struct { + Score int `default:"100"` + RootDir string `default:"/w"` + Matches []Match +} + +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 := ParseLines(lines, conf) + formattedMessages := Format(messages) + + if executorResult.Status != stage.Status(envexec.StatusAccepted) { + if !((executorResult.Status == stage.Status(envexec.StatusNonzeroExitStatus)) && + (executorResult.ExitStatus == 1)) { + return stage.ParserResult{ + Score: 0, + Comment: fmt.Sprintf( + "Unexpected executor status: %s.\nStderr: %s", + executorResult.Status, stderr, + ), + } + } + } + return stage.ParserResult{ + Score: GetScore(formattedMessages, conf), + Comment: GetComment(formattedMessages), + } +} + +func (*ClangTidy) Run(results []stage.ExecutorResult, confAny any) ( + []stage.ParserResult, bool, error, +) { + conf, err := stage.DecodeConf[Conf](confAny) + if err != nil { + return nil, true, err + } + var res []stage.ParserResult + for _, result := range results { + res = append(res, Parse(result, *conf)) + } + return res, false, nil +} diff --git a/internal/parsers/clangtidy/score.go b/internal/parsers/clangtidy/score.go new file mode 100644 index 0000000..f6fa39e --- /dev/null +++ b/internal/parsers/clangtidy/score.go @@ -0,0 +1,77 @@ +package clangtidy + +import ( + "fmt" + "strings" +) + +func contains(arr []string, element string) bool { + for i := range arr { + // TODO: The keyword in json report might also be an array, need to split it + if strings.Contains(arr[i], element) { + return true + } + } + return false +} + +func GetScore(jsonMessages []JsonMessage, conf Conf) int { + fullmark := conf.Score + for _, jsonMessage := range jsonMessages { + keyword := jsonMessage.CheckName + for _, match := range conf.Matches { + if contains(match.Keyword, keyword) { + fullmark -= match.Score + break + } + } + } + return fullmark +} + +func GetComment(jsonMessages []JsonMessage) string { + res := "### Test results summary\n\n" + keys := [...]string{ + "codequality-unchecked-malloc-result", + "codequality-no-global-variables", + "codequality-no-header-guard", + "codequality-no-fflush-stdin", + "readability-function-size", + "readability-duplicate-include", + "readability-identifier-naming", + "readability-redundant", + "readability-misleading-indentation", + "readability-misplaced-array-index", + "cppcoreguidelines-init-variables", + "bugprone-suspicious-string-compare", + "google-global-names-in-headers", + "clang-diagnostic", + "clang-analyzer", + "misc", + "performance", + "others", + } + mapping := map[string]int{} + for _, key := range keys { + mapping[key] = 0 + } + for _, jsonMessage := range jsonMessages { + keyword := jsonMessage.CheckName + flag := true + for key := range mapping { + if strings.Contains(keyword, key) { + mapping[key] += 1 + flag = false + break + } + } + if flag { + mapping["others"] += 1 + } + } + + for i, key := range keys { + res = fmt.Sprintf("%s%d. %s: %d\n", res, i+1, key, mapping[key]) + } + return res +}