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..a7263e8213d7 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 @@ -692,49 +692,102 @@ private void initProject(MavenProject project, ModelBuilderResult result) { } /* - * `sourceDirectory`, `testSourceDirectory` and `scriptSourceDirectory` - * are ignored 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. + * Source directory handling depends on project type and configuration: * - * Additionally, for 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. + * 1. CLASSIC projects (no ): + * - All legacy directories are used + * + * 2. MODULAR projects (have in ): + * - ALL legacy directories are rejected (can't dispatch between modules) + * - Physical presence of default directories (src/main/java) also triggers ERROR + * + * 3. NON-MODULAR projects with : + * - Explicit legacy directories (differ from default) are always rejected + * - If has Java for a scope: legacy is not used (even if matching default) + * - If has no Java for a scope: legacy is used as implicit fallback + * only if it matches the default (could be inherited) + * - This allows incremental adoption (e.g., custom resources + default Java) */ - if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) { + if (sources.isEmpty()) { + // Classic fallback: no configured, use legacy directories project.addScriptSourceRoot(build.getScriptSourceDirectory()); - } - if (isModularProject) { - // Modular projects: unconditionally ignore legacy directories, warn if explicitly set - warnIfExplicitLegacyDirectory( - build.getSourceDirectory(), - baseDir.resolve("src/main/java"), - "", - project.getId(), - result); - warnIfExplicitLegacyDirectory( - build.getTestSourceDirectory(), - baseDir.resolve("src/test/java"), - "", - project.getId(), - result); + project.addCompileSourceRoot(build.getSourceDirectory()); + project.addTestCompileSourceRoot(build.getTestSourceDirectory()); + // Handle resources using legacy configuration + sourceContext.handleResourceConfiguration(ProjectScope.MAIN); + sourceContext.handleResourceConfiguration(ProjectScope.TEST); } else { - // Classic projects: use legacy directories if no sources defined in - if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.MAIN)) { - project.addCompileSourceRoot(build.getSourceDirectory()); + // Add script source root if no configured + if (!sourceContext.hasSources(Language.SCRIPT, ProjectScope.MAIN)) { + project.addScriptSourceRoot(build.getScriptSourceDirectory()); } - if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.TEST)) { - project.addTestCompileSourceRoot(build.getTestSourceDirectory()); + + if (isModularProject) { + // Modular: reject ALL legacy directory configurations + failIfLegacyDirectoryPresent( + build.getSourceDirectory(), + baseDir.resolve("src/main/java"), + "", + project.getId(), + result, + true); // check physical presence + failIfLegacyDirectoryPresent( + build.getTestSourceDirectory(), + baseDir.resolve("src/test/java"), + "", + project.getId(), + result, + true); // check physical presence + } else { + // Non-modular: always validate legacy directories (error if differs from default) + Path mainDefault = baseDir.resolve("src/main/java"); + Path testDefault = baseDir.resolve("src/test/java"); + + failIfLegacyDirectoryPresent( + build.getSourceDirectory(), + mainDefault, + "", + project.getId(), + result, + false); // no physical presence check + failIfLegacyDirectoryPresent( + build.getTestSourceDirectory(), + testDefault, + "", + project.getId(), + result, + false); // no physical presence check + + // Use legacy as fallback only if: + // 1. doesn't have Java for this scope + // 2. Legacy matches default (otherwise error was reported above) + if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.MAIN)) { + Path configuredMain = Path.of(build.getSourceDirectory()) + .toAbsolutePath() + .normalize(); + if (configuredMain.equals( + mainDefault.toAbsolutePath().normalize())) { + project.addCompileSourceRoot(build.getSourceDirectory()); + } + } + if (!sourceContext.hasSources(Language.JAVA_FAMILY, ProjectScope.TEST)) { + Path configuredTest = Path.of(build.getTestSourceDirectory()) + .toAbsolutePath() + .normalize(); + if (configuredTest.equals( + testDefault.toAbsolutePath().normalize())) { + project.addTestCompileSourceRoot(build.getTestSourceDirectory()); + } + } } - } - // Validate that modular and classic sources are not mixed within - sourceContext.validateNoMixedModularAndClassicSources(); + // Fail if modular and classic sources are mixed within + sourceContext.failIfMixedModularAndClassicSources(); - // Handle main and test resources using unified source handling - sourceContext.handleResourceConfiguration(ProjectScope.MAIN); - sourceContext.handleResourceConfiguration(ProjectScope.TEST); + // Handle main and test resources using unified source handling + sourceContext.handleResourceConfiguration(ProjectScope.MAIN); + sourceContext.handleResourceConfiguration(ProjectScope.TEST); + } } project.setActiveProfiles( @@ -906,44 +959,60 @@ private void initProject(MavenProject project, ModelBuilderResult result) { } /** - * Warns about legacy directory usage in a modular project. Two cases are handled: + * Validates that legacy directory configuration does not conflict with {@code }. + *

+ * When {@code } is configured, the build fails if: *

    - *
  • 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 legacy configuration differs from the default
  • + *
  • Physical presence: the default directory exists on the filesystem (only checked + * when {@code checkPhysicalPresence} is true, typically for modular projects where + * {@code } elements use different paths like {@code src//main/java})
  • *
- * Legacy directories are unconditionally ignored in modular projects because it is not clear - * how to dispatch their content between different modules. + *

+ * The presence of {@code } is the trigger for this validation, not whether the + * project is modular or non-modular. + *

+ * This ensures consistency with resource handling. + * + * @param configuredDir the configured legacy directory value + * @param defaultDir the default legacy directory path + * @param elementName the XML element name for error messages + * @param projectId the project ID for error messages + * @param result the model builder result for reporting problems + * @param checkPhysicalPresence whether to check for physical presence of the default directory + * @see SourceHandlingContext#handleResourceConfiguration(ProjectScope) */ - private void warnIfExplicitLegacyDirectory( + private void failIfLegacyDirectoryPresent( String configuredDir, Path defaultDir, String elementName, String projectId, - ModelBuilderResult result) { + ModelBuilderResult result, + boolean checkPhysicalPresence) { if (configuredDir != null) { 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. " - + "In modular projects, source directories must be defined via " - + "with a module element for each module.", - elementName, projectId); - logger.warn(message); + "Legacy %s cannot be used in project %s because sources are configured via . " + + "Remove the %s configuration.", + elementName, projectId, elementName); + logger.error(message); result.getProblemCollector() .reportProblem(new org.apache.maven.impl.model.DefaultModelProblem( - message, Severity.WARNING, Version.V41, null, -1, -1, null)); - } else if (Files.isDirectory(defaultPath)) { - // Case 1: Default configuration, but the default directory exists on filesystem + message, Severity.ERROR, Version.V41, null, -1, -1, null)); + } else if (checkPhysicalPresence && Files.isDirectory(defaultPath)) { + // Physical presence: default directory exists but would be ignored String message = String.format( - "Legacy %s '%s' exists but is ignored in modular project %s. " - + "In modular projects, source directories must be defined via .", - elementName, defaultPath, projectId); - logger.warn(message); + "Legacy directory '%s' exists but cannot be used in project %s " + + "because sources are configured via . " + + "Remove or rename the directory.", + defaultPath, projectId); + 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..400f9f5dc07d 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 @@ -152,7 +152,7 @@ boolean hasSources(Language language, ProjectScope scope) { } /** - * Validates that a project does not mix modular and classic (non-modular) sources. + * Fails the build if modular and classic (non-modular) sources are mixed within {@code }. *

* A project must be either fully modular (all sources have a module) or fully classic * (no sources have a module). Mixing modular and non-modular sources within the same @@ -161,7 +161,7 @@ boolean hasSources(Language language, ProjectScope scope) { * This validation checks each (language, scope) combination and reports an ERROR if * both modular and non-modular sources are found. */ - void validateNoMixedModularAndClassicSources() { + void failIfMixedModularAndClassicSources() { for (ProjectScope scope : List.of(ProjectScope.MAIN, ProjectScope.TEST)) { for (Language language : List.of(Language.JAVA_FAMILY, Language.RESOURCES)) { boolean hasModular = declaredSources.stream() @@ -200,6 +200,8 @@ void validateNoMixedModularAndClassicSources() { *

  • Modular project: use resources from {@code } if present, otherwise inject defaults
  • *
  • Classic project: use resources from {@code } if present, otherwise use legacy resources
  • * + *

    + * The error behavior for conflicting legacy configuration is consistent with source directory handling. * * @param scope the project scope (MAIN or TEST) */ @@ -221,11 +223,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 cannot 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 +246,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 cannot 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 +275,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 cannot 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 +337,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..c079278ba1e2 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 @@ -46,6 +46,7 @@ import org.junit.jupiter.api.io.TempDir; import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertFalse; import static org.junit.jupiter.api.Assertions.assertNotNull; import static org.junit.jupiter.api.Assertions.assertThrows; import static org.junit.jupiter.api.Assertions.assertTrue; @@ -422,19 +423,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 +450,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("cannot 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,27 +481,64 @@ void testModularSourcesWithExplicitResourcesIssuesWarning() throws Exception { } /** - * Tests that legacy sourceDirectory and testSourceDirectory are ignored 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). + * Tests AC8: ALL legacy directories are rejected when {@code } is configured. *

    - * This verifies: - * - WARNINGs are emitted for explicitly set legacy directories in modular projects - * - sourceDirectory and testSourceDirectory are both ignored - * - Only modular sources from {@code } are used + * Modular project with Java in {@code } for MAIN scope and explicit legacy + * {@code } that differs from default. The legacy directory is rejected + * because modular projects cannot use legacy directories (content cannot be dispatched + * between modules). + * + * @see Issue #11701 (AC8/AC9) + */ + @Test + void testModularWithJavaSourcesRejectsLegacySourceDirectory() throws Exception { + File pom = getProject("modular-java-with-explicit-source-dir"); + + MavenSession mavenSession = createMavenSession(null); + ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest(); + configuration.setRepositorySession(mavenSession.getRepositorySession()); + + ProjectBuildingResult result = getContainer() + .lookup(org.apache.maven.project.ProjectBuilder.class) + .build(pom, configuration); + + MavenProject project = result.getProject(); + + // Verify ERROR for (MAIN scope has Java in ) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used")) + .filter(p -> p.getMessage().contains("")) + .toList(); + + assertEquals(1, errors.size(), "Should have 1 error for "); + + // Verify modular source is used, not legacy + List mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY) + .toList(); + assertEquals(1, mainJavaRoots.size(), "Should have 1 modular main Java source root"); + assertEquals("org.foo.app", mainJavaRoots.get(0).module().orElse(null), "Should have module org.foo.app"); + + // Legacy sourceDirectory is NOT used + assertFalse( + mainJavaRoots.get(0).directory().toString().contains("src/custom/main/java"), + "Legacy sourceDirectory should not be used"); + } + + /** + * Tests AC8: Modular project rejects legacy {@code } even when + * {@code } has NO Java for TEST scope. *

    - * 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) + * Modular project with NO Java in {@code } for TEST scope and explicit legacy + * {@code } that differs from default. The legacy directory is rejected + * because modular projects cannot use legacy directories (content cannot be dispatched + * between modules). * - * @see Issue #11612 + * @see Issue #11701 (AC8/AC9) */ @Test - void testMixedSourcesModularMainClassicTest() throws Exception { - File pom = getProject("mixed-sources"); + void testModularWithoutTestSourcesRejectsLegacyTestSourceDirectory() throws Exception { + File pom = getProject("modular-no-test-java-with-explicit-test-source-dir"); MavenSession mavenSession = createMavenSession(null); ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest(); @@ -510,48 +550,271 @@ 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 ERROR for (modular projects reject all legacy directories) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used")) + .filter(p -> p.getMessage().contains("")) + .toList(); + + assertEquals(1, errors.size(), "Should have 1 error for "); + + // No test Java sources (legacy rejected, none in ) + List testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY) + .toList(); + assertEquals(0, testJavaRoots.size(), "Should have no test Java sources"); + } + + /** + * Tests AC9: explicit legacy directories raise an error in non-modular projects when + * {@code } has Java for that scope. + *

    + * This test uses a non-modular project (no {@code } attribute) with both: + *

      + *
    • {@code } with main and test Java sources
    • + *
    • Explicit {@code } and {@code } (conflicting)
    • + *
    + * Both legacy directories should trigger ERROR because {@code } has Java. + * + * @see Issue #11701 (AC8/AC9) + */ + @Test + void testClassicSourcesWithExplicitLegacyDirectories() throws Exception { + File pom = getProject("classic-sources-with-explicit-legacy"); + + MavenSession mavenSession = createMavenSession(null); + ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest(); + configuration.setRepositorySession(mavenSession.getRepositorySession()); + + ProjectBuildingResult result = getContainer() + .lookup(org.apache.maven.project.ProjectBuilder.class) + .build(pom, configuration); + + // Verify errors are raised for conflicting legacy directories (AC9) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used")) .toList(); - // Should have 2 warnings: one for sourceDirectory, one for testSourceDirectory - assertEquals(2, warnings.size(), "Should have 2 warnings for ignored legacy directories"); + assertEquals(2, errors.size(), "Should have 2 errors (one for sourceDirectory, one for testSourceDirectory)"); + + // Verify error messages mention the conflicting elements assertTrue( - warnings.stream().anyMatch(w -> w.getMessage().contains("")), - "Should warn about ignored "); + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should have error for "); assertTrue( - warnings.stream().anyMatch(w -> w.getMessage().contains("")), - "Should warn about ignored "); + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should have error for "); + } + + /** + * Tests AC9: Non-modular project with only resources in {@code } uses implicit Java fallback. + *

    + * When {@code } contains only resources (no Java sources), the legacy + * {@code } and {@code } are used as implicit fallback. + * This enables incremental adoption of {@code } - customize resources while + * keeping the default Java directory structure. + * + * @see Issue #11701 (AC8/AC9) + */ + @Test + void testNonModularResourcesOnlyWithImplicitJavaFallback() throws Exception { + File pom = getProject("non-modular-resources-only"); - // Get main Java source roots - should have modular sources, not classic sourceDirectory + MavenSession mavenSession = createMavenSession(null); + ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest(); + configuration.setRepositorySession(mavenSession.getRepositorySession()); + + ProjectBuildingResult result = getContainer() + .lookup(org.apache.maven.project.ProjectBuilder.class) + .build(pom, configuration); + + MavenProject project = result.getProject(); + + // Verify NO errors - legacy directories are used as fallback (AC9) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used")) + .toList(); + + assertEquals(0, errors.size(), "Should have no errors - legacy directories used as fallback (AC9)"); + + // Verify resources from are used + List mainResources = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES) + .toList(); + assertTrue( + mainResources.stream().anyMatch(sr -> sr.directory() + .toString() + .replace(File.separatorChar, '/') + .contains("src/main/custom-resources")), + "Should have custom main resources from "); + + // Verify legacy Java directories are used as fallback List mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY) .toList(); + assertEquals(1, mainJavaRoots.size(), "Should have 1 main Java source (implicit fallback)"); + assertTrue( + mainJavaRoots + .get(0) + .directory() + .toString() + .replace(File.separatorChar, '/') + .endsWith("src/main/java"), + "Should use default src/main/java as fallback"); - // Should have 2 modular main Java sources (moduleA and moduleB) - assertEquals(2, mainJavaRoots.size(), "Should have 2 modular main Java source roots"); + List testJavaRoots = project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY) + .toList(); + assertEquals(1, testJavaRoots.size(), "Should have 1 test Java source (implicit fallback)"); + assertTrue( + testJavaRoots + .get(0) + .directory() + .toString() + .replace(File.separatorChar, '/') + .endsWith("src/test/java"), + "Should use default src/test/java as fallback"); + } - Set mainModules = mainJavaRoots.stream() - .map(SourceRoot::module) - .flatMap(Optional::stream) - .collect(Collectors.toSet()); + /** + * Tests AC9 violation: Non-modular project with only resources in {@code } and explicit legacy directories. + *

    + * AC9 allows implicit fallback to legacy directories (when they match defaults). + * When legacy directories differ from the default, this is explicit configuration, + * which violates AC9's "implicit" requirement, so an ERROR is raised. + * + * @see Issue #11701 (AC8/AC9) + */ + @Test + void testNonModularResourcesOnlyWithExplicitLegacyDirectoriesRejected() throws Exception { + File pom = getProject("non-modular-resources-only-explicit-legacy"); - assertEquals(2, mainModules.size(), "Should have main sources for 2 modules"); - assertTrue(mainModules.contains("org.foo.moduleA"), "Should have main source for moduleA"); - assertTrue(mainModules.contains("org.foo.moduleB"), "Should have main source for moduleB"); + MavenSession mavenSession = createMavenSession(null); + ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest(); + configuration.setRepositorySession(mavenSession.getRepositorySession()); - // Verify the classic sourceDirectory is NOT used (should be ignored) - boolean hasClassicMainSource = mainJavaRoots.stream().anyMatch(sr -> sr.directory() - .toString() - .replace(File.separatorChar, '/') - .contains("src/classic/main/java")); - assertTrue(!hasClassicMainSource, "Classic sourceDirectory should be ignored"); + ProjectBuildingResult result = getContainer() + .lookup(org.apache.maven.project.ProjectBuilder.class) + .build(pom, configuration); + + MavenProject project = result.getProject(); + + // Verify ERRORs for explicit legacy directories (differ from default) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy") && p.getMessage().contains("cannot be used")) + .toList(); + + assertEquals(2, errors.size(), "Should have 2 errors for explicit legacy directories"); + assertTrue( + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should error about "); + assertTrue( + errors.stream().anyMatch(e -> e.getMessage().contains("")), + "Should error about "); + + // Verify resources from are still used + List mainResources = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES) + .toList(); + assertTrue( + mainResources.stream().anyMatch(sr -> sr.directory() + .toString() + .replace(File.separatorChar, '/') + .contains("src/main/custom-resources")), + "Should have custom main resources from "); + + // Verify NO Java source roots (legacy was rejected, none in ) + List mainJavaRoots = project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY) + .toList(); + assertEquals(0, mainJavaRoots.size(), "Should have no main Java sources (legacy rejected)"); - // Test sources should NOT be added (legacy testSourceDirectory is ignored 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 rejected)"); + } + + /** + * Tests AC8: Modular project with Java in {@code } and physical default legacy directories. + *

    + * Even when legacy directories use Super POM defaults (no explicit override), + * if the physical directories exist on the filesystem, an ERROR is raised. + * This is because modular projects use paths like {@code src//main/java}, + * so content in {@code src/main/java} would be silently ignored. + * + * @see Issue #11701 (AC8/AC9) + */ + @Test + void testModularWithPhysicalDefaultLegacyDirectory() throws Exception { + File pom = getProject("modular-with-physical-legacy"); + + MavenSession mavenSession = createMavenSession(null); + ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest(); + configuration.setRepositorySession(mavenSession.getRepositorySession()); + + ProjectBuildingResult result = getContainer() + .lookup(org.apache.maven.project.ProjectBuilder.class) + .build(pom, configuration); + + // Verify ERRORs are raised for physical presence of default directories (AC8) + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy directory") + && p.getMessage().contains("exists")) + .toList(); + + // Should have 2 errors: one for src/main/java, one for src/test/java + assertEquals(2, errors.size(), "Should have 2 errors for physical legacy directories"); + // Use File.separator for platform-independent path matching (backslash on Windows) + String mainJava = "src" + File.separator + "main" + File.separator + "java"; + String testJava = "src" + File.separator + "test" + File.separator + "java"; + assertTrue( + errors.stream().anyMatch(e -> e.getMessage().contains(mainJava)), + "Should error about physical src/main/java"); + assertTrue( + errors.stream().anyMatch(e -> e.getMessage().contains(testJava)), + "Should error about physical src/test/java"); + } + + /** + * Tests AC8: Modular project with only resources in {@code } and physical default legacy directories. + *

    + * Even when {@code } only contains resources (no Java), if the physical + * default directories exist, an ERROR is raised for modular projects. + * Unlike non-modular projects (AC9), modular projects cannot use legacy directories as fallback. + * + * @see Issue #11701 (AC8/AC9) + */ + @Test + void testModularResourcesOnlyWithPhysicalDefaultLegacyDirectory() throws Exception { + File pom = getProject("modular-resources-only-with-physical-legacy"); + + MavenSession mavenSession = createMavenSession(null); + ProjectBuildingRequest configuration = new DefaultProjectBuildingRequest(); + configuration.setRepositorySession(mavenSession.getRepositorySession()); + + ProjectBuildingResult result = getContainer() + .lookup(org.apache.maven.project.ProjectBuilder.class) + .build(pom, configuration); + + // Verify ERRORs are raised for physical presence of default directories (AC8) + // Unlike non-modular (AC9), modular projects cannot use legacy as fallback + List errors = result.getProblems().stream() + .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR) + .filter(p -> p.getMessage().contains("Legacy directory") + && p.getMessage().contains("exists")) + .toList(); + + // Should have 2 errors: one for src/main/java, one for src/test/java + assertEquals( + 2, errors.size(), "Should have 2 errors for physical legacy directories (no AC9 fallback for modular)"); + // Use File.separator for platform-independent path matching (backslash on Windows) + String mainJava = "src" + File.separator + "main" + File.separator + "java"; + String testJava = "src" + File.separator + "test" + File.separator + "java"; + assertTrue( + errors.stream().anyMatch(e -> e.getMessage().contains(mainJava)), + "Should error about physical src/main/java"); + assertTrue( + errors.stream().anyMatch(e -> e.getMessage().contains(testJava)), + "Should error about physical src/test/java"); } /** @@ -563,7 +826,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) diff --git a/impl/maven-core/src/test/projects/project-builder/classic-sources-with-explicit-legacy/pom.xml b/impl/maven-core/src/test/projects/project-builder/classic-sources-with-explicit-legacy/pom.xml new file mode 100644 index 000000000000..0c5726393a11 --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/classic-sources-with-explicit-legacy/pom.xml @@ -0,0 +1,30 @@ + + + 4.1.0 + + org.apache.maven.tests + classic-sources-explicit-legacy-test + 1.0-SNAPSHOT + jar + + + + + + main + java + src/main/java + + + test + java + src/test/java + + + + src/legacy/main/java + src/legacy/test/java + + diff --git a/impl/maven-core/src/test/projects/project-builder/mixed-sources/pom.xml b/impl/maven-core/src/test/projects/project-builder/mixed-sources/pom.xml deleted file mode 100644 index caa10d988502..000000000000 --- a/impl/maven-core/src/test/projects/project-builder/mixed-sources/pom.xml +++ /dev/null @@ -1,41 +0,0 @@ - - - - 4.1.0 - - org.apache.maven.tests - mixed-sources-test - 1.0-SNAPSHOT - jar - - - - src/classic/main/java - - src/classic/test/java - - - - - main - java - org.foo.moduleA - - - main - java - org.foo.moduleB - - - - - diff --git a/impl/maven-core/src/test/projects/project-builder/modular-java-with-explicit-source-dir/pom.xml b/impl/maven-core/src/test/projects/project-builder/modular-java-with-explicit-source-dir/pom.xml new file mode 100644 index 000000000000..dfab4a6b3ff2 --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/modular-java-with-explicit-source-dir/pom.xml @@ -0,0 +1,33 @@ + + + + 4.1.0 + + org.apache.maven.tests + modular-java-with-explicit-source-dir-test + 1.0-SNAPSHOT + jar + + + + src/custom/main/java + + + + + main + java + org.foo.app + + + + diff --git a/impl/maven-core/src/test/projects/project-builder/modular-no-test-java-with-explicit-test-source-dir/pom.xml b/impl/maven-core/src/test/projects/project-builder/modular-no-test-java-with-explicit-test-source-dir/pom.xml new file mode 100644 index 000000000000..dd27dac58612 --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/modular-no-test-java-with-explicit-test-source-dir/pom.xml @@ -0,0 +1,34 @@ + + + + 4.1.0 + + org.apache.maven.tests + modular-no-test-java-with-explicit-test-source-dir-test + 1.0-SNAPSHOT + jar + + + + src/custom/test/java + + + + + main + java + org.foo.app + + + + + diff --git a/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/pom.xml b/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/pom.xml new file mode 100644 index 000000000000..92f8cb52a4ea --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/pom.xml @@ -0,0 +1,42 @@ + + + + 4.1.0 + + org.apache.maven.tests + modular-resources-only-with-physical-legacy-test + 1.0-SNAPSHOT + jar + + + + + + + + + main + resources + org.example.app + + + test + resources + org.example.app + + + + diff --git a/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/src/main/java/.gitkeep b/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/src/main/java/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/src/test/java/.gitkeep b/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/src/test/java/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/pom.xml b/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/pom.xml new file mode 100644 index 000000000000..27267c0bc274 --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/pom.xml @@ -0,0 +1,41 @@ + + + + 4.1.0 + + org.apache.maven.tests + modular-with-physical-legacy-test + 1.0-SNAPSHOT + jar + + + + + + + + + main + java + org.example.app + + + test + java + org.example.app + + + + diff --git a/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/src/main/java/.gitkeep b/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/src/main/java/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/src/test/java/.gitkeep b/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/src/test/java/.gitkeep new file mode 100644 index 000000000000..e69de29bb2d1 diff --git a/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only-explicit-legacy/pom.xml b/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only-explicit-legacy/pom.xml new file mode 100644 index 000000000000..2bb12cd7a6ab --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only-explicit-legacy/pom.xml @@ -0,0 +1,42 @@ + + + + 4.1.0 + + org.apache.maven.tests + non-modular-resources-only-explicit-legacy-test + 1.0-SNAPSHOT + jar + + + + + + main + resources + src/main/custom-resources + + + test + resources + src/test/custom-resources + + + + + src/custom/main/java + src/custom/test/java + + diff --git a/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only/pom.xml b/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only/pom.xml new file mode 100644 index 000000000000..12eee4001d15 --- /dev/null +++ b/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only/pom.xml @@ -0,0 +1,38 @@ + + + + 4.1.0 + + org.apache.maven.tests + non-modular-resources-only-test + 1.0-SNAPSHOT + jar + + + + + + main + resources + src/main/custom-resources + + + test + resources + src/test/custom-resources + + + + + + +