diff --git a/cmd/analyze.go b/cmd/analyze.go index 0fd36faf..56dbe9f2 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -211,7 +211,7 @@ func runPmdAnalysis(workDirectory string, pathsToCheck []string, outputFile stri pmd := config.Config.Tools()["pmd"] pmdBinary := pmd.Binaries["pmd"] - err := tools.RunPmd(workDirectory, pmdBinary, pathsToCheck, outputFile, outputFormat, "") + err := tools.RunPmd(workDirectory, pmdBinary, pathsToCheck, outputFile, outputFormat, config.Config) if err != nil { log.Fatalf("Error running PMD: %v", err) } diff --git a/cmd/init.go b/cmd/init.go index 1e34a96e..31ab57f7 100644 --- a/cmd/init.go +++ b/cmd/init.go @@ -327,12 +327,12 @@ func createToolFileConfigurations(tool tools.Tool, patternConfiguration []domain func createPMDConfigFile(config []domain.PatternConfiguration, toolsConfigDir string) error { pmdConfigurationString := tools.CreatePmdConfig(config) - return os.WriteFile(filepath.Join(toolsConfigDir, "pmd-ruleset.xml"), []byte(pmdConfigurationString), utils.DefaultFilePerms) + return os.WriteFile(filepath.Join(toolsConfigDir, "ruleset.xml"), []byte(pmdConfigurationString), utils.DefaultFilePerms) } func createDefaultPMDConfigFile(toolsConfigDir string) error { content := tools.CreatePmdConfig([]domain.PatternConfiguration{}) - return os.WriteFile(filepath.Join(toolsConfigDir, "pmd-ruleset.xml"), []byte(content), utils.DefaultFilePerms) + return os.WriteFile(filepath.Join(toolsConfigDir, "ruleset.xml"), []byte(content), utils.DefaultFilePerms) } diff --git a/config/config.go b/config/config.go index 24239296..74f91f11 100644 --- a/config/config.go +++ b/config/config.go @@ -11,7 +11,8 @@ import ( ) type ConfigType struct { - homePath string + repositoryDirectory string + globalCacheDirectory string runtimesDirectory string toolsDirectory string @@ -24,8 +25,8 @@ type ConfigType struct { tools map[string]*plugins.ToolInfo } -func (c *ConfigType) HomePath() string { - return c.homePath +func (c *ConfigType) RepositoryDirectory() string { + return c.repositoryDirectory } func (c *ConfigType) CodacyDirectory() string { @@ -98,15 +99,42 @@ func (c *ConfigType) ToolsConfigDirectory() string { return c.toolsConfigDirectory } -func (c *ConfigType) setupCodacyPaths() { - c.globalCacheDirectory = filepath.Join(c.homePath, ".cache", "codacy") +func NewConfigType(repositoryDirectory string, repositoryCache string, globalCache string) *ConfigType { + c := &ConfigType{} + + c.repositoryDirectory = repositoryDirectory + c.localCodacyDirectory = repositoryCache + c.globalCacheDirectory = globalCache + c.runtimesDirectory = filepath.Join(c.globalCacheDirectory, "runtimes") c.toolsDirectory = filepath.Join(c.globalCacheDirectory, "tools") - c.localCodacyDirectory = ".codacy" c.toolsConfigDirectory = filepath.Join(c.localCodacyDirectory, "tools-configs") c.projectConfigFile = filepath.Join(c.localCodacyDirectory, "codacy.yaml") c.cliConfigFile = filepath.Join(c.localCodacyDirectory, "cli-config.yaml") + + c.runtimes = make(map[string]*plugins.RuntimeInfo) + c.tools = make(map[string]*plugins.ToolInfo) + return c +} + +// TODO: Consider not having a global config and instead pass the config object around +func setupGlobalConfig(repositoryDirectory string, repositoryCache string, globalCache string) { + newConfig := NewConfigType(repositoryDirectory, repositoryCache, globalCache) + + Config.repositoryDirectory = newConfig.repositoryDirectory + Config.localCodacyDirectory = newConfig.localCodacyDirectory + Config.globalCacheDirectory = newConfig.globalCacheDirectory + + Config.runtimesDirectory = newConfig.runtimesDirectory + Config.toolsDirectory = newConfig.toolsDirectory + Config.toolsConfigDirectory = newConfig.toolsConfigDirectory + + Config.projectConfigFile = newConfig.projectConfigFile + Config.cliConfigFile = newConfig.cliConfigFile + + Config.runtimes = newConfig.runtimes + Config.tools = newConfig.tools } func (c *ConfigType) CreateCodacyDirs() error { @@ -136,11 +164,12 @@ func Init() { if err != nil { log.Fatal(err) } - Config.homePath = homePath + // Repository directory is the current working directory + repositoryDirectory := "" + repositoryCache := ".codacy" + globalCache := filepath.Join(homePath, ".cache", "codacy") - Config.setupCodacyPaths() - Config.runtimes = make(map[string]*plugins.RuntimeInfo) - Config.tools = make(map[string]*plugins.ToolInfo) + setupGlobalConfig(repositoryDirectory, repositoryCache, globalCache) } // IsRuntimeInstalled checks if a runtime is already installed diff --git a/tools/eslintRunner.go b/tools/eslintRunner.go index 3ae58b0b..4000a5e3 100644 --- a/tools/eslintRunner.go +++ b/tools/eslintRunner.go @@ -20,9 +20,10 @@ func RunEslint(repositoryToAnalyseDirectory string, eslintInstallationDirectory // https://eslint.org/docs/v8.x/use/configure/configuration-files-new cmd.Env = append(cmd.Env, "ESLINT_USE_FLAT_CONFIG=true") - // Add config file from tools-configs directory - configFile := filepath.Join(config.Config.ToolsConfigDirectory(), "eslint.config.mjs") - cmd.Args = append(cmd.Args, "-c", configFile) + // Add config file from tools-configs directory if it exists + if configFile, exists := ConfigFileExists(config.Config, "eslint.config.mjs"); exists { + cmd.Args = append(cmd.Args, "-c", configFile) + } if autoFix { cmd.Args = append(cmd.Args, "--fix") diff --git a/tools/pmdRunner.go b/tools/pmdRunner.go index 173094d0..901d09d5 100644 --- a/tools/pmdRunner.go +++ b/tools/pmdRunner.go @@ -4,7 +4,6 @@ import ( "codacy/cli-v2/config" "os" "os/exec" - "path/filepath" "strings" ) @@ -20,15 +19,12 @@ import ( // // Returns: // - error: nil if analysis succeeds or violations found, error otherwise -func RunPmd(repositoryToAnalyseDirectory string, pmdBinary string, pathsToCheck []string, outputFile string, outputFormat string, rulesetFile string) error { +func RunPmd(repositoryToAnalyseDirectory string, pmdBinary string, pathsToCheck []string, outputFile string, outputFormat string, config config.ConfigType) error { cmd := exec.Command(pmdBinary, "pmd") - // Add config file from tools-configs directory if not specified - if rulesetFile == "" { - configFile := filepath.Join(config.Config.ToolsConfigDirectory(), "pmd-ruleset.xml") + // Add config file from tools-configs directory if it exists + if configFile, exists := ConfigFileExists(config, "ruleset.xml"); exists { cmd.Args = append(cmd.Args, "-R", configFile) - } else { - cmd.Args = append(cmd.Args, "-R", rulesetFile) } // Add source directories (comma-separated list for PMD) diff --git a/tools/pmdRunner_test.go b/tools/pmdRunner_test.go index 453bfe11..30bea013 100644 --- a/tools/pmdRunner_test.go +++ b/tools/pmdRunner_test.go @@ -1,6 +1,7 @@ package tools import ( + "codacy/cli-v2/config" "encoding/json" "log" "os" @@ -51,19 +52,24 @@ func TestRunPmdToFile(t *testing.T) { // Use the correct path relative to tools directory testDirectory := filepath.Join(currentDirectory, "testdata", "repositories", "pmd") + repositoryCache := filepath.Join(testDirectory, ".codacy") + globalCache := filepath.Join(homeDirectory, ".cache/codacy") + tempResultFile := filepath.Join(os.TempDir(), "pmd.sarif") defer os.Remove(tempResultFile) + config := *config.NewConfigType(testDirectory, repositoryCache, globalCache) + // Use absolute paths repositoryToAnalyze := testDirectory // Use the standard ruleset file for testing the PMD runner functionality - rulesetFile := filepath.Join(testDirectory, "pmd-ruleset.xml") + //rulesetFile := filepath.Join(testDirectory, "ruleset.xml") // Use the same path as defined in plugin.yaml - pmdBinary := filepath.Join(homeDirectory, ".cache/codacy/tools/pmd@6.55.0/pmd-bin-6.55.0/bin/run.sh") + pmdBinary := filepath.Join(globalCache, "tools/pmd@6.55.0/pmd-bin-6.55.0/bin/run.sh") // Run PMD - err = RunPmd(repositoryToAnalyze, pmdBinary, nil, tempResultFile, "sarif", rulesetFile) + err = RunPmd(repositoryToAnalyze, pmdBinary, nil, tempResultFile, "sarif", config) if err != nil { t.Fatalf("Failed to run pmd: %v", err) } diff --git a/tools/pylintRunner.go b/tools/pylintRunner.go index 40e1d8be..5d907837 100644 --- a/tools/pylintRunner.go +++ b/tools/pylintRunner.go @@ -1,6 +1,7 @@ package tools import ( + "codacy/cli-v2/config" "codacy/cli-v2/plugins" "codacy/cli-v2/utils" "fmt" @@ -19,9 +20,10 @@ func RunPylint(workDirectory string, toolInfo *plugins.ToolInfo, files []string, // Always use JSON output format since we'll convert to SARIF if needed args = append(args, "--output-format=json") - // Use the configuration file from .codacy/tools-configs/pylintrc - configPath := filepath.Join(workDirectory, ".codacy", "tools-configs", "pylint.rc") - args = append(args, fmt.Sprintf("--rcfile=%s", configPath)) + // Check if a config file exists in the expected location and use it if present + if configFile, exists := ConfigFileExists(config.Config, "pylint.rc"); exists { + args = append(args, fmt.Sprintf("--rcfile=%s", configFile)) + } // Create a temporary file for JSON output if we need to convert to SARIF var tempFile string diff --git a/tools/runnerUtils.go b/tools/runnerUtils.go new file mode 100644 index 00000000..46e37986 --- /dev/null +++ b/tools/runnerUtils.go @@ -0,0 +1,30 @@ +package tools + +import ( + "codacy/cli-v2/config" + "os" + "path/filepath" +) + +// ConfigFileExists checks if a specific configuration file exists in the .codacy/tools-configs/ +// or on the root of the repository directory. +// +// Parameters: +// - conf: The configuration object containing the tools config directory +// - fileName: The configuration file name to check for +// +// Returns: +// - string: The relative path to the configuration file (for cmd args) +// - bool: True if the file exists, false otherwise +func ConfigFileExists(conf config.ConfigType, fileName string) (string, bool) { + generatedConfigFile := filepath.Join(conf.ToolsConfigDirectory(), fileName) + existingConfigFile := filepath.Join(conf.RepositoryDirectory(), fileName) + + if _, err := os.Stat(generatedConfigFile); err == nil { + return generatedConfigFile, true + } else if _, err := os.Stat(existingConfigFile); err == nil { + return existingConfigFile, true + } + + return "", false +} diff --git a/tools/runnerUtils_test.go b/tools/runnerUtils_test.go new file mode 100644 index 00000000..706548ea --- /dev/null +++ b/tools/runnerUtils_test.go @@ -0,0 +1,108 @@ +package tools + +import ( + "codacy/cli-v2/config" + "os" + "path/filepath" + "testing" + + "github.com/stretchr/testify/assert" +) + +func TestConfigFileExistsInToolsConfigDirectory(t *testing.T) { + // Create a test directory structure + tempDir := t.TempDir() + repoDir := filepath.Join(tempDir, "src") + repositoryCache := filepath.Join(repoDir, ".codacy") + + // Create configuration + config := *config.NewConfigType(repoDir, repositoryCache, "unused-global-cache") + + // Create .codacy/tools-configs directory + configDir := filepath.Join(repoDir, ".codacy", "tools-configs") + err := os.MkdirAll(configDir, 0755) + assert.NoError(t, err, "Failed to create test directory structure") + + // Create a test config file on the configDir + generatedConfigFile := filepath.Join(configDir, "generated-config.yaml") + err = os.WriteFile(generatedConfigFile, []byte("test content"), 0644) + assert.NoError(t, err, "Failed to create test config file") + + // Test case: Config file exists in tools config directory + configPath, exists := ConfigFileExists(config, "generated-config.yaml") + assert.True(t, exists, "Config file should exist in tools config directory") + assert.Equal(t, filepath.Join(config.ToolsConfigDirectory(), "generated-config.yaml"), configPath, + "Config path should be correctly formed relative path") +} + +func TestConfigFileExistsInRepositoryDirectory(t *testing.T) { + // Create a test directory structure + tempDir := t.TempDir() + repoDir := filepath.Join(tempDir, "src") + repositoryCache := filepath.Join(repoDir, ".codacy") + + // Create configuration + config := *config.NewConfigType(repoDir, repositoryCache, "unused-global-cache") + + // Create .codacy/tools-configs directory + configDir := filepath.Join(repoDir, ".codacy", "tools-configs") + err := os.MkdirAll(configDir, 0755) + assert.NoError(t, err, "Failed to create test directory structure") + + // Create a test config file on the repository directory + existingConfigFile := filepath.Join(repoDir, "existing-config.yaml") + err = os.WriteFile(existingConfigFile, []byte("test content"), 0644) + assert.NoError(t, err, "Failed to create test config file") + + // Test case: The existing config file gets picked up + configPath, exists := ConfigFileExists(config, "existing-config.yaml") + assert.True(t, exists, "Config file should exist in tools config directory") + assert.Equal(t, filepath.Join(config.RepositoryDirectory(), "existing-config.yaml"), configPath, + "Config path should be correctly formed relative path") +} + +func TestConfigFilePrefersToolsConfigDirectory(t *testing.T) { + // Create a test directory structure + tempDir := t.TempDir() + repoDir := filepath.Join(tempDir, "src") + repositoryCache := filepath.Join(repoDir, ".codacy") + + // Create configuration + config := *config.NewConfigType(repoDir, repositoryCache, "unused-global-cache") + + // Create .codacy/tools-configs directory + configDir := filepath.Join(repoDir, ".codacy", "tools-configs") + err := os.MkdirAll(configDir, 0755) + assert.NoError(t, err, "Failed to create test directory structure") + + // Create a test config file in both locations + generatedConfigFile := filepath.Join(configDir, "some-config.yaml") + existingConfigFile := filepath.Join(repoDir, "some-config.yaml") + + err = os.WriteFile(generatedConfigFile, []byte("tools config content"), 0644) + assert.NoError(t, err, "Failed to create test config file in tools config directory") + + err = os.WriteFile(existingConfigFile, []byte("repository config content"), 0644) + assert.NoError(t, err, "Failed to create test config file in repository directory") + + // Test case: Config file in tools config directory is preferred + configPath, exists := ConfigFileExists(config, "some-config.yaml") + assert.True(t, exists, "Config file should exist") + assert.Equal(t, filepath.Join(config.ToolsConfigDirectory(), "some-config.yaml"), configPath, + "Config path should prefer tools config directory") +} + +func TestConfigFileDoesNotExist(t *testing.T) { + // Create a test directory structure + tempDir := t.TempDir() + repoDir := filepath.Join(tempDir, "src") + repositoryCache := filepath.Join(repoDir, ".codacy") + + // Create configuration + config := *config.NewConfigType(repoDir, repositoryCache, "unused-global-cache") + + // Test case: Config file does not exist + configPath, exists := ConfigFileExists(config, "non-existent-config.yaml") + assert.False(t, exists, "Config file should not exist") + assert.Equal(t, "", configPath, "Config path should be empty for non-existent file") +} diff --git a/tools/testdata/repositories/pmd/pmd-ruleset.xml b/tools/testdata/repositories/pmd/ruleset.xml similarity index 100% rename from tools/testdata/repositories/pmd/pmd-ruleset.xml rename to tools/testdata/repositories/pmd/ruleset.xml diff --git a/tools/trivyRunner.go b/tools/trivyRunner.go index b3636087..8e8e3522 100644 --- a/tools/trivyRunner.go +++ b/tools/trivyRunner.go @@ -4,16 +4,16 @@ import ( "codacy/cli-v2/config" "os" "os/exec" - "path/filepath" ) // RunTrivy executes Trivy vulnerability scanner with the specified options func RunTrivy(repositoryToAnalyseDirectory string, trivyBinary string, pathsToCheck []string, outputFile string, outputFormat string) error { cmd := exec.Command(trivyBinary, "fs") - // Add config file from tools-configs directory - configFile := filepath.Join(config.Config.ToolsConfigDirectory(), "trivy.yaml") - cmd.Args = append(cmd.Args, "--config", configFile) + // Add config file from tools-configs directory if it exists + if configFile, exists := ConfigFileExists(config.Config, "trivy.yaml"); exists { + cmd.Args = append(cmd.Args, "--config", configFile) + } // Add format options if outputFile != "" {