From c1b431b1363b5afcede6bc54e551c783b4d460c9 Mon Sep 17 00:00:00 2001 From: Gerd Aschemann Date: Fri, 6 Feb 2026 09:19:51 +0100 Subject: [PATCH 1/2] Fail on legacy config in modular projects In modular projects, legacy directories and resources that would be silently ignored now trigger an ERROR instead of WARNING: - Explicit / differing from defaults - Default src/main/java or src/test/java existing on filesystem - Explicit / differing from Super POM defaults This prevents silent loss of user-configured sources/resources. AC8 supersedes AC7 which originally used WARNING. Fixes #11701 See https://github.com/apache/maven/issues/11701#issuecomment-3858462609 Co-Authored-By: Claude Opus 4.5 --- .../maven/project/DefaultProjectBuilder.java | 40 +++++----- .../maven/project/SourceHandlingContext.java | 44 +++++++---- .../maven/project/ProjectBuilderTest.java | 77 ++++++++++--------- 3 files changed, 91 insertions(+), 70 deletions(-) diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java index 9de2ca1348ff..306529f9f4a7 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java @@ -693,27 +693,27 @@ private void initProject(MavenProject project, ModelBuilderResult result) { /* * `sourceDirectory`, `testSourceDirectory` and `scriptSourceDirectory` - * are ignored if the POM file contains at least one enabled element + * are not used if the POM file contains at least one enabled element * for the corresponding scope and language. This rule exists because * Maven provides default values for those elements which may conflict * with user's configuration. * * Additionally, for modular projects, legacy directories are unconditionally - * ignored because it is not clear how to dispatch their content between + * rejected because it is not clear how to dispatch their content between * different modules. A warning is emitted if these properties are explicitly set. */ if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) { project.addScriptSourceRoot(build.getScriptSourceDirectory()); } if (isModularProject) { - // Modular projects: unconditionally ignore legacy directories, warn if explicitly set - warnIfExplicitLegacyDirectory( + // Modular projects: legacy directories conflict with modular sources + failIfLegacyDirectoryPresent( build.getSourceDirectory(), baseDir.resolve("src/main/java"), "", project.getId(), result); - warnIfExplicitLegacyDirectory( + failIfLegacyDirectoryPresent( build.getTestSourceDirectory(), baseDir.resolve("src/test/java"), "", @@ -906,15 +906,17 @@ private void initProject(MavenProject project, ModelBuilderResult result) { } /** - * Warns about legacy directory usage in a modular project. Two cases are handled: + * Fails the build if a legacy directory is present in a modular project. + *

+ * "Present" means either: *

    - *
  • Case 1: The default legacy directory exists on the filesystem (e.g., src/main/java exists)
  • - *
  • Case 2: An explicit legacy directory is configured that differs from the default
  • + *
  • Configuration presence: an explicit configuration differs from the default
  • + *
  • Physical presence: the default directory exists on the filesystem
  • *
- * Legacy directories are unconditionally ignored in modular projects because it is not clear - * how to dispatch their content between different modules. + * In both cases, the legacy directory conflicts with modular sources and must not be used. + * Failing the build forces the user to resolve the conflict explicitly. */ - private void warnIfExplicitLegacyDirectory( + private void failIfLegacyDirectoryPresent( String configuredDir, Path defaultDir, String elementName, @@ -924,26 +926,26 @@ private void warnIfExplicitLegacyDirectory( Path configuredPath = Path.of(configuredDir).toAbsolutePath().normalize(); Path defaultPath = defaultDir.toAbsolutePath().normalize(); if (!configuredPath.equals(defaultPath)) { - // Case 2: Explicit configuration differs from default - always warn + // Configuration presence: explicit config differs from default String message = String.format( - "Legacy %s is ignored in modular project %s. " + "Legacy %s must not be used in modular project %s." + "In modular projects, source directories must be defined via " + "with a module element for each module.", elementName, projectId); - logger.warn(message); + logger.error(message); result.getProblemCollector() .reportProblem(new org.apache.maven.impl.model.DefaultModelProblem( - message, Severity.WARNING, Version.V41, null, -1, -1, null)); + message, Severity.ERROR, Version.V41, null, -1, -1, null)); } else if (Files.isDirectory(defaultPath)) { - // Case 1: Default configuration, but the default directory exists on filesystem + // Physical presence: default directory exists on filesystem String message = String.format( - "Legacy %s '%s' exists but is ignored in modular project %s. " + "Legacy %s '%s' exists but must not be used in modular project %s. " + "In modular projects, source directories must be defined via .", elementName, defaultPath, projectId); - logger.warn(message); + logger.error(message); result.getProblemCollector() .reportProblem(new org.apache.maven.impl.model.DefaultModelProblem( - message, Severity.WARNING, Version.V41, null, -1, -1, null)); + message, Severity.ERROR, Version.V41, null, -1, -1, null)); } } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java b/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java index e7691eb86bcf..d7fd6e37a04b 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java @@ -221,11 +221,19 @@ void handleResourceConfiguration(ProjectScope scope) { if (hasResourcesInSources) { // Modular project with resources configured via - already added above if (hasExplicitLegacyResources(resources, scopeId)) { - LOGGER.warn( - "Legacy {} element is ignored because {} resources are configured via {} in .", - legacyElement, - scopeId, - sourcesConfig); + String message = String.format( + "Legacy %s element must not be used because %s resources are configured via %s in .", + legacyElement, scopeId, sourcesConfig); + LOGGER.error(message); + result.getProblemCollector() + .reportProblem(new DefaultModelProblem( + message, + Severity.ERROR, + Version.V41, + project.getModel().getDelegate(), + -1, + -1, + null)); } else { LOGGER.debug( "{} resources configured via element, ignoring legacy {} element.", @@ -236,13 +244,13 @@ void handleResourceConfiguration(ProjectScope scope) { // Modular project without resources in - inject module-aware defaults if (hasExplicitLegacyResources(resources, scopeId)) { String message = "Legacy " + legacyElement - + " element is ignored because modular sources are configured. " + + " element must not be used because modular sources are configured. " + "Use " + sourcesConfig + " in for custom resource paths."; - LOGGER.warn(message); + LOGGER.error(message); result.getProblemCollector() .reportProblem(new DefaultModelProblem( message, - Severity.WARNING, + Severity.ERROR, Version.V41, project.getModel().getDelegate(), -1, @@ -265,11 +273,19 @@ void handleResourceConfiguration(ProjectScope scope) { if (hasResourcesInSources) { // Resources configured via - already added above if (hasExplicitLegacyResources(resources, scopeId)) { - LOGGER.warn( - "Legacy {} element is ignored because {} resources are configured via {} in .", - legacyElement, - scopeId, - sourcesConfig); + String message = String.format( + "Legacy %s element must not be used because %s resources are configured via %s in .", + legacyElement, scopeId, sourcesConfig); + LOGGER.error(message); + result.getProblemCollector() + .reportProblem(new DefaultModelProblem( + message, + Severity.ERROR, + Version.V41, + project.getModel().getDelegate(), + -1, + -1, + null)); } else { LOGGER.debug( "{} resources configured via element, ignoring legacy {} element.", @@ -319,7 +335,7 @@ private DefaultSourceRoot createModularResourceRoot(String module, ProjectScope * * @param resources list of resources to check * @param scope scope (main or test) - * @return true if explicit legacy resources are present that would be ignored + * @return true if explicit legacy resources are present that conflict with modular sources */ private boolean hasExplicitLegacyResources(List resources, String scope) { if (resources.isEmpty()) { diff --git a/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java index 5da89a0f58b9..2fad5c90232b 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java @@ -422,19 +422,21 @@ void testModularSourcesInjectResourceRoots() throws Exception { } /** - * Tests that when modular sources are configured alongside explicit legacy resources, - * the legacy resources are ignored and a warning is issued. + * Tests that when modular sources are configured alongside explicit legacy resources, an error is raised. *

* This verifies the behavior described in the design: - * - Modular projects with explicit legacy {@code } configuration should issue a warning + * - Modular projects with explicit legacy {@code } configuration should raise an error * - The modular resource roots are injected instead of using the legacy configuration *

- * Acceptance Criterion: AC2 (unified source tracking for all lang/scope combinations) + * Acceptance Criteria: + * - AC2 (unified source tracking for all lang/scope combinations) + * - AC8 (legacy directories error - supersedes AC7 which originally used WARNING) * * @see Issue #11612 + * @see AC8 definition */ @Test - void testModularSourcesWithExplicitResourcesIssuesWarning() throws Exception { + void testModularSourcesWithExplicitResourcesIssuesError() throws Exception { File pom = getProject("modular-sources-with-explicit-resources"); MavenSession mavenSession = createMavenSession(null); @@ -447,19 +449,19 @@ void testModularSourcesWithExplicitResourcesIssuesWarning() throws Exception { MavenProject project = result.getProject(); - // Verify warnings are issued for ignored legacy resources - List warnings = result.getProblems().stream() - .filter(p -> p.getSeverity() == ModelProblem.Severity.WARNING) - .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("ignored")) + // Verify errors are raised for conflicting legacy resources (AC8) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("must not be used")) .toList(); - assertEquals(2, warnings.size(), "Should have 2 warnings (one for resources, one for testResources)"); + assertEquals(2, errors.size(), "Should have 2 errors (one for resources, one for testResources)"); assertTrue( - warnings.stream().anyMatch(w -> w.getMessage().contains("")), - "Should warn about ignored "); + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should error about conflicting "); assertTrue( - warnings.stream().anyMatch(w -> w.getMessage().contains("")), - "Should warn about ignored "); + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should error about conflicting "); // Verify modular resources are still injected correctly List mainResourceRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES) @@ -478,23 +480,23 @@ void testModularSourcesWithExplicitResourcesIssuesWarning() throws Exception { } /** - * Tests that legacy sourceDirectory and testSourceDirectory are ignored in modular projects. + * Tests that legacy sourceDirectory and testSourceDirectory raise an error in modular projects. *

- * In modular projects, legacy directories are unconditionally ignored because it is not clear - * how to dispatch their content between different modules. A warning is emitted if these - * properties are explicitly set (differ from Super POM defaults). + * In modular projects, legacy directories must not occur because it is not clear + * how to dispatch their content between different modules. An error is raised if these + * properties are explicitly set (and differ from Super POM defaults). *

* This verifies: - * - WARNINGs are emitted for explicitly set legacy directories in modular projects - * - sourceDirectory and testSourceDirectory are both ignored + * - ERRORs are raised for explicitly set legacy directories in modular projects * - Only modular sources from {@code } are used *

* Acceptance Criteria: * - AC1 (boolean flags eliminated - uses hasSources() for main/test detection) - * - AC7 (legacy directories warning - {@code } and {@code } - * are unconditionally ignored with a WARNING in modular projects) + * - AC8 (legacy directories error - supersedes AC7 which originally used WARNING; + * {@code } and {@code } are raising an ERROR in modular projects) * * @see Issue #11612 + * @see AC8 definition */ @Test void testMixedSourcesModularMainClassicTest() throws Exception { @@ -510,20 +512,21 @@ void testMixedSourcesModularMainClassicTest() throws Exception { MavenProject project = result.getProject(); - // Verify WARNINGs are emitted for explicitly set legacy directories - List warnings = result.getProblems().stream() - .filter(p -> p.getSeverity() == ModelProblem.Severity.WARNING) - .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("ignored in modular project")) + // Verify ERRORs are raised for explicitly set legacy directories (AC8) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy") + && p.getMessage().contains("must not be used in modular project")) .toList(); - // Should have 2 warnings: one for sourceDirectory, one for testSourceDirectory - assertEquals(2, warnings.size(), "Should have 2 warnings for ignored legacy directories"); + // Should have 2 errors: one for sourceDirectory, one for testSourceDirectory + assertEquals(2, errors.size(), "Should have 2 errors for conflicting legacy directories"); assertTrue( - warnings.stream().anyMatch(w -> w.getMessage().contains("")), - "Should warn about ignored "); + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should error about conflicting "); assertTrue( - warnings.stream().anyMatch(w -> w.getMessage().contains("")), - "Should warn about ignored "); + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should error about conflicting "); // Get main Java source roots - should have modular sources, not classic sourceDirectory List mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY) @@ -541,17 +544,17 @@ void testMixedSourcesModularMainClassicTest() throws Exception { assertTrue(mainModules.contains("org.foo.moduleA"), "Should have main source for moduleA"); assertTrue(mainModules.contains("org.foo.moduleB"), "Should have main source for moduleB"); - // Verify the classic sourceDirectory is NOT used (should be ignored) + // Verify the classic sourceDirectory is NOT used (rejected in modular projects) boolean hasClassicMainSource = mainJavaRoots.stream().anyMatch(sr -> sr.directory() .toString() .replace(File.separatorChar, '/') .contains("src/classic/main/java")); - assertTrue(!hasClassicMainSource, "Classic sourceDirectory should be ignored"); + assertTrue(!hasClassicMainSource, "Classic sourceDirectory must not be used"); - // Test sources should NOT be added (legacy testSourceDirectory is ignored in modular projects) + // Test sources should NOT be added (legacy testSourceDirectory is rejected in modular projects) List testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY) .toList(); - assertEquals(0, testJavaRoots.size(), "Should have no test Java sources (legacy is ignored)"); + assertEquals(0, testJavaRoots.size(), "Should have no test Java sources (legacy is rejected)"); } /** @@ -563,7 +566,7 @@ void testMixedSourcesModularMainClassicTest() throws Exception { *

* This verifies: * - An ERROR is reported when both modular and non-modular sources exist in {@code } - * - sourceDirectory is ignored because {@code } exists + * - sourceDirectory is not used because {@code } exists *

* Acceptance Criteria: * - AC1 (boolean flags eliminated - uses hasSources() for source detection) From a89fc07a3012292da5dbaa2782afb8bb51fdf879 Mon Sep 17 00:00:00 2001 From: Gerd Aschemann Date: Sun, 8 Feb 2026 11:32:45 +0100 Subject: [PATCH 2/2] Address review feedback on wording - Use "cannot" instead of "must not" in error messages - Update Javadoc: "The build fails" instead of "A warning is emitted" --- .../org/apache/maven/project/DefaultProjectBuilder.java | 8 ++++---- .../org/apache/maven/project/SourceHandlingContext.java | 6 +++--- .../java/org/apache/maven/project/ProjectBuilderTest.java | 8 ++++---- 3 files changed, 11 insertions(+), 11 deletions(-) diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java index 306529f9f4a7..deb59f24df01 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/DefaultProjectBuilder.java @@ -700,7 +700,7 @@ private void initProject(MavenProject project, ModelBuilderResult result) { * * Additionally, for modular projects, legacy directories are unconditionally * rejected because it is not clear how to dispatch their content between - * different modules. A warning is emitted if these properties are explicitly set. + * different modules. The build fails if these properties are explicitly set. */ if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) { project.addScriptSourceRoot(build.getScriptSourceDirectory()); @@ -913,7 +913,7 @@ private void initProject(MavenProject project, ModelBuilderResult result) { *

  • Configuration presence: an explicit configuration differs from the default
  • *
  • Physical presence: the default directory exists on the filesystem
  • * - * In both cases, the legacy directory conflicts with modular sources and must not be used. + * In both cases, the legacy directory conflicts with modular sources and cannot be used. * Failing the build forces the user to resolve the conflict explicitly. */ private void failIfLegacyDirectoryPresent( @@ -928,7 +928,7 @@ private void failIfLegacyDirectoryPresent( if (!configuredPath.equals(defaultPath)) { // Configuration presence: explicit config differs from default String message = String.format( - "Legacy %s must not be used in modular project %s." + "Legacy %s cannot be used in modular project %s." + "In modular projects, source directories must be defined via " + "with a module element for each module.", elementName, projectId); @@ -939,7 +939,7 @@ private void failIfLegacyDirectoryPresent( } else if (Files.isDirectory(defaultPath)) { // Physical presence: default directory exists on filesystem String message = String.format( - "Legacy %s '%s' exists but must not be used in modular project %s. " + "Legacy %s '%s' exists but cannot be used in modular project %s." + "In modular projects, source directories must be defined via .", elementName, defaultPath, projectId); logger.error(message); diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java b/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java index d7fd6e37a04b..d704ace5f961 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java @@ -222,7 +222,7 @@ void handleResourceConfiguration(ProjectScope scope) { // Modular project with resources configured via - already added above if (hasExplicitLegacyResources(resources, scopeId)) { String message = String.format( - "Legacy %s element must not be used because %s resources are configured via %s in .", + "Legacy %s element cannot be used because %s resources are configured via %s in .", legacyElement, scopeId, sourcesConfig); LOGGER.error(message); result.getProblemCollector() @@ -244,7 +244,7 @@ void handleResourceConfiguration(ProjectScope scope) { // Modular project without resources in - inject module-aware defaults if (hasExplicitLegacyResources(resources, scopeId)) { String message = "Legacy " + legacyElement - + " element must not be used because modular sources are configured. " + + " element cannot be used because modular sources are configured. " + "Use " + sourcesConfig + " in for custom resource paths."; LOGGER.error(message); result.getProblemCollector() @@ -274,7 +274,7 @@ void handleResourceConfiguration(ProjectScope scope) { // Resources configured via - already added above if (hasExplicitLegacyResources(resources, scopeId)) { String message = String.format( - "Legacy %s element must not be used because %s resources are configured via %s in .", + "Legacy %s element cannot be used because %s resources are configured via %s in .", legacyElement, scopeId, sourcesConfig); LOGGER.error(message); result.getProblemCollector() diff --git a/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java index 2fad5c90232b..ee6f5200f662 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/project/ProjectBuilderTest.java @@ -452,7 +452,7 @@ void testModularSourcesWithExplicitResourcesIssuesError() throws Exception { // Verify errors are raised for conflicting legacy resources (AC8) List errors = result.getProblems().stream() .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) - .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("must not be used")) + .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used")) .toList(); assertEquals(2, errors.size(), "Should have 2 errors (one for resources, one for testResources)"); @@ -482,7 +482,7 @@ void testModularSourcesWithExplicitResourcesIssuesError() throws Exception { /** * Tests that legacy sourceDirectory and testSourceDirectory raise an error in modular projects. *

    - * In modular projects, legacy directories must not occur because it is not clear + * Legacy directories cannot be used in modular projects because it is not clear * how to dispatch their content between different modules. An error is raised if these * properties are explicitly set (and differ from Super POM defaults). *

    @@ -516,7 +516,7 @@ void testMixedSourcesModularMainClassicTest() throws Exception { List errors = result.getProblems().stream() .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) .filter(p -> p.getMessage().contains("Legacy") - && p.getMessage().contains("must not be used in modular project")) + && p.getMessage().contains("cannot be used in modular project")) .toList(); // Should have 2 errors: one for sourceDirectory, one for testSourceDirectory @@ -549,7 +549,7 @@ void testMixedSourcesModularMainClassicTest() throws Exception { .toString() .replace(File.separatorChar, '/') .contains("src/classic/main/java")); - assertTrue(!hasClassicMainSource, "Classic sourceDirectory must not be used"); + assertTrue(!hasClassicMainSource, "Classic sourceDirectory cannot be used"); // Test sources should NOT be added (legacy testSourceDirectory is rejected in modular projects) List testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)