From a638e63336800f82be31f0e7c43968b6074dcaa5 Mon Sep 17 00:00:00 2001 From: Kendrick Curtis Date: Tue, 1 Jul 2025 15:25:31 +0100 Subject: [PATCH] added basic support for both pyrefly and solid-grader --- cli-v2.go | 2 + cmd/analyze.go | 29 +++++++ domain/tool.go | 2 + plugins/tools/pyrefly/plugin.yaml | 15 ++++ plugins/tools/solid_grader/plugin.yaml | 13 +++ tools/language_config.go | 5 ++ tools/pyreflyRunner.go | 78 ++++++++++++++++++ tools/solidGraderRunner.go | 47 +++++++++++ utils/sarif.go | 106 +++++++++++++++++++++++++ 9 files changed, 297 insertions(+) create mode 100644 plugins/tools/pyrefly/plugin.yaml create mode 100644 plugins/tools/solid_grader/plugin.yaml create mode 100644 tools/pyreflyRunner.go create mode 100644 tools/solidGraderRunner.go diff --git a/cli-v2.go b/cli-v2.go index e990590f..11824b47 100644 --- a/cli-v2.go +++ b/cli-v2.go @@ -14,6 +14,8 @@ import ( ) func main() { + + fmt.Printf("TESTING CLI BUILD") // Initialize config global object config.Init() diff --git a/cmd/analyze.go b/cmd/analyze.go index 100c693a..8b71e28e 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -412,6 +412,31 @@ func runEnigmaAnalysis(workDirectory string, pathsToCheck []string, outputFile s return tools.RunEnigma(workDirectory, enigma.InstallDir, enigma.Binaries["codacy-enigma-cli"], pathsToCheck, outputFile, outputFormat) } +func runSolidGraderAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error { + solidGrader := config.Config.Tools()["solid_grader"] + if solidGrader == nil { + log.Fatal("solid_grader tool configuration not found") + } + solidGraderBinary := solidGrader.Binaries["solid_grader"] + return tools.RunSolidGrader( + workDirectory, + solidGrader.InstallDir, + solidGraderBinary, + pathsToCheck, + outputFile, + outputFormat, + ) +} + +func runPyreflyAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error { + pyrefly := config.Config.Tools()["pyrefly"] + if pyrefly == nil { + log.Fatal("Pyrefly tool configuration not found") + } + pyreflyBinary := pyrefly.Binaries["pyrefly"] + return tools.RunPyrefly(workDirectory, pyreflyBinary, pathsToCheck, outputFile, outputFormat) +} + var analyzeCmd = &cobra.Command{ Use: "analyze", Short: "Runs all configured linters.", @@ -522,6 +547,10 @@ func runTool(workDirectory string, toolName string, args []string, outputFile st return runLizardAnalysis(workDirectory, args, outputFile, outputFormat) case "codacy-enigma-cli": return runEnigmaAnalysis(workDirectory, args, outputFile, outputFormat) + case "solid_grader": + return runSolidGraderAnalysis(workDirectory, args, outputFile, outputFormat) + case "pyrefly": + return runPyreflyAnalysis(workDirectory, args, outputFile, outputFormat) default: return fmt.Errorf("unsupported tool: %s", toolName) } diff --git a/domain/tool.go b/domain/tool.go index 60bfb7f9..97cecfe3 100644 --- a/domain/tool.go +++ b/domain/tool.go @@ -27,6 +27,7 @@ const ( DartAnalyzer string = "d203d615-6cf1-41f9-be5f-e2f660f7850f" Semgrep string = "6792c561-236d-41b7-ba5e-9d6bee0d548b" Lizard string = "76348462-84b3-409a-90d3-955e90abfb87" + SolidGrader string = "716FCE5C-F5E8-4B9D-A7D2-86CDFAE79D45" ) type ToolInfo struct { @@ -45,4 +46,5 @@ var SupportedToolsMetadata = map[string]ToolInfo{ DartAnalyzer: {Name: "dartanalyzer", Priority: 0}, Lizard: {Name: "lizard", Priority: 0}, Semgrep: {Name: "semgrep", Priority: 0}, + SolidGrader: {Name: "solid_grader", Priority: 0}, } diff --git a/plugins/tools/pyrefly/plugin.yaml b/plugins/tools/pyrefly/plugin.yaml new file mode 100644 index 00000000..6ce4eb70 --- /dev/null +++ b/plugins/tools/pyrefly/plugin.yaml @@ -0,0 +1,15 @@ +name: pyrefly +# Pyrefly: Fast, modern Python type checker (https://pyrefly.org/en/docs/installation/) +description: Pyrefly is a fast, modern static type checker for Python, designed for developer productivity and CI integration. +default_version: 0.22.0 +runtime: python +runtime_binaries: + package_manager: python3 + execution: python3 +binaries: + - name: pyrefly + path: "venv/bin/pyrefly" +output_options: + file_flag: "--output" +analysis_options: + default_path: "." \ No newline at end of file diff --git a/plugins/tools/solid_grader/plugin.yaml b/plugins/tools/solid_grader/plugin.yaml new file mode 100644 index 00000000..d0177878 --- /dev/null +++ b/plugins/tools/solid_grader/plugin.yaml @@ -0,0 +1,13 @@ +name: solid_grader +# TODO: Update description with accurate details +description: Solid Grader scans your code for SOLID principle violations. +default_version: 0.1.0 # TODO: Update with actual version if needed +binaries: + - name: solid_grader + path: "/Users/kendrickcurtis/Documents/GitHub/solid-grader2/solid-grader2" +formatters: + - name: text + flag: "" + - name: sarif + flag: "-f sarif" +# TODO: Add download section if binary is to be downloaded automatically \ No newline at end of file diff --git a/tools/language_config.go b/tools/language_config.go index 61360902..69b27f4e 100644 --- a/tools/language_config.go +++ b/tools/language_config.go @@ -58,6 +58,11 @@ func CreateLanguagesConfigFile(apiTools []domain.Tool, toolsConfigDir string, to Languages: []string{"C", "CPP", "C#", "Generic", "Go", "Java", "JavaScript", "JSON", "Kotlin", "Python", "TypeScript", "Ruby", "Rust", "JSX", "PHP", "Scala", "Swift", "Terraform"}, Extensions: []string{".c", ".cpp", ".h", ".hpp", ".cs", ".go", ".java", ".js", ".json", ".kt", ".py", ".ts", ".rb", ".rs", ".jsx", ".php", ".scala", ".swift", ".tf", ".tfvars"}, }, + "pyrefly": { + Name: "pyrefly", + Languages: []string{"Python"}, + Extensions: []string{".py"}, + }, } // Build a list of tool language info for enabled tools diff --git a/tools/pyreflyRunner.go b/tools/pyreflyRunner.go new file mode 100644 index 00000000..a94304ac --- /dev/null +++ b/tools/pyreflyRunner.go @@ -0,0 +1,78 @@ +package tools + +import ( + "codacy/cli-v2/utils" + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" +) + +// RunPyrefly executes Pyrefly type checking on the specified directory or files +func RunPyrefly(workDirectory string, binary string, files []string, outputFile string, outputFormat string) error { + args := []string{"check"} + + // Always use JSON output for SARIF conversion + var tempFile string + if outputFormat == "sarif" { + tmp, err := ioutil.TempFile("", "pyrefly-*.json") + if err != nil { + return fmt.Errorf("failed to create temporary file: %w", err) + } + tempFile = tmp.Name() + tmp.Close() + defer os.Remove(tempFile) + args = append(args, "--output", tempFile, "--output-format", "json") + } else if outputFile != "" { + args = append(args, "--output", outputFile) + } + if outputFormat == "json" && outputFile == "" { + args = append(args, "--output-format", "json") + } + + // Detect config file (pyrefly.toml or pyproject.toml) + configFiles := []string{"pyrefly.toml", "pyproject.toml"} + for _, configFile := range configFiles { + if _, err := os.Stat(filepath.Join(workDirectory, configFile)); err == nil { + // Pyrefly auto-detects config, so no need to add a flag + break + } + } + + // Add files to check, or "." for current directory + if len(files) > 0 { + args = append(args, files...) + } else { + args = append(args, ".") + } + + cmd := exec.Command(binary, args...) + cmd.Dir = workDirectory + cmd.Stdout = os.Stdout + cmd.Stderr = os.Stderr + + err := cmd.Run() + if err != nil { + if _, ok := err.(*exec.ExitError); !ok { + return fmt.Errorf("failed to run Pyrefly: %w", err) + } + } + + if outputFormat == "sarif" { + jsonOutput, err := os.ReadFile(tempFile) + if err != nil { + return fmt.Errorf("failed to read Pyrefly output: %w", err) + } + sarifOutput := utils.ConvertPyreflyToSarif(jsonOutput) + if outputFile != "" { + err = os.WriteFile(outputFile, sarifOutput, 0644) + if err != nil { + return fmt.Errorf("failed to write SARIF output: %w", err) + } + } else { + fmt.Println(string(sarifOutput)) + } + } + return nil +} diff --git a/tools/solidGraderRunner.go b/tools/solidGraderRunner.go new file mode 100644 index 00000000..34b8b281 --- /dev/null +++ b/tools/solidGraderRunner.go @@ -0,0 +1,47 @@ +package tools + +import ( + "fmt" + "os" + "os/exec" + "path/filepath" +) + +func RunSolidGrader(workDirectory string, installationDirectory string, binary string, files []string, outputFile string, outputFormat string) error { + args := []string{} + + if outputFormat == "sarif" { + args = append(args, "-f", "sarif") + } + + if len(files) > 0 { + args = append(args, files...) + } else { + args = append(args, ".") + } + + /*if configExists != "" { + log.Println("Config file found, using it") + args = append(args, "--config", configExists) + } else { + log.Println("No config file found, using tool defaults") + }*/ + + cmd := exec.Command(binary, args...) + cmd.Dir = workDirectory + cmd.Stderr = os.Stderr + if outputFile != "" { + outputWriter, err := os.Create(filepath.Clean(outputFile)) + if err != nil { + return fmt.Errorf("failed to create output file: %w", err) + } + defer outputWriter.Close() + cmd.Stdout = outputWriter + } else { + cmd.Stdout = os.Stdout + } + if err := cmd.Run(); err != nil { + return fmt.Errorf("failed to run solid_grader: %w", err) + } + return nil +} diff --git a/utils/sarif.go b/utils/sarif.go index 1cdef47a..bca8319b 100644 --- a/utils/sarif.go +++ b/utils/sarif.go @@ -259,3 +259,109 @@ func FilterRulesFromSarif(sarifData []byte) ([]byte, error) { return filteredData, nil } + +// PyreflyIssue represents a single issue in Pyrefly's JSON output +// Example fields: line, column, stop_line, stop_column, path, code, name, description, concise_description +// See: https://pyrefly.org/en/docs/usage/#output-formats + +type PyreflyIssue struct { + Line int `json:"line"` + Column int `json:"column"` + StopLine int `json:"stop_line"` + StopColumn int `json:"stop_column"` + Path string `json:"path"` + Code int `json:"code"` + Name string `json:"name"` + Description string `json:"description"` + ConciseDescription string `json:"concise_description"` +} + +// ConvertPyreflyToSarif converts Pyrefly JSON output to SARIF format +func ConvertPyreflyToSarif(pyreflyOutput []byte) []byte { + // Pyrefly outputs: { "errors": [ ... ] } + type pyreflyRoot struct { + Errors []PyreflyIssue `json:"errors"` + } + var root pyreflyRoot + var sarifReport SarifReport + if err := json.Unmarshal(pyreflyOutput, &root); err != nil { + // If parsing fails, return empty SARIF report with Pyrefly metadata + sarifReport = SarifReport{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + Runs: []Run{ + { + Tool: Tool{ + Driver: Driver{ + Name: "Pyrefly", + Version: "0.22.0", + InformationURI: "https://pyrefly.org", + }, + }, + Results: []Result{}, + }, + }, + } + } else { + sarifReport = SarifReport{ + Version: "2.1.0", + Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + Runs: []Run{ + { + Tool: Tool{ + Driver: Driver{ + Name: "Pyrefly", + Version: "0.22.0", + InformationURI: "https://pyrefly.org", + }, + }, + Results: make([]Result, 0, len(root.Errors)), + }, + }, + } + for _, issue := range root.Errors { + result := Result{ + RuleID: issue.Name, + Level: "error", // Pyrefly only reports errors + Message: MessageText{ + Text: issue.Description, + }, + Locations: []Location{ + { + PhysicalLocation: PhysicalLocation{ + ArtifactLocation: ArtifactLocation{ + URI: issue.Path, + }, + Region: Region{ + StartLine: issue.Line, + StartColumn: issue.Column, + }, + }, + }, + }, + } + sarifReport.Runs[0].Results = append(sarifReport.Runs[0].Results, result) + } + } + sarifData, err := json.MarshalIndent(sarifReport, "", " ") + if err != nil { + // If marshaling fails, return a minimal SARIF report with Pyrefly metadata + return []byte(`{ + "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", + "version": "2.1.0", + "runs": [ + { + "tool": { + "driver": { + "name": "Pyrefly", + "version": "0.22.0", + "informationUri": "https://pyrefly.org" + } + }, + "results": [] + } + ] +}`) + } + return sarifData +}