feat(internal/executors/clang_tidy,-internal/parsers/clang_tidy,-cmd/joj3/main_test.go): Parsers and executors for clang-tidy
This commit is contained in:
		
							parent
							
								
									eb4815f10d
								
							
						
					
					
						commit
						e133b13a84
					
				|  | @ -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\\."}, | ||||
|  |  | |||
|  | @ -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 | ||||
| } | ||||
|  |  | |||
							
								
								
									
										135
									
								
								internal/parsers/clang_tidy/convert.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										135
									
								
								internal/parsers/clang_tidy/convert.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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<filepath>.+):(?P<line>\d+):(?P<column>\d+): (?P<level>\S+): (?P<message>.*?)(?: \[(?P<diagnostic_name>.*)\])?\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) | ||||
| } | ||||
							
								
								
									
										201
									
								
								internal/parsers/clang_tidy/formatter.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										201
									
								
								internal/parsers/clang_tidy/formatter.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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))
 | ||||
| // }
 | ||||
|  | @ -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: "", | ||||
| 	} | ||||
| } | ||||
| 
 | ||||
|  |  | |||
							
								
								
									
										31
									
								
								internal/parsers/clang_tidy/score.go
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								internal/parsers/clang_tidy/score.go
									
									
									
									
									
										Normal file
									
								
							|  | @ -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 | ||||
| } | ||||
		Loading…
	
		Reference in New Issue
	
	Block a user