Skip to content

Commit 1c23a76

Browse files
feature: analyze with initFlags and fetch config file if non existent - PLUTO-1423 (#153)
* analyze now receives initFlags and fetches config file if non existent * added test * code review changes * fix test * 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 * fetching default configs in local mode * fixed test * fixing tests and adding logs * fixing eslint tests * fixing pylint tests * rebase --------- Co-authored-by: Andrzej Janczak <[email protected]>
1 parent 66c17c5 commit 1c23a76

File tree

11 files changed

+600
-152
lines changed

11 files changed

+600
-152
lines changed

cmd/analyze.go

Lines changed: 76 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,10 @@
11
package cmd
22

33
import (
4+
"codacy/cli-v2/cmd/cmdutils"
5+
"codacy/cli-v2/cmd/configsetup"
46
"codacy/cli-v2/config"
7+
"codacy/cli-v2/constants"
58
"codacy/cli-v2/domain"
69
"codacy/cli-v2/plugins"
710
"codacy/cli-v2/tools"
@@ -44,7 +47,7 @@ type LanguagesConfig struct {
4447
// LoadLanguageConfig loads the language configuration from the file
4548
func LoadLanguageConfig() (*LanguagesConfig, error) {
4649
// First, try to load the YAML config
47-
yamlPath := filepath.Join(config.Config.ToolsConfigDirectory(), "languages-config.yaml")
50+
yamlPath := filepath.Join(config.Config.ToolsConfigDirectory(), constants.LanguagesConfigFileName)
4851

4952
// Check if the YAML file exists
5053
if _, err := os.Stat(yamlPath); err == nil {
@@ -219,6 +222,7 @@ func init() {
219222
analyzeCmd.Flags().StringVarP(&toolsToAnalyzeParam, "tool", "t", "", "Which tool to run analysis with. If not specified, all configured tools will be run")
220223
analyzeCmd.Flags().StringVar(&outputFormat, "format", "", "Output format (use 'sarif' for SARIF format)")
221224
analyzeCmd.Flags().BoolVar(&autoFix, "fix", false, "Apply auto fix to your issues when available")
225+
cmdutils.AddCloudFlags(analyzeCmd, &initFlags)
222226
rootCmd.AddCommand(analyzeCmd)
223227
}
224228

@@ -279,7 +283,56 @@ func validateToolName(toolName string) error {
279283
return nil
280284
}
281285

282-
func runToolByName(toolName string, workDirectory string, pathsToCheck []string, autoFix bool, outputFile string, outputFormat string, tool *plugins.ToolInfo, runtime *plugins.RuntimeInfo) error {
286+
// checkIfConfigExistsAndIsNeeded validates if a tool has config file and creates one if needed
287+
func checkIfConfigExistsAndIsNeeded(toolName string, cliLocalMode bool) error {
288+
configFileName := constants.ToolConfigFileNames[toolName]
289+
if configFileName == "" {
290+
// Tool doesn't use config file
291+
return nil
292+
}
293+
294+
// Use the configuration system to get the tools config directory
295+
toolsConfigDir := config.Config.ToolsConfigDirectory()
296+
toolConfigPath := filepath.Join(toolsConfigDir, configFileName)
297+
298+
// Check if the config file exists
299+
if _, err := os.Stat(toolConfigPath); os.IsNotExist(err) {
300+
// Config file does not exist - create it if we have the means to do so
301+
if (!cliLocalMode && initFlags.ApiToken != "") || cliLocalMode {
302+
fmt.Printf("Creating new config file for tool %s\n", toolName)
303+
if err := configsetup.CreateToolConfigurationFile(toolName, initFlags); err != nil {
304+
return fmt.Errorf("failed to create config file for tool %s: %w", toolName, err)
305+
}
306+
307+
// Ensure .gitignore exists FIRST to prevent config files from being analyzed
308+
if err := configsetup.CreateGitIgnoreFile(); err != nil {
309+
logger.Warn("Failed to create .gitignore file", logrus.Fields{
310+
"error": err,
311+
})
312+
}
313+
} else {
314+
logger.Debug("Config file not found for tool, using tool defaults", logrus.Fields{
315+
"tool": toolName,
316+
"toolConfigPath": toolConfigPath,
317+
"message": "No API token provided",
318+
})
319+
}
320+
} else if err != nil {
321+
return fmt.Errorf("error checking config file for tool %s: %w", toolName, err)
322+
} else {
323+
logger.Info("Config file found for tool", logrus.Fields{
324+
"tool": toolName,
325+
"toolConfigPath": toolConfigPath,
326+
})
327+
}
328+
return nil
329+
}
330+
331+
func runToolByName(toolName string, workDirectory string, pathsToCheck []string, autoFix bool, outputFile string, outputFormat string, tool *plugins.ToolInfo, runtime *plugins.RuntimeInfo, cliLocalMode bool) error {
332+
err := checkIfConfigExistsAndIsNeeded(toolName, cliLocalMode)
333+
if err != nil {
334+
return err
335+
}
283336
switch toolName {
284337
case "eslint":
285338
binaryPath := runtime.Binaries[tool.Runtime]
@@ -310,13 +363,13 @@ func runToolByName(toolName string, workDirectory string, pathsToCheck []string,
310363
return fmt.Errorf("unsupported tool: %s", toolName)
311364
}
312365

313-
func runTool(workDirectory string, toolName string, pathsToCheck []string, outputFile string, autoFix bool, outputFormat string) error {
366+
func runTool(workDirectory string, toolName string, pathsToCheck []string, outputFile string, autoFix bool, outputFormat string, cliLocalMode bool) error {
314367
err := validateToolName(toolName)
315368
if err != nil {
316369
return err
317370
}
318371
log.Println("Running tools for the specified file(s)...")
319-
log.Printf("Running %s...\n", toolName)
372+
log.Printf("Running %s...", toolName)
320373

321374
tool := config.Config.Tools()[toolName]
322375
var isToolInstalled bool
@@ -348,7 +401,7 @@ func runTool(workDirectory string, toolName string, pathsToCheck []string, outpu
348401
runtime = config.Config.Runtimes()[tool.Runtime]
349402
isRuntimeInstalled = runtime == nil || config.Config.IsRuntimeInstalled(tool.Runtime, runtime)
350403
if !isRuntimeInstalled {
351-
fmt.Printf("%s runtime is not installed, installing...\n", tool.Runtime)
404+
fmt.Printf("%s runtime is not installed, installing...", tool.Runtime)
352405
err := config.InstallRuntime(tool.Runtime, runtime)
353406
if err != nil {
354407
return fmt.Errorf("failed to install %s runtime: %w", tool.Runtime, err)
@@ -360,15 +413,15 @@ func runTool(workDirectory string, toolName string, pathsToCheck []string, outpu
360413
runtime = config.Config.Runtimes()[tool.Runtime]
361414
isRuntimeInstalled = runtime == nil || config.Config.IsRuntimeInstalled(tool.Runtime, runtime)
362415
if !isRuntimeInstalled {
363-
fmt.Printf("%s runtime is not installed, installing...\n", tool.Runtime)
416+
fmt.Printf("%s runtime is not installed, installing...", tool.Runtime)
364417
err := config.InstallRuntime(tool.Runtime, runtime)
365418
if err != nil {
366419
return fmt.Errorf("failed to install %s runtime: %w", tool.Runtime, err)
367420
}
368421
runtime = config.Config.Runtimes()[tool.Runtime]
369422
}
370423
}
371-
return runToolByName(toolName, workDirectory, pathsToCheck, autoFix, outputFile, outputFormat, tool, runtime)
424+
return runToolByName(toolName, workDirectory, pathsToCheck, autoFix, outputFile, outputFormat, tool, runtime, cliLocalMode)
372425
}
373426

374427
// validatePaths checks if all provided paths exist and returns an error if any don't
@@ -384,10 +437,19 @@ func validatePaths(paths []string) error {
384437
return nil
385438
}
386439

440+
func validateCloudMode(cliLocalMode bool) error {
441+
if cliLocalMode {
442+
fmt.Println("Warning: cannot run in cloud mode")
443+
}
444+
return nil
445+
}
446+
387447
var analyzeCmd = &cobra.Command{
388448
Use: "analyze",
389449
Short: "Analyze code using configured tools",
390-
Long: `Analyze code using configured tools and output results in the specified format.`,
450+
Long: `Analyze code using configured tools and output results in the specified format.
451+
452+
Supports API token, provider, and repository flags to automatically fetch tool configurations from Codacy API if they don't exist locally.`,
391453
Run: func(cmd *cobra.Command, args []string) {
392454
// Validate paths before proceeding
393455
if err := validatePaths(args); err != nil {
@@ -401,6 +463,10 @@ var analyzeCmd = &cobra.Command{
401463
log.Fatalf("Failed to get current working directory: %v", err)
402464
}
403465

466+
cliLocalMode := len(initFlags.ApiToken) == 0
467+
468+
validateCloudMode(cliLocalMode)
469+
404470
var toolsToRun map[string]*plugins.ToolInfo
405471

406472
if toolsToAnalyzeParam != "" {
@@ -437,7 +503,7 @@ var analyzeCmd = &cobra.Command{
437503
var sarifOutputs []string
438504
for toolName := range toolsToRun {
439505
tmpFile := filepath.Join(tmpDir, fmt.Sprintf("%s.sarif", toolName))
440-
if err := runTool(workDirectory, toolName, args, tmpFile, autoFix, outputFormat); err != nil {
506+
if err := runTool(workDirectory, toolName, args, tmpFile, autoFix, outputFormat, cliLocalMode); err != nil {
441507
log.Printf("Tool failed to run: %v\n", err)
442508
}
443509
sarifOutputs = append(sarifOutputs, tmpFile)
@@ -472,7 +538,7 @@ var analyzeCmd = &cobra.Command{
472538
} else {
473539
// Run tools without merging outputs
474540
for toolName := range toolsToRun {
475-
if err := runTool(workDirectory, toolName, args, outputFile, autoFix, outputFormat); err != nil {
541+
if err := runTool(workDirectory, toolName, args, outputFile, autoFix, outputFormat, cliLocalMode); err != nil {
476542
log.Printf("Tool failed to run: %v\n", err)
477543
}
478544
}

cmd/analyze_integration_test.go

Lines changed: 193 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,193 @@
1+
package cmd
2+
3+
import (
4+
"os"
5+
"path/filepath"
6+
"strings"
7+
"testing"
8+
9+
"codacy/cli-v2/config"
10+
"codacy/cli-v2/constants"
11+
"codacy/cli-v2/domain"
12+
13+
"github.com/stretchr/testify/assert"
14+
"github.com/stretchr/testify/require"
15+
)
16+
17+
// TestCheckIfConfigExistsAndIsNeededBehavior tests the behavior of checkIfConfigExistsAndIsNeeded
18+
// without creating actual config files in the project directory
19+
func TestCheckIfConfigExistsAndIsNeededBehavior(t *testing.T) {
20+
// Save original state
21+
originalFlags := initFlags
22+
originalConfig := config.Config
23+
originalWorkingDir, _ := os.Getwd()
24+
defer func() {
25+
initFlags = originalFlags
26+
config.Config = originalConfig
27+
os.Chdir(originalWorkingDir)
28+
}()
29+
30+
tests := []struct {
31+
name string
32+
toolName string
33+
cliLocalMode bool
34+
apiToken string
35+
description string
36+
expectNoError bool
37+
}{
38+
{
39+
name: "tool_without_config_file",
40+
toolName: "unsupported-tool",
41+
cliLocalMode: false,
42+
apiToken: "test-token",
43+
description: "Tool that doesn't use config files should return without error",
44+
expectNoError: true,
45+
},
46+
{
47+
name: "eslint_local_mode",
48+
toolName: "eslint",
49+
cliLocalMode: true,
50+
apiToken: "",
51+
description: "ESLint in local mode should not error",
52+
expectNoError: true,
53+
},
54+
{
55+
name: "eslint_remote_mode_without_token",
56+
toolName: "eslint",
57+
cliLocalMode: false,
58+
apiToken: "",
59+
description: "ESLint in remote mode without token should not error",
60+
expectNoError: true,
61+
},
62+
{
63+
name: "trivy_local_mode",
64+
toolName: "trivy",
65+
cliLocalMode: true,
66+
apiToken: "",
67+
description: "Trivy in local mode should not error",
68+
expectNoError: true,
69+
},
70+
}
71+
72+
for _, tt := range tests {
73+
t.Run(tt.name, func(t *testing.T) {
74+
// Create temporary directory and change to it to avoid creating files in project dir
75+
tmpDir, err := os.MkdirTemp("", "codacy-test-isolated-*")
76+
require.NoError(t, err)
77+
defer os.RemoveAll(tmpDir)
78+
79+
// Change to temp directory to avoid creating config files in project
80+
err = os.Chdir(tmpDir)
81+
require.NoError(t, err)
82+
83+
// Setup initFlags
84+
initFlags = domain.InitFlags{
85+
ApiToken: tt.apiToken,
86+
}
87+
88+
// Mock config to use our temporary directory
89+
config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir)
90+
91+
// Create tools-configs directory since the function will try to create config files
92+
toolsConfigDir := config.Config.ToolsConfigDirectory()
93+
err = os.MkdirAll(toolsConfigDir, constants.DefaultDirPerms)
94+
require.NoError(t, err)
95+
96+
// Execute the function - this tests it doesn't panic or return unexpected errors
97+
err = checkIfConfigExistsAndIsNeeded(tt.toolName, tt.cliLocalMode)
98+
99+
// Verify results
100+
if tt.expectNoError {
101+
assert.NoError(t, err, "Function should not return error: %s", tt.description)
102+
} else {
103+
assert.Error(t, err, "Function should return error: %s", tt.description)
104+
}
105+
})
106+
}
107+
}
108+
109+
// TestToolConfigFileNameMapCompleteness ensures all expected tools have config mappings
110+
func TestToolConfigFileNameMapCompleteness(t *testing.T) {
111+
expectedTools := map[string]string{
112+
"eslint": constants.ESLintConfigFileName,
113+
"trivy": constants.TrivyConfigFileName,
114+
"pmd": constants.PMDConfigFileName,
115+
"pylint": constants.PylintConfigFileName,
116+
"dartanalyzer": constants.DartAnalyzerConfigFileName,
117+
"semgrep": constants.SemgrepConfigFileName,
118+
"revive": constants.ReviveConfigFileName,
119+
"lizard": constants.LizardConfigFileName,
120+
}
121+
122+
t.Run("all_expected_tools_present", func(t *testing.T) {
123+
for toolName, expectedFileName := range expectedTools {
124+
actualFileName, exists := constants.ToolConfigFileNames[toolName]
125+
assert.True(t, exists, "Tool %s should exist in constants.ToolConfigFileNames map", toolName)
126+
assert.Equal(t, expectedFileName, actualFileName, "Config filename for %s should match expected", toolName)
127+
}
128+
})
129+
130+
t.Run("no_unexpected_tools", func(t *testing.T) {
131+
for toolName := range constants.ToolConfigFileNames {
132+
_, expected := expectedTools[toolName]
133+
assert.True(t, expected, "Unexpected tool %s found in constants.ToolConfigFileNames map", toolName)
134+
}
135+
})
136+
137+
t.Run("config_files_have_proper_extensions", func(t *testing.T) {
138+
validExtensions := map[string]bool{
139+
".mjs": true,
140+
".js": true,
141+
".yaml": true,
142+
".yml": true,
143+
".xml": true,
144+
".rc": true,
145+
".toml": true,
146+
}
147+
148+
for toolName, fileName := range constants.ToolConfigFileNames {
149+
ext := filepath.Ext(fileName)
150+
assert.True(t, validExtensions[ext], "Tool %s has config file %s with unexpected extension %s", toolName, fileName, ext)
151+
}
152+
})
153+
}
154+
155+
// TestCheckIfConfigExistsAndIsNeededEdgeCases tests edge cases and error conditions
156+
func TestCheckIfConfigExistsAndIsNeededEdgeCases(t *testing.T) {
157+
originalFlags := initFlags
158+
originalConfig := config.Config
159+
originalWorkingDir, _ := os.Getwd()
160+
defer func() {
161+
initFlags = originalFlags
162+
config.Config = originalConfig
163+
os.Chdir(originalWorkingDir)
164+
}()
165+
166+
// Create temporary directory for edge case tests
167+
tmpDir, err := os.MkdirTemp("", "codacy-test-edge-*")
168+
require.NoError(t, err)
169+
defer os.RemoveAll(tmpDir)
170+
171+
// Change to temp directory
172+
err = os.Chdir(tmpDir)
173+
require.NoError(t, err)
174+
175+
// Mock config
176+
config.Config = *config.NewConfigType(tmpDir, tmpDir, tmpDir)
177+
178+
t.Run("empty_tool_name", func(t *testing.T) {
179+
err := checkIfConfigExistsAndIsNeeded("", false)
180+
assert.NoError(t, err, "Empty tool name should not cause error")
181+
})
182+
183+
t.Run("tool_name_with_special_characters", func(t *testing.T) {
184+
err := checkIfConfigExistsAndIsNeeded("tool-with-dashes_and_underscores", false)
185+
assert.NoError(t, err, "Tool name with special characters should not cause error")
186+
})
187+
188+
t.Run("very_long_tool_name", func(t *testing.T) {
189+
longToolName := strings.Repeat("verylongtoolname", 10)
190+
err := checkIfConfigExistsAndIsNeeded(longToolName, false)
191+
assert.NoError(t, err, "Very long tool name should not cause error")
192+
})
193+
}

0 commit comments

Comments
 (0)