Skip to content

Commit f97be27

Browse files
feature: Automatic Tool and Runtime Installation During Analysis - PLUTO-1422 (#145)
* not fail when analysing tool/runtime that is not installed * added tests and fixed bug with runtime installation * refactoring runTools to be more generic * lizard refactor to have logic of running in runner * refactor upload to use codacyclient, use domain types and run lizard like other tools * removed extra prints on downloading runtimes * fixed small issues * add default_version to enigma and validate tool name to be sure we are not trying to install typo tool * reverted default_version from enigma plugin and code review changes * Refactor configuration management functions to be more generic and reduce code duplication. Updated runtime and tool handling functions to use a unified entry list update method. Improved addToCodacyYaml function for better maintainability. (#149) --------- Co-authored-by: Andrzej Janczak <[email protected]>
1 parent 73c0a88 commit f97be27

File tree

14 files changed

+427
-339
lines changed

14 files changed

+427
-339
lines changed

cmd/analyze.go

Lines changed: 103 additions & 186 deletions
Original file line numberDiff line numberDiff line change
@@ -9,15 +9,15 @@ import (
99
reviveTool "codacy/cli-v2/tools/revive"
1010
"encoding/json"
1111
"fmt"
12-
"io"
1312
"log"
14-
"net/http"
1513
"os"
1614
"path/filepath"
1715
"strings"
1816

1917
"codacy/cli-v2/utils"
2018

19+
codacyclient "codacy/cli-v2/codacy-client"
20+
2121
"github.com/spf13/cobra"
2222
"gopkg.in/yaml.v3"
2323
)
@@ -212,21 +212,6 @@ type CodacyIssue struct {
212212
Category string `json:"category"`
213213
}
214214

215-
type Tool struct {
216-
UUID string `json:"uuid"`
217-
ShortName string `json:"shortName"`
218-
Prefix string `json:"prefix"`
219-
}
220-
221-
type Pattern struct {
222-
UUID string `json:"uuid"`
223-
ID string `json:"id"`
224-
Name string `json:"name"`
225-
Category string `json:"category"`
226-
Description string `json:"description"`
227-
Level string `json:"level"`
228-
}
229-
230215
func init() {
231216
analyzeCmd.Flags().StringVarP(&outputFile, "output", "o", "", "Output file for analysis results")
232217
analyzeCmd.Flags().StringVarP(&toolsToAnalyzeParam, "tool", "t", "", "Which tool to run analysis with. If not specified, all configured tools will be run")
@@ -235,66 +220,24 @@ func init() {
235220
rootCmd.AddCommand(analyzeCmd)
236221
}
237222

238-
func loadsToolAndPatterns(toolName string) (Tool, []Pattern) {
239-
var toolsURL = "https://app.codacy.com/api/v3/tools"
240-
241-
req, err := http.NewRequest("GET", toolsURL, nil)
223+
func loadsToolAndPatterns(toolName string) (domain.Tool, []domain.PatternConfiguration) {
224+
var toolsResponse, err = codacyclient.GetToolsVersions()
242225
if err != nil {
243-
fmt.Printf("Error creating request: %v\n", err)
244-
panic("panic")
226+
fmt.Println("Error:", err)
227+
return domain.Tool{}, []domain.PatternConfiguration{}
245228
}
246-
req.Header.Set("Content-Type", "application/json")
247-
248-
resp, err := http.DefaultClient.Do(req)
249-
if err != nil {
250-
fmt.Printf("Error fetching patterns: %v\n", err)
251-
panic("panic")
252-
}
253-
defer resp.Body.Close()
254-
255-
body, _ := io.ReadAll(resp.Body)
256-
257-
var toolsResponse struct {
258-
Data []Tool `json:"data"`
259-
}
260-
json.Unmarshal(body, &toolsResponse)
261-
var tool Tool
262-
for _, t := range toolsResponse.Data {
263-
if t.ShortName == toolName {
229+
var tool domain.Tool
230+
for _, t := range toolsResponse {
231+
if t.Name == toolName {
264232
tool = t
265233
break
266234
}
267235
}
268-
var patterns []Pattern
269-
var hasNext bool = true
270-
cursor := ""
271-
client := &http.Client{}
272-
273-
for hasNext {
274-
baseURL := fmt.Sprintf("https://app.codacy.com/api/v3/tools/%s/patterns?limit=1000%s", tool.UUID, cursor)
275-
req, _ := http.NewRequest("GET", baseURL, nil)
276-
req.Header.Set("Content-Type", "application/json")
277-
278-
resp, err := client.Do(req)
279-
if err != nil {
280-
fmt.Println("Error:", err)
281-
break
282-
}
283-
defer resp.Body.Close()
284-
body, _ := io.ReadAll(resp.Body)
285-
286-
var patternsResponse struct {
287-
Data []Pattern `json:"data"`
288-
Pagination struct {
289-
Cursor string `json:"cursor"`
290-
} `json:"pagination"`
291-
}
292-
json.Unmarshal(body, &patternsResponse)
293-
patterns = append(patterns, patternsResponse.Data...)
294-
hasNext = patternsResponse.Pagination.Cursor != ""
295-
if hasNext {
296-
cursor = "&cursor=" + patternsResponse.Pagination.Cursor
297-
}
236+
var patterns []domain.PatternConfiguration
237+
patterns, err = codacyclient.GetDefaultToolPatternsConfig(domain.InitFlags{}, tool.Uuid)
238+
if err != nil {
239+
fmt.Println("Error:", err)
240+
return domain.Tool{}, []domain.PatternConfiguration{}
298241
}
299242
return tool, patterns
300243
}
@@ -317,110 +260,113 @@ func getToolName(toolName string, version string) string {
317260
return toolName
318261
}
319262

320-
func runEslintAnalysis(workDirectory string, pathsToCheck []string, autoFix bool, outputFile string, outputFormat string) error {
321-
eslint := config.Config.Tools()["eslint"]
322-
eslintInstallationDirectory := eslint.InstallDir
323-
nodeRuntime := config.Config.Runtimes()["node"]
324-
nodeBinary := nodeRuntime.Binaries["node"]
325-
326-
return tools.RunEslint(workDirectory, eslintInstallationDirectory, nodeBinary, pathsToCheck, autoFix, outputFile, outputFormat)
327-
}
328-
329-
func runTrivyAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
330-
trivy := config.Config.Tools()["trivy"]
331-
if trivy == nil {
332-
log.Fatal("Trivy tool configuration not found")
263+
func validateToolName(toolName string) error {
264+
if toolName == "" {
265+
return fmt.Errorf("tool name cannot be empty")
333266
}
334-
trivyBinary := trivy.Binaries["trivy"]
335267

336-
return tools.RunTrivy(workDirectory, trivyBinary, pathsToCheck, outputFile, outputFormat)
337-
}
268+
// Get plugin manager to access the tools filesystem
269+
pluginManager := plugins.GetPluginManager()
338270

339-
func runPmdAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
340-
pmd := config.Config.Tools()["pmd"]
341-
if pmd == nil {
342-
log.Fatal("Pmd tool configuration not found")
271+
// Try to get the tool configuration - this will fail if the tool doesn't exist
272+
_, err := pluginManager.GetToolConfig(toolName)
273+
if err != nil {
274+
return fmt.Errorf("tool '%s' is not supported", toolName)
343275
}
344-
pmdBinary := pmd.Binaries["pmd"]
345276

346-
return tools.RunPmd(workDirectory, pmdBinary, pathsToCheck, outputFile, outputFormat, config.Config)
277+
return nil
347278
}
348279

349-
func runPylintAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
350-
pylint := config.Config.Tools()["pylint"]
351-
if pylint == nil {
352-
log.Fatal("Pylint tool configuration not found")
280+
func runToolByName(toolName string, workDirectory string, pathsToCheck []string, autoFix bool, outputFile string, outputFormat string, tool *plugins.ToolInfo, runtime *plugins.RuntimeInfo) error {
281+
switch toolName {
282+
case "eslint":
283+
binaryPath := runtime.Binaries[tool.Runtime]
284+
return tools.RunEslint(workDirectory, tool.InstallDir, binaryPath, pathsToCheck, autoFix, outputFile, outputFormat)
285+
case "trivy":
286+
binaryPath := tool.Binaries[toolName]
287+
return tools.RunTrivy(workDirectory, binaryPath, pathsToCheck, outputFile, outputFormat)
288+
case "pmd":
289+
binaryPath := tool.Binaries[toolName]
290+
return tools.RunPmd(workDirectory, binaryPath, pathsToCheck, outputFile, outputFormat, config.Config)
291+
case "pylint":
292+
binaryPath := tool.Binaries[tool.Runtime]
293+
return tools.RunPylint(workDirectory, binaryPath, pathsToCheck, outputFile, outputFormat)
294+
case "dartanalyzer":
295+
binaryPath := tool.Binaries[tool.Runtime]
296+
return tools.RunDartAnalyzer(workDirectory, tool.InstallDir, binaryPath, pathsToCheck, outputFile, outputFormat)
297+
case "semgrep":
298+
binaryPath := tool.Binaries[toolName]
299+
return tools.RunSemgrep(workDirectory, binaryPath, pathsToCheck, outputFile, outputFormat)
300+
case "lizard":
301+
binaryPath := tool.Binaries[tool.Runtime]
302+
return lizard.RunLizard(workDirectory, binaryPath, pathsToCheck, outputFile, outputFormat)
303+
case "codacy-enigma-cli":
304+
return tools.RunEnigma(workDirectory, tool.InstallDir, tool.Binaries["codacy-enigma-cli"], pathsToCheck, outputFile, outputFormat)
305+
case "revive":
306+
return reviveTool.RunRevive(workDirectory, tool.Binaries["revive"], pathsToCheck, outputFile, outputFormat)
353307
}
354-
pylintBinary := pylint.Binaries["python"]
355-
356-
return tools.RunPylint(workDirectory, pylintBinary, pathsToCheck, outputFile, outputFormat)
308+
return fmt.Errorf("unsupported tool: %s", toolName)
357309
}
358310

359-
func runDartAnalyzer(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
360-
dartanalyzer := config.Config.Tools()["dartanalyzer"]
361-
if dartanalyzer == nil {
362-
log.Fatal("Dart analyzer tool configuration not found")
311+
func runTool(workDirectory string, toolName string, pathsToCheck []string, outputFile string, autoFix bool, outputFormat string) error {
312+
err := validateToolName(toolName)
313+
if err != nil {
314+
return err
363315
}
364-
return tools.RunDartAnalyzer(workDirectory, dartanalyzer.InstallDir, dartanalyzer.Binaries["dart"], pathsToCheck, outputFile, outputFormat)
365-
}
316+
log.Println("Running tools for the specified file(s)...")
317+
log.Printf("Running %s...\n", toolName)
366318

367-
func runSemgrepAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
368-
semgrep := config.Config.Tools()["semgrep"]
369-
if semgrep == nil {
370-
log.Fatal("Semgrep tool configuration not found")
319+
tool := config.Config.Tools()[toolName]
320+
var isToolInstalled bool
321+
if tool == nil {
322+
isToolInstalled = false
323+
} else {
324+
isToolInstalled = config.Config.IsToolInstalled(toolName, tool)
371325
}
372-
semgrepBinary := semgrep.Binaries["semgrep"]
373-
374-
return tools.RunSemgrep(workDirectory, semgrepBinary, pathsToCheck, outputFile, outputFormat)
375-
}
326+
var isRuntimeInstalled bool
376327

377-
func runLizardAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
378-
lizardTool := config.Config.Tools()["lizard"]
328+
var runtime *plugins.RuntimeInfo
379329

380-
if lizardTool == nil {
381-
log.Fatal("Lizard plugin configuration not found")
330+
if toolName == "codacy-enigma-cli" {
331+
isToolInstalled = true
382332
}
383333

384-
lizardBinary := lizardTool.Binaries["python"]
385-
386-
configFile, exists := tools.ConfigFileExists(config.Config, "lizard.yaml")
387-
var patterns []domain.PatternDefinition
388-
var err error
389-
390-
if exists {
391-
// Configuration exists, read from file
392-
patterns, err = lizard.ReadConfig(configFile)
393-
if err != nil {
394-
return fmt.Errorf("error reading config file: %v", err)
334+
if tool == nil || !isToolInstalled {
335+
if tool == nil {
336+
fmt.Println("Tool configuration not found, adding and installing...")
395337
}
396-
} else {
397-
fmt.Println("No configuration file found for Lizard, using default patterns, run init with repository token to get a custom configuration")
398-
patterns, err = tools.FetchDefaultEnabledPatterns(domain.Lizard)
338+
if !isToolInstalled {
339+
fmt.Println("Tool is not installed, installing...")
340+
}
341+
err := config.InstallTool(toolName, tool, "")
399342
if err != nil {
400-
return fmt.Errorf("failed to fetch default patterns: %v", err)
343+
return fmt.Errorf("failed to install %s: %w", toolName, err)
344+
}
345+
tool = config.Config.Tools()[toolName]
346+
runtime = config.Config.Runtimes()[tool.Runtime]
347+
isRuntimeInstalled = runtime == nil || config.Config.IsRuntimeInstalled(tool.Runtime, runtime)
348+
if !isRuntimeInstalled {
349+
fmt.Printf("%s runtime is not installed, installing...\n", tool.Runtime)
350+
err := config.InstallRuntime(tool.Runtime, runtime)
351+
if err != nil {
352+
return fmt.Errorf("failed to install %s runtime: %w", tool.Runtime, err)
353+
}
354+
runtime = config.Config.Runtimes()[tool.Runtime]
401355
}
402-
}
403-
404-
return lizard.RunLizard(workDirectory, lizardBinary, pathsToCheck, outputFile, outputFormat, patterns)
405-
}
406-
407-
func runEnigmaAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
408-
enigma := config.Config.Tools()["codacy-enigma-cli"]
409-
if enigma == nil {
410-
log.Fatal("Enigma tool configuration not found")
411-
}
412-
413-
return tools.RunEnigma(workDirectory, enigma.InstallDir, enigma.Binaries["codacy-enigma-cli"], pathsToCheck, outputFile, outputFormat)
414-
}
415356

416-
func runReviveAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
417-
revive := config.Config.Tools()["revive"]
418-
if revive == nil {
419-
log.Fatal("Revive tool configuration not found")
357+
} else {
358+
runtime = config.Config.Runtimes()[tool.Runtime]
359+
isRuntimeInstalled = runtime == nil || config.Config.IsRuntimeInstalled(tool.Runtime, runtime)
360+
if !isRuntimeInstalled {
361+
fmt.Printf("%s runtime is not installed, installing...\n", tool.Runtime)
362+
err := config.InstallRuntime(tool.Runtime, runtime)
363+
if err != nil {
364+
return fmt.Errorf("failed to install %s runtime: %w", tool.Runtime, err)
365+
}
366+
runtime = config.Config.Runtimes()[tool.Runtime]
367+
}
420368
}
421-
reviveBinary := revive.Binaries["revive"]
422-
423-
return reviveTool.RunRevive(workDirectory, reviveBinary, pathsToCheck, outputFile, outputFormat)
369+
return runToolByName(toolName, workDirectory, pathsToCheck, autoFix, outputFile, outputFormat, tool, runtime)
424370
}
425371

426372
var analyzeCmd = &cobra.Command{
@@ -457,8 +403,6 @@ var analyzeCmd = &cobra.Command{
457403
return
458404
}
459405

460-
log.Println("Running tools for the specified file(s)...")
461-
462406
if outputFormat == "sarif" {
463407
// Create temporary directory for individual tool outputs
464408
tmpDir, err := os.MkdirTemp("", "codacy-analysis-*")
@@ -469,10 +413,9 @@ var analyzeCmd = &cobra.Command{
469413

470414
var sarifOutputs []string
471415
for toolName := range toolsToRun {
472-
log.Printf("Running %s...\n", toolName)
473416
tmpFile := filepath.Join(tmpDir, fmt.Sprintf("%s.sarif", toolName))
474-
if err := runTool(workDirectory, toolName, args, tmpFile); err != nil {
475-
log.Printf("Tool failed to run: %s: %v\n", toolName, err)
417+
if err := runTool(workDirectory, toolName, args, tmpFile, autoFix, outputFormat); err != nil {
418+
log.Printf("Tool failed to run: %v\n", err)
476419
}
477420
sarifOutputs = append(sarifOutputs, tmpFile)
478421
}
@@ -506,36 +449,10 @@ var analyzeCmd = &cobra.Command{
506449
} else {
507450
// Run tools without merging outputs
508451
for toolName := range toolsToRun {
509-
log.Printf("Running %s...\n", toolName)
510-
if err := runTool(workDirectory, toolName, args, outputFile); err != nil {
511-
log.Printf("Tool failed to run: %s: %v\n", toolName, err)
452+
if err := runTool(workDirectory, toolName, args, outputFile, autoFix, outputFormat); err != nil {
453+
log.Printf("Tool failed to run: %v\n", err)
512454
}
513455
}
514456
}
515457
},
516458
}
517-
518-
func runTool(workDirectory string, toolName string, args []string, outputFile string) error {
519-
switch toolName {
520-
case "eslint":
521-
return runEslintAnalysis(workDirectory, args, autoFix, outputFile, outputFormat)
522-
case "trivy":
523-
return runTrivyAnalysis(workDirectory, args, outputFile, outputFormat)
524-
case "pmd":
525-
return runPmdAnalysis(workDirectory, args, outputFile, outputFormat)
526-
case "pylint":
527-
return runPylintAnalysis(workDirectory, args, outputFile, outputFormat)
528-
case "semgrep":
529-
return runSemgrepAnalysis(workDirectory, args, outputFile, outputFormat)
530-
case "dartanalyzer":
531-
return runDartAnalyzer(workDirectory, args, outputFile, outputFormat)
532-
case "lizard":
533-
return runLizardAnalysis(workDirectory, args, outputFile, outputFormat)
534-
case "codacy-enigma-cli":
535-
return runEnigmaAnalysis(workDirectory, args, outputFile, outputFormat)
536-
case "revive":
537-
return runReviveAnalysis(workDirectory, args, outputFile, outputFormat)
538-
default:
539-
return fmt.Errorf("unsupported tool: %s", toolName)
540-
}
541-
}

0 commit comments

Comments
 (0)