diff --git a/cmd/analyze.go b/cmd/analyze.go index 60323104..bd1a86fa 100644 --- a/cmd/analyze.go +++ b/cmd/analyze.go @@ -1,7 +1,10 @@ package cmd 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" @@ -44,7 +47,7 @@ type LanguagesConfig struct { // 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 { @@ -219,6 +222,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 +283,56 @@ 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 { +// 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 == "" { + // 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) { + // 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 { + 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 { + logger.Info("Config file found for tool", logrus.Fields{ + "tool": toolName, + "toolConfigPath": 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,13 +363,13 @@ 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 } 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 @@ -348,7 +401,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) @@ -360,7 +413,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) @@ -368,7 +421,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,10 +437,19 @@ 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", - 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 { @@ -401,6 +463,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 +503,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 +538,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_integration_test.go b/cmd/analyze_integration_test.go new file mode 100644 index 00000000..1e593ad7 --- /dev/null +++ b/cmd/analyze_integration_test.go @@ -0,0 +1,193 @@ +package cmd + +import ( + "os" + "path/filepath" + "strings" + "testing" + + "codacy/cli-v2/config" + "codacy/cli-v2/constants" + "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) + + // 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) + + // 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": 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 := 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 constants.ToolConfigFileNames { + _, expected := expectedTools[toolName] + assert.True(t, expected, "Unexpected tool %s found in constants.ToolConfigFileNames 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 constants.ToolConfigFileNames { + 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 b46f61de..e8de72ed 100644 --- a/cmd/analyze_test.go +++ b/cmd/analyze_test.go @@ -1,10 +1,17 @@ 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/plugins" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" ) func TestGetFileExtension(t *testing.T) { @@ -262,3 +269,146 @@ func TestValidatePaths(t *testing.T) { }) } } + +func TestCheckIfConfigExistsAndIsNeeded(t *testing.T) { + // Save original initFlags and config + 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 + 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_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 create config file if tools-configs directory exists", + }, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + // 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) + + // 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 && 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, constants.DefaultDirPerms) + require.NoError(t, err) + + configPath := filepath.Join(toolsConfigDir, constants.ToolConfigFileNames[tt.toolName]) + err = os.WriteFile(configPath, []byte("test config"), constants.DefaultFilePerms) + 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 + initFlags = domain.InitFlags{ + 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) + + // Clean up any files that might have been created by the function under test + if !tt.configFileExists && constants.ToolConfigFileNames[tt.toolName] != "" { + toolsConfigDir := config.Config.ToolsConfigDirectory() + configPath := filepath.Join(toolsConfigDir, constants.ToolConfigFileNames[tt.toolName]) + if _, statErr := os.Stat(configPath); statErr == nil { + os.Remove(configPath) + } + } + + // 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": 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 := 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 fdd0c684..128b133a 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 @@ -213,11 +204,11 @@ 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 { - 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)) } @@ -483,6 +474,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] @@ -492,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/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, +} 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 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" +} diff --git a/tools/eslintConfigCreator.go b/tools/eslintConfigCreator.go index 7679ed4d..6416484b 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 _, ok := rulesWithoutOptions[rule]; ok { + // 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" + } } } 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 {