Skip to content

Commit f97bac3

Browse files
aschemanclaude
andauthored
Unify source tracking with SourceHandlingContext (#11632)
- Replace boolean flags (hasMain, hasTest, etc.) with flexible set-based tracking for all language/scope combinations. - Rename ResourceHandlingContext to SourceHandlingContext - Add duplicate detection with WARNING for enabled sources - Add hasSources() method for checking language/scope combinations - Rename 'src' variable to 'sourceRoot' for clarity - Replace Collectors.toList() with .toList() in DefaultProjectBuilder - Use Java 17+ Stream.toList() instead of Collectors.toList(). - Fix Windows path separator issue in ProjectBuilderTest - Implement validation rules for modular source handling (#11612): - AC6: ERROR when mixing modular (with module) and classic (without module) sources within <sources> - AC7: WARNING when legacy <sourceDirectory>/<testSourceDirectory> are used in modular projects (both explicit config and filesystem existence) - Add validateNoMixedModularAndClassicSources() to SourceHandlingContext - Add warnIfExplicitLegacyDirectory() to DefaultProjectBuilder - Update tests to verify AC6 and AC7 behavior - Update test project comments to reflect correct behavior Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 37479cf commit f97bac3

File tree

7 files changed

+719
-68
lines changed

7 files changed

+719
-68
lines changed

impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java

Lines changed: 103 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -27,6 +27,7 @@
2727
import java.io.IOException;
2828
import java.io.InputStream;
2929
import java.nio.charset.StandardCharsets;
30+
import java.nio.file.Files;
3031
import java.nio.file.Path;
3132
import java.util.AbstractMap;
3233
import java.util.ArrayList;
@@ -525,7 +526,7 @@ List<ProjectBuildingResult> doBuild(List<File> pomFiles, boolean recursive) {
525526
return pomFiles.stream()
526527
.map(pomFile -> build(pomFile, recursive))
527528
.flatMap(List::stream)
528-
.collect(Collectors.toList());
529+
.toList();
529530
} finally {
530531
Thread.currentThread().setContextClassLoader(oldContextClassLoader);
531532
}
@@ -571,7 +572,7 @@ private List<ProjectBuildingResult> build(File pomFile, boolean recursive) {
571572
project.setCollectedProjects(results(r)
572573
.filter(cr -> cr != r && cr.getEffectiveModel() != null)
573574
.map(cr -> projectIndex.get(cr.getEffectiveModel().getId()))
574-
.collect(Collectors.toList()));
575+
.toList());
575576

576577
DependencyResolutionResult resolutionResult = null;
577578
if (request.isResolveDependencies()) {
@@ -665,65 +666,75 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
665666
return build.getDirectory();
666667
}
667668
};
668-
boolean hasScript = false;
669-
boolean hasMain = false;
670-
boolean hasTest = false;
671-
boolean hasMainResources = false;
672-
boolean hasTestResources = false;
669+
// Extract modules from sources to detect modular projects
670+
Set<String> modules = extractModules(sources);
671+
boolean isModularProject = !modules.isEmpty();
672+
673+
logger.trace(
674+
"Module detection for project {}: found {} module(s) {} - modular project: {}.",
675+
project.getId(),
676+
modules.size(),
677+
modules,
678+
isModularProject);
679+
680+
// Create source handling context for unified tracking of all lang/scope combinations
681+
SourceHandlingContext sourceContext =
682+
new SourceHandlingContext(project, baseDir, modules, isModularProject, result);
683+
684+
// Process all sources, tracking enabled ones and detecting duplicates
673685
for (var source : sources) {
674-
var src = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source);
675-
project.addSourceRoot(src);
676-
Language language = src.language();
677-
if (Language.JAVA_FAMILY.equals(language)) {
678-
ProjectScope scope = src.scope();
679-
if (ProjectScope.MAIN.equals(scope)) {
680-
hasMain = true;
681-
} else {
682-
hasTest |= ProjectScope.TEST.equals(scope);
683-
}
684-
} else if (Language.RESOURCES.equals(language)) {
685-
ProjectScope scope = src.scope();
686-
if (ProjectScope.MAIN.equals(scope)) {
687-
hasMainResources = true;
688-
} else if (ProjectScope.TEST.equals(scope)) {
689-
hasTestResources = true;
690-
}
691-
} else {
692-
hasScript |= Language.SCRIPT.equals(language);
686+
var sourceRoot = DefaultSourceRoot.fromModel(session, baseDir, outputDirectory, source);
687+
// Track enabled sources for duplicate detection and hasSources() queries
688+
// Only add source if it's not a duplicate enabled source (first enabled wins)
689+
if (sourceContext.shouldAddSource(sourceRoot)) {
690+
project.addSourceRoot(sourceRoot);
693691
}
694692
}
693+
695694
/*
696695
* `sourceDirectory`, `testSourceDirectory` and `scriptSourceDirectory`
697-
* are ignored if the POM file contains at least one <source> element
696+
* are ignored if the POM file contains at least one enabled <source> element
698697
* for the corresponding scope and language. This rule exists because
699698
* Maven provides default values for those elements which may conflict
700699
* with user's configuration.
700+
*
701+
* Additionally, for modular projects, legacy directories are unconditionally
702+
* ignored because it is not clear how to dispatch their content between
703+
* different modules. A warning is emitted if these properties are explicitly set.
701704
*/
702-
if (!hasScript) {
705+
if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) {
703706
project.addScriptSourceRoot(build.getScriptSourceDirectory());
704707
}
705-
if (!hasMain) {
706-
project.addCompileSourceRoot(build.getSourceDirectory());
707-
}
708-
if (!hasTest) {
709-
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
708+
if (isModularProject) {
709+
// Modular projects: unconditionally ignore legacy directories, warn if explicitly set
710+
warnIfExplicitLegacyDirectory(
711+
build.getSourceDirectory(),
712+
baseDir.resolve("src/main/java"),
713+
"<sourceDirectory>",
714+
project.getId(),
715+
result);
716+
warnIfExplicitLegacyDirectory(
717+
build.getTestSourceDirectory(),
718+
baseDir.resolve("src/test/java"),
719+
"<testSourceDirectory>",
720+
project.getId(),
721+
result);
722+
} else {
723+
// Classic projects: use legacy directories if no sources defined in <sources>
724+
if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.MAIN)) {
725+
project.addCompileSourceRoot(build.getSourceDirectory());
726+
}
727+
if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.TEST)) {
728+
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
729+
}
710730
}
711-
// Extract modules from sources to detect modular projects
712-
Set<String> modules = extractModules(sources);
713-
boolean isModularProject = !modules.isEmpty();
714731

715-
logger.trace(
716-
"Module detection for project {}: found {} module(s) {} - modular project: {}.",
717-
project.getId(),
718-
modules.size(),
719-
modules,
720-
isModularProject);
732+
// Validate that modular and classic sources are not mixed within <sources>
733+
sourceContext.validateNoMixedModularAndClassicSources();
721734

722-
// Handle main and test resources
723-
ResourceHandlingContext resourceContext =
724-
new ResourceHandlingContext(project, baseDir, modules, isModularProject, result);
725-
resourceContext.handleResourceConfiguration(ProjectScope.MAIN, hasMainResources);
726-
resourceContext.handleResourceConfiguration(ProjectScope.TEST, hasTestResources);
735+
// Handle main and test resources using unified source handling
736+
sourceContext.handleResourceConfiguration(ProjectScope.MAIN);
737+
sourceContext.handleResourceConfiguration(ProjectScope.TEST);
727738
}
728739

729740
project.setActiveProfiles(
@@ -894,6 +905,49 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
894905
project.setRemoteArtifactRepositories(remoteRepositories);
895906
}
896907

908+
/**
909+
* Warns about legacy directory usage in a modular project. Two cases are handled:
910+
* <ul>
911+
* <li>Case 1: The default legacy directory exists on the filesystem (e.g., src/main/java exists)</li>
912+
* <li>Case 2: An explicit legacy directory is configured that differs from the default</li>
913+
* </ul>
914+
* Legacy directories are unconditionally ignored in modular projects because it is not clear
915+
* how to dispatch their content between different modules.
916+
*/
917+
private void warnIfExplicitLegacyDirectory(
918+
String configuredDir,
919+
Path defaultDir,
920+
String elementName,
921+
String projectId,
922+
ModelBuilderResult result) {
923+
if (configuredDir != null) {
924+
Path configuredPath = Path.of(configuredDir).toAbsolutePath().normalize();
925+
Path defaultPath = defaultDir.toAbsolutePath().normalize();
926+
if (!configuredPath.equals(defaultPath)) {
927+
// Case 2: Explicit configuration differs from default - always warn
928+
String message = String.format(
929+
"Legacy %s is ignored in modular project %s. "
930+
+ "In modular projects, source directories must be defined via <sources> "
931+
+ "with a module element for each module.",
932+
elementName, projectId);
933+
logger.warn(message);
934+
result.getProblemCollector()
935+
.reportProblem(new org.apache.maven.impl.model.DefaultModelProblem(
936+
message, Severity.WARNING, Version.V41, null, -1, -1, null));
937+
} else if (Files.isDirectory(defaultPath)) {
938+
// Case 1: Default configuration, but the default directory exists on filesystem
939+
String message = String.format(
940+
"Legacy %s '%s' exists but is ignored in modular project %s. "
941+
+ "In modular projects, source directories must be defined via <sources>.",
942+
elementName, defaultPath, projectId);
943+
logger.warn(message);
944+
result.getProblemCollector()
945+
.reportProblem(new org.apache.maven.impl.model.DefaultModelProblem(
946+
message, Severity.WARNING, Version.V41, null, -1, -1, null));
947+
}
948+
}
949+
}
950+
897951
private void initParent(MavenProject project, ModelBuilderResult result) {
898952
Model parentModel = result.getParentModel();
899953

@@ -1035,8 +1089,8 @@ private DependencyResolutionResult resolveDependencies(MavenProject project) {
10351089
}
10361090
}
10371091

1038-
private List<String> getProfileIds(List<Profile> profiles) {
1039-
return profiles.stream().map(Profile::getId).collect(Collectors.toList());
1092+
private static List<String> getProfileIds(List<Profile> profiles) {
1093+
return profiles.stream().map(Profile::getId).toList();
10401094
}
10411095

10421096
private static ModelSource createStubModelSource(Artifact artifact) {

0 commit comments

Comments
 (0)