diff --git a/utils/techutils/techutils.go b/utils/techutils/techutils.go index cb537e60..56d6a53f 100644 --- a/utils/techutils/techutils.go +++ b/utils/techutils/techutils.go @@ -41,6 +41,8 @@ const ( ) const Pypi = "pypi" +var AllTechnologiesStrings = []string{Maven.String(), Gradle.String(), Npm.String(), Pnpm.String(), Yarn.String(), Go.String(), Pip.String(), Pipenv.String(), Poetry.String(), Nuget.String(), Dotnet.String(), Docker.String(), Oci.String(), Conan.String()} + type CodeLanguage string const ( @@ -400,13 +402,19 @@ func isExclude(path string, techData TechData) bool { func mapWorkingDirectoriesToTechnologies(workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology, requestedTechs []Technology, requestedDescriptors map[Technology][]string) (technologiesDetected map[Technology]map[string][]string, err error) { // Get the relevant technologies to check technologies := requestedTechs + var techProvidedByUser bool if len(technologies) == 0 { technologies = GetAllTechnologiesList() + } else { + // If the project's technology was provided by the user, and isn't detected by us, we want to enable capturing the technology by its descriptor as well as by its indicators. + // In case we execute our auto-detection we want to avoid that since it may lead to collisions between package managers with the same descriptor (like Npm and Yarn) + techProvidedByUser = true + log.Debug(fmt.Sprintf("Technologies were identified either from the command flags supplied by the user or inferred from the provided installation command. Detected technologies: %s.", technologies)) } technologiesDetected = make(map[Technology]map[string][]string) // Map working directories to technologies for _, tech := range technologies { - if techWorkingDirs, e := getTechInformationFromWorkingDir(tech, workingDirectoryToIndicators, excludedTechAtWorkingDir, requestedDescriptors); e != nil { + if techWorkingDirs, e := getTechInformationFromWorkingDir(tech, workingDirectoryToIndicators, excludedTechAtWorkingDir, requestedDescriptors, techProvidedByUser); e != nil { err = errors.Join(err, fmt.Errorf("failed to get information from working directory for %s", tech)) } else if len(techWorkingDirs) > 0 { // Found indicators of the technology, add to detected. @@ -423,11 +431,12 @@ func mapWorkingDirectoriesToTechnologies(workingDirectoryToIndicators map[string return } -func getTechInformationFromWorkingDir(tech Technology, workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology, requestedDescriptors map[Technology][]string) (techWorkingDirs map[string][]string, err error) { +func getTechInformationFromWorkingDir(tech Technology, workingDirectoryToIndicators map[string][]string, excludedTechAtWorkingDir map[string][]Technology, requestedDescriptors map[Technology][]string, techProvidedByUser bool) (techWorkingDirs map[string][]string, err error) { techWorkingDirs = make(map[string][]string) for wd, indicators := range workingDirectoryToIndicators { descriptorsAtWd := []string{} foundIndicator := false + foundDescriptor := false if isTechExcludedInWorkingDir(tech, wd, excludedTechAtWorkingDir) { // Exclude this technology from this working directory continue @@ -436,6 +445,7 @@ func getTechInformationFromWorkingDir(tech Technology, workingDirectoryToIndicat for _, path := range indicators { if tech.isDescriptor(path) || isRequestedDescriptor(path, requestedDescriptors[tech]) { descriptorsAtWd = append(descriptorsAtWd, path) + foundDescriptor = true } if indicator, e := tech.isIndicator(path); e != nil { err = errors.Join(err, fmt.Errorf("failed to check if %s is an indicator of %s: %w", path, tech, e)) @@ -444,8 +454,9 @@ func getTechInformationFromWorkingDir(tech Technology, workingDirectoryToIndicat foundIndicator = true } } - if foundIndicator { - // Found indicators of the technology in the current working directory, add to detected. + if foundIndicator || (foundDescriptor && techProvidedByUser) { + // If indicators of the technology were found in the current working directory, add to detected. + // If descriptors were found for a specific tech that was provided by the user, we add the descriptor to detected. techWorkingDirs[wd] = descriptorsAtWd } } diff --git a/utils/techutils/techutils_test.go b/utils/techutils/techutils_test.go index 18be1100..0230e952 100644 --- a/utils/techutils/techutils_test.go +++ b/utils/techutils/techutils_test.go @@ -152,8 +152,7 @@ func TestMapWorkingDirectoriesToTechnologies(t *testing.T) { excludedTechAtWorkingDir map[string][]Technology requestedTechs []Technology requestedDescriptors map[Technology][]string - - expected map[Technology]map[string][]string + expected map[Technology]map[string][]string }{ { name: "noTechTest", @@ -210,6 +209,20 @@ func TestMapWorkingDirectoriesToTechnologies(t *testing.T) { Dotnet: {"dir": {filepath.Join("dir", "project.sln"), filepath.Join("dir", "sub1", "project.csproj")}}, }, }, + { + // When tech is requested by user we detect technology by indicator as well as by descriptors, therefore we can relate descriptor files to tech even when indicator doesn't exist + name: "tech requested by user test", + workingDirectoryToIndicators: map[string][]string{ + "dir3": {filepath.Join("dir3", "package.json")}, + projectDir: {filepath.Join(projectDir, "pyproject.toml")}, + }, + requestedTechs: []Technology{Yarn, Poetry}, + requestedDescriptors: noRequestSpecialDescriptors, + expected: map[Technology]map[string][]string{ + Yarn: {"dir3": {filepath.Join("dir3", "package.json")}}, + Poetry: {projectDir: {filepath.Join(projectDir, "pyproject.toml")}}, + }, + }, } for _, test := range tests { @@ -385,6 +398,7 @@ func TestGetTechInformationFromWorkingDir(t *testing.T) { "dir": {filepath.Join("dir", "package.json"), filepath.Join("dir", "package-lock.json"), filepath.Join("dir", "build.gradle.kts"), filepath.Join("dir", "project.sln"), filepath.Join("dir", "blabla.txt")}, "directory": {filepath.Join("directory", "npm-shrinkwrap.json")}, "dir3": {filepath.Join("dir3", "package.json"), filepath.Join("dir3", ".yarn")}, + "dir4": {filepath.Join("dir4", "package.json")}, projectDir: {filepath.Join(projectDir, "pyproject.toml")}, filepath.Join("dir3", "dir"): {filepath.Join("dir3", "dir", "package.json"), filepath.Join("dir3", "dir", "pnpm-lock.yaml")}, filepath.Join("dir", "dir2"): {filepath.Join("dir", "dir2", "go.mod")}, @@ -403,12 +417,14 @@ func TestGetTechInformationFromWorkingDir(t *testing.T) { name string tech Technology requestedDescriptors map[Technology][]string + techProvidedByUser bool expected map[string][]string }{ { name: "mavenTest", tech: Maven, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{ "folder": { filepath.Join("folder", "pom.xml"), @@ -421,8 +437,10 @@ func TestGetTechInformationFromWorkingDir(t *testing.T) { name: "npmTest", tech: Npm, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{ "dir": {filepath.Join("dir", "package.json")}, + "dir4": {filepath.Join("dir4", "package.json")}, "directory": {}, }, }, @@ -430,24 +448,28 @@ func TestGetTechInformationFromWorkingDir(t *testing.T) { name: "pnpmTest", tech: Pnpm, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{filepath.Join("dir3", "dir"): {filepath.Join("dir3", "dir", "package.json")}}, }, { name: "yarnTest", tech: Yarn, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{"dir3": {filepath.Join("dir3", "package.json")}}, }, { name: "golangTest", tech: Go, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{filepath.Join("dir", "dir2"): {filepath.Join("dir", "dir2", "go.mod")}}, }, { name: "pipTest", tech: Pip, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{ filepath.Join("users_dir", "test", "package"): {filepath.Join("users_dir", "test", "package", "setup.py")}, filepath.Join("users_dir", "test", "package2"): {filepath.Join("users_dir", "test", "package2", "requirements.txt")}, @@ -458,6 +480,7 @@ func TestGetTechInformationFromWorkingDir(t *testing.T) { name: "pipRequestedDescriptorTest", tech: Pip, requestedDescriptors: map[Technology][]string{Pip: {"blabla.txt"}}, + techProvidedByUser: false, expected: map[string][]string{ "dir": {filepath.Join("dir", "blabla.txt")}, filepath.Join("users_dir", "test", "package"): {filepath.Join("users_dir", "test", "package", "setup.py")}, @@ -469,12 +492,14 @@ func TestGetTechInformationFromWorkingDir(t *testing.T) { name: "pipenvTest", tech: Pipenv, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{filepath.Join("users", "test", "package"): {filepath.Join("users", "test", "package", "Pipfile")}}, }, { name: "gradleTest", tech: Gradle, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{ filepath.Join("users", "test", "package"): {filepath.Join("users", "test", "package", "build.gradle")}, "dir": {filepath.Join("dir", "build.gradle.kts")}, @@ -484,19 +509,33 @@ func TestGetTechInformationFromWorkingDir(t *testing.T) { name: "nugetTest", tech: Nuget, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{"dir": {filepath.Join("dir", "project.sln"), filepath.Join("dir", "sub1", "project.csproj")}}, }, { name: "dotnetTest", tech: Dotnet, requestedDescriptors: map[Technology][]string{}, + techProvidedByUser: false, expected: map[string][]string{"dir": {filepath.Join("dir", "project.sln"), filepath.Join("dir", "sub1", "project.csproj")}}, }, + // When tech is provided by the user we detect technology by indicator and descriptors and not just by indicator. Test cases are provided only for technologies that might experience conflicts. + { + name: "yarnTestWithProvidedTechFromUser", + tech: Yarn, + requestedDescriptors: make(map[Technology][]string), + techProvidedByUser: true, + expected: map[string][]string{ + "dir": {filepath.Join("dir", "package.json")}, + "dir3": {filepath.Join("dir3", "package.json")}, + "dir4": {filepath.Join("dir4", "package.json")}, + }, + }, } for _, test := range tests { t.Run(test.name, func(t *testing.T) { - techInformation, err := getTechInformationFromWorkingDir(test.tech, workingDirectoryToIndicators, excludedTechAtWorkingDir, test.requestedDescriptors) + techInformation, err := getTechInformationFromWorkingDir(test.tech, workingDirectoryToIndicators, excludedTechAtWorkingDir, test.requestedDescriptors, test.techProvidedByUser) assert.NoError(t, err) expectedKeys := maps.Keys(test.expected) actualKeys := maps.Keys(techInformation)