clang-tidy parser and executor #26
							
								
								
									
										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