Skip to content

Commit c95c3ec

Browse files
committed
Add pylint tool
1 parent fbcf3b3 commit c95c3ec

File tree

8 files changed

+333
-5
lines changed

8 files changed

+333
-5
lines changed

cmd/analyze.go

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ type Pattern struct {
9191
}
9292

9393
func init() {
94-
analyzeCmd.Flags().StringVarP(&outputFile, "output", "o", "", "output file for the results")
94+
analyzeCmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file for analysis results")
9595
analyzeCmd.Flags().StringVarP(&toolToAnalyze, "tool", "t", "", "Which tool to run analysis with")
9696
analyzeCmd.Flags().StringVar(&outputFormat, "format", "", "Output format (use 'sarif' for SARIF format)")
9797
analyzeCmd.Flags().BoolVar(&autoFix, "fix", false, "Apply auto fix to your issues when available")
@@ -203,6 +203,15 @@ func runTrivyAnalysis(workDirectory string, pathsToCheck []string, outputFile st
203203
}
204204
}
205205

206+
func runPylintAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) {
207+
pylint := config.Config.Tools()["pylint"]
208+
209+
err := tools.RunPylint(workDirectory, pylint, pathsToCheck, outputFile, outputFormat)
210+
if err != nil {
211+
log.Fatalf("Error running Pylint: %v", err)
212+
}
213+
}
214+
206215
var analyzeCmd = &cobra.Command{
207216
Use: "analyze",
208217
Short: "Runs all linters.",
@@ -226,6 +235,8 @@ var analyzeCmd = &cobra.Command{
226235
runEslintAnalysis(workDirectory, args, autoFix, outputFile, outputFormat)
227236
case "trivy":
228237
runTrivyAnalysis(workDirectory, args, outputFile, outputFormat)
238+
case "pylint":
239+
runPylintAnalysis(workDirectory, args, outputFile, outputFormat)
229240
case "":
230241
log.Fatal("You need to specify a tool to run analysis with, e.g., '--tool eslint'")
231242
default:

cmd/init.go

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ func configFileTemplate(tools []tools.Tool) string {
7575
// Default versions
7676
eslintVersion := "9.3.0"
7777
trivyVersion := "0.59.1" // Latest stable version
78+
pylintVersion := "3.3.6"
7879

7980
for _, tool := range tools {
8081
if tool.Uuid == "f8b29663-2cb2-498d-b923-a10c6a8c05cd" {
@@ -83,6 +84,9 @@ func configFileTemplate(tools []tools.Tool) string {
8384
if tool.Uuid == "2fd7fbe0-33f9-4ab3-ab73-e9b62404e2cb" {
8485
trivyVersion = tool.Version
8586
}
87+
if tool.Uuid == "31677b6d-4ae0-4f56-8041-606a8d7a8e61" {
88+
pylintVersion = tool.Version
89+
}
8690
}
8791

8892
return fmt.Sprintf(`runtimes:
@@ -91,7 +95,8 @@ func configFileTemplate(tools []tools.Tool) string {
9195
tools:
9296
- eslint@%s
9397
- trivy@%s
94-
`, eslintVersion, trivyVersion)
98+
- pylint@%s
99+
`, eslintVersion, trivyVersion, pylintVersion)
95100
}
96101

97102
func buildRepositoryConfigurationFiles(token string) error {

config/tools-installer.go

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -44,8 +44,16 @@ func InstallTool(name string, toolInfo *plugins.ToolInfo) error {
4444
return installDownloadBasedTool(toolInfo)
4545
}
4646

47-
// This is a runtime-based tool, proceed with regular installation
47+
// Handle Python tools differently
48+
if toolInfo.Runtime == "python" {
49+
return installPythonTool(name, toolInfo)
50+
}
51+
52+
// Handle other runtime-based tools
53+
return installRuntimeTool(name, toolInfo)
54+
}
4855

56+
func installRuntimeTool(name string, toolInfo *plugins.ToolInfo) error {
4957
// Get the runtime for this tool
5058
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
5159
if !ok {
@@ -159,6 +167,38 @@ func installDownloadBasedTool(toolInfo *plugins.ToolInfo) error {
159167
return nil
160168
}
161169

170+
func installPythonTool(name string, toolInfo *plugins.ToolInfo) error {
171+
log.Printf("Installing %s v%s...\n", toolInfo.Name, toolInfo.Version)
172+
173+
runtimeInfo, ok := Config.Runtimes()[toolInfo.Runtime]
174+
if !ok {
175+
return fmt.Errorf("required runtime %s not found for tool %s", toolInfo.Runtime, name)
176+
}
177+
178+
pythonBinary, ok := runtimeInfo.Binaries["python3"]
179+
if !ok {
180+
return fmt.Errorf("python3 binary not found in runtime")
181+
}
182+
183+
// Create venv
184+
cmd := exec.Command(pythonBinary, "-m", "venv", filepath.Join(toolInfo.InstallDir, "venv"))
185+
output, err := cmd.CombinedOutput()
186+
if err != nil {
187+
return fmt.Errorf("failed to create venv: %s\nError: %w", string(output), err)
188+
}
189+
190+
// Install the tool using pip from venv
191+
pipPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "pip")
192+
cmd = exec.Command(pipPath, "install", fmt.Sprintf("%s==%s", toolInfo.Name, toolInfo.Version))
193+
output, err = cmd.CombinedOutput()
194+
if err != nil {
195+
return fmt.Errorf("failed to install tool: %s\nError: %w", string(output), err)
196+
}
197+
198+
log.Printf("Successfully installed %s v%s\n", toolInfo.Name, toolInfo.Version)
199+
return nil
200+
}
201+
162202
// isToolInstalled checks if a tool is already installed by checking for the binary
163203
func isToolInstalled(toolInfo *plugins.ToolInfo) bool {
164204
// If there are no binaries, check the install directory

plugins/runtime-utils.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,11 @@ func (p *runtimePlugin) getDownloadURL(version string) string {
254254

255255
// GetInstallationDirectoryPath returns the installation directory path for the runtime
256256
func (p *runtimePlugin) getInstallationDirectoryPath(runtimesDir string, version string) string {
257+
// For Python, we want to use a simpler directory structure
258+
if p.Config.Name == "python" {
259+
return path.Join(runtimesDir, "python")
260+
}
261+
// For other runtimes, keep using the filename-based directory
257262
fileName := p.getFileName(version)
258263
return path.Join(runtimesDir, fileName)
259264
}

plugins/runtimes/python/plugin.yaml

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,6 @@ download:
1818
"windows": "pc-windows-msvc"
1919
binaries:
2020
- name: python3
21-
path: "python/bin/python3"
21+
path: "bin/python3"
2222
- name: pip
23-
path: "python/bin/pip"
23+
path: "bin/pip"

plugins/tools/pylint/plugin.yaml

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
name: pylint
2+
description: Python linter
3+
runtime: python
4+
runtime_binaries:
5+
package_manager: python3
6+
execution: python3
7+
binaries:
8+
- name: python
9+
path: "venv/bin/python3"
10+
formatters:
11+
- name: json
12+
flag: "--output-format=json"
13+
output_options:
14+
file_flag: "--output"
15+
analysis_options:
16+
default_path: "."

tools/pylintRunner.go

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
package tools
2+
3+
import (
4+
"codacy/cli-v2/plugins"
5+
"codacy/cli-v2/utils"
6+
"fmt"
7+
"os"
8+
"os/exec"
9+
"path/filepath"
10+
)
11+
12+
func RunPylint(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error {
13+
// Get Python binary from venv
14+
pythonPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "python3")
15+
16+
// Construct base command with -m pylint to run pylint module
17+
args := []string{"-m", "pylint"}
18+
19+
// Always use JSON output format since we'll convert to SARIF if needed
20+
args = append(args, "--output-format=json")
21+
22+
// Create a temporary file for JSON output if we need to convert to SARIF
23+
var tempFile string
24+
if outputFormat == "sarif" {
25+
tmp, err := os.CreateTemp("", "pylint-*.json")
26+
if err != nil {
27+
return fmt.Errorf("failed to create temporary file: %w", err)
28+
}
29+
tempFile = tmp.Name()
30+
tmp.Close()
31+
defer os.Remove(tempFile)
32+
args = append(args, fmt.Sprintf("--output=%s", tempFile))
33+
} else if outputFile != "" {
34+
args = append(args, fmt.Sprintf("--output=%s", outputFile))
35+
}
36+
37+
// Add files to analyze - if no files specified, analyze current directory
38+
if len(files) > 0 {
39+
args = append(args, files...)
40+
} else {
41+
args = append(args, ".")
42+
}
43+
44+
// Create and run command
45+
cmd := exec.Command(pythonPath, args...)
46+
cmd.Dir = workDirectory
47+
cmd.Stdout = os.Stdout
48+
cmd.Stderr = os.Stderr
49+
50+
// Run the command
51+
err := cmd.Run()
52+
if err != nil {
53+
// Pylint returns non-zero exit code when it finds issues
54+
// We should not treat this as an error
55+
if _, ok := err.(*exec.ExitError); !ok {
56+
return fmt.Errorf("failed to run pylint: %w", err)
57+
}
58+
}
59+
60+
// If SARIF output is requested, convert JSON to SARIF
61+
if outputFormat == "sarif" {
62+
jsonOutput, err := os.ReadFile(tempFile)
63+
if err != nil {
64+
return fmt.Errorf("failed to read pylint output: %w", err)
65+
}
66+
67+
sarifOutput := utils.ConvertPylintToSarif(jsonOutput)
68+
69+
if outputFile != "" {
70+
err = os.WriteFile(outputFile, sarifOutput, 0644)
71+
if err != nil {
72+
return fmt.Errorf("failed to write SARIF output: %w", err)
73+
}
74+
} else {
75+
fmt.Println(string(sarifOutput))
76+
}
77+
}
78+
79+
return nil
80+
}

0 commit comments

Comments
 (0)