Skip to content

Commit 71bd10f

Browse files
aschemanclaude
andauthored
Fail on legacy config in modular projects (apache#11702)
In modular projects, legacy directories and resources that would be silently ignored now trigger an ERROR instead of WARNING: - Explicit <sourceDirectory>/<testSourceDirectory> differing from defaults - Default src/main/java or src/test/java existing on filesystem - Explicit <resources>/<testResources> differing from Super POM defaults This prevents silent loss of user-configured sources/resources. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
1 parent 1847bb4 commit 71bd10f

File tree

15 files changed

+746
-174
lines changed

15 files changed

+746
-174
lines changed

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

Lines changed: 130 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -692,49 +692,105 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
692692
}
693693

694694
/*
695-
* `sourceDirectory`, `testSourceDirectory` and `scriptSourceDirectory`
696-
* are ignored if the POM file contains at least one enabled <source> element
697-
* for the corresponding scope and language. This rule exists because
698-
* Maven provides default values for those elements which may conflict
699-
* 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.
704-
*/
705-
if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) {
695+
Source directory handling depends on project type and <sources> configuration:
696+
697+
1. CLASSIC projects (no <sources>):
698+
- All legacy directories are used
699+
700+
2. MODULAR projects (have <module> in <sources>):
701+
- ALL legacy directories cause the build to fail (cannot dispatch
702+
between modules)
703+
- The build also fails if default directories (src/main/java)
704+
physically exist on the filesystem
705+
706+
3. NON-MODULAR projects with <sources>:
707+
- Explicit legacy directories (differ from default) always cause
708+
the build to fail
709+
- Legacy directories for scopes where <sources> defines Java are ignored
710+
- Legacy directories for scopes where <sources> has no Java serve as
711+
implicit fallback (only if they match the default, e.g., inherited)
712+
- This allows incremental adoption (e.g., custom resources + default Java)
713+
*/
714+
if (sources.isEmpty()) {
715+
// Classic fallback: no <sources> configured, use legacy directories
706716
project.addScriptSourceRoot(build.getScriptSourceDirectory());
707-
}
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);
717+
project.addCompileSourceRoot(build.getSourceDirectory());
718+
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
719+
// Handle resources using legacy configuration
720+
sourceContext.handleResourceConfiguration(ProjectScope.MAIN);
721+
sourceContext.handleResourceConfiguration(ProjectScope.TEST);
722722
} 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());
723+
// Add script source root if no <sources lang="script"> configured
724+
if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) {
725+
project.addScriptSourceRoot(build.getScriptSourceDirectory());
726726
}
727-
if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.TEST)) {
728-
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
727+
728+
if (isModularProject) {
729+
// Modular: reject ALL legacy directory configurations
730+
failIfLegacyDirectoryPresent(
731+
build.getSourceDirectory(),
732+
baseDir.resolve("src/main/java"),
733+
"<sourceDirectory>",
734+
project.getId(),
735+
result,
736+
true); // check physical presence
737+
failIfLegacyDirectoryPresent(
738+
build.getTestSourceDirectory(),
739+
baseDir.resolve("src/test/java"),
740+
"<testSourceDirectory>",
741+
project.getId(),
742+
result,
743+
true); // check physical presence
744+
} else {
745+
// Non-modular: always validate legacy directories (error if differs from default)
746+
Path mainDefault = baseDir.resolve("src/main/java");
747+
Path testDefault = baseDir.resolve("src/test/java");
748+
749+
failIfLegacyDirectoryPresent(
750+
build.getSourceDirectory(),
751+
mainDefault,
752+
"<sourceDirectory>",
753+
project.getId(),
754+
result,
755+
false); // no physical presence check
756+
failIfLegacyDirectoryPresent(
757+
build.getTestSourceDirectory(),
758+
testDefault,
759+
"<testSourceDirectory>",
760+
project.getId(),
761+
result,
762+
false); // no physical presence check
763+
764+
// Use legacy as fallback only if:
765+
// 1. <sources> doesn't have Java for this scope
766+
// 2. Legacy matches default (otherwise error was reported above)
767+
if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.MAIN)) {
768+
Path configuredMain = Path.of(build.getSourceDirectory())
769+
.toAbsolutePath()
770+
.normalize();
771+
if (configuredMain.equals(
772+
mainDefault.toAbsolutePath().normalize())) {
773+
project.addCompileSourceRoot(build.getSourceDirectory());
774+
}
775+
}
776+
if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.TEST)) {
777+
Path configuredTest = Path.of(build.getTestSourceDirectory())
778+
.toAbsolutePath()
779+
.normalize();
780+
if (configuredTest.equals(
781+
testDefault.toAbsolutePath().normalize())) {
782+
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
783+
}
784+
}
729785
}
730-
}
731786

732-
// Validate that modular and classic sources are not mixed within <sources>
733-
sourceContext.validateNoMixedModularAndClassicSources();
787+
// Fail if modular and classic sources are mixed within <sources>
788+
sourceContext.failIfMixedModularAndClassicSources();
734789

735-
// Handle main and test resources using unified source handling
736-
sourceContext.handleResourceConfiguration(ProjectScope.MAIN);
737-
sourceContext.handleResourceConfiguration(ProjectScope.TEST);
790+
// Handle main and test resources using unified source handling
791+
sourceContext.handleResourceConfiguration(ProjectScope.MAIN);
792+
sourceContext.handleResourceConfiguration(ProjectScope.TEST);
793+
}
738794
}
739795

740796
project.setActiveProfiles(
@@ -906,44 +962,60 @@ private void initProject(MavenProject project, ModelBuilderResult result) {
906962
}
907963

908964
/**
909-
* Warns about legacy directory usage in a modular project. Two cases are handled:
965+
* Validates that legacy directory configuration does not conflict with {@code <sources>}.
966+
* <p>
967+
* When {@code <sources>} is configured, the build fails if:
910968
* <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>
969+
* <li><strong>Configuration presence</strong>: an explicit legacy configuration differs from the default</li>
970+
* <li><strong>Physical presence</strong>: the default directory exists on the filesystem (only checked
971+
* when {@code checkPhysicalPresence} is true, typically for modular projects where
972+
* {@code <source>} elements use different paths like {@code src/<module>/main/java})</li>
913973
* </ul>
914-
* Legacy directories are unconditionally ignored in modular projects because it is not clear
915-
* how to dispatch their content between different modules.
974+
* <p>
975+
* The presence of {@code <sources>} is the trigger for this validation, not whether the
976+
* project is modular or non-modular.
977+
* <p>
978+
* This ensures consistency with resource handling.
979+
*
980+
* @param configuredDir the configured legacy directory value
981+
* @param defaultDir the default legacy directory path
982+
* @param elementName the XML element name for error messages
983+
* @param projectId the project ID for error messages
984+
* @param result the model builder result for reporting problems
985+
* @param checkPhysicalPresence whether to check for physical presence of the default directory
986+
* @see SourceHandlingContext#handleResourceConfiguration(ProjectScope)
916987
*/
917-
private void warnIfExplicitLegacyDirectory(
988+
private void failIfLegacyDirectoryPresent(
918989
String configuredDir,
919990
Path defaultDir,
920991
String elementName,
921992
String projectId,
922-
ModelBuilderResult result) {
993+
ModelBuilderResult result,
994+
boolean checkPhysicalPresence) {
923995
if (configuredDir != null) {
924996
Path configuredPath = Path.of(configuredDir).toAbsolutePath().normalize();
925997
Path defaultPath = defaultDir.toAbsolutePath().normalize();
926998
if (!configuredPath.equals(defaultPath)) {
927-
// Case 2: Explicit configuration differs from default - always warn
999+
// Configuration presence: explicit config differs from default
9281000
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);
1001+
"Legacy %s cannot be used in project %s because sources are configured via <sources>. "
1002+
+ "Remove the %s configuration.",
1003+
elementName, projectId, elementName);
1004+
logger.error(message);
9341005
result.getProblemCollector()
9351006
.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
1007+
message, Severity.ERROR, Version.V41, null, -1, -1, null));
1008+
} else if (checkPhysicalPresence && Files.isDirectory(defaultPath)) {
1009+
// Physical presence: default directory exists but would be ignored
9391010
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);
1011+
"Legacy directory '%s' exists but cannot be used in project %s "
1012+
+ "because sources are configured via <sources>. "
1013+
+ "Remove or rename the directory.",
1014+
defaultPath, projectId);
1015+
logger.error(message);
9441016
result.getProblemCollector()
9451017
.reportProblem(new org.apache.maven.impl.model.DefaultModelProblem(
946-
message, Severity.WARNING, Version.V41, null, -1, -1, null));
1018+
message, Severity.ERROR, Version.V41, null, -1, -1, null));
9471019
}
9481020
}
9491021
}

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

Lines changed: 34 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -152,7 +152,7 @@ boolean hasSources(Language language, ProjectScope scope) {
152152
}
153153

154154
/**
155-
* Validates that a project does not mix modular and classic (non-modular) sources.
155+
* Fails the build if modular and classic (non-modular) sources are mixed within {@code <sources>}.
156156
* <p>
157157
* A project must be either fully modular (all sources have a module) or fully classic
158158
* (no sources have a module). Mixing modular and non-modular sources within the same
@@ -161,7 +161,7 @@ boolean hasSources(Language language, ProjectScope scope) {
161161
* This validation checks each (language, scope) combination and reports an ERROR if
162162
* both modular and non-modular sources are found.
163163
*/
164-
void validateNoMixedModularAndClassicSources() {
164+
void failIfMixedModularAndClassicSources() {
165165
for (ProjectScope scope : List.of(ProjectScope.MAIN, ProjectScope.TEST)) {
166166
for (Language language : List.of(Language.JAVA_FAMILY, Language.RESOURCES)) {
167167
boolean hasModular = declaredSources.stream()
@@ -200,6 +200,8 @@ void validateNoMixedModularAndClassicSources() {
200200
* <li>Modular project: use resources from {@code <sources>} if present, otherwise inject defaults</li>
201201
* <li>Classic project: use resources from {@code <sources>} if present, otherwise use legacy resources</li>
202202
* </ol>
203+
* <p>
204+
* The error behavior for conflicting legacy configuration is consistent with source directory handling.
203205
*
204206
* @param scope the project scope (MAIN or TEST)
205207
*/
@@ -221,11 +223,19 @@ void handleResourceConfiguration(ProjectScope scope) {
221223
if (hasResourcesInSources) {
222224
// Modular project with resources configured via <sources> - already added above
223225
if (hasExplicitLegacyResources(resources, scopeId)) {
224-
LOGGER.warn(
225-
"Legacy {} element is ignored because {} resources are configured via {} in <sources>.",
226-
legacyElement,
227-
scopeId,
228-
sourcesConfig);
226+
String message = String.format(
227+
"Legacy %s element cannot be used because %s resources are configured via %s in <sources>.",
228+
legacyElement, scopeId, sourcesConfig);
229+
LOGGER.error(message);
230+
result.getProblemCollector()
231+
.reportProblem(new DefaultModelProblem(
232+
message,
233+
Severity.ERROR,
234+
Version.V41,
235+
project.getModel().getDelegate(),
236+
-1,
237+
-1,
238+
null));
229239
} else {
230240
LOGGER.debug(
231241
"{} resources configured via <sources> element, ignoring legacy {} element.",
@@ -236,13 +246,13 @@ void handleResourceConfiguration(ProjectScope scope) {
236246
// Modular project without resources in <sources> - inject module-aware defaults
237247
if (hasExplicitLegacyResources(resources, scopeId)) {
238248
String message = "Legacy " + legacyElement
239-
+ " element is ignored because modular sources are configured. "
249+
+ " element cannot be used because modular sources are configured. "
240250
+ "Use " + sourcesConfig + " in <sources> for custom resource paths.";
241-
LOGGER.warn(message);
251+
LOGGER.error(message);
242252
result.getProblemCollector()
243253
.reportProblem(new DefaultModelProblem(
244254
message,
245-
Severity.WARNING,
255+
Severity.ERROR,
246256
Version.V41,
247257
project.getModel().getDelegate(),
248258
-1,
@@ -265,11 +275,19 @@ void handleResourceConfiguration(ProjectScope scope) {
265275
if (hasResourcesInSources) {
266276
// Resources configured via <sources> - already added above
267277
if (hasExplicitLegacyResources(resources, scopeId)) {
268-
LOGGER.warn(
269-
"Legacy {} element is ignored because {} resources are configured via {} in <sources>.",
270-
legacyElement,
271-
scopeId,
272-
sourcesConfig);
278+
String message = String.format(
279+
"Legacy %s element cannot be used because %s resources are configured via %s in <sources>.",
280+
legacyElement, scopeId, sourcesConfig);
281+
LOGGER.error(message);
282+
result.getProblemCollector()
283+
.reportProblem(new DefaultModelProblem(
284+
message,
285+
Severity.ERROR,
286+
Version.V41,
287+
project.getModel().getDelegate(),
288+
-1,
289+
-1,
290+
null));
273291
} else {
274292
LOGGER.debug(
275293
"{} resources configured via <sources> element, ignoring legacy {} element.",
@@ -319,7 +337,7 @@ private DefaultSourceRoot createModularResourceRoot(String module, ProjectScope
319337
*
320338
* @param resources list of resources to check
321339
* @param scope scope (main or test)
322-
* @return true if explicit legacy resources are present that would be ignored
340+
* @return true if explicit legacy resources are present that conflict with modular sources
323341
*/
324342
private boolean hasExplicitLegacyResources(List<Resource> resources, String scope) {
325343
if (resources.isEmpty()) {

0 commit comments

Comments
 (0)