Skip to content

Commit 86a2cb2

Browse files
feature: Discover command (#152)
Implement configuration discovery feature with comprehensive testing - Add configuration discovery logic to automatically detect and generate tool configurations - Refactor tool UUID selection logic for better reliability - Update PMD plugin version configuration - Add extensive unit tests for configuration management and file detection - Implement integration tests for config discovery across multiple languages - Fix languages-config file generation during discovery process - Add sample files and expected configurations for Java, Dart, JavaScript, and Python - Update integration test runner to support config discovery scenarios This feature enables automatic detection and setup of appropriate tool configurations based on project structure and file types, significantly improving the initial setup experience for users.
1 parent dcd97cf commit 86a2cb2

File tree

24 files changed

+35805
-185
lines changed

24 files changed

+35805
-185
lines changed

cmd/config.go

Lines changed: 33 additions & 101 deletions
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,6 @@ import (
55
"log"
66
"os"
77
"path/filepath"
8-
"slices"
98
"sort"
109
"strings"
1110

@@ -186,17 +185,17 @@ var configDiscoverCmd = &cobra.Command{
186185
}
187186
}
188187

189-
detectedLanguages, err := config.DetectLanguages(discoverPath, defaultToolLangMap)
188+
detectedTools, err := config.DetectRelevantTools(discoverPath, defaultToolLangMap)
190189
if err != nil {
191-
log.Fatalf("Error detecting languages: %v", err)
190+
log.Fatalf("Error detecting relevant tools: %v", err)
192191
}
193-
if len(detectedLanguages) == 0 {
194-
fmt.Println("No known languages detected in the provided path.")
192+
if len(detectedTools) == 0 {
193+
fmt.Println("No relevant tools found for the file extensions in the provided path.")
195194
return
196195
}
197196

198197
toolsConfigDir := config.Config.ToolsConfigsDirectory()
199-
if err := updateLanguagesConfig(detectedLanguages, toolsConfigDir, defaultToolLangMap); err != nil {
198+
if err := updateLanguagesConfigForTools(detectedTools, toolsConfigDir, defaultToolLangMap); err != nil {
200199
log.Fatalf("Error updating .codacy/tools-configs/languages-config.yaml: %v", err)
201200
}
202201
fmt.Println("Updated .codacy/tools-configs/languages-config.yaml")
@@ -209,7 +208,8 @@ var configDiscoverCmd = &cobra.Command{
209208
}
210209

211210
codacyYAMLPath := config.Config.ProjectConfigFile()
212-
if err := updateCodacyYAML(detectedLanguages, codacyYAMLPath, defaultToolLangMap, configResetInitFlags, currentCliMode); err != nil {
211+
212+
if err := updateCodacyYAMLForTools(detectedTools, codacyYAMLPath, configResetInitFlags, currentCliMode); err != nil {
213213
if strings.Contains(err.Error(), "❌ Fatal:") {
214214
fmt.Println(err)
215215
os.Exit(1)
@@ -218,21 +218,10 @@ var configDiscoverCmd = &cobra.Command{
218218
}
219219
fmt.Printf("Updated %s with relevant tools.\n", filepath.Base(codacyYAMLPath))
220220

221-
// Determine which tools are relevant for discovered languages and create their configurations
222-
discoveredToolNames := make(map[string]struct{})
223-
for toolName, toolInfo := range defaultToolLangMap {
224-
for _, toolLang := range toolInfo.Languages {
225-
if _, detected := detectedLanguages[toolLang]; detected {
226-
discoveredToolNames[toolName] = struct{}{}
227-
break
228-
}
229-
}
230-
}
231-
232221
// Create tool configuration files for discovered tools
233-
if len(discoveredToolNames) > 0 {
222+
if len(detectedTools) > 0 {
234223
fmt.Printf("\nCreating tool configurations for discovered tools...\n")
235-
if err := configsetup.CreateConfigurationFilesForDiscoveredTools(discoveredToolNames, toolsConfigDir, configResetInitFlags); err != nil {
224+
if err := configsetup.CreateConfigurationFilesForDiscoveredTools(detectedTools, toolsConfigDir, configResetInitFlags); err != nil {
236225
log.Printf("Warning: Failed to create some tool configurations: %v", err)
237226
}
238227
}
@@ -242,83 +231,29 @@ var configDiscoverCmd = &cobra.Command{
242231
},
243232
}
244233

245-
// updateLanguagesConfig updates the .codacy/tools-configs/languages-config.yaml file.
246-
func updateLanguagesConfig(detectedLanguages map[string]struct{}, toolsConfigDir string, defaultToolLangMap map[string]domain.ToolLanguageInfo) error {
234+
// updateLanguagesConfigForTools updates the .codacy/tools-configs/languages-config.yaml file based on detected tools.
235+
func updateLanguagesConfigForTools(detectedTools map[string]struct{}, toolsConfigDir string, defaultToolLangMap map[string]domain.ToolLanguageInfo) error {
247236
langConfigPath := filepath.Join(toolsConfigDir, "languages-config.yaml")
248-
var langConf domain.LanguagesConfig
249237

250-
if _, err := os.Stat(langConfigPath); err == nil {
251-
data, err := os.ReadFile(langConfigPath)
252-
if err != nil {
253-
return fmt.Errorf("failed to read existing languages-config.yaml: %w", err)
254-
}
255-
if err := yaml.Unmarshal(data, &langConf); err != nil {
238+
// Read existing languages config if it exists
239+
var langConf domain.LanguagesConfig
240+
if content, err := os.ReadFile(langConfigPath); err == nil {
241+
if err := yaml.Unmarshal(content, &langConf); err != nil {
256242
return fmt.Errorf("failed to parse existing languages-config.yaml: %w", err)
257243
}
258-
} else if !os.IsNotExist(err) {
259-
return fmt.Errorf("failed to stat languages-config.yaml: %w", err)
260244
}
261245

262-
// Create a map of existing tools for easier update
263-
existingToolsMap := make(map[string]*domain.ToolLanguageInfo)
264-
for i := range langConf.Tools {
265-
existingToolsMap[langConf.Tools[i].Name] = &langConf.Tools[i]
246+
// Create a map of existing tools for quick lookup
247+
existingTools := make(map[string]domain.ToolLanguageInfo)
248+
for _, tool := range langConf.Tools {
249+
existingTools[tool.Name] = tool
266250
}
267251

268-
for toolName, toolInfoFromDefaults := range defaultToolLangMap {
269-
isRelevantTool := false
270-
relevantLangsForThisTool := []string{}
271-
relevantExtsForThisToolMap := make(map[string]struct{})
272-
273-
for _, langDefault := range toolInfoFromDefaults.Languages {
274-
if _, isDetected := detectedLanguages[langDefault]; isDetected {
275-
isRelevantTool = true
276-
if !slices.Contains(relevantLangsForThisTool, langDefault) {
277-
relevantLangsForThisTool = append(relevantLangsForThisTool, langDefault)
278-
}
279-
// Add extensions associated with this detected language for this tool
280-
for _, defaultExt := range toolInfoFromDefaults.Extensions {
281-
// A simple heuristic: if a tool supports a language, and that language is detected,
282-
// all default extensions of that tool for that language group are considered relevant.
283-
// This assumes toolInfoFromDefaults.Extensions are relevant for all toolInfoFromDefaults.Languages.
284-
// A more precise mapping might be needed if a tool's extensions vary significantly per language it supports.
285-
relevantExtsForThisToolMap[defaultExt] = struct{}{}
286-
}
287-
}
288-
}
289-
290-
if isRelevantTool {
291-
relevantExtsForThisTool := config.GetSortedKeys(relevantExtsForThisToolMap)
292-
sort.Strings(relevantLangsForThisTool)
293-
294-
if existingEntry, ok := existingToolsMap[toolName]; ok {
295-
// Merge languages and extensions, keeping them unique and sorted
296-
existingLangsSet := make(map[string]struct{})
297-
for _, lang := range existingEntry.Languages {
298-
existingLangsSet[lang] = struct{}{}
299-
}
300-
for _, lang := range relevantLangsForThisTool {
301-
existingLangsSet[lang] = struct{}{}
302-
}
303-
existingEntry.Languages = config.GetSortedKeys(existingLangsSet)
304-
305-
existingExtsSet := make(map[string]struct{})
306-
for _, ext := range existingEntry.Extensions {
307-
existingExtsSet[ext] = struct{}{}
308-
}
309-
for _, ext := range relevantExtsForThisTool {
310-
existingExtsSet[ext] = struct{}{}
311-
}
312-
existingEntry.Extensions = config.GetSortedKeys(existingExtsSet)
313-
314-
} else {
315-
newEntry := domain.ToolLanguageInfo{
316-
Name: toolName,
317-
Languages: relevantLangsForThisTool,
318-
Extensions: relevantExtsForThisTool,
319-
}
320-
langConf.Tools = append(langConf.Tools, newEntry)
321-
existingToolsMap[toolName] = &langConf.Tools[len(langConf.Tools)-1] // update map with pointer to new entry
252+
// Add detected tools that are not already present
253+
for toolName := range detectedTools {
254+
if _, alreadyExists := existingTools[toolName]; !alreadyExists {
255+
if toolInfo, exists := defaultToolLangMap[toolName]; exists {
256+
langConf.Tools = append(langConf.Tools, toolInfo)
322257
}
323258
}
324259
}
@@ -338,8 +273,9 @@ func updateLanguagesConfig(detectedLanguages map[string]struct{}, toolsConfigDir
338273
return os.WriteFile(langConfigPath, data, utils.DefaultFilePerms)
339274
}
340275

341-
// updateCodacyYAML updates the codacy.yaml file with newly relevant tools.
342-
func updateCodacyYAML(detectedLanguages map[string]struct{}, codacyYAMLPath string, defaultToolLangMap map[string]domain.ToolLanguageInfo, initFlags domain.InitFlags, cliMode string) error {
276+
// updateCodacyYAMLForTools updates the codacy.yaml file with detected tools.
277+
func updateCodacyYAMLForTools(detectedTools map[string]struct{}, codacyYAMLPath string, initFlags domain.InitFlags, cliMode string) error {
278+
343279
var configData map[string]interface{}
344280

345281
// Read and parse codacy.yaml (validation is done globally in PersistentPreRun)
@@ -368,15 +304,7 @@ func updateCodacyYAML(detectedLanguages map[string]struct{}, codacyYAMLPath stri
368304
}
369305
}
370306

371-
candidateToolsToAdd := make(map[string]struct{}) // tool names like "eslint"
372-
for toolName, toolInfo := range defaultToolLangMap {
373-
for _, lang := range toolInfo.Languages {
374-
if _, detected := detectedLanguages[lang]; detected {
375-
candidateToolsToAdd[toolName] = struct{}{}
376-
break
377-
}
378-
}
379-
}
307+
candidateToolsToAdd := detectedTools
380308

381309
if cliMode == "remote" && initFlags.ApiToken != "" {
382310
fmt.Println("Cloud mode: Verifying tools against repository settings...")
@@ -411,9 +339,12 @@ func updateCodacyYAML(detectedLanguages map[string]struct{}, codacyYAMLPath stri
411339
}
412340

413341
defaultToolVersions := plugins.GetToolVersions()
414-
finalToolsList := currentToolsList // Start with existing tools
415342

343+
// Discover mode: preserve existing tools and add only new discovered ones
344+
finalToolsList := currentToolsList // Start with existing tools
416345
addedNewTool := false
346+
347+
// Add only newly discovered tools that aren't already configured
417348
for toolNameToAdd := range candidateToolsToAdd {
418349
if _, alreadyConfigured := currentToolSetByName[toolNameToAdd]; !alreadyConfigured {
419350
version, ok := defaultToolVersions[toolNameToAdd]
@@ -502,6 +433,7 @@ func updateCodacyYAML(detectedLanguages map[string]struct{}, codacyYAMLPath stri
502433
if err := os.MkdirAll(filepath.Dir(codacyYAMLPath), utils.DefaultDirPerms); err != nil {
503434
return fmt.Errorf("error creating .codacy directory: %w", err)
504435
}
436+
505437
return os.WriteFile(codacyYAMLPath, yamlData, utils.DefaultFilePerms)
506438
}
507439

0 commit comments

Comments
 (0)