Repository: codacy/codacy-cli-v2
Pull request #91: [PLUTO-1392] Lizard installation and running
Author: login: zhamborova
Created: 2025-04-25T09:40:28Z | Merged: 2025-04-29T10:29:29Z

Note:
1. Sarif format will be handled in the configuration ticket
2. There was an extra dependency needed for lizard to be able to use html formatting flag, so I temporary added that in the installation flow for lizard only. I will add a dependencies field in the plugin config to handle it better in a separate pr


Diff:
diff --git a/.codacy/codacy.yaml b/.codacy/codacy.yaml
index 9762e4ac..00edc2e5 100644
--- a/.codacy/codacy.yaml
+++ b/.codacy/codacy.yaml
@@ -8,4 +8,5 @@ tools:
     - pylint@3.3.6
     - pmd@6.55.0
     - dartanalyzer@3.7.2
-    - semgrep@1.78.0
\ No newline at end of file
+    - semgrep@1.78.0
+    - lizard@1.17.19
\ No newline at end of file
diff --git a/cmd/analyze.go b/cmd/analyze.go
index 4cc0a153..a0edcc20 100644
--- a/cmd/analyze.go
+++ b/cmd/analyze.go
@@ -4,6 +4,7 @@ import (
 	"codacy/cli-v2/config"
 	"codacy/cli-v2/plugins"
 	"codacy/cli-v2/tools"
+	"codacy/cli-v2/tools/lizard"
 	"encoding/json"
 	"fmt"
 	"io"
@@ -199,6 +200,9 @@ func runEslintAnalysis(workDirectory string, pathsToCheck []string, autoFix bool
 
 func runTrivyAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
 	trivy := config.Config.Tools()["trivy"]
+	if trivy == nil {
+		log.Fatal("Trivy tool configuration not found")
+	}
 	trivyBinary := trivy.Binaries["trivy"]
 
 	return tools.RunTrivy(workDirectory, trivyBinary, pathsToCheck, outputFile, outputFormat)
@@ -206,6 +210,9 @@ func runTrivyAnalysis(workDirectory string, pathsToCheck []string, outputFile st
 
 func runPmdAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
 	pmd := config.Config.Tools()["pmd"]
+	if pmd == nil {
+		log.Fatal("Pmd tool configuration not found")
+	}
 	pmdBinary := pmd.Binaries["pmd"]
 
 	return tools.RunPmd(workDirectory, pmdBinary, pathsToCheck, outputFile, outputFormat, config.Config)
@@ -213,12 +220,19 @@ func runPmdAnalysis(workDirectory string, pathsToCheck []string, outputFile stri
 
 func runPylintAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
 	pylint := config.Config.Tools()["pylint"]
+	if pylint == nil {
+		log.Fatal("Pylint tool configuration not found")
+	}
+	pylintBinary := pylint.Binaries["python"]
 
-	return tools.RunPylint(workDirectory, pylint, pathsToCheck, outputFile, outputFormat)
+	return tools.RunPylint(workDirectory, pylintBinary, pathsToCheck, outputFile, outputFormat)
 }
 
 func runDartAnalyzer(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
 	dartanalyzer := config.Config.Tools()["dartanalyzer"]
+	if dartanalyzer == nil {
+		log.Fatal("Dart analyzer tool configuration not found")
+	}
 	return tools.RunDartAnalyzer(workDirectory, dartanalyzer.InstallDir, dartanalyzer.Binaries["dart"], pathsToCheck, outputFile, outputFormat)
 }
 
@@ -227,8 +241,21 @@ func runSemgrepAnalysis(workDirectory string, pathsToCheck []string, outputFile
 	if semgrep == nil {
 		log.Fatal("Semgrep tool configuration not found")
 	}
+	semgrepBinary := semgrep.Binaries["python"]
 
-	return tools.RunSemgrep(workDirectory, semgrep, pathsToCheck, outputFile, outputFormat)
+	return tools.RunSemgrep(workDirectory, semgrepBinary, pathsToCheck, outputFile, outputFormat)
+}
+
+func runLizardAnalysis(workDirectory string, pathsToCheck []string, outputFile string, outputFormat string) error {
+	lizardTool := config.Config.Tools()["lizard"]
+
+	if lizardTool == nil {
+		log.Fatal("Lizard tool configuration not found")
+	}
+
+	lizardBinary := lizardTool.Binaries["python"]
+
+	return lizard.RunLizard(workDirectory, lizardBinary, pathsToCheck, outputFile, outputFormat)
 }
 
 var analyzeCmd = &cobra.Command{
@@ -250,14 +277,13 @@ var analyzeCmd = &cobra.Command{
 		} else {
 			// Run all configured tools
 			toolsToRun = config.Config.Tools()
+			log.Println("Running all configured tools...")
 		}
 
 		if len(toolsToRun) == 0 {
 			log.Fatal("No tools configured. Please run 'codacy-cli init' and 'codacy-cli install' first")
 		}
 
-		log.Println("Running all configured tools...")
-
 		if outputFormat == "sarif" {
 			// Create temporary directory for individual tool outputs
 			tmpDir, err := os.MkdirTemp("", "codacy-analysis-*")
@@ -325,6 +351,8 @@ func runTool(workDirectory string, toolName string, args []string, outputFile st
 		return runSemgrepAnalysis(workDirectory, args, outputFile, outputFormat)
 	case "dartanalyzer":
 		return runDartAnalyzer(workDirectory, args, outputFile, outputFormat)
+	case "lizard":
+		return runLizardAnalysis(workDirectory, args, outputFile, outputFormat)
 	default:
 		return fmt.Errorf("unsupported tool: %s", toolName)
 	}
diff --git a/cmd/init.go b/cmd/init.go
index 21b2225d..9a0169e3 100644
--- a/cmd/init.go
+++ b/cmd/init.go
@@ -139,6 +139,7 @@ func configFileTemplate(tools []tools.Tool) string {
 	needsNode := false
 	needsPython := false
 	needsDart := false
+
 	// Default versions
 	defaultVersions := map[string]string{
 		ESLint:       "9.3.0",
@@ -147,6 +148,7 @@ func configFileTemplate(tools []tools.Tool) string {
 		PMD:          "6.55.0",
 		DartAnalyzer: "3.7.2",
 		Semgrep:      "1.78.0",
+		Lizard:       "1.17.19",
 	}
 
 	// Build map of enabled tools with their versions
@@ -161,7 +163,7 @@ func configFileTemplate(tools []tools.Tool) string {
 		// Check if tool needs a runtime
 		if tool.Uuid == ESLint {
 			needsNode = true
-		} else if tool.Uuid == PyLint {
+		} else if tool.Uuid == PyLint || tool.Uuid == Lizard {
 			needsPython = true
 		} else if tool.Uuid == DartAnalyzer {
 			needsDart = true
@@ -202,6 +204,7 @@ func configFileTemplate(tools []tools.Tool) string {
 			PMD:          "pmd",
 			DartAnalyzer: "dartanalyzer",
 			Semgrep:      "semgrep",
+			Lizard:       "lizard",
 		}
 
 		for uuid, name := range uuidToName {
@@ -217,6 +220,7 @@ func configFileTemplate(tools []tools.Tool) string {
 		sb.WriteString(fmt.Sprintf("    - pmd@%s\n", defaultVersions[PMD]))
 		sb.WriteString(fmt.Sprintf("    - dartanalyzer@%s\n", defaultVersions[DartAnalyzer]))
 		sb.WriteString(fmt.Sprintf("    - semgrep@%s\n", defaultVersions[Semgrep]))
+		sb.WriteString(fmt.Sprintf("    - lizard@%s\n", defaultVersions[Lizard]))
 	}
 
 	return sb.String()
@@ -519,4 +523,5 @@ const (
 	PyLint       string = "31677b6d-4ae0-4f56-8041-606a8d7a8e61"
 	DartAnalyzer string = "d203d615-6cf1-41f9-be5f-e2f660f7850f"
 	Semgrep      string = "6792c561-236d-41b7-ba5e-9d6bee0d548b"
+	Lizard       string = "76348462-84b3-409a-90d3-955e90abfb87"
 )
diff --git a/config/tools-installer.go b/config/tools-installer.go
index 3f53f863..d6e2fe42 100644
--- a/config/tools-installer.go
+++ b/config/tools-installer.go
@@ -203,6 +203,17 @@ func installPythonTool(name string, toolInfo *plugins.ToolInfo) error {
 		return fmt.Errorf("failed to install tool: %s\nError: %w", string(output), err)
 	}
 
+	// Install jinja2 if this is Lizard,
+	// todo: temporary dependency for lizard
+	// will add field to plugin.yaml to specify extra dependencies
+	if name == "lizard" {
+		cmd = exec.Command(pipPath, "install", "jinja2")
+		output, err = cmd.CombinedOutput()
+		if err != nil {
+			return fmt.Errorf("failed to install jinja2 dependency: %s\nError: %w", string(output), err)
+		}
+	}
+
 	log.Printf("Successfully installed %s v%s\n", toolInfo.Name, toolInfo.Version)
 	return nil
 }
diff --git a/plugins/tool-utils_test.go b/plugins/tool-utils_test.go
index 6375c6e7..b8734abd 100644
--- a/plugins/tool-utils_test.go
+++ b/plugins/tool-utils_test.go
@@ -168,6 +168,7 @@ func TestGetSupportedTools(t *testing.T) {
 				"trivy",
 				"dartanalyzer",
 				"semgrep",
+				"lizard",
 			},
 			expectedError: false,
 		},
diff --git a/plugins/tools/lizard/plugin.yaml b/plugins/tools/lizard/plugin.yaml
new file mode 100644
index 00000000..9d3e19ff
--- /dev/null
+++ b/plugins/tools/lizard/plugin.yaml
@@ -0,0 +1,13 @@
+name: lizard
+description: Lizard code complexity analyzer
+runtime: python
+runtime_binaries:
+  package_manager: python3
+  execution: python3
+binaries:
+  - name: python
+    path: "venv/bin/python3"
+output_options:
+  file_flag: "-o"
+analysis_options:
+  default_path: "." 
\ No newline at end of file
diff --git a/tools/lizard/lizardRunner.go b/tools/lizard/lizardRunner.go
new file mode 100644
index 00000000..8124f2e0
--- /dev/null
+++ b/tools/lizard/lizardRunner.go
@@ -0,0 +1,43 @@
+package lizard
+
+import (
+	"fmt"
+	"os"
+	"os/exec"
+)
+
+// RunLizard executes Lizard code complexity analyzer with the specified options
+func RunLizard(workDirectory string, binary string, files []string, outputFile string, outputFormat string) error {
+
+	// Construct base command with -m lizard to run lizard module
+	args := []string{"-m", "lizard"}
+
+	// Add output file if specified
+	if outputFile != "" {
+		args = append(args, "-o", outputFile)
+	}
+
+	// Add files to analyze - if no files specified, analyze current directory
+	if len(files) > 0 {
+		args = append(args, files...)
+	} else {
+		args = append(args, ".")
+	}
+
+	// Create and run command
+	cmd := exec.Command(binary, args...)
+	cmd.Dir = workDirectory
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	// Run the command
+	err := cmd.Run()
+	if err != nil {
+		// Lizard returns 1 when it finds issues, which is not a failure
+		if exitErr, ok := err.(*exec.ExitError); ok && exitErr.ExitCode() == 1 {
+			return nil
+		}
+		return fmt.Errorf("failed to run Lizard: %w", err)
+	}
+	return nil
+}
diff --git a/tools/lizard/test/complex.py b/tools/lizard/test/complex.py
new file mode 100644
index 00000000..b0b030e5
--- /dev/null
+++ b/tools/lizard/test/complex.py
@@ -0,0 +1,73 @@
+def complex_analysis(data, options=None):
+    """A function with high complexity"""
+    if not data:
+        return None
+    
+    results = {
+        'summary': {},
+        'details': [],
+        'warnings': [],
+        'errors': []
+    }
+    
+    # Process different types of data
+    if isinstance(data, dict):
+        for key, value in data.items():
+            if isinstance(value, (int, float)):
+                if value > 100:
+                    results['summary'][key] = 'high'
+                    results['details'].append({
+                        'type': 'numeric',
+                        'value': value,
+                        'status': 'high'
+                    })
+                elif value > 50:
+                    results['summary'][key] = 'medium'
+                    results['details'].append({
+                        'type': 'numeric',
+                        'value': value,
+                        'status': 'medium'
+                    })
+                else:
+                    results['summary'][key] = 'low'
+                    results['details'].append({
+                        'type': 'numeric',
+                        'value': value,
+                        'status': 'low'
+                    })
+            elif isinstance(value, str):
+                if len(value) > 100:
+                    results['warnings'].append(f"Long string found in {key}")
+                results['details'].append({
+                    'type': 'string',
+                    'length': len(value),
+                    'key': key
+                })
+            elif isinstance(value, list):
+                for i, item in enumerate(value):
+                    if isinstance(item, dict):
+                        for subkey, subvalue in item.items():
+                            if isinstance(subvalue, (int, float)):
+                                if subvalue < 0:
+                                    results['errors'].append(f"Negative value in {key}[{i}].{subkey}")
+                    elif isinstance(item, str) and len(item) > 50:
+                        results['warnings'].append(f"Long string in list {key}[{i}]")
+    
+    # Apply options if provided
+    if options:
+        if 'threshold' in options:
+            threshold = options['threshold']
+            for key in list(results['summary'].keys()):
+                if results['summary'][key] == 'high' and threshold == 'medium':
+                    results['summary'][key] = 'medium'
+                elif results['summary'][key] in ['high', 'medium'] and threshold == 'low':
+                    results['summary'][key] = 'low'
+        
+        if 'filter' in options:
+            filter_type = options['filter']
+            if filter_type == 'warnings':
+                results['details'] = [d for d in results['details'] if d.get('status') == 'warning']
+            elif filter_type == 'errors':
+                results['details'] = [d for d in results['details'] if d.get('status') == 'error']
+    
+    return results 
\ No newline at end of file
diff --git a/tools/lizard/test/expected.csv b/tools/lizard/test/expected.csv
new file mode 100644
index 00000000..ed0d59b4
--- /dev/null
+++ b/tools/lizard/test/expected.csv
@@ -0,0 +1 @@
+65,31,463,2,73,"complex_analysis@1-73@complex.py","complex.py","complex_analysis","complex_analysis( data , options = None )",1,73 
diff --git a/tools/lizard/test/lizardRunner_test.go b/tools/lizard/test/lizardRunner_test.go
new file mode 100644
index 00000000..f15af5d0
--- /dev/null
+++ b/tools/lizard/test/lizardRunner_test.go
@@ -0,0 +1,66 @@
+package test
+
+import (
+	"codacy/cli-v2/plugins"
+	"codacy/cli-v2/tools/lizard"
+	"log"
+	"os"
+	"path/filepath"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/assert"
+)
+
+func TestRunLizard(t *testing.T) {
+	homeDirectory, err := os.UserHomeDir()
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+	currentDirectory, err := os.Getwd()
+	if err != nil {
+		log.Fatal(err.Error())
+	}
+
+	// Use the current directory containing our sample complex code
+	testDirectory := currentDirectory
+	globalCache := filepath.Join(homeDirectory, ".cache/codacy")
+
+	// Setup tool info
+	toolInfo := &plugins.ToolInfo{
+		Binaries: map[string]string{
+			"python": filepath.Join(globalCache, "tools/lizard@1.17.19/venv/bin/python"),
+		},
+	}
+
+	t.Run("Output to file", func(t *testing.T) {
+		tempResultFile := filepath.Join(os.TempDir(), "lizard.csv")
+		defer os.Remove(tempResultFile)
+
+		err := lizard.RunLizard(testDirectory, toolInfo.Binaries["python"], []string{"complex.py"}, tempResultFile, "")
+		assert.NoError(t, err)
+
+		// Read actual output
+		actualData, err := os.ReadFile(tempResultFile)
+		assert.NoError(t, err)
+
+		// Read expected output
+		expectedData, err := os.ReadFile("expected.csv")
+		assert.NoError(t, err)
+
+		// Normalize and compare the files
+		actualStr := strings.TrimSpace(string(actualData))
+		expectedStr := strings.TrimSpace(string(expectedData))
+		assert.Equal(t, expectedStr, actualStr, "Output does not match expected output")
+	})
+
+	t.Run("No output file specified", func(t *testing.T) {
+		err := lizard.RunLizard(testDirectory, toolInfo.Binaries["python"], []string{"complex.py"}, "", "")
+		assert.NoError(t, err)
+	})
+
+	t.Run("No files specified", func(t *testing.T) {
+		err := lizard.RunLizard(testDirectory, toolInfo.Binaries["python"], nil, "", "")
+		assert.NoError(t, err)
+	})
+}
diff --git a/tools/pylintRunner.go b/tools/pylintRunner.go
index 84cfecdf..f71bf9cf 100644
--- a/tools/pylintRunner.go
+++ b/tools/pylintRunner.go
@@ -2,17 +2,13 @@ package tools
 
 import (
 	"codacy/cli-v2/config"
-	"codacy/cli-v2/plugins"
 	"codacy/cli-v2/utils"
 	"fmt"
 	"os"
 	"os/exec"
-	"path/filepath"
 )
 
-func RunPylint(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error {
-	// Get Python binary from venv
-	pythonPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "python3")
+func RunPylint(workDirectory string, binary string, files []string, outputFile string, outputFormat string) error {
 
 	// Construct base command with -m pylint to run pylint module
 	args := []string{"-m", "pylint"}
@@ -48,7 +44,7 @@ func RunPylint(workDirectory string, toolInfo *plugins.ToolInfo, files []string,
 	}
 
 	// Create and run command
-	cmd := exec.Command(pythonPath, args...)
+	cmd := exec.Command(binary, args...)
 	cmd.Dir = workDirectory
 	cmd.Stdout = os.Stdout
 	cmd.Stderr = os.Stderr
diff --git a/tools/semgrepRunner.go b/tools/semgrepRunner.go
index e2fae3cb..955cd184 100644
--- a/tools/semgrepRunner.go
+++ b/tools/semgrepRunner.go
@@ -2,7 +2,6 @@ package tools
 
 import (
 	"codacy/cli-v2/config"
-	"codacy/cli-v2/plugins"
 	"fmt"
 	"os"
 	"os/exec"
@@ -10,7 +9,7 @@ import (
 )
 
 // RunSemgrep executes Semgrep analysis on the specified directory
-func RunSemgrep(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error {
+func RunSemgrep(workDirectory string, binary string, files []string, outputFile string, outputFormat string) error {
 	// Construct base command with -m semgrep to run semgrep module
 	cmdArgs := []string{"scan"}
 
@@ -44,11 +43,8 @@ func RunSemgrep(workDirectory string, toolInfo *plugins.ToolInfo, files []string
 		cmdArgs = append(cmdArgs, ".")
 	}
 
-	// Get Semgrep binary from the specified installation path
-	semgrepPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "semgrep")
-
 	// Create Semgrep command
-	cmd := exec.Command(semgrepPath, cmdArgs...)
+	cmd := exec.Command(binary, cmdArgs...)
 	cmd.Dir = workDirectory
 
 	if outputFile != "" {
diff --git a/tools/semgrepRunner_test.go b/tools/semgrepRunner_test.go
index 235bf0d2..1d81344f 100644
--- a/tools/semgrepRunner_test.go
+++ b/tools/semgrepRunner_test.go
@@ -26,14 +26,16 @@ func TestRunSemgrepWithSpecificFiles(t *testing.T) {
 
 	// Create tool info for semgrep
 	toolInfo := &plugins.ToolInfo{
-		InstallDir: filepath.Join(homeDirectory, ".cache/codacy/tools/semgrep@1.78.0"),
+		Binaries: map[string]string{
+			"semgrep": filepath.Join(homeDirectory, ".cache/codacy/tools/semgrep@1.78.0/venv/bin/semgrep"),
+		},
 	}
 
 	// Specify files to analyze
 	filesToAnalyze := []string{"sample.js"}
 
 	// Run Semgrep analysis on specific files
-	err = RunSemgrep(testDirectory, toolInfo, filesToAnalyze, tempResultFile, "sarif")
+	err = RunSemgrep(testDirectory, toolInfo.Binaries["semgrep"], filesToAnalyze, tempResultFile, "sarif")
 	if err != nil {
 		t.Fatalf("Failed to run semgrep on specific files: %v", err)
 	}


Reviews:
user: login: copilot-pull-request-reviewer[bot]
body: ## Pull Request Overview

This PR integrates the Lizard code complexity analyzer into the Codacy CLI by introducing its runner, tests, configuration, and support in existing commands.  
- Adds Lizard integration including a dedicated runner and associated tests  
- Updates configuration files and command dispatch to include Lizard  
- Modifies the project and plugin initialization to recognize Lizard

### Reviewed Changes

Copilot reviewed 7 out of 8 changed files in this pull request and generated 1 comment.

<details>
<summary>Show a summary per file</summary>

| File                                    | Description                                                               |
| --------------------------------------- | ------------------------------------------------------------------------- |
| tools/lizard/test/lizardRunner_test.go  | Unit tests for Lizard runner demonstrating various output scenarios       |
| tools/lizard/test/complex.py            | A sample complexity analysis function potentially used for testing         |
| tools/lizard/lizardRunner.go              | Implements the Lizard runner command execution using a virtual environment  |
| plugins/tools/lizard/plugin.yaml        | Registers the Lizard tool and its formatters in the plugin manifest          |
| cmd/init.go                            | Updates default configuration to include Lizard with version 1.17.19         |
| cmd/analyze.go                         | Introduces runLizardAnalysis and includes Lizard in the tool dispatcher       |
| .codacy/codacy.yaml                    | Adds Lizard to the list of tools for repository configuration                |
</details>


<details>
<summary>Files not reviewed (1)</summary>

* **tools/lizard/test/expected.csv**: Language not supported
</details>



state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-25T09:52:28Z
user: login: machadoit
body: We talked about not having the SARIF yet, but the part that I don't like is to have a test against the CSV. All our tests of tools should be against the SARIF format. But I am fine moving forward if you want to change it on a further PR to the feature branch
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T10:17:38Z
user: login: zhamborova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T10:36:38Z
user: login: zhamborova
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-04-28T20:23:07Z
user: login: machadoit
body: LGTM!
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-04-29T10:28:48Z

Review comments:
diff_hunk: @@ -0,0 +1,20 @@
+name: lizard
+description: Lizard code complexity analyzer
+runtime: python
+runtime_binaries:
+  package_manager: python3
+  execution: python3
+binaries:
+  - name: python
+    path: "venv/bin/python3"
+formatters:
path: plugins/tools/lizard/plugin.yaml
user: login: machadoit
body: Do we this formatters thing for other tools as well? I would consider aiming at the one(s) that we want to use. As the CLI is focused on 'native' or 'SARIF'.
created_at: 2025-04-28T10:12:47Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package lizard
+
+import (
+	"codacy/cli-v2/plugins"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+)
+
+// RunLizard executes Lizard code complexity analyzer with the specified options
+func RunLizard(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error {
+	// Get Python binary from venv
+	pythonPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "python3")
path: tools/lizard/lizardRunner.go
user: login: machadoit
body: This should be coming from the binaries from the config
created_at: 2025-04-28T10:13:25Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package lizard
+
+import (
+	"codacy/cli-v2/plugins"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+)
+
+// RunLizard executes Lizard code complexity analyzer with the specified options
+func RunLizard(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error {
+	// Get Python binary from venv
+	pythonPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "python3")
+
+	// Construct base command with -m lizard to run lizard module
+	args := []string{"-m", "lizard"}
+
+	// Add output file if specified
+	if outputFile != "" {
+		args = append(args, "-o", outputFile)
+	}
+
+	// Add files to analyze - if no files specified, analyze current directory
+	if len(files) > 0 {
+		args = append(args, files...)
+	} else {
+		args = append(args, ".")
+	}
+
+	// Create and run command
+	cmd := exec.Command(pythonPath, args...)
+	cmd.Dir = workDirectory
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	// Run the command
+	err := cmd.Run()
+	if err != nil {
+		// Lizard returns non-zero exit code when it finds issues
+		// We should not treat this as an error
+		if _, ok := err.(*exec.ExitError); !ok {
+			return fmt.Errorf("failed to run Lizard: %w", err)
path: tools/lizard/lizardRunner.go
user: login: machadoit
body: I see a `Lizard returns non-zero exit code when it finds issues` but then we are saying it has an error? Is this not missing something about checking the exact error code or something? Like maybe ESLint does?
created_at: 2025-04-28T10:15:41Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,20 @@
+name: lizard
+description: Lizard code complexity analyzer
+runtime: python
+runtime_binaries:
+  package_manager: python3
+  execution: python3
+binaries:
+  - name: python
+    path: "venv/bin/python3"
+formatters:
path: plugins/tools/lizard/plugin.yaml
user: login: zhamborova
body: I only see some specifications for json, but why should all tool formatters not be available for use along with sarif? 
created_at: 2025-04-28T10:36:38Z
author_association: CONTRIBUTOR
diff_hunk: @@ -0,0 +1,48 @@
+package lizard
+
+import (
+	"codacy/cli-v2/plugins"
+	"fmt"
+	"os"
+	"os/exec"
+	"path/filepath"
+)
+
+// RunLizard executes Lizard code complexity analyzer with the specified options
+func RunLizard(workDirectory string, toolInfo *plugins.ToolInfo, files []string, outputFile string, outputFormat string) error {
+	// Get Python binary from venv
+	pythonPath := filepath.Join(toolInfo.InstallDir, "venv", "bin", "python3")
+
+	// Construct base command with -m lizard to run lizard module
+	args := []string{"-m", "lizard"}
+
+	// Add output file if specified
+	if outputFile != "" {
+		args = append(args, "-o", outputFile)
+	}
+
+	// Add files to analyze - if no files specified, analyze current directory
+	if len(files) > 0 {
+		args = append(args, files...)
+	} else {
+		args = append(args, ".")
+	}
+
+	// Create and run command
+	cmd := exec.Command(pythonPath, args...)
+	cmd.Dir = workDirectory
+	cmd.Stdout = os.Stdout
+	cmd.Stderr = os.Stderr
+
+	// Run the command
+	err := cmd.Run()
+	if err != nil {
+		// Lizard returns non-zero exit code when it finds issues
+		// We should not treat this as an error
+		if _, ok := err.(*exec.ExitError); !ok {
+			return fmt.Errorf("failed to run Lizard: %w", err)
path: tools/lizard/lizardRunner.go
user: login: zhamborova
body: Yes, checked it's only 1, and everything else is an error
created_at: 2025-04-28T20:23:07Z
author_association: CONTRIBUTOR

PR comments:
user: login: codacy-production[bot]
created_at: 2025-04-25T09:56:40Z
body: ## Coverage summary from Codacy
### [See diff coverage on Codacy](https://app.codacy.com/gh/codacy/codacy-cli-v2/pull-requests/91/coverage?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryPullRequest)

 | Coverage variation | Diff coverage | 
 | ------------- | ------------- |
 | :white_check_mark: **+0.29%** | :white_check_mark: **47.89%** |

<details>
<summary>Coverage variation details</summary>

| | Coverable lines | Covered lines | Coverage |
| ------------- | ------------- | ------------- | ------------- |
| Common ancestor commit (9e718c32752a9198e8b2d1822b695f83a343533f) | 3013 | 1073 | 35.61% | |
| Head commit (72e500333ce5af41af178c73865df842821d49a7) | 3067 (+54) | 1101 (+28) | 35.90% (**+0.29%**) |

**Coverage variation** is the difference between the coverage for the head and common ancestor commits of the pull request branch: `<coverage of head commit> - <coverage of common ancestor commit>`
</details>

<details>
<summary>Diff coverage details</summary>

| | Coverable lines | Covered lines | Diff coverage |
| ------------- | ------------- | ------------- | ------------- |
| Pull request (#91) | 71 | 34 | **47.89%** |

**Diff coverage** is the percentage of lines that are covered by tests out of the coverable lines that the pull request added or modified: `<covered lines added or modified>/<coverable lines added or modified> * 100%`
</details>

#### [See your quality gate settings](https://app.codacy.com/gh/codacy/codacy-cli-v2/settings/quality?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummaryGates)&nbsp;&nbsp;&nbsp;&nbsp;[Change summary preferences](https://app.codacy.com/gh/codacy/codacy-cli-v2/settings/integrations?utm_source=github.com&utm_medium=coverageSummary&utm_campaign=coverageSummarySettings)

<!-- 108f945d-e553-4918-8ccf-027c6d4acc2c -->
author_association: NONE
user: login: zhamborova
created_at: 2025-04-28T10:22:37Z
body: > We talked about not having the SARIF yet, but the part that I don't like is to have a test against the CSV. All our tests of tools should be against the SARIF format. But I am fine moving forward if you want to change it on a further PR to the feature branch

I tested csv since that's what is available, once sarif is implemented I will also add a test for that, here I am checking running functionality 👍 
author_association: CONTRIBUTOR