This is an automated email from the ASF dual-hosted git repository.

desruisseaux pushed a commit to branch maven-4.0.x
in repository https://gitbox.apache.org/repos/asf/maven.git


The following commit(s) were added to refs/heads/maven-4.0.x by this push:
     new 3003a40663 Add module-aware resource handling for modular sources 
(#11700)
3003a40663 is described below

commit 3003a40663e22335f86d74de6dd1f670cf60bbd8
Author: Martin Desruisseaux <[email protected]>
AuthorDate: Sun Mar 8 22:49:21 2026 +0100

    Add module-aware resource handling for modular sources (#11700)
    
    This is a combination of pull requests #11505, #11632 and #11702 on master 
branch.
    
    Summary:
    Maven 4.x introduces a unified <sources> element that supports modular
    project layouts (src/<module>/<scope>/<lang>). However, resource handling
    did not follow the modular layout - resources were always loaded from
    the legacy <resources> element which defaults to src/main/resources.
    This change implements automatic module-aware resource injection.
    
    - For modular projects without resource configuration in <sources>,
      automatically inject resource roots following the modular layout:
      src/<module>/main/resources and src/<module>/test/resources.
    - Resources configured via <sources> take priority over legacy <resources>.
    - Fail the build (as ModelProblem) when explicit legacy resources are 
ignored.
    
    Co-authored-by: Gerd Aschemann <[email protected]>
---
 .../maven/project/DefaultProjectBuilder.java       | 234 ++++++--
 .../maven/project/SourceHandlingContext.java       | 366 ++++++++++++
 .../apache/maven/project/ProjectBuilderTest.java   | 619 +++++++++++++++++++++
 .../classic-sources-with-explicit-legacy/pom.xml   |  30 +
 .../duplicate-enabled-sources/pom.xml              |  64 +++
 .../modular-java-with-explicit-source-dir/pom.xml  |  33 ++
 .../pom.xml                                        |  34 ++
 .../pom.xml                                        |  42 ++
 .../src/main/java/.gitkeep                         |   0
 .../src/test/java/.gitkeep                         |   0
 .../pom.xml                                        |  39 ++
 .../project-builder/modular-sources/pom.xml        |  40 ++
 .../modular-with-physical-legacy/pom.xml           |  41 ++
 .../src/main/java/.gitkeep                         |   0
 .../src/test/java/.gitkeep                         |   0
 .../multiple-directories-same-module/pom.xml       |  51 ++
 .../pom.xml                                        |  42 ++
 .../non-modular-resources-only/pom.xml             |  38 ++
 .../project-builder/sources-mixed-modules/pom.xml  |  49 ++
 19 files changed, 1685 insertions(+), 37 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 5bfa2d3f32..b2116b285b 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
@@ -27,6 +27,7 @@
 import java.io.IOException;
 import java.io.InputStream;
 import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
 import java.nio.file.Path;
 import java.util.AbstractMap;
 import java.util.ArrayList;
@@ -63,7 +64,6 @@
 import org.apache.maven.api.model.Plugin;
 import org.apache.maven.api.model.Profile;
 import org.apache.maven.api.model.ReportPlugin;
-import org.apache.maven.api.model.Resource;
 import org.apache.maven.api.services.ArtifactResolver;
 import org.apache.maven.api.services.ArtifactResolverException;
 import org.apache.maven.api.services.ArtifactResolverRequest;
@@ -520,7 +520,7 @@ List<ProjectBuildingResult> doBuild(List<File> pomFiles, 
boolean recursive) {
                 return pomFiles.stream()
                         .map(pomFile -> build(pomFile, recursive))
                         .flatMap(List::stream)
-                        .collect(Collectors.toList());
+                        .toList();
             } finally {
                 
Thread.currentThread().setContextClassLoader(oldContextClassLoader);
             }
@@ -566,7 +566,7 @@ private List<ProjectBuildingResult> build(File pomFile, 
boolean recursive) {
                     project.setCollectedProjects(results(r)
                             .filter(cr -> cr != r && cr.getEffectiveModel() != 
null)
                             .map(cr -> 
projectIndex.get(cr.getEffectiveModel().getId()))
-                            .collect(Collectors.toList()));
+                            .toList());
 
                     DependencyResolutionResult resolutionResult = null;
                     if (request.isResolveDependencies()) {
@@ -660,45 +660,130 @@ private void initProject(MavenProject project, 
ModelBuilderResult result) {
                         return build.getDirectory();
                     }
                 };
-                boolean hasScript = false;
-                boolean hasMain = false;
-                boolean hasTest = false;
+                // Extract modules from sources to detect modular projects
+                Set<String> modules = extractModules(sources);
+                boolean isModularProject = !modules.isEmpty();
+
+                logger.trace(
+                        "Module detection for project {}: found {} module(s) 
{} - modular project: {}.",
+                        project.getId(),
+                        modules.size(),
+                        modules,
+                        isModularProject);
+
+                // Create source handling context for unified tracking of all 
lang/scope combinations
+                SourceHandlingContext sourceContext =
+                        new SourceHandlingContext(project, baseDir, modules, 
isModularProject, result);
+
+                // Process all sources, tracking enabled ones and detecting 
duplicates
                 for (var source : sources) {
-                    var src = DefaultSourceRoot.fromModel(session, baseDir, 
outputDirectory, source);
-                    project.addSourceRoot(src);
-                    Language language = src.language();
-                    if (Language.JAVA_FAMILY.equals(language)) {
-                        ProjectScope scope = src.scope();
-                        if (ProjectScope.MAIN.equals(scope)) {
-                            hasMain = true;
-                        } else {
-                            hasTest |= ProjectScope.TEST.equals(scope);
-                        }
-                    } else {
-                        hasScript |= Language.SCRIPT.equals(language);
+                    var sourceRoot = DefaultSourceRoot.fromModel(session, 
baseDir, outputDirectory, source);
+                    // Track enabled sources for duplicate detection and 
hasSources() queries
+                    // Only add source if it's not a duplicate enabled source 
(first enabled wins)
+                    if (sourceContext.shouldAddSource(sourceRoot)) {
+                        project.addSourceRoot(sourceRoot);
                     }
                 }
+
                 /*
-                 * `sourceDirectory`, `testSourceDirectory` and 
`scriptSourceDirectory`
-                 * are ignored if the POM file contains at least one <source> 
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.
-                 */
-                if (!hasScript) {
+                  Source directory handling depends on project type and 
<sources> configuration:
+
+                  1. CLASSIC projects (no <sources>):
+                     - All legacy directories are used
+
+                  2. MODULAR projects (have <module> in <sources>):
+                     - ALL legacy directories cause the build to fail (cannot 
dispatch
+                       between modules)
+                     - The build also fails if default directories 
(src/main/java)
+                       physically exist on the filesystem
+
+                  3. NON-MODULAR projects with <sources>:
+                     - Explicit legacy directories (differ from default) 
always cause
+                       the build to fail
+                     - Legacy directories for scopes where <sources> defines 
Java are ignored
+                     - Legacy directories for scopes where <sources> has no 
Java serve as
+                       implicit fallback (only if they match the default, 
e.g., inherited)
+                     - This allows incremental adoption (e.g., custom 
resources + default Java)
+                */
+                if (sources.isEmpty()) {
+                    // Classic fallback: no <sources> configured, use legacy 
directories
                     
project.addScriptSourceRoot(build.getScriptSourceDirectory());
-                }
-                if (!hasMain) {
                     project.addCompileSourceRoot(build.getSourceDirectory());
-                }
-                if (!hasTest) {
                     
project.addTestCompileSourceRoot(build.getTestSourceDirectory());
-                }
-                for (Resource resource : 
project.getBuild().getDelegate().getResources()) {
-                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
ProjectScope.MAIN, resource));
-                }
-                for (Resource resource : 
project.getBuild().getDelegate().getTestResources()) {
-                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
ProjectScope.TEST, resource));
+                    // Handle resources using legacy configuration
+                    
sourceContext.handleResourceConfiguration(ProjectScope.MAIN);
+                    
sourceContext.handleResourceConfiguration(ProjectScope.TEST);
+                } else {
+                    // Add script source root if no <sources lang="script"> 
configured
+                    if (!sourceContext.hasSources(Language.SCRIPT, 
ProjectScope.MAIN)) {
+                        
project.addScriptSourceRoot(build.getScriptSourceDirectory());
+                    }
+
+                    if (isModularProject) {
+                        // Modular: reject ALL legacy directory configurations
+                        failIfLegacyDirectoryPresent(
+                                build.getSourceDirectory(),
+                                baseDir.resolve("src/main/java"),
+                                "<sourceDirectory>",
+                                project.getId(),
+                                result,
+                                true); // check physical presence
+                        failIfLegacyDirectoryPresent(
+                                build.getTestSourceDirectory(),
+                                baseDir.resolve("src/test/java"),
+                                "<testSourceDirectory>",
+                                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,
+                                "<sourceDirectory>",
+                                project.getId(),
+                                result,
+                                false); // no physical presence check
+                        failIfLegacyDirectoryPresent(
+                                build.getTestSourceDirectory(),
+                                testDefault,
+                                "<testSourceDirectory>",
+                                project.getId(),
+                                result,
+                                false); // no physical presence check
+
+                        // Use legacy as fallback only if:
+                        // 1. <sources> 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());
+                            }
+                        }
+                    }
+
+                    // Fail if modular and classic sources are mixed within 
<sources>
+                    sourceContext.failIfMixedModularAndClassicSources();
+
+                    // Handle main and test resources using unified source 
handling
+                    
sourceContext.handleResourceConfiguration(ProjectScope.MAIN);
+                    
sourceContext.handleResourceConfiguration(ProjectScope.TEST);
                 }
             }
 
@@ -870,6 +955,65 @@ private void initProject(MavenProject project, 
ModelBuilderResult result) {
             project.setRemoteArtifactRepositories(remoteRepositories);
         }
 
+        /**
+         * Validates that legacy directory configuration does not conflict 
with {@code <sources>}.
+         * <p>
+         * When {@code <sources>} is configured, the build fails if:
+         * <ul>
+         *   <li><strong>Configuration presence</strong>: an explicit legacy 
configuration differs from the default</li>
+         *   <li><strong>Physical presence</strong>: the default directory 
exists on the filesystem (only checked
+         *       when {@code checkPhysicalPresence} is true, typically for 
modular projects where
+         *       {@code <source>} elements use different paths like {@code 
src/<module>/main/java})</li>
+         * </ul>
+         * <p>
+         * The presence of {@code <sources>} is the trigger for this 
validation, not whether the
+         * project is modular or non-modular.
+         * <p>
+         * 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 failIfLegacyDirectoryPresent(
+                String configuredDir,
+                Path defaultDir,
+                String elementName,
+                String projectId,
+                ModelBuilderResult result,
+                boolean checkPhysicalPresence) {
+            if (configuredDir != null) {
+                Path configuredPath = 
Path.of(configuredDir).toAbsolutePath().normalize();
+                Path defaultPath = defaultDir.toAbsolutePath().normalize();
+                if (!configuredPath.equals(defaultPath)) {
+                    // Configuration presence: explicit config differs from 
default
+                    String message = String.format(
+                            "Legacy %s cannot be used in project %s because 
sources are configured via <sources>. "
+                                    + "Remove the %s configuration.",
+                            elementName, projectId, elementName);
+                    logger.error(message);
+                    result.getProblemCollector()
+                            .reportProblem(new 
org.apache.maven.impl.model.DefaultModelProblem(
+                                    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 directory '%s' exists but cannot be used 
in project %s "
+                                    + "because sources are configured via 
<sources>. "
+                                    + "Remove or rename the directory.",
+                            defaultPath, projectId);
+                    logger.error(message);
+                    result.getProblemCollector()
+                            .reportProblem(new 
org.apache.maven.impl.model.DefaultModelProblem(
+                                    message, Severity.ERROR, Version.V41, 
null, -1, -1, null));
+                }
+            }
+        }
+
         private void initParent(MavenProject project, ModelBuilderResult 
result) {
             Model parentModel = result.getParentModel();
 
@@ -1011,8 +1155,8 @@ private DependencyResolutionResult 
resolveDependencies(MavenProject project) {
         }
     }
 
-    private List<String> getProfileIds(List<Profile> profiles) {
-        return 
profiles.stream().map(Profile::getId).collect(Collectors.toList());
+    private static List<String> getProfileIds(List<Profile> profiles) {
+        return profiles.stream().map(Profile::getId).toList();
     }
 
     private static ModelSource createStubModelSource(Artifact artifact) {
@@ -1093,6 +1237,22 @@ public Set<Entry<K, V>> entrySet() {
         }
     }
 
+    /**
+     * Extracts unique module names from the given list of source elements.
+     * A project is considered modular if it has at least one module name.
+     *
+     * @param sources list of source elements from the build
+     * @return set of non-blank module names
+     */
+    private static Set<String> 
extractModules(List<org.apache.maven.api.model.Source> sources) {
+        return sources.stream()
+                .map(org.apache.maven.api.model.Source::getModule)
+                .filter(Objects::nonNull)
+                .map(String::trim)
+                .filter(s -> !s.isBlank())
+                .collect(Collectors.toSet());
+    }
+
     private Model injectLifecycleBindings(
             Model model,
             ModelBuilderRequest request,
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
new file mode 100644
index 0000000000..400f9f5dc0
--- /dev/null
+++ 
b/impl/maven-core/src/main/java/org/apache/maven/project/SourceHandlingContext.java
@@ -0,0 +1,366 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you under the Apache License, Version 2.0 (the
+ * "License"); you may not use this file except in compliance
+ * with the License.  You may obtain a copy of the License at
+ *
+ *   http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing,
+ * software distributed under the License is distributed on an
+ * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
+ * KIND, either express or implied.  See the License for the
+ * specific language governing permissions and limitations
+ * under the License.
+ */
+package org.apache.maven.project;
+
+import java.nio.file.Path;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.maven.api.Language;
+import org.apache.maven.api.ProjectScope;
+import org.apache.maven.api.SourceRoot;
+import org.apache.maven.api.model.Resource;
+import org.apache.maven.api.services.BuilderProblem.Severity;
+import org.apache.maven.api.services.ModelBuilderResult;
+import org.apache.maven.api.services.ModelProblem.Version;
+import org.apache.maven.impl.DefaultSourceRoot;
+import org.apache.maven.impl.model.DefaultModelProblem;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/**
+ * Handles source configuration for Maven projects with unified tracking for 
all language/scope combinations.
+ * <p>
+ * This class replaces the previous approach of hardcoded boolean flags 
(hasMain, hasTest, etc.)
+ * with a flexible set-based tracking mechanism that works for any language 
and scope combination.
+ * <p>
+ * Key features:
+ * <ul>
+ *   <li>Tracks declared sources using {@code (language, scope, module, 
directory)} identity</li>
+ *   <li>Only tracks enabled sources - disabled sources are effectively 
no-ops</li>
+ *   <li>Detects duplicate enabled sources and emits warnings</li>
+ *   <li>Provides {@link #hasSources(Language, ProjectScope)} to check if 
sources exist for a combination</li>
+ * </ul>
+ *
+ * @since 4.0.0
+ */
+class SourceHandlingContext {
+
+    private static final Logger LOGGER = 
LoggerFactory.getLogger(SourceHandlingContext.class);
+
+    /**
+     * Identity key for source tracking. Two sources with the same key are 
considered duplicates.
+     */
+    record SourceKey(Language language, ProjectScope scope, String module, 
Path directory) {}
+
+    private final MavenProject project;
+    private final Path baseDir;
+    private final Set<String> modules;
+    private final boolean modularProject;
+    private final ModelBuilderResult result;
+    private final Set<SourceKey> declaredSources;
+
+    SourceHandlingContext(
+            MavenProject project,
+            Path baseDir,
+            Set<String> modules,
+            boolean modularProject,
+            ModelBuilderResult result) {
+        this.project = project;
+        this.baseDir = baseDir;
+        this.modules = modules;
+        this.modularProject = modularProject;
+        this.result = result;
+        // Each module typically has main, test, main resources, test 
resources = 4 sources
+        this.declaredSources = new HashSet<>(4 * modules.size());
+    }
+
+    /**
+     * Determines if a source root should be added to the project and tracks 
it for duplicate detection.
+     * <p>
+     * Rules:
+     * <ul>
+     *   <li>Disabled sources are always added (they're filtered by {@code 
getEnabledSourceRoots()})</li>
+     *   <li>First enabled source for an identity is added and tracked</li>
+     *   <li>Subsequent enabled sources with same identity trigger a WARNING 
and are NOT added</li>
+     * </ul>
+     *
+     * @param sourceRoot the source root to evaluate
+     * @return true if the source should be added to the project, false if 
it's a duplicate enabled source
+     */
+    boolean shouldAddSource(SourceRoot sourceRoot) {
+        if (!sourceRoot.enabled()) {
+            // Disabled sources are always added - they're filtered out by 
getEnabledSourceRoots()
+            LOGGER.trace(
+                    "Adding disabled source (will be filtered by 
getEnabledSourceRoots): lang={}, scope={}, module={}, dir={}",
+                    sourceRoot.language(),
+                    sourceRoot.scope(),
+                    sourceRoot.module().orElse(null),
+                    sourceRoot.directory());
+            return true;
+        }
+
+        // Normalize path for consistent duplicate detection (handles 
symlinks, relative paths)
+        Path normalizedDir = 
sourceRoot.directory().toAbsolutePath().normalize();
+        SourceKey key = new SourceKey(
+                sourceRoot.language(), sourceRoot.scope(), 
sourceRoot.module().orElse(null), normalizedDir);
+
+        if (declaredSources.contains(key)) {
+            String message = String.format(
+                    "Duplicate enabled source detected: lang=%s, scope=%s, 
module=%s, directory=%s. "
+                            + "First enabled source wins, this duplicate is 
ignored.",
+                    key.language(), key.scope(), key.module() != null ? 
key.module() : "(none)", key.directory());
+            LOGGER.warn(message);
+            result.getProblemCollector()
+                    .reportProblem(new DefaultModelProblem(
+                            message,
+                            Severity.WARNING,
+                            Version.V41,
+                            project.getModel().getDelegate(),
+                            -1,
+                            -1,
+                            null));
+            return false; // Don't add duplicate enabled source
+        }
+
+        declaredSources.add(key);
+        LOGGER.debug(
+                "Adding and tracking enabled source: lang={}, scope={}, 
module={}, dir={}",
+                key.language(),
+                key.scope(),
+                key.module(),
+                key.directory());
+        return true; // Add first enabled source with this identity
+    }
+
+    /**
+     * Checks if any enabled sources have been declared for the given language 
and scope combination.
+     *
+     * @param language the language to check (e.g., {@link 
Language#JAVA_FAMILY}, {@link Language#RESOURCES})
+     * @param scope the scope to check (e.g., {@link ProjectScope#MAIN}, 
{@link ProjectScope#TEST})
+     * @return true if at least one enabled source exists for this combination
+     */
+    boolean hasSources(Language language, ProjectScope scope) {
+        return declaredSources.stream().anyMatch(key -> 
language.equals(key.language()) && scope.equals(key.scope()));
+    }
+
+    /**
+     * Fails the build if modular and classic (non-modular) sources are mixed 
within {@code <sources>}.
+     * <p>
+     * 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
+     * project is not supported because the compiler plugin cannot handle such 
configurations.
+     * <p>
+     * This validation checks each (language, scope) combination and reports 
an ERROR if
+     * both modular and non-modular sources are found.
+     */
+    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()
+                        .anyMatch(key ->
+                                language.equals(key.language()) && 
scope.equals(key.scope()) && key.module() != null);
+                boolean hasClassic = declaredSources.stream()
+                        .anyMatch(key ->
+                                language.equals(key.language()) && 
scope.equals(key.scope()) && key.module() == null);
+
+                if (hasModular && hasClassic) {
+                    String message = String.format(
+                            "Mixed modular and classic sources detected for 
lang=%s, scope=%s. "
+                                    + "A project must be either fully modular 
(all sources have a module) "
+                                    + "or fully classic (no sources have a 
module). "
+                                    + "The compiler plugin cannot handle mixed 
configurations.",
+                            language.id(), scope.id());
+                    LOGGER.error(message);
+                    result.getProblemCollector()
+                            .reportProblem(new DefaultModelProblem(
+                                    message,
+                                    Severity.ERROR,
+                                    Version.V41,
+                                    project.getModel().getDelegate(),
+                                    -1,
+                                    -1,
+                                    null));
+                }
+            }
+        }
+    }
+
+    /**
+     * Handles resource configuration for a given scope (main or test).
+     * This method applies the resource priority rules:
+     * <ol>
+     *   <li>Modular project: use resources from {@code <sources>} if present, 
otherwise inject defaults</li>
+     *   <li>Classic project: use resources from {@code <sources>} if present, 
otherwise use legacy resources</li>
+     * </ol>
+     * <p>
+     * The error behavior for conflicting legacy configuration is consistent 
with source directory handling.
+     *
+     * @param scope the project scope (MAIN or TEST)
+     */
+    void handleResourceConfiguration(ProjectScope scope) {
+        boolean hasResourcesInSources = hasSources(Language.RESOURCES, scope);
+
+        List<Resource> resources = scope == ProjectScope.MAIN
+                ? project.getBuild().getDelegate().getResources()
+                : project.getBuild().getDelegate().getTestResources();
+
+        String scopeId = scope.id();
+        String scopeName = scope == ProjectScope.MAIN ? "Main" : "Test";
+        String legacyElement = scope == ProjectScope.MAIN ? "<resources>" : 
"<testResources>";
+        String sourcesConfig = scope == ProjectScope.MAIN
+                ? "<source><lang>resources</lang></source>"
+                : "<source><lang>resources</lang><scope>test</scope></source>";
+
+        if (modularProject) {
+            if (hasResourcesInSources) {
+                // Modular project with resources configured via <sources> - 
already added above
+                if (hasExplicitLegacyResources(resources, scopeId)) {
+                    String message = String.format(
+                            "Legacy %s element cannot be used because %s 
resources are configured via %s in <sources>.",
+                            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 <sources> element, 
ignoring legacy {} element.",
+                            scopeName,
+                            legacyElement);
+                }
+            } else {
+                // Modular project without resources in <sources> - inject 
module-aware defaults
+                if (hasExplicitLegacyResources(resources, scopeId)) {
+                    String message = "Legacy " + legacyElement
+                            + " element cannot be used because modular sources 
are configured. "
+                            + "Use " + sourcesConfig + " in <sources> for 
custom resource paths.";
+                    LOGGER.error(message);
+                    result.getProblemCollector()
+                            .reportProblem(new DefaultModelProblem(
+                                    message,
+                                    Severity.ERROR,
+                                    Version.V41,
+                                    project.getModel().getDelegate(),
+                                    -1,
+                                    -1,
+                                    null));
+                }
+                for (String module : modules) {
+                    project.addSourceRoot(createModularResourceRoot(module, 
scope));
+                }
+                if (!modules.isEmpty()) {
+                    LOGGER.debug(
+                            "Injected {} module-aware {} resource root(s) for 
modules: {}.",
+                            modules.size(),
+                            scopeId,
+                            modules);
+                }
+            }
+        } else {
+            // Classic (non-modular) project
+            if (hasResourcesInSources) {
+                // Resources configured via <sources> - already added above
+                if (hasExplicitLegacyResources(resources, scopeId)) {
+                    String message = String.format(
+                            "Legacy %s element cannot be used because %s 
resources are configured via %s in <sources>.",
+                            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 <sources> element, 
ignoring legacy {} element.",
+                            scopeName,
+                            legacyElement);
+                }
+            } else {
+                // Use legacy resources element
+                LOGGER.debug(
+                        "Using explicit or default {} resources ({} resources 
configured).", scopeId, resources.size());
+                for (Resource resource : resources) {
+                    project.addSourceRoot(new DefaultSourceRoot(baseDir, 
scope, resource));
+                }
+            }
+        }
+    }
+
+    /**
+     * Creates a DefaultSourceRoot for module-aware resource directories.
+     * Generates paths following the pattern: {@code 
src/<module>/<scope>/resources}
+     *
+     * @param module module name
+     * @param scope project scope (main or test)
+     * @return configured DefaultSourceRoot for the module's resources
+     */
+    private DefaultSourceRoot createModularResourceRoot(String module, 
ProjectScope scope) {
+        Path resourceDir =
+                
baseDir.resolve("src").resolve(module).resolve(scope.id()).resolve("resources");
+
+        return new DefaultSourceRoot(
+                scope,
+                Language.RESOURCES,
+                module,
+                null, // targetVersion
+                resourceDir,
+                null, // includes
+                null, // excludes
+                false, // stringFiltering
+                Path.of(module), // targetPath - resources go to 
target/classes/<module>
+                true // enabled
+                );
+    }
+
+    /**
+     * Checks if the given resource list contains explicit legacy resources 
that differ
+     * from Super POM defaults. Super POM defaults are: src/{scope}/resources 
and src/{scope}/resources-filtered
+     *
+     * @param resources list of resources to check
+     * @param scope scope (main or test)
+     * @return true if explicit legacy resources are present that conflict 
with modular sources
+     */
+    private boolean hasExplicitLegacyResources(List<Resource> resources, 
String scope) {
+        if (resources.isEmpty()) {
+            return false; // No resources means no explicit legacy resources 
to warn about
+        }
+
+        // Super POM default paths
+        String defaultPath =
+                
baseDir.resolve("src").resolve(scope).resolve("resources").toString();
+        String defaultFilteredPath = baseDir.resolve("src")
+                .resolve(scope)
+                .resolve("resources-filtered")
+                .toString();
+
+        // Check if any resource differs from Super POM defaults
+        for (Resource resource : resources) {
+            String resourceDir = resource.getDirectory();
+            if (resourceDir != null && !resourceDir.equals(defaultPath) && 
!resourceDir.equals(defaultFilteredPath)) {
+                // Found an explicit legacy resource
+                return true;
+            }
+        }
+
+        return false;
+    }
+}
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 541ac8063c..fac2b79dee 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
@@ -25,10 +25,16 @@
 import java.util.ArrayList;
 import java.util.Collections;
 import java.util.List;
+import java.util.Optional;
 import java.util.Properties;
+import java.util.Set;
 import java.util.concurrent.atomic.AtomicInteger;
+import java.util.stream.Collectors;
 
 import org.apache.maven.AbstractCoreMavenComponentTestCase;
+import org.apache.maven.api.Language;
+import org.apache.maven.api.ProjectScope;
+import org.apache.maven.api.SourceRoot;
 import org.apache.maven.execution.MavenSession;
 import org.apache.maven.model.Dependency;
 import org.apache.maven.model.InputLocation;
@@ -49,6 +55,7 @@
 import static org.hamcrest.Matchers.hasKey;
 import static org.hamcrest.Matchers.is;
 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;
@@ -381,4 +388,616 @@ void testLocationTrackingResolution() throws Exception {
         assertEquals(
                 "org.apache.maven.its:parent:0.1", 
pluginLocation.getSource().getModelId());
     }
+    /**
+     * Tests that a project with multiple modules defined in sources is 
detected as modular,
+     * and module-aware resource roots are injected for each module.
+     * <p>
+     * Acceptance Criterion: AC2 (unified source tracking for all lang/scope 
combinations)
+     *
+     * @see <a href="https://github.com/apache/maven/issues/11612";>Issue 
#11612</a>
+     */
+    @Test
+    void testModularSourcesInjectResourceRoots() throws Exception {
+        File pom = getProject("modular-sources");
+
+        MavenSession session = createMavenSession(pom);
+        MavenProject project = session.getCurrentProject();
+
+        // Get all resource source roots for main scope
+        List<SourceRoot> mainResourceRoots = 
project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
+                .toList();
+
+        // Should have resource roots for both modules
+        Set<String> modules = mainResourceRoots.stream()
+                .map(SourceRoot::module)
+                .flatMap(Optional::stream)
+                .collect(Collectors.toSet());
+
+        assertEquals(2, modules.size(), "Should have resource roots for 2 
modules");
+        assertTrue(modules.contains("org.foo.moduleA"), "Should have resource 
root for moduleA");
+        assertTrue(modules.contains("org.foo.moduleB"), "Should have resource 
root for moduleB");
+
+        // Get all resource source roots for test scope
+        List<SourceRoot> testResourceRoots = 
project.getEnabledSourceRoots(ProjectScope.TEST, Language.RESOURCES)
+                .toList();
+
+        // Should have test resource roots for both modules
+        Set<String> testModules = testResourceRoots.stream()
+                .map(SourceRoot::module)
+                .flatMap(Optional::stream)
+                .collect(Collectors.toSet());
+
+        assertEquals(2, testModules.size(), "Should have test resource roots 
for 2 modules");
+        assertTrue(testModules.contains("org.foo.moduleA"), "Should have test 
resource root for moduleA");
+        assertTrue(testModules.contains("org.foo.moduleB"), "Should have test 
resource root for moduleB");
+    }
+
+    /**
+     * Tests that when modular sources are configured alongside explicit 
legacy resources, an error is raised.
+     * <p>
+     * This verifies the behavior described in the design:
+     * - Modular projects with explicit legacy {@code <resources>} 
configuration should raise an error
+     * - The modular resource roots are injected instead of using the legacy 
configuration
+     * <p>
+     * Acceptance Criteria:
+     * - AC2 (unified source tracking for all lang/scope combinations)
+     * - AC8 (legacy directories error - supersedes AC7 which originally used 
WARNING)
+     *
+     * @see <a href="https://github.com/apache/maven/issues/11612";>Issue 
#11612</a>
+     * @see <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3858462609";>AC8 
definition</a>
+     */
+    @Test
+    void testModularSourcesWithExplicitResourcesIssuesError() throws Exception 
{
+        File pom = getProject("modular-sources-with-explicit-resources");
+
+        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 errors are raised for conflicting legacy resources (AC8)
+        List<ModelProblem> 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 (one for 
resources, one for testResources)");
+        assertTrue(
+                errors.stream().anyMatch(e -> 
e.getMessage().contains("<resources>")),
+                "Should error about conflicting <resources>");
+        assertTrue(
+                errors.stream().anyMatch(e -> 
e.getMessage().contains("<testResources>")),
+                "Should error about conflicting <testResources>");
+
+        // Verify modular resources are still injected correctly
+        List<SourceRoot> mainResourceRoots = 
project.getEnabledSourceRoots(ProjectScope.MAIN, Language.RESOURCES)
+                .toList();
+
+        assertEquals(2, mainResourceRoots.size(), "Should have 2 modular 
resource roots (one per module)");
+
+        Set<String> mainModules = mainResourceRoots.stream()
+                .map(SourceRoot::module)
+                .flatMap(Optional::stream)
+                .collect(Collectors.toSet());
+
+        assertEquals(2, mainModules.size(), "Should have resource roots for 2 
modules");
+        assertTrue(mainModules.contains("org.foo.moduleA"), "Should have 
resource root for moduleA");
+        assertTrue(mainModules.contains("org.foo.moduleB"), "Should have 
resource root for moduleB");
+    }
+
+    /**
+     * Tests AC8: ALL legacy directories are rejected when {@code <sources>} 
is configured.
+     * <p>
+     * Modular project with Java in {@code <sources>} for MAIN scope and 
explicit legacy
+     * {@code <sourceDirectory>} that differs from default. The legacy 
directory is rejected
+     * because modular projects cannot use legacy directories (content cannot 
be dispatched
+     * between modules).
+     *
+     * @see <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755";>Issue
 #11701 (AC8/AC9)</a>
+     */
+    @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 <sourceDirectory> (MAIN scope has Java in 
<sources>)
+        List<ModelProblem> 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("<sourceDirectory>"))
+                .toList();
+
+        assertEquals(1, errors.size(), "Should have 1 error for 
<sourceDirectory>");
+
+        // Verify modular source is used, not legacy
+        List<SourceRoot> 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 <testSourceDirectory>} 
even when
+     * {@code <sources>} has NO Java for TEST scope.
+     * <p>
+     * Modular project with NO Java in {@code <sources>} for TEST scope and 
explicit legacy
+     * {@code <testSourceDirectory>} that differs from default. The legacy 
directory is rejected
+     * because modular projects cannot use legacy directories (content cannot 
be dispatched
+     * between modules).
+     *
+     * @see <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755";>Issue
 #11701 (AC8/AC9)</a>
+     */
+    @Test
+    void testModularWithoutTestSourcesRejectsLegacyTestSourceDirectory() 
throws Exception {
+        File pom = 
getProject("modular-no-test-java-with-explicit-test-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 <testSourceDirectory> (modular projects reject all 
legacy directories)
+        List<ModelProblem> 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("<testSourceDirectory>"))
+                .toList();
+
+        assertEquals(1, errors.size(), "Should have 1 error for 
<testSourceDirectory>");
+
+        // No test Java sources (legacy rejected, none in <sources>)
+        List<SourceRoot> 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 <sources>} has Java for that scope.
+     * <p>
+     * This test uses a non-modular project (no {@code <module>} attribute) 
with both:
+     * <ul>
+     *   <li>{@code <sources>} with main and test Java sources</li>
+     *   <li>Explicit {@code <sourceDirectory>} and {@code 
<testSourceDirectory>} (conflicting)</li>
+     * </ul>
+     * Both legacy directories should trigger ERROR because {@code <sources>} 
has Java.
+     *
+     * @see <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755";>Issue
 #11701 (AC8/AC9)</a>
+     */
+    @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<ModelProblem> 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 (one for 
sourceDirectory, one for testSourceDirectory)");
+
+        // Verify error messages mention the conflicting elements
+        assertTrue(
+                errors.stream().anyMatch(e -> 
e.getMessage().contains("<sourceDirectory>")),
+                "Should have error for <sourceDirectory>");
+        assertTrue(
+                errors.stream().anyMatch(e -> 
e.getMessage().contains("<testSourceDirectory>")),
+                "Should have error for <testSourceDirectory>");
+    }
+
+    /**
+     * Tests AC9: Non-modular project with only resources in {@code <sources>} 
uses implicit Java fallback.
+     * <p>
+     * When {@code <sources>} contains only resources (no Java sources), the 
legacy
+     * {@code <sourceDirectory>} and {@code <testSourceDirectory>} are used as 
implicit fallback.
+     * This enables incremental adoption of {@code <sources>} - customize 
resources while
+     * keeping the default Java directory structure.
+     *
+     * @see <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755";>Issue
 #11701 (AC8/AC9)</a>
+     */
+    @Test
+    void testNonModularResourcesOnlyWithImplicitJavaFallback() throws 
Exception {
+        File pom = getProject("non-modular-resources-only");
+
+        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<ModelProblem> 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 <sources> are used
+        List<SourceRoot> 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 <sources>");
+
+        // Verify legacy Java directories are used as fallback
+        List<SourceRoot> 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");
+
+        List<SourceRoot> 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");
+    }
+
+    /**
+     * Tests AC9 violation: Non-modular project with only resources in {@code 
<sources>} and explicit legacy directories.
+     * <p>
+     * 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 <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755";>Issue
 #11701 (AC8/AC9)</a>
+     */
+    @Test
+    void testNonModularResourcesOnlyWithExplicitLegacyDirectoriesRejected() 
throws Exception {
+        File pom = getProject("non-modular-resources-only-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);
+
+        MavenProject project = result.getProject();
+
+        // Verify ERRORs for explicit legacy directories (differ from default)
+        List<ModelProblem> 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("<sourceDirectory>")),
+                "Should error about <sourceDirectory>");
+        assertTrue(
+                errors.stream().anyMatch(e -> 
e.getMessage().contains("<testSourceDirectory>")),
+                "Should error about <testSourceDirectory>");
+
+        // Verify resources from <sources> are still used
+        List<SourceRoot> 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 <sources>");
+
+        // Verify NO Java source roots (legacy was rejected, none in <sources>)
+        List<SourceRoot> mainJavaRoots = 
project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
+                .toList();
+        assertEquals(0, mainJavaRoots.size(), "Should have no main Java 
sources (legacy rejected)");
+
+        List<SourceRoot> testJavaRoots = 
project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
+                .toList();
+        assertEquals(0, testJavaRoots.size(), "Should have no test Java 
sources (legacy rejected)");
+    }
+
+    /**
+     * Tests AC8: Modular project with Java in {@code <sources>} and physical 
default legacy directories.
+     * <p>
+     * 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/<module>/main/java},
+     * so content in {@code src/main/java} would be silently ignored.
+     *
+     * @see <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755";>Issue
 #11701 (AC8/AC9)</a>
+     */
+    @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<ModelProblem> 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 <sources>} and 
physical default legacy directories.
+     * <p>
+     * Even when {@code <sources>} 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 <a 
href="https://github.com/apache/maven/issues/11701#issuecomment-3897961755";>Issue
 #11701 (AC8/AC9)</a>
+     */
+    @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<ModelProblem> 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");
+    }
+
+    /**
+     * Tests that mixing modular and non-modular sources within {@code 
<sources>} is not allowed.
+     * <p>
+     * A project must be either fully modular (all sources have a module) or 
fully classic
+     * (no sources have a module). Mixing them within the same project is not 
supported
+     * because the compiler plugin cannot handle such configurations.
+     * <p>
+     * This verifies:
+     * - An ERROR is reported when both modular and non-modular sources exist 
in {@code <sources>}
+     * - sourceDirectory is not used because {@code <sources>} exists
+     * <p>
+     * Acceptance Criteria:
+     * - AC1 (boolean flags eliminated - uses hasSources() for source 
detection)
+     * - AC6 (mixed sources error - mixing modular and classic sources within 
{@code <sources>}
+     *   triggers an ERROR)
+     *
+     * @see <a href="https://github.com/apache/maven/issues/11612";>Issue 
#11612</a>
+     */
+    @Test
+    void testSourcesMixedModulesWithinSources() throws Exception {
+        File pom = getProject("sources-mixed-modules");
+
+        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 an ERROR is reported for mixing modular and non-modular 
sources
+        List<ModelProblem> errors = result.getProblems().stream()
+                .filter(p -> p.getSeverity() == ModelProblem.Severity.ERROR)
+                .filter(p -> p.getMessage().contains("Mixed modular and 
classic sources"))
+                .toList();
+
+        assertEquals(1, errors.size(), "Should have 1 error for mixed 
modular/classic configuration");
+        assertTrue(errors.get(0).getMessage().contains("lang=java"), "Error 
should mention java language");
+        assertTrue(errors.get(0).getMessage().contains("scope=main"), "Error 
should mention main scope");
+    }
+
+    /**
+     * Tests that multiple source directories for the same (lang, scope, 
module) combination
+     * are allowed and all are added as source roots.
+     * <p>
+     * This is a valid use case for Phase 2: users may have generated sources 
alongside regular sources,
+     * both belonging to the same module. Different directories = different 
identities = not duplicates.
+     * <p>
+     * Acceptance Criterion: AC2 (unified source tracking - multiple 
directories per module supported)
+     *
+     * @see <a href="https://github.com/apache/maven/issues/11612";>Issue 
#11612</a>
+     */
+    @Test
+    void testMultipleDirectoriesSameModule() throws Exception {
+        File pom = getProject("multiple-directories-same-module");
+
+        MavenSession session = createMavenSession(pom);
+        MavenProject project = session.getCurrentProject();
+
+        // Get main Java source roots
+        List<SourceRoot> mainJavaRoots = 
project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
+                .toList();
+
+        // Should have 2 main sources: both for com.example.app but different 
directories
+        assertEquals(2, mainJavaRoots.size(), "Should have 2 main Java source 
roots for same module");
+
+        // Both should be for the same module
+        long moduleCount = mainJavaRoots.stream()
+                .filter(sr -> 
"com.example.app".equals(sr.module().orElse(null)))
+                .count();
+        assertEquals(2, moduleCount, "Both main sources should be for 
com.example.app module");
+
+        // One should be implicit directory, one should be generated-sources
+        boolean hasImplicitDir = mainJavaRoots.stream().anyMatch(sr -> 
sr.directory()
+                .toString()
+                .replace(File.separatorChar, '/')
+                .contains("src/com.example.app/main/java"));
+        boolean hasGeneratedDir = mainJavaRoots.stream().anyMatch(sr -> 
sr.directory()
+                .toString()
+                .replace(File.separatorChar, '/')
+                .contains("target/generated-sources/com.example.app/java"));
+
+        assertTrue(hasImplicitDir, "Should have implicit source directory for 
module");
+        assertTrue(hasGeneratedDir, "Should have generated-sources directory 
for module");
+
+        // Get test Java source roots
+        List<SourceRoot> testJavaRoots = 
project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
+                .toList();
+
+        // Should have 2 test sources: both for com.example.app
+        assertEquals(2, testJavaRoots.size(), "Should have 2 test Java source 
roots for same module");
+
+        // Both test sources should be for the same module
+        long testModuleCount = testJavaRoots.stream()
+                .filter(sr -> 
"com.example.app".equals(sr.module().orElse(null)))
+                .count();
+        assertEquals(2, testModuleCount, "Both test sources should be for 
com.example.app module");
+    }
+
+    /**
+     * Tests duplicate handling with enabled discriminator.
+     * <p>
+     * Test scenario:
+     * - Same (lang, scope, module, directory) with enabled=true appearing 
twice → triggers WARNING
+     * - Same identity with enabled=false → should be filtered out (disabled 
sources are no-ops)
+     * - Different modules should be added normally
+     * <p>
+     * Verifies:
+     * - First enabled source wins, subsequent duplicates trigger WARNING
+     * - Disabled sources don't count as duplicates
+     * - Different modules are unaffected
+     * <p>
+     * Acceptance Criteria:
+     * - AC3 (duplicate detection - duplicates trigger WARNING)
+     * - AC4 (first enabled wins - duplicates are skipped)
+     * - AC5 (disabled sources unchanged - still added but filtered by 
getEnabledSourceRoots)
+     *
+     * @see <a href="https://github.com/apache/maven/issues/11612";>Issue 
#11612</a>
+     */
+    @Test
+    void testDuplicateEnabledSources() throws Exception {
+        File pom = getProject("duplicate-enabled-sources");
+
+        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 warnings are issued for duplicate enabled sources
+        List<ModelProblem> duplicateWarnings = result.getProblems().stream()
+                .filter(p -> p.getSeverity() == ModelProblem.Severity.WARNING)
+                .filter(p -> p.getMessage().contains("Duplicate enabled 
source"))
+                .toList();
+
+        // We have 2 duplicate pairs: main scope and test scope for 
com.example.dup
+        assertEquals(2, duplicateWarnings.size(), "Should have 2 duplicate 
warnings (main and test scope)");
+
+        // Get main Java source roots
+        List<SourceRoot> mainJavaRoots = 
project.getEnabledSourceRoots(ProjectScope.MAIN, Language.JAVA_FAMILY)
+                .toList();
+
+        // Should have 2 main sources: 1 for com.example.dup (first wins) + 1 
for com.example.other
+        // Note: MavenProject.addSourceRoot still adds all sources, but 
tracking only counts first enabled
+        assertEquals(2, mainJavaRoots.size(), "Should have 2 main Java source 
roots");
+
+        // Verify com.example.other module is present
+        boolean hasOtherModule = mainJavaRoots.stream()
+                .anyMatch(sr -> 
"com.example.other".equals(sr.module().orElse(null)));
+        assertTrue(hasOtherModule, "Should have source root for 
com.example.other module");
+
+        // Verify com.example.dup module is present (first enabled wins)
+        boolean hasDupModule = mainJavaRoots.stream()
+                .anyMatch(sr -> 
"com.example.dup".equals(sr.module().orElse(null)));
+        assertTrue(hasDupModule, "Should have source root for com.example.dup 
module");
+
+        // Get test Java source roots
+        List<SourceRoot> testJavaRoots = 
project.getEnabledSourceRoots(ProjectScope.TEST, Language.JAVA_FAMILY)
+                .toList();
+
+        // Test scope has 1 source for com.example.dup (first wins)
+        assertEquals(1, testJavaRoots.size(), "Should have 1 test Java source 
root");
+
+        // Verify it's for the dup module
+        assertEquals(
+                "com.example.dup",
+                testJavaRoots.get(0).module().orElse(null),
+                "Test source root should be for com.example.dup module");
+    }
 }
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 0000000000..0c5726393a
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/classic-sources-with-explicit-legacy/pom.xml
@@ -0,0 +1,30 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>classic-sources-explicit-legacy-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Classic (non-modular) source - no module attribute -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <directory>src/main/java</directory>
+            </source>
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <directory>src/test/java</directory>
+            </source>
+        </sources>
+        <!-- Explicit legacy directories that conflict with <sources> - should 
trigger ERROR (AC9) -->
+        <sourceDirectory>src/legacy/main/java</sourceDirectory>
+        <testSourceDirectory>src/legacy/test/java</testSourceDirectory>
+    </build>
+</project>
diff --git 
a/impl/maven-core/src/test/projects/project-builder/duplicate-enabled-sources/pom.xml
 
b/impl/maven-core/src/test/projects/project-builder/duplicate-enabled-sources/pom.xml
new file mode 100644
index 0000000000..42d48ddcdc
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/duplicate-enabled-sources/pom.xml
@@ -0,0 +1,64 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests duplicate handling with the enabled discriminator.
+
+  Expected behavior:
+  - Same (lang, scope, module, directory) with enabled=true appearing twice 
should trigger a WARNING
+  - Same identity with enabled=false is harmless (explicitly disabled)
+  - First enabled=true entry wins and defines the effective configuration
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>duplicate-enabled-sources-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- First enabled source for module (this should win) -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>com.example.dup</module>
+                <enabled>true</enabled>
+            </source>
+            <!-- Duplicate enabled source for same module (implicit directory) 
- should trigger WARNING -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>com.example.dup</module>
+                <enabled>true</enabled>
+            </source>
+            <!-- Disabled source for same module - this is harmless -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>com.example.dup</module>
+                <enabled>false</enabled>
+            </source>
+            <!-- Different module - should be added normally -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>com.example.other</module>
+            </source>
+            <!-- Test scope duplicate scenario -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>com.example.dup</module>
+                <enabled>true</enabled>
+            </source>
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>com.example.dup</module>
+                <enabled>true</enabled>
+            </source>
+        </sources>
+    </build>
+</project>
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 0000000000..dfab4a6b3f
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/modular-java-with-explicit-source-dir/pom.xml
@@ -0,0 +1,33 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests AC8 scenario 2: Modular project with Java in <sources> for MAIN scope
+  and explicit legacy <sourceDirectory> that differs from default.
+
+  Expected behavior:
+  - ERROR for <sourceDirectory> (modular project, <sources> has Java for MAIN)
+  - Modular Java sources are used, not legacy
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>modular-java-with-explicit-source-dir-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <!-- Explicit legacy sourceDirectory - triggers ERROR (AC8) -->
+        <sourceDirectory>src/custom/main/java</sourceDirectory>
+
+        <sources>
+            <!-- Modular main Java source -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.app</module>
+            </source>
+        </sources>
+    </build>
+</project>
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 0000000000..dd27dac586
--- /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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests AC8 scenario 5: Modular project with NO Java in <sources> for TEST 
scope
+  and explicit legacy <testSourceDirectory> that differs from default.
+
+  Expected behavior:
+  - ERROR for <testSourceDirectory> (modular project, no AC9 fallback)
+  - No test Java sources (legacy rejected, none in <sources>)
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    
<artifactId>modular-no-test-java-with-explicit-test-source-dir-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <!-- Explicit legacy testSourceDirectory - triggers ERROR (AC8, no AC9 
fallback for modular) -->
+        <testSourceDirectory>src/custom/test/java</testSourceDirectory>
+
+        <sources>
+            <!-- Modular main Java sources only - no test Java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.app</module>
+            </source>
+            <!-- No test Java sources in <sources> -->
+        </sources>
+    </build>
+</project>
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 0000000000..92f8cb52a4
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/modular-resources-only-with-physical-legacy/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests AC8: Modular project with only resources in <sources> and physical 
default legacy directory.
+
+  Expected behavior:
+  - Modular resources from <sources> are processed
+  - Physical src/main/java and src/test/java directories exist but would be 
ignored
+  - ERROR is raised because physical default directories exist (AC8 physical 
presence check)
+  - Unlike non-modular projects (AC9), modular projects cannot use legacy 
directories as fallback
+
+  Note: The src/main/java and src/test/java directories are created empty to 
trigger
+  the physical presence check.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>modular-resources-only-with-physical-legacy-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <!-- No explicit sourceDirectory/testSourceDirectory - uses Super POM 
defaults -->
+        <!-- But physical src/main/java and src/test/java directories exist -->
+
+        <sources>
+            <!-- Only modular resources - no Java sources -->
+            <source>
+                <scope>main</scope>
+                <lang>resources</lang>
+                <module>org.example.app</module>
+            </source>
+            <source>
+                <scope>test</scope>
+                <lang>resources</lang>
+                <module>org.example.app</module>
+            </source>
+        </sources>
+    </build>
+</project>
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 0000000000..e69de29bb2
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 0000000000..e69de29bb2
diff --git 
a/impl/maven-core/src/test/projects/project-builder/modular-sources-with-explicit-resources/pom.xml
 
b/impl/maven-core/src/test/projects/project-builder/modular-sources-with-explicit-resources/pom.xml
new file mode 100644
index 0000000000..d2bd1a614b
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/modular-sources-with-explicit-resources/pom.xml
@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>modular-sources-explicit-resources-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Module A - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+            <!-- Module B - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleB</module>
+            </source>
+        </sources>
+        <!-- Legacy resources are IGNORED when modular sources are configured 
- a warning should be issued -->
+        <resources>
+            <resource>
+                <directory>src/custom/resources</directory>
+            </resource>
+        </resources>
+        <testResources>
+            <testResource>
+                <directory>src/custom/test-resources</directory>
+            </testResource>
+        </testResources>
+    </build>
+</project>
\ No newline at end of file
diff --git 
a/impl/maven-core/src/test/projects/project-builder/modular-sources/pom.xml 
b/impl/maven-core/src/test/projects/project-builder/modular-sources/pom.xml
new file mode 100644
index 0000000000..2f9b1e7b03
--- /dev/null
+++ b/impl/maven-core/src/test/projects/project-builder/modular-sources/pom.xml
@@ -0,0 +1,40 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>modular-sources-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Module A - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+            <!-- Module A - test java -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+            <!-- Module B - main java -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleB</module>
+            </source>
+            <!-- Module B - test java -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleB</module>
+            </source>
+        </sources>
+    </build>
+</project>
\ No newline at end of file
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 0000000000..27267c0bc2
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/modular-with-physical-legacy/pom.xml
@@ -0,0 +1,41 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests AC8: Modular project with physical default legacy directory.
+
+  Expected behavior:
+  - Modular Java sources from <sources> are processed
+  - Physical src/main/java and src/test/java directories exist but would be 
ignored
+  - ERROR is raised because physical default directories exist (AC8 physical 
presence check)
+
+  Note: The src/main/java and src/test/java directories are created empty to 
trigger
+  the physical presence check.
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>modular-with-physical-legacy-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <!-- No explicit sourceDirectory/testSourceDirectory - uses Super POM 
defaults -->
+        <!-- But physical src/main/java and src/test/java directories exist -->
+
+        <sources>
+            <!-- Modular Java sources -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.example.app</module>
+            </source>
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>org.example.app</module>
+            </source>
+        </sources>
+    </build>
+</project>
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 0000000000..e69de29bb2
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 0000000000..e69de29bb2
diff --git 
a/impl/maven-core/src/test/projects/project-builder/multiple-directories-same-module/pom.xml
 
b/impl/maven-core/src/test/projects/project-builder/multiple-directories-same-module/pom.xml
new file mode 100644
index 0000000000..a1128eaa56
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/multiple-directories-same-module/pom.xml
@@ -0,0 +1,51 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests multiple source directories for the same (lang, scope, module) 
combination.
+  This is a valid use case: users may have generated sources alongside regular 
sources.
+
+  Expected behavior:
+  - Both directories should be added as source roots
+  - Both should have the same module attribute
+  - The compiler will merge sources from both directories into the same output
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>multiple-directories-same-module-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Primary Java source for module (uses implicit directory) -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>com.example.app</module>
+            </source>
+            <!-- Generated sources for the same module (explicit directory) -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>com.example.app</module>
+                
<directory>target/generated-sources/com.example.app/java</directory>
+            </source>
+            <!-- Test sources for module -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>com.example.app</module>
+            </source>
+            <!-- Generated test sources -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>com.example.app</module>
+                
<directory>target/generated-test-sources/com.example.app/java</directory>
+            </source>
+        </sources>
+    </build>
+</project>
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 0000000000..2bb12cd7a6
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only-explicit-legacy/pom.xml
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests AC9 violation: Non-modular project with only resources in <sources> 
and explicit legacy directories.
+
+  AC9 allows implicit fallback to legacy directories (when they match 
defaults).
+  This test verifies that explicit configuration (differs from default) is 
rejected.
+
+  Expected behavior:
+  - Resources from <sources> are used
+  - ERROR for sourceDirectory and testSourceDirectory because they differ from 
defaults
+  - No Java source roots are added (legacy rejected, none in <sources>)
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>non-modular-resources-only-explicit-legacy-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Only resources configured - no Java sources -->
+            <source>
+                <scope>main</scope>
+                <lang>resources</lang>
+                <directory>src/main/custom-resources</directory>
+            </source>
+            <source>
+                <scope>test</scope>
+                <lang>resources</lang>
+                <directory>src/test/custom-resources</directory>
+            </source>
+            <!-- No <source lang="java"> elements -->
+        </sources>
+        <!-- Explicit legacy directories - triggers ERROR (differ from 
default) -->
+        <sourceDirectory>src/custom/main/java</sourceDirectory>
+        <testSourceDirectory>src/custom/test/java</testSourceDirectory>
+    </build>
+</project>
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 0000000000..12eee4001d
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/non-modular-resources-only/pom.xml
@@ -0,0 +1,38 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests AC9: Non-modular project with only resources in <sources>.
+
+  Expected behavior:
+  - Resources from <sources> are used
+  - Legacy sourceDirectory/testSourceDirectory are used as implicit fallback 
(AC9)
+  - No errors since <sources> doesn't have Java for these scopes
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>non-modular-resources-only-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <sources>
+            <!-- Only resources configured - no Java sources -->
+            <source>
+                <scope>main</scope>
+                <lang>resources</lang>
+                <directory>src/main/custom-resources</directory>
+            </source>
+            <source>
+                <scope>test</scope>
+                <lang>resources</lang>
+                <directory>src/test/custom-resources</directory>
+            </source>
+            <!-- No <source lang="java"> elements -->
+        </sources>
+        <!-- Legacy directories should be used as fallback (AC9) -->
+        <!-- Using Super POM defaults: src/main/java and src/test/java -->
+    </build>
+</project>
diff --git 
a/impl/maven-core/src/test/projects/project-builder/sources-mixed-modules/pom.xml
 
b/impl/maven-core/src/test/projects/project-builder/sources-mixed-modules/pom.xml
new file mode 100644
index 0000000000..0c658483c0
--- /dev/null
+++ 
b/impl/maven-core/src/test/projects/project-builder/sources-mixed-modules/pom.xml
@@ -0,0 +1,49 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+  Tests that mixing modular and non-modular sources within <sources> is NOT 
allowed.
+
+  Configuration:
+  - Some <source> elements WITH module (modular)
+  - Some <source> elements WITHOUT module (classic)
+
+  Expected behavior:
+  - An ERROR is reported because mixing modular and classic sources is not 
supported
+  - The compiler plugin cannot handle mixed configurations
+  - A project must be either fully modular or fully classic
+-->
+<project xmlns="http://maven.apache.org/POM/4.1.0";
+         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance";
+         xsi:schemaLocation="http://maven.apache.org/POM/4.1.0 
https://maven.apache.org/xsd/maven-4.1.0.xsd";>
+    <modelVersion>4.1.0</modelVersion>
+
+    <groupId>org.apache.maven.tests</groupId>
+    <artifactId>sources-mixed-modules-test</artifactId>
+    <version>1.0-SNAPSHOT</version>
+    <packaging>jar</packaging>
+
+    <build>
+        <!-- This should be IGNORED because <sources> has main java -->
+        <sourceDirectory>src/should-be-ignored/java</sourceDirectory>
+
+        <sources>
+            <!-- Modular source -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+            <!-- Non-modular source (no module attribute) - mixing with 
modular triggers ERROR -->
+            <source>
+                <scope>main</scope>
+                <lang>java</lang>
+                <!-- No module - this creates a mixed configuration which is 
not allowed -->
+            </source>
+            <!-- Another modular source for test -->
+            <source>
+                <scope>test</scope>
+                <lang>java</lang>
+                <module>org.foo.moduleA</module>
+            </source>
+        </sources>
+    </build>
+</project>


Reply via email to