feat: clang-tidy parser (#26)
All checks were successful
continuous-integration/drone/push Build is passing
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:
parent
16a59a7fc2
commit
a042ac01ea
10
.gitmodules
vendored
10
.gitmodules
vendored
|
@ -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
|
||||
|
|
1
examples/clangtidy/sillycode
Submodule
1
examples/clangtidy/sillycode
Submodule
|
@ -0,0 +1 @@
|
|||
Subproject commit a69a7e87fddaddf21fc4f9cd6774e310fa7137c1
|
|
@ -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"
|
||||
|
|
134
internal/parsers/clangtidy/convert.go
Normal file
134
internal/parsers/clangtidy/convert.go
Normal 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)
|
||||
}
|
184
internal/parsers/clangtidy/formatter.go
Normal file
184
internal/parsers/clangtidy/formatter.go
Normal 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"
|
||||
}
|
||||
}
|
9
internal/parsers/clangtidy/meta.go
Normal file
9
internal/parsers/clangtidy/meta.go
Normal 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{})
|
||||
}
|
62
internal/parsers/clangtidy/parser.go
Normal file
62
internal/parsers/clangtidy/parser.go
Normal 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
|
||||
}
|
77
internal/parsers/clangtidy/score.go
Normal file
77
internal/parsers/clangtidy/score.go
Normal 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
|
||||
}
|
Loading…
Reference in New Issue
Block a user