Skip to content

Commit 474700c

Browse files
joke1196sonartech
authored andcommitted
SONARPY-3771: Define a fallback root when pyproject.toml is missing (#842)
GitOrigin-RevId: 6dd2cbefaaa9d103f441c3c2db5a8127f4ef2a85
1 parent de789a8 commit 474700c

2 files changed

Lines changed: 326 additions & 0 deletions

File tree

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python.indexer;
18+
19+
import java.io.File;
20+
import java.util.ArrayList;
21+
import java.util.Arrays;
22+
import java.util.List;
23+
import org.sonar.api.config.Configuration;
24+
25+
/**
26+
* Resolves package root directories for Python projects.
27+
*
28+
* <p>This class validates and resolves package roots extracted from build system configurations
29+
* (e.g., pyproject.toml) and provides fallback resolution when no roots are configured.
30+
*/
31+
public class PackageRootResolver {
32+
33+
static final String SONAR_SOURCES_KEY = "sonar.sources";
34+
static final List<String> CONVENTIONAL_FOLDERS = List.of("src", "lib");
35+
36+
private PackageRootResolver() {
37+
}
38+
39+
/**
40+
* Resolves package root directories.
41+
*
42+
* <p>If extracted roots from build system configuration are provided, resolves them to absolute paths.
43+
* Otherwise, applies a fallback chain to determine appropriate roots.
44+
*
45+
* @param extractedRoots roots extracted from build system config (e.g., from BuildSystemSourceRoots.extract())
46+
* @param config the Sonar configuration to read sonar.sources property
47+
* @param baseDir the project base directory
48+
* @return list of resolved package root absolute paths
49+
*/
50+
public static List<String> resolve(List<String> extractedRoots, Configuration config, File baseDir) {
51+
if (!extractedRoots.isEmpty()) {
52+
return toAbsolutePaths(extractedRoots, baseDir);
53+
}
54+
return resolveFallback(config, baseDir);
55+
}
56+
57+
/**
58+
* Resolves fallback package roots when no build system configuration is available.
59+
*
60+
* <p>Fallback priority:
61+
* <ol>
62+
* <li>sonar.sources property if set</li>
63+
* <li>"src" and/or "lib" folders if they exist</li>
64+
* <li>Project base directory absolute path as last resort</li>
65+
* </ol>
66+
*
67+
* @param config the Sonar configuration
68+
* @param baseDir the project base directory
69+
* @return list of fallback package root absolute paths
70+
*/
71+
static List<String> resolveFallback(Configuration config, File baseDir) {
72+
String[] sonarSources = config.getStringArray(SONAR_SOURCES_KEY);
73+
if (sonarSources.length > 0) {
74+
return toAbsolutePaths(Arrays.asList(sonarSources), baseDir);
75+
}
76+
77+
List<String> conventionalFolders = findConventionalFolders(baseDir);
78+
if (!conventionalFolders.isEmpty()) {
79+
return toAbsolutePaths(conventionalFolders, baseDir);
80+
}
81+
82+
return List.of(baseDir.getAbsolutePath());
83+
}
84+
85+
private static List<String> toAbsolutePaths(List<String> paths, File baseDir) {
86+
return paths.stream()
87+
.map(path -> new File(baseDir, path).getAbsolutePath())
88+
.toList();
89+
}
90+
91+
private static List<String> findConventionalFolders(File baseDir) {
92+
List<String> folders = new ArrayList<>();
93+
for (String folderName : CONVENTIONAL_FOLDERS) {
94+
File folder = new File(baseDir, folderName);
95+
if (folder.exists() && folder.isDirectory()) {
96+
folders.add(folderName);
97+
}
98+
}
99+
return folders;
100+
}
101+
}
102+
Lines changed: 224 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,224 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource Sàrl
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.plugins.python.indexer;
18+
19+
import java.io.File;
20+
import java.io.IOException;
21+
import java.nio.file.Files;
22+
import java.nio.file.Path;
23+
import java.util.List;
24+
import org.junit.jupiter.api.Test;
25+
import org.junit.jupiter.api.io.TempDir;
26+
import org.sonar.api.config.Configuration;
27+
28+
import static org.assertj.core.api.Assertions.assertThat;
29+
import static org.mockito.Mockito.mock;
30+
import static org.mockito.Mockito.when;
31+
32+
class PackageRootResolverTest {
33+
34+
@TempDir
35+
Path tempDir;
36+
37+
@Test
38+
void resolve_withExtractedRoots_returnsAbsolutePaths() {
39+
Configuration config = mock(Configuration.class);
40+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
41+
42+
File baseDir = tempDir.toFile();
43+
List<String> extractedRoots = List.of("src", "lib");
44+
List<String> result = PackageRootResolver.resolve(extractedRoots, config, baseDir);
45+
46+
assertThat(result).containsExactly(
47+
new File(baseDir, "src").getAbsolutePath(),
48+
new File(baseDir, "lib").getAbsolutePath());
49+
}
50+
51+
@Test
52+
void resolve_withExtractedRoots_ignoresSonarSources() {
53+
Configuration config = mock(Configuration.class);
54+
when(config.getStringArray("sonar.sources")).thenReturn(new String[]{"other"});
55+
56+
File baseDir = tempDir.toFile();
57+
List<String> extractedRoots = List.of("src");
58+
List<String> result = PackageRootResolver.resolve(extractedRoots, config, baseDir);
59+
60+
assertThat(result).containsExactly(new File(baseDir, "src").getAbsolutePath());
61+
}
62+
63+
@Test
64+
void resolve_emptyExtractedRoots_fallsBackToSonarSources() {
65+
Configuration config = mock(Configuration.class);
66+
when(config.getStringArray("sonar.sources")).thenReturn(new String[]{"sources", "lib"});
67+
68+
File baseDir = tempDir.toFile();
69+
List<String> result = PackageRootResolver.resolve(List.of(), config, baseDir);
70+
71+
assertThat(result).containsExactly(
72+
new File(baseDir, "sources").getAbsolutePath(),
73+
new File(baseDir, "lib").getAbsolutePath());
74+
}
75+
76+
@Test
77+
void resolve_emptyExtractedRootsAndNoSonarSources_fallsBackToSrcFolder() throws IOException {
78+
Configuration config = mock(Configuration.class);
79+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
80+
81+
File baseDir = tempDir.toFile();
82+
Files.createDirectory(tempDir.resolve("src"));
83+
84+
List<String> result = PackageRootResolver.resolve(List.of(), config, baseDir);
85+
86+
assertThat(result).containsExactly(new File(baseDir, "src").getAbsolutePath());
87+
}
88+
89+
@Test
90+
void resolve_emptyExtractedRootsAndNoSonarSources_fallsBackToLibFolder() throws IOException {
91+
Configuration config = mock(Configuration.class);
92+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
93+
94+
File baseDir = tempDir.toFile();
95+
Files.createDirectory(tempDir.resolve("lib"));
96+
97+
List<String> result = PackageRootResolver.resolve(List.of(), config, baseDir);
98+
99+
assertThat(result).containsExactly(new File(baseDir, "lib").getAbsolutePath());
100+
}
101+
102+
@Test
103+
void resolve_emptyExtractedRootsAndNoSonarSources_fallsBackToBothSrcAndLib() throws IOException {
104+
Configuration config = mock(Configuration.class);
105+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
106+
107+
File baseDir = tempDir.toFile();
108+
Files.createDirectory(tempDir.resolve("src"));
109+
Files.createDirectory(tempDir.resolve("lib"));
110+
111+
List<String> result = PackageRootResolver.resolve(List.of(), config, baseDir);
112+
113+
assertThat(result).containsExactly(
114+
new File(baseDir, "src").getAbsolutePath(),
115+
new File(baseDir, "lib").getAbsolutePath());
116+
}
117+
118+
@Test
119+
void resolve_emptyExtractedRootsNoSonarSourcesNoSrcFolder_fallsBackToBaseDirAbsolutePath() {
120+
Configuration config = mock(Configuration.class);
121+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
122+
123+
File baseDir = tempDir.toFile();
124+
List<String> result = PackageRootResolver.resolve(List.of(), config, baseDir);
125+
126+
assertThat(result).containsExactly(baseDir.getAbsolutePath());
127+
}
128+
129+
@Test
130+
void resolve_srcExistsAsFile_fallsBackToBaseDirAbsolutePath() throws IOException {
131+
Configuration config = mock(Configuration.class);
132+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
133+
134+
Files.createFile(tempDir.resolve("src"));
135+
136+
File baseDir = tempDir.toFile();
137+
List<String> result = PackageRootResolver.resolve(List.of(), config, baseDir);
138+
139+
assertThat(result).containsExactly(baseDir.getAbsolutePath());
140+
}
141+
142+
// Tests for resolveFallback method directly
143+
144+
@Test
145+
void resolveFallback_withSonarSources_returnsAbsolutePaths() {
146+
Configuration config = mock(Configuration.class);
147+
when(config.getStringArray("sonar.sources")).thenReturn(new String[]{"app", "core"});
148+
149+
File baseDir = tempDir.toFile();
150+
List<String> result = PackageRootResolver.resolveFallback(config, baseDir);
151+
152+
assertThat(result).containsExactly(
153+
new File(baseDir, "app").getAbsolutePath(),
154+
new File(baseDir, "core").getAbsolutePath());
155+
}
156+
157+
@Test
158+
void resolveFallback_noSonarSourcesButSrcExists_returnsAbsolutePath() throws IOException {
159+
Configuration config = mock(Configuration.class);
160+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
161+
162+
File baseDir = tempDir.toFile();
163+
Files.createDirectory(tempDir.resolve("src"));
164+
165+
List<String> result = PackageRootResolver.resolveFallback(config, baseDir);
166+
167+
assertThat(result).containsExactly(new File(baseDir, "src").getAbsolutePath());
168+
}
169+
170+
@Test
171+
void resolveFallback_noSonarSourcesButLibExists_returnsAbsolutePath() throws IOException {
172+
Configuration config = mock(Configuration.class);
173+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
174+
175+
File baseDir = tempDir.toFile();
176+
Files.createDirectory(tempDir.resolve("lib"));
177+
178+
List<String> result = PackageRootResolver.resolveFallback(config, baseDir);
179+
180+
assertThat(result).containsExactly(new File(baseDir, "lib").getAbsolutePath());
181+
}
182+
183+
@Test
184+
void resolveFallback_noSonarSourcesBothSrcAndLibExist_returnsAbsolutePaths() throws IOException {
185+
Configuration config = mock(Configuration.class);
186+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
187+
188+
File baseDir = tempDir.toFile();
189+
Files.createDirectory(tempDir.resolve("src"));
190+
Files.createDirectory(tempDir.resolve("lib"));
191+
192+
List<String> result = PackageRootResolver.resolveFallback(config, baseDir);
193+
194+
assertThat(result).containsExactly(
195+
new File(baseDir, "src").getAbsolutePath(),
196+
new File(baseDir, "lib").getAbsolutePath());
197+
}
198+
199+
@Test
200+
void resolveFallback_noSonarSourcesNoSrcNoLib_returnsBaseDirAbsolutePath() {
201+
Configuration config = mock(Configuration.class);
202+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
203+
204+
File baseDir = tempDir.toFile();
205+
List<String> result = PackageRootResolver.resolveFallback(config, baseDir);
206+
207+
assertThat(result).containsExactly(baseDir.getAbsolutePath());
208+
}
209+
210+
@Test
211+
void resolveFallback_srcAndLibExistAsFiles_returnsBaseDirAbsolutePath() throws IOException {
212+
Configuration config = mock(Configuration.class);
213+
when(config.getStringArray("sonar.sources")).thenReturn(new String[0]);
214+
215+
Files.createFile(tempDir.resolve("src"));
216+
Files.createFile(tempDir.resolve("lib"));
217+
218+
File baseDir = tempDir.toFile();
219+
List<String> result = PackageRootResolver.resolveFallback(config, baseDir);
220+
221+
assertThat(result).containsExactly(baseDir.getAbsolutePath());
222+
}
223+
}
224+

0 commit comments

Comments
 (0)