feat: clang-tidy parser (#26)
All checks were successful
continuous-integration/drone/push Build is passing

Co-authored-by: zjc_he <zjc_he@sjtu.edu.cn>
Reviewed-on: FOCS-dev/JOJ3#26
Reviewed-by: 张泊明518370910136 <bomingzh@sjtu.edu.cn>
Co-authored-by: 张佳澈520370910044 <zjc_he@sjtu.edu.cn>
Co-committed-by: 张佳澈520370910044 <zjc_he@sjtu.edu.cn>
This commit is contained in:
张佳澈520370910044 2024-05-18 02:50:13 +08:00 committed by 张泊明518370910136
parent 16a59a7fc2
commit a042ac01ea
9 changed files with 475 additions and 3 deletions

10
.gitmodules vendored
View File

@ -22,7 +22,11 @@
path = examples/keyword/cpplint/sillycode path = examples/keyword/cpplint/sillycode
url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git
branch = keyword/cpplint/sillycode branch = keyword/cpplint/sillycode
[submodule "examples/keyword/clang-tidy/sillycode"] [submodule "examples/clangtidy/sillycode"]
path = examples/keyword/clang-tidy/sillycode path = examples/clangtidy/sillycode
url = ssh://git@focs.ji.sjtu.edu.cn:2222/FOCS-dev/JOJ3-examples.git 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

@ -0,0 +1 @@
Subproject commit a69a7e87fddaddf21fc4f9cd6774e310fa7137c1

View File

@ -1,6 +1,7 @@
package parsers package parsers
import ( 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/cpplint"
_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/diff" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/diff"
_ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/dummy" _ "focs.ji.sjtu.edu.cn/git/FOCS-dev/JOJ3/internal/parsers/dummy"

View File

@ -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<filepath>.+):(?P<line>\d+):(?P<column>\d+): (?P<level>\S+): (?P<message>.*?)(?: \[(?P<diagnostic_name>.*)\])?\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)
}

View File

@ -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"
}
}

View File

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

View File

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

View File

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