From 3f2c9656f7952390718460fce65e996e8f78b899 Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Mon, 16 Jun 2025 16:23:03 +0100 Subject: [PATCH 01/11] analyze now receives initFlags and fetches config file if non existent --- cmd/analyze.go | 70 +++++++++++++++-- cmd/analyze_test.go | 147 +++++++++++++++++++++++++++++++++++ cmd/configsetup/setup.go | 28 +++++++ tools/eslintConfigCreator.go | 84 ++++++++++++++++---- 4 files changed, 310 insertions(+), 19 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index 60323104..a67c1081 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -1,6 +1,8 @@ package cmd import ( + "codacy/cli-v2/cmd/cmdutils" + "codacy/cli-v2/cmd/configsetup" "codacy/cli-v2/config" "codacy/cli-v2/domain" "codacy/cli-v2/plugins" @@ -41,6 +43,18 @@ type LanguagesConfig struct { } `yaml:"tools" json:"tools"` } +// toolConfigFileName maps tool names to their configuration filenames +var toolConfigFileName = map[string]string{ + "eslint": "eslint.config.mjs", + "trivy": "trivy.yaml", + "pmd": "ruleset.xml", + "pylint": "pylint.rc", + "dartanalyzer": "analysis_options.yaml", + "semgrep": "semgrep.yaml", + "revive": "revive.toml", + "lizard": "lizard.yaml", +} + // LoadLanguageConfig loads the language configuration from the file func LoadLanguageConfig() (*LanguagesConfig, error) { // First, try to load the YAML config @@ -219,6 +233,7 @@ func init() { analyzeCmd.Flags().StringVarP(&toolsToAnalyzeParam, "tool", "t", "", "Which tool to run analysis with. If not specified, all configured tools will be run") analyzeCmd.Flags().StringVar(&outputFormat, "format", "", "Output format (use 'sarif' for SARIF format)") analyzeCmd.Flags().BoolVar(&autoFix, "fix", false, "Apply auto fix to your issues when available") + cmdutils.AddCloudFlags(analyzeCmd, &initFlags) rootCmd.AddCommand(analyzeCmd) } @@ -279,7 +294,41 @@ func validateToolName(toolName string) error { return nil } -func runToolByName(toolName string, workDirectory string, pathsToCheck []string, autoFix bool, outputFile string, outputFormat string, tool *plugins.ToolInfo, runtime *plugins.RuntimeInfo) error { +func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { + configFileName := toolConfigFileName[toolName] + if configFileName == "" { + // Tool doesn't use config file + return nil + } + + // Use the configuration system to get the tools config directory + toolsConfigDir := config.Config.ToolsConfigDirectory() + toolConfigPath := filepath.Join(toolsConfigDir, configFileName) + + // Check if the config file exists + if _, err := os.Stat(toolConfigPath); os.IsNotExist(err) { + // Only show error if we're in remote mode and need the config file + if !cliLocalMode && initFlags.ApiToken != "" { + fmt.Printf("Creating new config file for tool %s\n", toolName) + configsetup.CreateToolConfigurationFile(toolName, initFlags) + } else if !cliLocalMode { + fmt.Printf("config file not found for tool %s: %s and no api token provided \n", toolName, toolConfigPath) + } else { + fmt.Printf("config file not found for tool %s: %s please add a config file to the tools-configs directory\n", toolName, toolConfigPath) + } + } else if err != nil { + fmt.Printf("error checking config file for tool %s: %v\n", toolName, err) + } else { + fmt.Printf("Config file found for %s: %s\n", toolName, toolConfigPath) + } + return nil +} + +func runToolByName(toolName string, workDirectory string, pathsToCheck []string, autoFix bool, outputFile string, outputFormat string, tool *plugins.ToolInfo, runtime *plugins.RuntimeInfo, cliLocalMode bool) error { + err := checkIfConfigExistsAndIsNeeded(toolName, cliLocalMode) + if err != nil { + return err + } switch toolName { case "eslint": binaryPath := runtime.Binaries[tool.Runtime] @@ -310,7 +359,7 @@ func runToolByName(toolName string, workDirectory string, pathsToCheck []string, return fmt.Errorf("unsupported tool: %s", toolName) } -func runTool(workDirectory string, toolName string, pathsToCheck []string, outputFile string, autoFix bool, outputFormat string) error { +func runTool(workDirectory string, toolName string, pathsToCheck []string, outputFile string, autoFix bool, outputFormat string, cliLocalMode bool) error { err := validateToolName(toolName) if err != nil { return err @@ -368,7 +417,7 @@ func runTool(workDirectory string, toolName string, pathsToCheck []string, outpu runtime = config.Config.Runtimes()[tool.Runtime] } } - return runToolByName(toolName, workDirectory, pathsToCheck, autoFix, outputFile, outputFormat, tool, runtime) + return runToolByName(toolName, workDirectory, pathsToCheck, autoFix, outputFile, outputFormat, tool, runtime, cliLocalMode) } // validatePaths checks if all provided paths exist and returns an error if any don't @@ -384,6 +433,13 @@ func validatePaths(paths []string) error { return nil } +func validateCloudMode(cliLocalMode bool) error { + if cliLocalMode { + fmt.Println("Warning: cannot run in cloud mode") + } + return nil +} + var analyzeCmd = &cobra.Command{ Use: "analyze", Short: "Analyze code using configured tools", @@ -401,6 +457,10 @@ var analyzeCmd = &cobra.Command{ log.Fatalf("Failed to get current working directory: %v", err) } + cliLocalMode := len(initFlags.ApiToken) == 0 + + validateCloudMode(cliLocalMode) + var toolsToRun map[string]*plugins.ToolInfo if toolsToAnalyzeParam != "" { @@ -437,7 +497,7 @@ var analyzeCmd = &cobra.Command{ var sarifOutputs []string for toolName := range toolsToRun { tmpFile := filepath.Join(tmpDir, fmt.Sprintf("%s.sarif", toolName)) - if err := runTool(workDirectory, toolName, args, tmpFile, autoFix, outputFormat); err != nil { + if err := runTool(workDirectory, toolName, args, tmpFile, autoFix, outputFormat, cliLocalMode); err != nil { log.Printf("Tool failed to run: %v\n", err) } sarifOutputs = append(sarifOutputs, tmpFile) @@ -472,7 +532,7 @@ var analyzeCmd = &cobra.Command{ } else { // Run tools without merging outputs for toolName := range toolsToRun { - if err := runTool(workDirectory, toolName, args, outputFile, autoFix, outputFormat); err != nil { + if err := runTool(workDirectory, toolName, args, outputFile, autoFix, outputFormat, cliLocalMode); err != nil { log.Printf("Tool failed to run: %v\n", err) } } diff --git a/cmd/analyze_test.go b/cmd/analyze_test.go index b46f61de..0b67ae77 100644 --- a/cmd/analyze_test.go +++ b/cmd/analyze_test.go @@ -2,9 +2,14 @@ package cmd import ( "codacy/cli-v2/plugins" + "os" + "path/filepath" "testing" + "codacy/cli-v2/domain" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetFileExtension(t *testing.T) { @@ -262,3 +267,145 @@ func TestValidatePaths(t *testing.T) { }) } } + +func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { + // Save original initFlags + originalFlags := initFlags + defer func() { initFlags = originalFlags }() + + tests := []struct { + name string + toolName string + cliLocalMode bool + apiToken string + configFileExists bool + expectError bool + description string + }{ + { + name: "tool_without_config_file", + toolName: "unsupported-tool", + cliLocalMode: false, + apiToken: "test-token", + configFileExists: false, + expectError: false, + description: "Tool that doesn't use config files should return without error", + }, + { + name: "config_file_exists", + toolName: "eslint", + cliLocalMode: false, + apiToken: "test-token", + configFileExists: true, + expectError: false, + description: "When config file exists, should find it successfully", + }, + { + name: "remote_mode_with_token_no_config", + toolName: "eslint", + cliLocalMode: false, + apiToken: "test-token", + configFileExists: false, + expectError: false, + description: "Remote mode with token should attempt to create config", + }, + { + name: "remote_mode_without_token_no_config", + toolName: "eslint", + cliLocalMode: false, + apiToken: "", + configFileExists: false, + expectError: false, + description: "Remote mode without token should show appropriate message", + }, + { + name: "local_mode_no_config", + toolName: "eslint", + cliLocalMode: true, + apiToken: "", + configFileExists: false, + expectError: false, + description: "Local mode should show message about adding config file", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Setup temporary directory + tmpDir, err := os.MkdirTemp("", "codacy-test-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Create config file if needed + if tt.configFileExists && toolConfigFileName[tt.toolName] != "" { + configPath := filepath.Join(tmpDir, toolConfigFileName[tt.toolName]) + err := os.WriteFile(configPath, []byte("test config"), 0644) + require.NoError(t, err) + } + + // Setup initFlags + initFlags = domain.InitFlags{ + ApiToken: tt.apiToken, + } + + // Mock config directory (this would need actual mocking in real implementation) + // For now, we'll create a simple test that verifies the function doesn't panic + + // Execute the function + err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode) + + // Verify results + if tt.expectError { + assert.Error(t, err, tt.description) + } else { + assert.NoError(t, err, tt.description) + } + }) + } +} + +func TestToolConfigFileNameMap(t *testing.T) { + expectedTools := map[string]string{ + "eslint": "eslint.config.mjs", + "trivy": "trivy.yaml", + "pmd": "ruleset.xml", + "pylint": "pylint.rc", + "dartanalyzer": "analysis_options.yaml", + "semgrep": "semgrep.yaml", + "revive": "revive.toml", + "lizard": "lizard.yaml", + } + + for toolName, expectedFileName := range expectedTools { + t.Run(toolName, func(t *testing.T) { + actualFileName, exists := toolConfigFileName[toolName] + assert.True(t, exists, "Tool %s should exist in toolConfigFileName map", toolName) + assert.Equal(t, expectedFileName, actualFileName, "Config filename for %s should match expected", toolName) + }) + } +} + +func TestGetFileExtension(t *testing.T) { + tests := []struct { + filePath string + expected string + }{ + {"test.js", ".js"}, + {"test.jsx", ".jsx"}, + {"test.ts", ".ts"}, + {"test.tsx", ".tsx"}, + {"test.py", ".py"}, + {"test.java", ".java"}, + {"test", ""}, + {"test.JS", ".js"}, // Should be lowercase + {"/path/to/test.py", ".py"}, + {"", ""}, + } + + for _, tt := range tests { + t.Run(tt.filePath, func(t *testing.T) { + result := GetFileExtension(tt.filePath) + assert.Equal(t, tt.expected, result) + }) + } +} diff --git a/cmd/configsetup/setup.go b/cmd/configsetup/setup.go index fdd0c684..675138ab 100644 --- a/cmd/configsetup/setup.go +++ b/cmd/configsetup/setup.go @@ -483,6 +483,34 @@ func createToolConfigurationFiles(tools []domain.Tool, flags domain.InitFlags) e return nil } +// CreateToolConfigurationFile creates a configuration file for a single tool +func CreateToolConfigurationFile(toolName string, flags domain.InitFlags) error { + // Find the tool UUID by tool name + toolUuid := getToolUuidByName(toolName) + if toolUuid == "" { + return fmt.Errorf("tool '%s' not found in supported tools", toolName) + } + + patternsConfig, err := codacyclient.GetDefaultToolPatternsConfig(flags, toolUuid) + if err != nil { + return fmt.Errorf("failed to get default patterns: %w", err) + } + + // Get the tool object to pass to createToolFileConfiguration + tool := domain.Tool{Uuid: toolUuid} + return createToolFileConfiguration(tool, patternsConfig) +} + +// getToolUuidByName finds the UUID for a tool given its name +func getToolUuidByName(toolName string) string { + for uuid, toolInfo := range domain.SupportedToolsMetadata { + if toolInfo.Name == toolName { + return uuid + } + } + return "" +} + // createToolFileConfiguration creates a configuration file for a single tool using the registry func createToolFileConfiguration(tool domain.Tool, patternConfiguration []domain.PatternConfiguration) error { creator, exists := toolConfigRegistry[tool.Uuid] diff --git a/tools/eslintConfigCreator.go b/tools/eslintConfigCreator.go index 7679ed4d..d9f7c612 100644 --- a/tools/eslintConfigCreator.go +++ b/tools/eslintConfigCreator.go @@ -103,6 +103,54 @@ func writeFile(filePath string, content string) error { return nil } +// rulesWithoutOptions contains ESLint rules that don't accept any configuration options +var rulesWithoutOptions = map[string]bool{ + "no-misleading-character-class": true, + "constructor-super": true, + "for-direction": true, + "no-async-promise-executor": true, + "no-case-declarations": true, + "no-class-assign": true, + "no-compare-neg-zero": true, + "no-const-assign": true, + "no-control-regex": true, + "no-debugger": true, + "no-delete-var": true, + "no-dupe-args": true, + "no-dupe-class-members": true, + "no-dupe-else-if": true, + "no-dupe-keys": true, + "no-duplicate-case": true, + "no-empty-character-class": true, + "no-ex-assign": true, + "no-extra-semi": true, + "no-func-assign": true, + "no-global-assign": true, + "no-import-assign": true, + "no-invalid-regexp": true, + "no-loss-of-precision": true, + "no-mixed-spaces-and-tabs": true, + "no-new-symbol": true, + "no-nonoctal-decimal-escape": true, + "no-obj-calls": true, + "no-octal": true, + "no-prototype-builtins": true, + "no-regex-spaces": true, + "no-setter-return": true, + "no-shadow-restricted-names": true, + "no-sparse-arrays": true, + "no-this-before-super": true, + "no-unexpected-multiline": true, + "no-unreachable": true, + "no-unsafe-finally": true, + "no-unused-labels": true, + "no-useless-backreference": true, + "no-useless-catch": true, + "no-useless-escape": true, + "no-with": true, + "require-yield": true, +} + func CreateEslintConfig(toolsConfigDir string, configuration []domain.PatternConfiguration) error { result := `export default [ { @@ -150,23 +198,31 @@ func CreateEslintConfig(toolsConfigDir string, configuration []domain.PatternCon parametersString += quoteWhenIsNotJson(defaultUnnamedParamValue) } - // Use the new helper method to build named parameters JSON object - namedParametersString := buildNamedParametersString(patternConfiguration.Parameters, patternConfiguration.PatternDefinition) - - if parametersString != "" && namedParametersString != "" { - parametersString = fmt.Sprintf("%s, %s", parametersString, namedParametersString) - } else { - parametersString += namedParametersString - } - - result += " " - - if parametersString == "" { + // Check if this rule accepts options + if rulesWithoutOptions[rule] { + // Rule doesn't accept options, only use error level + result += " " result += fmt.Sprintf(`"%s": ["error"],`, rule) result += "\n" } else { - result += fmt.Sprintf(`"%s": ["error", %s],`, rule, parametersString) - result += "\n" + // Use the new helper method to build named parameters JSON object + namedParametersString := buildNamedParametersString(patternConfiguration.Parameters, patternConfiguration.PatternDefinition) + + if parametersString != "" && namedParametersString != "" { + parametersString = fmt.Sprintf("%s, %s", parametersString, namedParametersString) + } else { + parametersString += namedParametersString + } + + result += " " + + if parametersString == "" { + result += fmt.Sprintf(`"%s": ["error"],`, rule) + result += "\n" + } else { + result += fmt.Sprintf(`"%s": ["error", %s],`, rule, parametersString) + result += "\n" + } } } From aac40ffe1afe01bef6b0013b747c5547a1bd4abd Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Mon, 16 Jun 2025 18:50:48 +0100 Subject: [PATCH 02/11] added test --- cmd/analyze_integration_test.go | 187 ++++++++++++++++++++++++++++++++ cmd/analyze_test.go | 55 +++------- 2 files changed, 203 insertions(+), 39 deletions(-) create mode 100644 cmd/analyze_integration_test.go diff --git a/cmd/analyze_integration_test.go b/cmd/analyze_integration_test.go new file mode 100644 index 00000000..c794486c --- /dev/null +++ b/cmd/analyze_integration_test.go @@ -0,0 +1,187 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "codacy/cli-v2/config" + "codacy/cli-v2/domain" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestCheckIfConfigExistsAndIsNeededBehavior tests the behavior of checkIfConfigExistsAndIsNeeded +// without creating actual config files in the project directory +func TestCheckIfConfigExistsAndIsNeededBehavior(t *testing.T) { + // Save original state + originalFlags := initFlags + originalConfig := config.Config + originalWorkingDir, _ := os.Getwd() + defer func() { + initFlags = originalFlags + config.Config = originalConfig + os.Chdir(originalWorkingDir) + }() + + tests := []struct { + name string + toolName string + cliLocalMode bool + apiToken string + description string + expectNoError bool + }{ + { + name: "tool_without_config_file", + toolName: "unsupported-tool", + cliLocalMode: false, + apiToken: "test-token", + description: "Tool that doesn't use config files should return without error", + expectNoError: true, + }, + { + name: "eslint_local_mode", + toolName: "eslint", + cliLocalMode: true, + apiToken: "", + description: "ESLint in local mode should not error", + expectNoError: true, + }, + { + name: "eslint_remote_mode_without_token", + toolName: "eslint", + cliLocalMode: false, + apiToken: "", + description: "ESLint in remote mode without token should not error", + expectNoError: true, + }, + { + name: "trivy_local_mode", + toolName: "trivy", + cliLocalMode: true, + apiToken: "", + description: "Trivy in local mode should not error", + expectNoError: true, + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // Create temporary directory and change to it to avoid creating files in project dir + tmpDir, err := os.MkdirTemp("", "codacy-test-isolated-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Change to temp directory to avoid creating config files in project + err = os.Chdir(tmpDir) + require.NoError(t, err) + + // Setup initFlags + initFlags = domain.InitFlags{ + ApiToken: tt.apiToken, + } + + // Mock config to use our temporary directory + config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir) + + // Execute the function - this tests it doesn't panic or return unexpected errors + err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode) + + // Verify results + if tt.expectNoError { + assert.NoError(t, err, "Function should not return error: %s", tt.description) + } else { + assert.Error(t, err, "Function should return error: %s", tt.description) + } + }) + } +} + +// TestToolConfigFileNameMapCompleteness ensures all expected tools have config mappings +func TestToolConfigFileNameMapCompleteness(t *testing.T) { + expectedTools := map[string]string{ + "eslint": "eslint.config.mjs", + "trivy": "trivy.yaml", + "pmd": "ruleset.xml", + "pylint": "pylint.rc", + "dartanalyzer": "analysis_options.yaml", + "semgrep": "semgrep.yaml", + "revive": "revive.toml", + "lizard": "lizard.yaml", + } + + t.Run("all_expected_tools_present", func(t *testing.T) { + for toolName, expectedFileName := range expectedTools { + actualFileName, exists := toolConfigFileName[toolName] + assert.True(t, exists, "Tool %s should exist in toolConfigFileName map", toolName) + assert.Equal(t, expectedFileName, actualFileName, "Config filename for %s should match expected", toolName) + } + }) + + t.Run("no_unexpected_tools", func(t *testing.T) { + for toolName := range toolConfigFileName { + _, expected := expectedTools[toolName] + assert.True(t, expected, "Unexpected tool %s found in toolConfigFileName map", toolName) + } + }) + + t.Run("config_files_have_proper_extensions", func(t *testing.T) { + validExtensions := map[string]bool{ + ".mjs": true, + ".js": true, + ".yaml": true, + ".yml": true, + ".xml": true, + ".rc": true, + ".toml": true, + } + + for toolName, fileName := range toolConfigFileName { + ext := filepath.Ext(fileName) + assert.True(t, validExtensions[ext], "Tool %s has config file %s with unexpected extension %s", toolName, fileName, ext) + } + }) +} + +// TestCheckIfConfigExistsAndIsNeededEdgeCases tests edge cases and error conditions +func TestCheckIfConfigExistsAndIsNeededEdgeCases(t *testing.T) { + originalFlags := initFlags + originalConfig := config.Config + originalWorkingDir, _ := os.Getwd() + defer func() { + initFlags = originalFlags + config.Config = originalConfig + os.Chdir(originalWorkingDir) + }() + + // Create temporary directory for edge case tests + tmpDir, err := os.MkdirTemp("", "codacy-test-edge-*") + require.NoError(t, err) + defer os.RemoveAll(tmpDir) + + // Change to temp directory + err = os.Chdir(tmpDir) + require.NoError(t, err) + + // Mock config + config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir) + + t.Run("empty_tool_name", func(t *testing.T) { + err := checkIfConfigExistsAndIsNeeded("", false) + assert.NoError(t, err, "Empty tool name should not cause error") + }) + + t.Run("tool_name_with_special_characters", func(t *testing.T) { + err := checkIfConfigExistsAndIsNeeded("tool-with-dashes_and_underscores", false) + assert.NoError(t, err, "Tool name with special characters should not cause error") + }) + + t.Run("very_long_tool_name", func(t *testing.T) { + longToolName := strings.Repeat("verylongtoolname", 10) + err := checkIfConfigExistsAndIsNeeded(longToolName, false) + assert.NoError(t, err, "Very long tool name should not cause error") + }) +} diff --git a/cmd/analyze_test.go b/cmd/analyze_test.go index 0b67ae77..29c3b53f 100644 --- a/cmd/analyze_test.go +++ b/cmd/analyze_test.go @@ -6,6 +6,7 @@ import ( "path/filepath" "testing" + "codacy/cli-v2/config" "codacy/cli-v2/domain" "github.com/stretchr/testify/assert" @@ -269,9 +270,15 @@ func TestValidatePaths(t *testing.T) { } func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { - // Save original initFlags + // Save original initFlags and config originalFlags := initFlags - defer func() { initFlags = originalFlags }() + originalConfig := config.Config + originalWorkingDir, _ := os.Getwd() + defer func() { + initFlags = originalFlags + config.Config = originalConfig + os.Chdir(originalWorkingDir) + }() tests := []struct { name string @@ -300,15 +307,6 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { expectError: false, description: "When config file exists, should find it successfully", }, - { - name: "remote_mode_with_token_no_config", - toolName: "eslint", - cliLocalMode: false, - apiToken: "test-token", - configFileExists: false, - expectError: false, - description: "Remote mode with token should attempt to create config", - }, { name: "remote_mode_without_token_no_config", toolName: "eslint", @@ -331,11 +329,15 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { - // Setup temporary directory + // Setup temporary directory and change to it to avoid creating files in project dir tmpDir, err := os.MkdirTemp("", "codacy-test-*") require.NoError(t, err) defer os.RemoveAll(tmpDir) + // Change to temp directory to prevent config file creation in project + err = os.Chdir(tmpDir) + require.NoError(t, err) + // Create config file if needed if tt.configFileExists && toolConfigFileName[tt.toolName] != "" { configPath := filepath.Join(tmpDir, toolConfigFileName[tt.toolName]) @@ -348,8 +350,8 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { ApiToken: tt.apiToken, } - // Mock config directory (this would need actual mocking in real implementation) - // For now, we'll create a simple test that verifies the function doesn't panic + // Mock config to use our temporary directory + config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir) // Execute the function err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode) @@ -384,28 +386,3 @@ func TestToolConfigFileNameMap(t *testing.T) { }) } } - -func TestGetFileExtension(t *testing.T) { - tests := []struct { - filePath string - expected string - }{ - {"test.js", ".js"}, - {"test.jsx", ".jsx"}, - {"test.ts", ".ts"}, - {"test.tsx", ".tsx"}, - {"test.py", ".py"}, - {"test.java", ".java"}, - {"test", ""}, - {"test.JS", ".js"}, // Should be lowercase - {"/path/to/test.py", ".py"}, - {"", ""}, - } - - for _, tt := range tests { - t.Run(tt.filePath, func(t *testing.T) { - result := GetFileExtension(tt.filePath) - assert.Equal(t, tt.expected, result) - }) - } -} From 079cb607eddc78f90063f813f38d40a819d2765a Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Tue, 17 Jun 2025 15:36:43 +0100 Subject: [PATCH 03/11] code review changes --- cmd/analyze.go | 20 ++++++++++++-------- tools/eslintConfigCreator.go | 2 +- 2 files changed, 13 insertions(+), 9 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index a67c1081..f8fcedbc 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -310,14 +310,16 @@ func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { // Only show error if we're in remote mode and need the config file if !cliLocalMode && initFlags.ApiToken != "" { fmt.Printf("Creating new config file for tool %s\n", toolName) - configsetup.CreateToolConfigurationFile(toolName, initFlags) + if err := configsetup.CreateToolConfigurationFile(toolName, initFlags); err != nil { + return fmt.Errorf("failed to create config file for tool %s: %w", toolName, err) + } } else if !cliLocalMode { - fmt.Printf("config file not found for tool %s: %s and no api token provided \n", toolName, toolConfigPath) + fmt.Printf("Config file not found for tool %s: %s and no API token provided\n", toolName, toolConfigPath) } else { - fmt.Printf("config file not found for tool %s: %s please add a config file to the tools-configs directory\n", toolName, toolConfigPath) + fmt.Printf("Config file not found for tool %s: %s. Please add a config file to the tools-configs directory\n", toolName, toolConfigPath) } } else if err != nil { - fmt.Printf("error checking config file for tool %s: %v\n", toolName, err) + return fmt.Errorf("Error checking config file for tool %s: %w", toolName, err) } else { fmt.Printf("Config file found for %s: %s\n", toolName, toolConfigPath) } @@ -365,7 +367,7 @@ func runTool(workDirectory string, toolName string, pathsToCheck []string, outpu return err } log.Println("Running tools for the specified file(s)...") - log.Printf("Running %s...\n", toolName) + log.Printf("Running %s...", toolName) tool := config.Config.Tools()[toolName] var isToolInstalled bool @@ -397,7 +399,7 @@ func runTool(workDirectory string, toolName string, pathsToCheck []string, outpu runtime = config.Config.Runtimes()[tool.Runtime] isRuntimeInstalled = runtime == nil || config.Config.IsRuntimeInstalled(tool.Runtime, runtime) if !isRuntimeInstalled { - fmt.Printf("%s runtime is not installed, installing...\n", tool.Runtime) + fmt.Printf("%s runtime is not installed, installing...", tool.Runtime) err := config.InstallRuntime(tool.Runtime, runtime) if err != nil { return fmt.Errorf("failed to install %s runtime: %w", tool.Runtime, err) @@ -409,7 +411,7 @@ func runTool(workDirectory string, toolName string, pathsToCheck []string, outpu runtime = config.Config.Runtimes()[tool.Runtime] isRuntimeInstalled = runtime == nil || config.Config.IsRuntimeInstalled(tool.Runtime, runtime) if !isRuntimeInstalled { - fmt.Printf("%s runtime is not installed, installing...\n", tool.Runtime) + fmt.Printf("%s runtime is not installed, installing...", tool.Runtime) err := config.InstallRuntime(tool.Runtime, runtime) if err != nil { return fmt.Errorf("failed to install %s runtime: %w", tool.Runtime, err) @@ -443,7 +445,9 @@ func validateCloudMode(cliLocalMode bool) error { var analyzeCmd = &cobra.Command{ Use: "analyze", Short: "Analyze code using configured tools", - Long: `Analyze code using configured tools and output results in the specified format.`, + Long: `Analyze code using configured tools and output results in the specified format. + +Supports API token, provider, and repository flags to automatically fetch tool configurations from Codacy API if they don't exist locally.`, Run: func(cmd *cobra.Command, args []string) { // Validate paths before proceeding if err := validatePaths(args); err != nil { diff --git a/tools/eslintConfigCreator.go b/tools/eslintConfigCreator.go index d9f7c612..6416484b 100644 --- a/tools/eslintConfigCreator.go +++ b/tools/eslintConfigCreator.go @@ -199,7 +199,7 @@ func CreateEslintConfig(toolsConfigDir string, configuration []domain.PatternCon } // Check if this rule accepts options - if rulesWithoutOptions[rule] { + if _, ok := rulesWithoutOptions[rule]; ok { // Rule doesn't accept options, only use error level result += " " result += fmt.Sprintf(`"%s": ["error"],`, rule) From f363fe39587b6aca68f05c32d9553ea025611092 Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Tue, 17 Jun 2025 16:43:57 +0100 Subject: [PATCH 04/11] fix test --- cmd/analyze_test.go | 30 ++++++++++++++++++++++++------ 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/cmd/analyze_test.go b/cmd/analyze_test.go index 29c3b53f..3c2871eb 100644 --- a/cmd/analyze_test.go +++ b/cmd/analyze_test.go @@ -338,11 +338,23 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { err = os.Chdir(tmpDir) require.NoError(t, err) - // Create config file if needed + // Mock config to use our temporary directory BEFORE creating files + config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir) + + // Create config file if needed - using the same path logic as the function under test if tt.configFileExists && toolConfigFileName[tt.toolName] != "" { - configPath := filepath.Join(tmpDir, toolConfigFileName[tt.toolName]) - err := os.WriteFile(configPath, []byte("test config"), 0644) + // Use config.Config.ToolsConfigDirectory() to get the exact same path the function will use + toolsConfigDir := config.Config.ToolsConfigDirectory() + err := os.MkdirAll(toolsConfigDir, 0755) require.NoError(t, err) + + configPath := filepath.Join(toolsConfigDir, toolConfigFileName[tt.toolName]) + err = os.WriteFile(configPath, []byte("test config"), 0644) + require.NoError(t, err) + + // Ensure the file was created and can be found + _, err = os.Stat(configPath) + require.NoError(t, err, "Config file should exist at %s", configPath) } // Setup initFlags @@ -350,12 +362,18 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { ApiToken: tt.apiToken, } - // Mock config to use our temporary directory - config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir) - // Execute the function err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode) + // Clean up any files that might have been created by the function under test + if !tt.configFileExists && toolConfigFileName[tt.toolName] != "" { + toolsConfigDir := config.Config.ToolsConfigDirectory() + configPath := filepath.Join(toolsConfigDir, toolConfigFileName[tt.toolName]) + if _, statErr := os.Stat(configPath); statErr == nil { + os.Remove(configPath) + } + } + // Verify results if tt.expectError { assert.Error(t, err, tt.description) From 9f60591a9820b6422826a1e3b4eb316f16b60845 Mon Sep 17 00:00:00 2001 From: Andrzej Janczak <122018786+andrzej-janczak@users.noreply.github.com> Date: Wed, 18 Jun 2025 10:13:40 +0200 Subject: [PATCH 05/11] Pluto 1423 refactor (#154) * Refactor: Centralize tool configuration file names into constants - Introduced a new constants package to store tool configuration file names, reducing duplication across the codebase. - Updated references in the analyze and config setup files to use the new constants for improved maintainability. - Adjusted tests to align with the new constants structure, ensuring consistency in tool configuration handling. * fix test --- cmd/analyze.go | 17 +++----------- cmd/analyze_integration_test.go | 27 +++++++++++----------- cmd/analyze_test.go | 35 ++++++++++++++-------------- cmd/configsetup/setup.go | 41 +++++++++++++-------------------- constants/tool_configs.go | 30 ++++++++++++++++++++++++ 5 files changed, 81 insertions(+), 69 deletions(-) create mode 100644 constants/tool_configs.go diff --git a/cmd/analyze.go b/cmd/analyze.go index f8fcedbc..0ce0227b 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -4,6 +4,7 @@ import ( "codacy/cli-v2/cmd/cmdutils" "codacy/cli-v2/cmd/configsetup" "codacy/cli-v2/config" + "codacy/cli-v2/constants" "codacy/cli-v2/domain" "codacy/cli-v2/plugins" "codacy/cli-v2/tools" @@ -43,22 +44,10 @@ type LanguagesConfig struct { } `yaml:"tools" json:"tools"` } -// toolConfigFileName maps tool names to their configuration filenames -var toolConfigFileName = map[string]string{ - "eslint": "eslint.config.mjs", - "trivy": "trivy.yaml", - "pmd": "ruleset.xml", - "pylint": "pylint.rc", - "dartanalyzer": "analysis_options.yaml", - "semgrep": "semgrep.yaml", - "revive": "revive.toml", - "lizard": "lizard.yaml", -} - // LoadLanguageConfig loads the language configuration from the file func LoadLanguageConfig() (*LanguagesConfig, error) { // First, try to load the YAML config - yamlPath := filepath.Join(config.Config.ToolsConfigDirectory(), "languages-config.yaml") + yamlPath := filepath.Join(config.Config.ToolsConfigDirectory(), constants.LanguagesConfigFileName) // Check if the YAML file exists if _, err := os.Stat(yamlPath); err == nil { @@ -295,7 +284,7 @@ func validateToolName(toolName string) error { } func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { - configFileName := toolConfigFileName[toolName] + configFileName := constants.ToolConfigFileNames[toolName] if configFileName == "" { // Tool doesn't use config file return nil diff --git a/cmd/analyze_integration_test.go b/cmd/analyze_integration_test.go index c794486c..5a3f9b99 100644 --- a/cmd/analyze_integration_test.go +++ b/cmd/analyze_integration_test.go @@ -7,6 +7,7 @@ import ( "testing" "codacy/cli-v2/config" + "codacy/cli-v2/constants" "codacy/cli-v2/domain" "github.com/stretchr/testify/assert" @@ -103,28 +104,28 @@ func TestCheckIfConfigExistsAndIsNeededBehavior(t *testing.T) { // TestToolConfigFileNameMapCompleteness ensures all expected tools have config mappings func TestToolConfigFileNameMapCompleteness(t *testing.T) { expectedTools := map[string]string{ - "eslint": "eslint.config.mjs", - "trivy": "trivy.yaml", - "pmd": "ruleset.xml", - "pylint": "pylint.rc", - "dartanalyzer": "analysis_options.yaml", - "semgrep": "semgrep.yaml", - "revive": "revive.toml", - "lizard": "lizard.yaml", + "eslint": constants.ESLintConfigFileName, + "trivy": constants.TrivyConfigFileName, + "pmd": constants.PMDConfigFileName, + "pylint": constants.PylintConfigFileName, + "dartanalyzer": constants.DartAnalyzerConfigFileName, + "semgrep": constants.SemgrepConfigFileName, + "revive": constants.ReviveConfigFileName, + "lizard": constants.LizardConfigFileName, } t.Run("all_expected_tools_present", func(t *testing.T) { for toolName, expectedFileName := range expectedTools { - actualFileName, exists := toolConfigFileName[toolName] - assert.True(t, exists, "Tool %s should exist in toolConfigFileName map", toolName) + actualFileName, exists := constants.ToolConfigFileNames[toolName] + assert.True(t, exists, "Tool %s should exist in constants.ToolConfigFileNames map", toolName) assert.Equal(t, expectedFileName, actualFileName, "Config filename for %s should match expected", toolName) } }) t.Run("no_unexpected_tools", func(t *testing.T) { - for toolName := range toolConfigFileName { + for toolName := range constants.ToolConfigFileNames { _, expected := expectedTools[toolName] - assert.True(t, expected, "Unexpected tool %s found in toolConfigFileName map", toolName) + assert.True(t, expected, "Unexpected tool %s found in constants.ToolConfigFileNames map", toolName) } }) @@ -139,7 +140,7 @@ func TestToolConfigFileNameMapCompleteness(t *testing.T) { ".toml": true, } - for toolName, fileName := range toolConfigFileName { + for toolName, fileName := range constants.ToolConfigFileNames { ext := filepath.Ext(fileName) assert.True(t, validExtensions[ext], "Tool %s has config file %s with unexpected extension %s", toolName, fileName, ext) } diff --git a/cmd/analyze_test.go b/cmd/analyze_test.go index 3c2871eb..c3c67ea3 100644 --- a/cmd/analyze_test.go +++ b/cmd/analyze_test.go @@ -1,13 +1,14 @@ package cmd import ( - "codacy/cli-v2/plugins" + "codacy/cli-v2/config" + "codacy/cli-v2/constants" + "codacy/cli-v2/domain" "os" "path/filepath" "testing" - "codacy/cli-v2/config" - "codacy/cli-v2/domain" + "codacy/cli-v2/plugins" "github.com/stretchr/testify/assert" "github.com/stretchr/testify/require" @@ -342,13 +343,13 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir) // Create config file if needed - using the same path logic as the function under test - if tt.configFileExists && toolConfigFileName[tt.toolName] != "" { + if tt.configFileExists && constants.ToolConfigFileNames[tt.toolName] != "" { // Use config.Config.ToolsConfigDirectory() to get the exact same path the function will use toolsConfigDir := config.Config.ToolsConfigDirectory() err := os.MkdirAll(toolsConfigDir, 0755) require.NoError(t, err) - configPath := filepath.Join(toolsConfigDir, toolConfigFileName[tt.toolName]) + configPath := filepath.Join(toolsConfigDir, constants.ToolConfigFileNames[tt.toolName]) err = os.WriteFile(configPath, []byte("test config"), 0644) require.NoError(t, err) @@ -366,9 +367,9 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode) // Clean up any files that might have been created by the function under test - if !tt.configFileExists && toolConfigFileName[tt.toolName] != "" { + if !tt.configFileExists && constants.ToolConfigFileNames[tt.toolName] != "" { toolsConfigDir := config.Config.ToolsConfigDirectory() - configPath := filepath.Join(toolsConfigDir, toolConfigFileName[tt.toolName]) + configPath := filepath.Join(toolsConfigDir, constants.ToolConfigFileNames[tt.toolName]) if _, statErr := os.Stat(configPath); statErr == nil { os.Remove(configPath) } @@ -386,20 +387,20 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { func TestToolConfigFileNameMap(t *testing.T) { expectedTools := map[string]string{ - "eslint": "eslint.config.mjs", - "trivy": "trivy.yaml", - "pmd": "ruleset.xml", - "pylint": "pylint.rc", - "dartanalyzer": "analysis_options.yaml", - "semgrep": "semgrep.yaml", - "revive": "revive.toml", - "lizard": "lizard.yaml", + "eslint": constants.ESLintConfigFileName, + "trivy": constants.TrivyConfigFileName, + "pmd": constants.PMDConfigFileName, + "pylint": constants.PylintConfigFileName, + "dartanalyzer": constants.DartAnalyzerConfigFileName, + "semgrep": constants.SemgrepConfigFileName, + "revive": constants.ReviveConfigFileName, + "lizard": constants.LizardConfigFileName, } for toolName, expectedFileName := range expectedTools { t.Run(toolName, func(t *testing.T) { - actualFileName, exists := toolConfigFileName[toolName] - assert.True(t, exists, "Tool %s should exist in toolConfigFileName map", toolName) + actualFileName, exists := constants.ToolConfigFileNames[toolName] + assert.True(t, exists, "Tool %s should exist in constants.ToolConfigFileNames map", toolName) assert.Equal(t, expectedFileName, actualFileName, "Config filename for %s should match expected", toolName) }) } diff --git a/cmd/configsetup/setup.go b/cmd/configsetup/setup.go index 675138ab..4f2f7024 100644 --- a/cmd/configsetup/setup.go +++ b/cmd/configsetup/setup.go @@ -22,17 +22,6 @@ import ( "gopkg.in/yaml.v3" ) -// Configuration file names - extracted as constants to avoid duplication -const ( - LanguagesConfigFileName = "languages-config.yaml" - GitIgnoreFileName = ".gitignore" - PMDConfigFileName = "ruleset.xml" - PylintConfigFileName = "pylint.rc" - TrivyConfigFileName = "trivy.yaml" - DartAnalyzerConfigFileName = "analysis_options.yaml" - SemgrepConfigFileName = "semgrep.yaml" -) - // ToolConfigCreator defines the interface for tool configuration creators type ToolConfigCreator interface { CreateConfig(toolsConfigDir string, patterns []domain.PatternConfiguration) error @@ -78,14 +67,14 @@ type trivyConfigCreator struct{} func (t *trivyConfigCreator) CreateConfig(toolsConfigDir string, patterns []domain.PatternConfiguration) error { configString := tools.CreateTrivyConfig(patterns) - err := writeConfigFile(filepath.Join(toolsConfigDir, TrivyConfigFileName), []byte(configString)) + err := writeConfigFile(filepath.Join(toolsConfigDir, constants.TrivyConfigFileName), []byte(configString)) if err == nil { fmt.Println("Trivy configuration created based on Codacy settings") } return err } -func (t *trivyConfigCreator) GetConfigFileName() string { return TrivyConfigFileName } +func (t *trivyConfigCreator) GetConfigFileName() string { return constants.TrivyConfigFileName } func (t *trivyConfigCreator) GetToolName() string { return "Trivy" } // pmdConfigCreator implements ToolConfigCreator for PMD @@ -93,10 +82,10 @@ type pmdConfigCreator struct{} func (p *pmdConfigCreator) CreateConfig(toolsConfigDir string, patterns []domain.PatternConfiguration) error { configString := tools.CreatePmd6Config(patterns) - return writeConfigFile(filepath.Join(toolsConfigDir, PMDConfigFileName), []byte(configString)) + return writeConfigFile(filepath.Join(toolsConfigDir, constants.PMDConfigFileName), []byte(configString)) } -func (p *pmdConfigCreator) GetConfigFileName() string { return PMDConfigFileName } +func (p *pmdConfigCreator) GetConfigFileName() string { return constants.PMDConfigFileName } func (p *pmdConfigCreator) GetToolName() string { return "PMD" } // pmd7ConfigCreator implements ToolConfigCreator for PMD7 @@ -104,14 +93,14 @@ type pmd7ConfigCreator struct{} func (p *pmd7ConfigCreator) CreateConfig(toolsConfigDir string, patterns []domain.PatternConfiguration) error { configString := tools.CreatePmd7Config(patterns) - err := writeConfigFile(filepath.Join(toolsConfigDir, PMDConfigFileName), []byte(configString)) + err := writeConfigFile(filepath.Join(toolsConfigDir, constants.PMDConfigFileName), []byte(configString)) if err == nil { fmt.Println("PMD7 configuration created based on Codacy settings") } return err } -func (p *pmd7ConfigCreator) GetConfigFileName() string { return PMDConfigFileName } +func (p *pmd7ConfigCreator) GetConfigFileName() string { return constants.PMDConfigFileName } func (p *pmd7ConfigCreator) GetToolName() string { return "PMD7" } // pylintConfigCreator implements ToolConfigCreator for Pylint @@ -119,14 +108,14 @@ type pylintConfigCreator struct{} func (p *pylintConfigCreator) CreateConfig(toolsConfigDir string, patterns []domain.PatternConfiguration) error { configString := pylint.GeneratePylintRC(patterns) - err := writeConfigFile(filepath.Join(toolsConfigDir, PylintConfigFileName), []byte(configString)) + err := writeConfigFile(filepath.Join(toolsConfigDir, constants.PylintConfigFileName), []byte(configString)) if err == nil { fmt.Println("Pylint configuration created based on Codacy settings") } return err } -func (p *pylintConfigCreator) GetConfigFileName() string { return PylintConfigFileName } +func (p *pylintConfigCreator) GetConfigFileName() string { return constants.PylintConfigFileName } func (p *pylintConfigCreator) GetToolName() string { return "Pylint" } // dartAnalyzerConfigCreator implements ToolConfigCreator for Dart Analyzer @@ -134,15 +123,17 @@ type dartAnalyzerConfigCreator struct{} func (d *dartAnalyzerConfigCreator) CreateConfig(toolsConfigDir string, patterns []domain.PatternConfiguration) error { configString := tools.CreateDartAnalyzerConfig(patterns) - err := writeConfigFile(filepath.Join(toolsConfigDir, DartAnalyzerConfigFileName), []byte(configString)) + err := writeConfigFile(filepath.Join(toolsConfigDir, constants.DartAnalyzerConfigFileName), []byte(configString)) if err == nil { fmt.Println("Dart configuration created based on Codacy settings") } return err } -func (d *dartAnalyzerConfigCreator) GetConfigFileName() string { return DartAnalyzerConfigFileName } -func (d *dartAnalyzerConfigCreator) GetToolName() string { return "Dart Analyzer" } +func (d *dartAnalyzerConfigCreator) GetConfigFileName() string { + return constants.DartAnalyzerConfigFileName +} +func (d *dartAnalyzerConfigCreator) GetToolName() string { return "Dart Analyzer" } // semgrepConfigCreator implements ToolConfigCreator for Semgrep type semgrepConfigCreator struct{} @@ -152,14 +143,14 @@ func (s *semgrepConfigCreator) CreateConfig(toolsConfigDir string, patterns []do if err != nil { return fmt.Errorf("failed to create Semgrep config: %v", err) } - err = writeConfigFile(filepath.Join(toolsConfigDir, SemgrepConfigFileName), configData) + err = writeConfigFile(filepath.Join(toolsConfigDir, constants.SemgrepConfigFileName), configData) if err == nil { fmt.Println("Semgrep configuration created based on Codacy settings") } return err } -func (s *semgrepConfigCreator) GetConfigFileName() string { return SemgrepConfigFileName } +func (s *semgrepConfigCreator) GetConfigFileName() string { return constants.SemgrepConfigFileName } func (s *semgrepConfigCreator) GetToolName() string { return "Semgrep" } // lizardConfigCreator implements ToolConfigCreator for Lizard @@ -217,7 +208,7 @@ func CreateLanguagesConfigFileLocal(toolsConfigDir string) error { } func CreateGitIgnoreFile() error { - gitIgnorePath := filepath.Join(config.Config.LocalCodacyDirectory(), GitIgnoreFileName) + gitIgnorePath := filepath.Join(config.Config.LocalCodacyDirectory(), constants.GitIgnoreFileName) content := "# Codacy CLI\ntools-configs/\n.gitignore\ncli-config.yaml\nlogs/\n" return writeConfigFile(gitIgnorePath, []byte(content)) } diff --git a/constants/tool_configs.go b/constants/tool_configs.go new file mode 100644 index 00000000..ad8b01f7 --- /dev/null +++ b/constants/tool_configs.go @@ -0,0 +1,30 @@ +package constants + +// Tool configuration file names - shared constants to avoid duplication +const ( + // Language and project configuration files + LanguagesConfigFileName = "languages-config.yaml" + GitIgnoreFileName = ".gitignore" + + // Tool-specific configuration files + ESLintConfigFileName = "eslint.config.mjs" + TrivyConfigFileName = "trivy.yaml" + PMDConfigFileName = "ruleset.xml" + PylintConfigFileName = "pylint.rc" + DartAnalyzerConfigFileName = "analysis_options.yaml" + SemgrepConfigFileName = "semgrep.yaml" + ReviveConfigFileName = "revive.toml" + LizardConfigFileName = "lizard.yaml" +) + +// ToolConfigFileNames maps tool names to their configuration filenames +var ToolConfigFileNames = map[string]string{ + "eslint": ESLintConfigFileName, + "trivy": TrivyConfigFileName, + "pmd": PMDConfigFileName, + "pylint": PylintConfigFileName, + "dartanalyzer": DartAnalyzerConfigFileName, + "semgrep": SemgrepConfigFileName, + "revive": ReviveConfigFileName, + "lizard": LizardConfigFileName, +} From 603108d6432336d175fb64b25378e9bd93f6b5ec Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Wed, 18 Jun 2025 11:10:39 +0100 Subject: [PATCH 06/11] fetching default configs in local mode --- cmd/analyze.go | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index 0ce0227b..679349cd 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -297,18 +297,16 @@ func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { // Check if the config file exists if _, err := os.Stat(toolConfigPath); os.IsNotExist(err) { // Only show error if we're in remote mode and need the config file - if !cliLocalMode && initFlags.ApiToken != "" { + if (!cliLocalMode && initFlags.ApiToken != "") || cliLocalMode { fmt.Printf("Creating new config file for tool %s\n", toolName) if err := configsetup.CreateToolConfigurationFile(toolName, initFlags); err != nil { return fmt.Errorf("failed to create config file for tool %s: %w", toolName, err) } - } else if !cliLocalMode { - fmt.Printf("Config file not found for tool %s: %s and no API token provided\n", toolName, toolConfigPath) } else { - fmt.Printf("Config file not found for tool %s: %s. Please add a config file to the tools-configs directory\n", toolName, toolConfigPath) + fmt.Printf("Config file not found for tool %s: %s and no API token provided\n", toolName, toolConfigPath) } } else if err != nil { - return fmt.Errorf("Error checking config file for tool %s: %w", toolName, err) + return fmt.Errorf("error checking config file for tool %s: %w", toolName, err) } else { fmt.Printf("Config file found for %s: %s\n", toolName, toolConfigPath) } From d7d206f91d2563d279cb9f8cc9b628fcd2ac5a68 Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Wed, 18 Jun 2025 11:35:56 +0100 Subject: [PATCH 07/11] fixed test --- cmd/analyze_integration_test.go | 5 +++++ cmd/analyze_test.go | 13 ++++++++++--- 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/cmd/analyze_integration_test.go b/cmd/analyze_integration_test.go index 5a3f9b99..1e593ad7 100644 --- a/cmd/analyze_integration_test.go +++ b/cmd/analyze_integration_test.go @@ -88,6 +88,11 @@ func TestCheckIfConfigExistsAndIsNeededBehavior(t *testing.T) { // Mock config to use our temporary directory config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir) + // Create tools-configs directory since the function will try to create config files + toolsConfigDir := config.Config.ToolsConfigDirectory() + err = os.MkdirAll(toolsConfigDir, constants.DefaultDirPerms) + require.NoError(t, err) + // Execute the function - this tests it doesn't panic or return unexpected errors err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode) diff --git a/cmd/analyze_test.go b/cmd/analyze_test.go index c3c67ea3..e8de72ed 100644 --- a/cmd/analyze_test.go +++ b/cmd/analyze_test.go @@ -324,7 +324,7 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { apiToken: "", configFileExists: false, expectError: false, - description: "Local mode should show message about adding config file", + description: "Local mode should create config file if tools-configs directory exists", }, } @@ -346,11 +346,11 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { if tt.configFileExists && constants.ToolConfigFileNames[tt.toolName] != "" { // Use config.Config.ToolsConfigDirectory() to get the exact same path the function will use toolsConfigDir := config.Config.ToolsConfigDirectory() - err := os.MkdirAll(toolsConfigDir, 0755) + err := os.MkdirAll(toolsConfigDir, constants.DefaultDirPerms) require.NoError(t, err) configPath := filepath.Join(toolsConfigDir, constants.ToolConfigFileNames[tt.toolName]) - err = os.WriteFile(configPath, []byte("test config"), 0644) + err = os.WriteFile(configPath, []byte("test config"), constants.DefaultFilePerms) require.NoError(t, err) // Ensure the file was created and can be found @@ -363,6 +363,13 @@ func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { ApiToken: tt.apiToken, } + // Ensure tools-configs directory exists if the function might try to create config files + if !tt.configFileExists && constants.ToolConfigFileNames[tt.toolName] != "" { + toolsConfigDir := config.Config.ToolsConfigDirectory() + err := os.MkdirAll(toolsConfigDir, constants.DefaultDirPerms) + require.NoError(t, err) + } + // Execute the function err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode) From 117cdabb3e61f6c99d611cfba97c665789389636 Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Mon, 23 Jun 2025 10:27:43 +0100 Subject: [PATCH 08/11] fixing tests and adding logs --- cmd/analyze.go | 21 ++++++++++++++++++--- cmd/configsetup/setup.go | 6 ++++++ tools/eslintRunner.go | 1 + 3 files changed, 25 insertions(+), 3 deletions(-) diff --git a/cmd/analyze.go b/cmd/analyze.go index 679349cd..bd1a86fa 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -283,6 +283,7 @@ func validateToolName(toolName string) error { return nil } +// checkIfConfigExistsAndIsNeeded validates if a tool has config file and creates one if needed func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { configFileName := constants.ToolConfigFileNames[toolName] if configFileName == "" { @@ -296,19 +297,33 @@ func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error { // Check if the config file exists if _, err := os.Stat(toolConfigPath); os.IsNotExist(err) { - // Only show error if we're in remote mode and need the config file + // Config file does not exist - create it if we have the means to do so if (!cliLocalMode && initFlags.ApiToken != "") || cliLocalMode { fmt.Printf("Creating new config file for tool %s\n", toolName) if err := configsetup.CreateToolConfigurationFile(toolName, initFlags); err != nil { return fmt.Errorf("failed to create config file for tool %s: %w", toolName, err) } + + // Ensure .gitignore exists FIRST to prevent config files from being analyzed + if err := configsetup.CreateGitIgnoreFile(); err != nil { + logger.Warn("Failed to create .gitignore file", logrus.Fields{ + "error": err, + }) + } } else { - fmt.Printf("Config file not found for tool %s: %s and no API token provided\n", toolName, toolConfigPath) + logger.Debug("Config file not found for tool, using tool defaults", logrus.Fields{ + "tool": toolName, + "toolConfigPath": toolConfigPath, + "message": "No API token provided", + }) } } else if err != nil { return fmt.Errorf("error checking config file for tool %s: %w", toolName, err) } else { - fmt.Printf("Config file found for %s: %s\n", toolName, toolConfigPath) + logger.Info("Config file found for tool", logrus.Fields{ + "tool": toolName, + "toolConfigPath": toolConfigPath, + }) } return nil } diff --git a/cmd/configsetup/setup.go b/cmd/configsetup/setup.go index 4f2f7024..4f8c22d1 100644 --- a/cmd/configsetup/setup.go +++ b/cmd/configsetup/setup.go @@ -511,6 +511,12 @@ func createToolFileConfiguration(tool domain.Tool, patternConfiguration []domain } toolsConfigDir := config.Config.ToolsConfigDirectory() + + // Ensure the tools-configs directory exists + if err := os.MkdirAll(toolsConfigDir, constants.DefaultDirPerms); err != nil { + return fmt.Errorf("failed to create tools-configs directory: %w", err) + } + return creator.CreateConfig(toolsConfigDir, patternConfiguration) } diff --git a/tools/eslintRunner.go b/tools/eslintRunner.go index f7212d6d..840d5a9a 100644 --- a/tools/eslintRunner.go +++ b/tools/eslintRunner.go @@ -38,6 +38,7 @@ func RunEslint(repositoryToAnalyseDirectory string, eslintInstallationDirectory //When writing to file, use the output file option cmd.Args = append(cmd.Args, "-o", outputFile) } + if len(pathsToCheck) > 0 { cmd.Args = append(cmd.Args, pathsToCheck...) } else { From 395b7158c30b0682a382ba656e4649e931b1b023 Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Wed, 25 Jun 2025 17:40:06 +0100 Subject: [PATCH 09/11] fixing eslint tests --- plugins/tools/eslint/test/expected.sarif | 7 ++++++- .../tools/eslint/test/src/.codacy/codacy.yaml | 1 - plugins/tools/eslint/test/src/.eslintrc.json | 16 ---------------- 3 files changed, 6 insertions(+), 18 deletions(-) delete mode 100644 plugins/tools/eslint/test/src/.eslintrc.json diff --git a/plugins/tools/eslint/test/expected.sarif b/plugins/tools/eslint/test/expected.sarif index 4f675f0d..1e42681b 100644 --- a/plugins/tools/eslint/test/expected.sarif +++ b/plugins/tools/eslint/test/expected.sarif @@ -3,6 +3,11 @@ "runs": [ { "artifacts": [ + { + "location": { + "uri": "file:///plugins/tools/eslint/test/src/.codacy/tools-configs/eslint.config.mjs" + } + }, { "location": { "uri": "file:///plugins/tools/eslint/test/src/Test.js" @@ -22,7 +27,7 @@ { "physicalLocation": { "artifactLocation": { - "index": 0, + "index": 1, "uri": "file:///plugins/tools/eslint/test/src/Test.js" }, "region": { diff --git a/plugins/tools/eslint/test/src/.codacy/codacy.yaml b/plugins/tools/eslint/test/src/.codacy/codacy.yaml index 01b7f6f2..c2cccd0d 100644 --- a/plugins/tools/eslint/test/src/.codacy/codacy.yaml +++ b/plugins/tools/eslint/test/src/.codacy/codacy.yaml @@ -2,4 +2,3 @@ runtimes: - node@22.2.0 tools: - eslint@8.57.0 - diff --git a/plugins/tools/eslint/test/src/.eslintrc.json b/plugins/tools/eslint/test/src/.eslintrc.json deleted file mode 100644 index 7fd66f65..00000000 --- a/plugins/tools/eslint/test/src/.eslintrc.json +++ /dev/null @@ -1,16 +0,0 @@ -{ - "env": { - "browser": true, - "es2021": true, - "node": true - }, - "extends": "eslint:recommended", - "parserOptions": { - "ecmaVersion": "latest", - "sourceType": "module" - }, - "rules": { - "no-unused-vars": "warn", - "no-console": "warn" - } -} \ No newline at end of file From 56264ee18acefab3705c017f3b03251642c45512 Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Wed, 25 Jun 2025 17:43:38 +0100 Subject: [PATCH 10/11] fixing pylint tests --- plugins/tools/pylint/test/expected.sarif | 105 +++++------------------ 1 file changed, 22 insertions(+), 83 deletions(-) diff --git a/plugins/tools/pylint/test/expected.sarif b/plugins/tools/pylint/test/expected.sarif index 640bf1ba..59af4541 100644 --- a/plugins/tools/pylint/test/expected.sarif +++ b/plugins/tools/pylint/test/expected.sarif @@ -1,44 +1,10 @@ { - "$schema": "https://raw.githubusercontent.com/oasis-tcs/sarif-spec/master/Schemata/sarif-schema-2.1.0.json", - "version": "2.1.0", "runs": [ { - "tool": { - "driver": { - "name": "Pylint", - "version": "3.3.6", - "informationUri": "https://pylint.org", - "rules": null - } - }, "results": [ { - "ruleId": "missing-final-newline", - "level": "note", - "message": { - "text": "Final newline missing" - }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "test_file.py" - }, - "region": { - "startLine": 33, - "startColumn": 0 - } - } - } - ] - }, - { - "ruleId": "too-many-arguments", - "level": "note", - "message": { - "text": "Too many arguments (11/5)" - }, + "level": "warning", "locations": [ { "physicalLocation": { @@ -46,39 +12,19 @@ "uri": "test_file.py" }, "region": { - "startLine": 16, - "startColumn": 0 + "startColumn": 0, + "startLine": 8 } } } - ] - }, - { - "ruleId": "too-many-positional-arguments", - "level": "note", + ], "message": { - "text": "Too many positional arguments (11/5)" + "text": "Unused import os" }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "test_file.py" - }, - "region": { - "startLine": 16, - "startColumn": 0 - } - } - } - ] + "ruleId": "unused-import" }, { - "ruleId": "unused-import", "level": "warning", - "message": { - "text": "Unused import os" - }, "locations": [ { "physicalLocation": { @@ -86,34 +32,27 @@ "uri": "test_file.py" }, "region": { - "startLine": 8, - "startColumn": 0 + "startColumn": 0, + "startLine": 9 } } } - ] - }, - { - "ruleId": "unused-import", - "level": "warning", + ], "message": { "text": "Unused import sys" }, - "locations": [ - { - "physicalLocation": { - "artifactLocation": { - "uri": "test_file.py" - }, - "region": { - "startLine": 9, - "startColumn": 0 - } - } - } - ] + "ruleId": "unused-import" + } + ], + "tool": { + "driver": { + "informationUri": "https://pylint.org", + "name": "Pylint", + "rules": null, + "version": "3.3.6" } - ] + } } - ] -} \ No newline at end of file + ], + "version": "2.1.0" +} From 42770733d7e95caab8e6e2392ea6b5f3b2222f75 Mon Sep 17 00:00:00 2001 From: franciscoazevedo Date: Mon, 30 Jun 2025 09:49:01 +0100 Subject: [PATCH 11/11] rebase --- cmd/configsetup/setup.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cmd/configsetup/setup.go b/cmd/configsetup/setup.go index 4f8c22d1..128b133a 100644 --- a/cmd/configsetup/setup.go +++ b/cmd/configsetup/setup.go @@ -204,7 +204,7 @@ func CreateLanguagesConfigFileLocal(toolsConfigDir string) error { return fmt.Errorf("failed to marshal languages config to YAML: %w", err) } - return writeConfigFile(filepath.Join(toolsConfigDir, LanguagesConfigFileName), data) + return writeConfigFile(filepath.Join(toolsConfigDir, constants.LanguagesConfigFileName), data) } func CreateGitIgnoreFile() error {