Skip to content

Commit a638e63

Browse files
added basic support for both pyrefly and solid-grader
1 parent 63a2be3 commit a638e63

9 files changed

Lines changed: 297 additions & 0 deletions

File tree

cli-v2.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,8 @@ import (
1414
)
1515

1616
func main() {
17+
18+
fmt.Printf("TESTING CLI BUILD")
1719
// Initialize config global object
1820
config.Init()
1921

cmd/analyze.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -412,6 +412,31 @@ func runEnigmaAnalysis(workDirectory string, pathsToCheck []string, outputFile s
412412
return tools.RunEnigma(workDirectory, enigma.InstallDir, enigma.Binaries["codacy-enigma-cli"], pathsToCheck, outputFile, outputFormat)
413413
}
414414

415+
func runSolidGraderAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
416+
solidGrader := config.Config.Tools()["solid_grader"]
417+
if solidGrader == nil {
418+
log.Fatal("solid_grader tool configuration not found")
419+
}
420+
solidGraderBinary := solidGrader.Binaries["solid_grader"]
421+
return tools.RunSolidGrader(
422+
workDirectory,
423+
solidGrader.InstallDir,
424+
solidGraderBinary,
425+
pathsToCheck,
426+
outputFile,
427+
outputFormat,
428+
)
429+
}
430+
431+
func runPyreflyAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
432+
pyrefly := config.Config.Tools()["pyrefly"]
433+
if pyrefly == nil {
434+
log.Fatal("Pyrefly tool configuration not found")
435+
}
436+
pyreflyBinary := pyrefly.Binaries["pyrefly"]
437+
return tools.RunPyrefly(workDirectory, pyreflyBinary, pathsToCheck, outputFile, outputFormat)
438+
}
439+
415440
var analyzeCmd = &cobra.Command{
416441
Use: "analyze",
417442
Short: "Runs all configured linters.",
@@ -522,6 +547,10 @@ func runTool(workDirectory string, toolName string, args []string, outputFile st
522547
return runLizardAnalysis(workDirectory, args, outputFile, outputFormat)
523548
case "codacy-enigma-cli":
524549
return runEnigmaAnalysis(workDirectory, args, outputFile, outputFormat)
550+
case "solid_grader":
551+
return runSolidGraderAnalysis(workDirectory, args, outputFile, outputFormat)
552+
case "pyrefly":
553+
return runPyreflyAnalysis(workDirectory, args, outputFile, outputFormat)
525554
default:
526555
return fmt.Errorf("unsupported tool: %s", toolName)
527556
}

domain/tool.go

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@ const (
2727
DartAnalyzer string = "d203d615-6cf1-41f9-be5f-e2f660f7850f"
2828
Semgrep string = "6792c561-236d-41b7-ba5e-9d6bee0d548b"
2929
Lizard string = "76348462-84b3-409a-90d3-955e90abfb87"
30+
SolidGrader string = "716FCE5C-F5E8-4B9D-A7D2-86CDFAE79D45"
3031
)
3132

3233
type ToolInfo struct {
@@ -45,4 +46,5 @@ var SupportedToolsMetadata = map[string]ToolInfo{
4546
DartAnalyzer: {Name: "dartanalyzer", Priority: 0},
4647
Lizard: {Name: "lizard", Priority: 0},
4748
Semgrep: {Name: "semgrep", Priority: 0},
49+
SolidGrader: {Name: "solid_grader", Priority: 0},
4850
}

plugins/tools/pyrefly/plugin.yaml

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
name: pyrefly
2+
# Pyrefly: Fast, modern Python type checker (https://pyrefly.org/en/docs/installation/)
3+
description: Pyrefly is a fast, modern static type checker for Python, designed for developer productivity and CI integration.
4+
default_version: 0.22.0
5+
runtime: python
6+
runtime_binaries:
7+
package_manager: python3
8+
execution: python3
9+
binaries:
10+
- name: pyrefly
11+
path: "venv/bin/pyrefly"
12+
output_options:
13+
file_flag: "--output"
14+
analysis_options:
15+
default_path: "."
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
name: solid_grader
2+
# TODO: Update description with accurate details
3+
description: Solid Grader scans your code for SOLID principle violations.
4+
default_version: 0.1.0 # TODO: Update with actual version if needed
5+
binaries:
6+
- name: solid_grader
7+
path: "/Users/kendrickcurtis/Documents/GitHub/solid-grader2/solid-grader2"
8+
formatters:
9+
- name: text
10+
flag: ""
11+
- name: sarif
12+
flag: "-f sarif"
13+
# TODO: Add download section if binary is to be downloaded automatically

tools/language_config.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -58,6 +58,11 @@ func CreateLanguagesConfigFile(apiTools []domain.Tool, toolsConfigDir string, to
5858
Languages: []string{"C", "CPP", "C#", "Generic", "Go", "Java", "JavaScript", "JSON", "Kotlin", "Python", "TypeScript", "Ruby", "Rust", "JSX", "PHP", "Scala", "Swift", "Terraform"},
5959
Extensions: []string{".c", ".cpp", ".h", ".hpp", ".cs", ".go", ".java", ".js", ".json", ".kt", ".py", ".ts", ".rb", ".rs", ".jsx", ".php", ".scala", ".swift", ".tf", ".tfvars"},
6060
},
61+
"pyrefly": {
62+
Name: "pyrefly",
63+
Languages: []string{"Python"},
64+
Extensions: []string{".py"},
65+
},
6166
}
6267

6368
// Build a list of tool language info for enabled tools

tools/pyreflyRunner.go

Lines changed: 78 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,78 @@
1+
package tools
2+
3+
import (
4+
"codacy/cli-v2/utils"
5+
"fmt"
6+
"io/ioutil"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
)
11+
12+
// RunPyrefly executes Pyrefly type checking on the specified directory or files
13+
func RunPyrefly(workDirectory string, binary string, files []string, outputFile string, outputFormat string) error {
14+
args := []string{"check"}
15+
16+
// Always use JSON output for SARIF conversion
17+
var tempFile string
18+
if outputFormat == "sarif" {
19+
tmp, err := ioutil.TempFile("", "pyrefly-*.json")
20+
if err != nil {
21+
return fmt.Errorf("failed to create temporary file: %w", err)
22+
}
23+
tempFile = tmp.Name()
24+
tmp.Close()
25+
defer os.Remove(tempFile)
26+
args = append(args, "--output", tempFile, "--output-format", "json")
27+
} else if outputFile != "" {
28+
args = append(args, "--output", outputFile)
29+
}
30+
if outputFormat == "json" && outputFile == "" {
31+
args = append(args, "--output-format", "json")
32+
}
33+
34+
// Detect config file (pyrefly.toml or pyproject.toml)
35+
configFiles := []string{"pyrefly.toml", "pyproject.toml"}
36+
for _, configFile := range configFiles {
37+
if _, err := os.Stat(filepath.Join(workDirectory, configFile)); err == nil {
38+
// Pyrefly auto-detects config, so no need to add a flag
39+
break
40+
}
41+
}
42+
43+
// Add files to check, or "." for current directory
44+
if len(files) > 0 {
45+
args = append(args, files...)
46+
} else {
47+
args = append(args, ".")
48+
}
49+
50+
cmd := exec.Command(binary, args...)
51+
cmd.Dir = workDirectory
52+
cmd.Stdout = os.Stdout
53+
cmd.Stderr = os.Stderr
54+
55+
err := cmd.Run()
56+
if err != nil {
57+
if _, ok := err.(*exec.ExitError); !ok {
58+
return fmt.Errorf("failed to run Pyrefly: %w", err)
59+
}
60+
}
61+
62+
if outputFormat == "sarif" {
63+
jsonOutput, err := os.ReadFile(tempFile)
64+
if err != nil {
65+
return fmt.Errorf("failed to read Pyrefly output: %w", err)
66+
}
67+
sarifOutput := utils.ConvertPyreflyToSarif(jsonOutput)
68+
if outputFile != "" {
69+
err = os.WriteFile(outputFile, sarifOutput, 0644)
70+
if err != nil {
71+
return fmt.Errorf("failed to write SARIF output: %w", err)
72+
}
73+
} else {
74+
fmt.Println(string(sarifOutput))
75+
}
76+
}
77+
return nil
78+
}

tools/solidGraderRunner.go

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
package tools
2+
3+
import (
4+
"fmt"
5+
"os"
6+
"os/exec"
7+
"path/filepath"
8+
)
9+
10+
func RunSolidGrader(workDirectory string, installationDirectory string, binary string, files []string, outputFile string, outputFormat string) error {
11+
args := []string{}
12+
13+
if outputFormat == "sarif" {
14+
args = append(args, "-f", "sarif")
15+
}
16+
17+
if len(files) > 0 {
18+
args = append(args, files...)
19+
} else {
20+
args = append(args, ".")
21+
}
22+
23+
/*if configExists != "" {
24+
log.Println("Config file found, using it")
25+
args = append(args, "--config", configExists)
26+
} else {
27+
log.Println("No config file found, using tool defaults")
28+
}*/
29+
30+
cmd := exec.Command(binary, args...)
31+
cmd.Dir = workDirectory
32+
cmd.Stderr = os.Stderr
33+
if outputFile != "" {
34+
outputWriter, err := os.Create(filepath.Clean(outputFile))
35+
if err != nil {
36+
return fmt.Errorf("failed to create output file: %w", err)
37+
}
38+
defer outputWriter.Close()
39+
cmd.Stdout = outputWriter
40+
} else {
41+
cmd.Stdout = os.Stdout
42+
}
43+
if err := cmd.Run(); err != nil {
44+
return fmt.Errorf("failed to run solid_grader: %w", err)
45+
}
46+
return nil
47+
}

utils/sarif.go

Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -259,3 +259,109 @@ func FilterRulesFromSarif(sarifData []byte) ([]byte, error) {
259259

260260
return filteredData, nil
261261
}
262+
263+
// PyreflyIssue represents a single issue in Pyrefly's JSON output
264+
// Example fields: line, column, stop_line, stop_column, path, code, name, description, concise_description
265+
// See: https://pyrefly.org/en/docs/usage/#output-formats
266+
267+
type PyreflyIssue struct {
268+
Line int `json:"line"`
269+
Column int `json:"column"`
270+
StopLine int `json:"stop_line"`
271+
StopColumn int `json:"stop_column"`
272+
Path string `json:"path"`
273+
Code int `json:"code"`
274+
Name string `json:"name"`
275+
Description string `json:"description"`
276+
ConciseDescription string `json:"concise_description"`
277+
}
278+
279+
// ConvertPyreflyToSarif converts Pyrefly JSON output to SARIF format
280+
func ConvertPyreflyToSarif(pyreflyOutput []byte) []byte {
281+
// Pyrefly outputs: { "errors": [ ... ] }
282+
type pyreflyRoot struct {
283+
Errors []PyreflyIssue `json:"errors"`
284+
}
285+
var root pyreflyRoot
286+
var sarifReport SarifReport
287+
if err := json.Unmarshal(pyreflyOutput, &root); err != nil {
288+
// If parsing fails, return empty SARIF report with Pyrefly metadata
289+
sarifReport = SarifReport{
290+
Version: "2.1.0",
291+
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
292+
Runs: []Run{
293+
{
294+
Tool: Tool{
295+
Driver: Driver{
296+
Name: "Pyrefly",
297+
Version: "0.22.0",
298+
InformationURI: "https://pyrefly.org",
299+
},
300+
},
301+
Results: []Result{},
302+
},
303+
},
304+
}
305+
} else {
306+
sarifReport = SarifReport{
307+
Version: "2.1.0",
308+
Schema: "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
309+
Runs: []Run{
310+
{
311+
Tool: Tool{
312+
Driver: Driver{
313+
Name: "Pyrefly",
314+
Version: "0.22.0",
315+
InformationURI: "https://pyrefly.org",
316+
},
317+
},
318+
Results: make([]Result, 0, len(root.Errors)),
319+
},
320+
},
321+
}
322+
for _, issue := range root.Errors {
323+
result := Result{
324+
RuleID: issue.Name,
325+
Level: "error", // Pyrefly only reports errors
326+
Message: MessageText{
327+
Text: issue.Description,
328+
},
329+
Locations: []Location{
330+
{
331+
PhysicalLocation: PhysicalLocation{
332+
ArtifactLocation: ArtifactLocation{
333+
URI: issue.Path,
334+
},
335+
Region: Region{
336+
StartLine: issue.Line,
337+
StartColumn: issue.Column,
338+
},
339+
},
340+
},
341+
},
342+
}
343+
sarifReport.Runs[0].Results = append(sarifReport.Runs[0].Results, result)
344+
}
345+
}
346+
sarifData, err := json.MarshalIndent(sarifReport, "", " ")
347+
if err != nil {
348+
// If marshaling fails, return a minimal SARIF report with Pyrefly metadata
349+
return []byte(`{
350+
"$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json",
351+
"version": "2.1.0",
352+
"runs": [
353+
{
354+
"tool": {
355+
"driver": {
356+
"name": "Pyrefly",
357+
"version": "0.22.0",
358+
"informationUri": "https://pyrefly.org"
359+
}
360+
},
361+
"results": []
362+
}
363+
]
364+
}`)
365+
}
366+
return sarifData
367+
}

0 commit comments

Comments
 (0)