diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java index 42f8758c14c1..f5605f13c7a6 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/LookupInvoker.java @@ -76,6 +76,7 @@ import org.apache.maven.eventspy.internal.EventSpyDispatcher; import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.impl.SettingsUtilsV4; +import org.apache.maven.internal.build.DefaultDiagnosticCollector; import org.apache.maven.jline.FastTerminal; import org.apache.maven.jline.MessageUtils; import org.apache.maven.logging.BuildEventListener; @@ -747,6 +748,13 @@ protected Runnable settings(C context, boolean emitSettingsWarnings, SettingsBui } } context.logger.info(""); + + // Pipe structured problems directly to DiagnosticCollector so that + // key, suggestion, documentationUrl, and source location are preserved + // in the build report (instead of being lost to plain-text logging). + // This runs before SessionStarted, so the SLF4J auto-collection hook + // is not active yet — no double-counting risk. + pipeSettingsProblems(context, settingsResult); } return () -> { context.installationSettingsPath = null; @@ -758,6 +766,20 @@ protected Runnable settings(C context, boolean emitSettingsWarnings, SettingsBui }; } + /** + * Pipes structured settings validation problems to the DiagnosticCollector. + * This preserves key, suggestion, documentationUrl, and source location + * that would otherwise be lost when problems are logged as plain text. + */ + private void pipeSettingsProblems(C context, SettingsBuilderResult settingsResult) { + context.lookup.lookupOptional(DefaultDiagnosticCollector.class).ifPresent(collector -> { + for (BuilderProblem problem : + settingsResult.getProblems().problems().toList()) { + collector.report(problem); + } + }); + } + protected void customizeSettingsRequest(C context, SettingsBuilderRequest settingsBuilderRequest) throws Exception {} diff --git a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java index 3924977e56e3..7a3cadce8624 100644 --- a/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java +++ b/impl/maven-cli/src/main/java/org/apache/maven/cling/invoker/mvn/MavenInvoker.java @@ -69,6 +69,7 @@ import org.apache.maven.execution.MavenExecutionResult; import org.apache.maven.execution.ProfileActivation; import org.apache.maven.execution.ProjectActivation; +import org.apache.maven.internal.build.DefaultDiagnosticCollector; import org.apache.maven.jline.MessageUtils; import org.apache.maven.lifecycle.LifecycleExecutionException; import org.apache.maven.logging.BuildEventListener; @@ -220,6 +221,17 @@ protected void toolchains(MavenContext context, MavenExecutionRequest request) t } context.logger.info(""); + + // Pipe structured problems directly to DiagnosticCollector so that + // key, suggestion, documentationUrl, and source location are preserved + // in the build report. This runs before SessionStarted, so the SLF4J + // auto-collection hook is not active yet — no double-counting risk. + context.lookup.lookupOptional(DefaultDiagnosticCollector.class).ifPresent(collector -> { + for (BuilderProblem problem : + toolchainsResult.getProblems().problems().toList()) { + collector.report(problem); + } + }); } } diff --git a/impl/maven-core/pom.xml b/impl/maven-core/pom.xml index 07ffa6662b12..efec4c8c0975 100644 --- a/impl/maven-core/pom.xml +++ b/impl/maven-core/pom.xml @@ -396,6 +396,8 @@ under the License. org.apache.maven.toolchain.ToolchainManagerPrivate org.apache.maven.toolchain.ToolchainPrivate org.apache.maven.toolchain.ToolchainsBuilder + + org.apache.maven.plugin.PluginValidationManager diff --git a/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java b/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java index 23e85a14b01e..91769faf509e 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java +++ b/impl/maven-core/src/main/java/org/apache/maven/DefaultMaven.java @@ -43,6 +43,7 @@ import org.apache.maven.api.model.Model; import org.apache.maven.api.model.Prerequisites; import org.apache.maven.api.model.Profile; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.api.services.Lookup; import org.apache.maven.api.services.LookupException; import org.apache.maven.artifact.ArtifactUtils; @@ -59,6 +60,7 @@ import org.apache.maven.execution.ProjectDependencyGraph; import org.apache.maven.graph.GraphBuilder; import org.apache.maven.graph.ProjectSelector; +import org.apache.maven.internal.build.DefaultDiagnosticCollector; import org.apache.maven.internal.impl.DefaultSessionFactory; import org.apache.maven.internal.impl.InternalMavenSession; import org.apache.maven.lifecycle.LifecycleExecutionException; @@ -113,6 +115,8 @@ public class DefaultMaven implements Maven { private final ProjectSelector projectSelector; + private final DefaultDiagnosticCollector diagnosticCollector; + @Inject @SuppressWarnings("checkstyle:ParameterNumber") public DefaultMaven( @@ -126,7 +130,8 @@ public DefaultMaven( BuildResumptionDataRepository buildResumptionDataRepository, SuperPomProvider superPomProvider, DefaultSessionFactory defaultSessionFactory, - @Nullable @Named("ide") WorkspaceReader ideWorkspaceReader) { + @Nullable @Named("ide") WorkspaceReader ideWorkspaceReader, + DefaultDiagnosticCollector diagnosticCollector) { this.lookup = lookup; this.eventCatapult = eventCatapult; this.legacySupport = legacySupport; @@ -138,6 +143,7 @@ public DefaultMaven( this.superPomProvider = superPomProvider; this.ideWorkspaceReader = ideWorkspaceReader; this.defaultSessionFactory = defaultSessionFactory; + this.diagnosticCollector = diagnosticCollector; this.projectSelector = new ProjectSelector(); // if necessary switch to DI } @@ -648,6 +654,10 @@ private Result buildGraph(MavenSession session } else { logger.error(problem.getMessage()); } + // Pipe structured problem directly to DiagnosticCollector so that + // source location and severity are preserved in the build report. + // The SLF4J hook excludes this logger to avoid double-counting. + diagnosticCollector.report(toBuilderProblem(problem)); } if (!graphResult.hasErrors()) { @@ -660,6 +670,28 @@ private Result buildGraph(MavenSession session return graphResult; } + /** + * Converts a compat {@link ModelProblem} to the Maven 4 {@link BuilderProblem} API, + * preserving source, line, column, severity, and message. + */ + private static BuilderProblem toBuilderProblem(ModelProblem problem) { + BuilderProblem.Severity severity = + switch (problem.getSeverity()) { + case FATAL -> BuilderProblem.Severity.FATAL; + case ERROR -> BuilderProblem.Severity.ERROR; + default -> BuilderProblem.Severity.WARNING; + }; + return BuilderProblem.builder() + .source(problem.getSource()) + .lineNumber(problem.getLineNumber()) + .columnNumber(problem.getColumnNumber()) + .exception(problem.getException()) + .message(problem.getMessage()) + .severity(severity) + .key("model:" + problem.getMessage().hashCode()) + .build(); + } + @Deprecated // 5 January 2014 protected Logger getLogger() { diff --git a/impl/maven-core/src/main/java/org/apache/maven/internal/build/BuildReportCollector.java b/impl/maven-core/src/main/java/org/apache/maven/internal/build/BuildReportCollector.java index 8df72cc4eadc..6d85b8393e3f 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/internal/build/BuildReportCollector.java +++ b/impl/maven-core/src/main/java/org/apache/maven/internal/build/BuildReportCollector.java @@ -92,6 +92,19 @@ public final class BuildReportCollector extends AbstractEventSpy { private static final int MAX_STACKTRACE_LINES = 30; + /** + * Logger names excluded from SLF4J auto-collection because these classes + * already pipe structured {@link org.apache.maven.api.services.BuilderProblem} + * objects directly to the {@link DefaultDiagnosticCollector}. Without this + * exclusion, each problem would be counted twice: once from the direct pipe + * and once from the SLF4J WARN interception. + */ + private static final Set EXCLUDED_LOGGERS = Set.of( + BuildReportCollector.class.getName(), + "org.apache.maven.DefaultMaven", + "org.apache.maven.project.collector.DefaultProjectsSelector", + "org.apache.maven.plugin.internal.DefaultPluginValidationManager"); + private final DefaultDiagnosticCollector diagnosticCollector; @Inject @@ -353,10 +366,10 @@ private void captureLogEvent(int level, String loggerName, String message, Throw // Auto-collect WARN-level log events as build problems, giving Maven 3 plugins // automatic deduplication and summary at end of build without code changes. - // Skip our own logger to avoid feedback loops from problem summary printing. - if (level == LocationAwareLogger.WARN_INT - && message != null - && !loggerName.equals(BuildReportCollector.class.getName())) { + // Skip loggers that already pipe structured BuilderProblems directly to the + // DiagnosticCollector (avoiding double-counting), and our own logger to avoid + // feedback loops from problem summary printing. + if (level == LocationAwareLogger.WARN_INT && message != null && !EXCLUDED_LOGGERS.contains(loggerName)) { String syntheticKey = syntheticDiagnosticKey(loggerName, message); diagnosticCollector.report(BuilderProblem.builder() .source(loggerName) diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/PluginValidationManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/PluginValidationManager.java index d65f8dad681a..6c2cc943add1 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/PluginValidationManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/PluginValidationManager.java @@ -18,6 +18,7 @@ */ package org.apache.maven.plugin; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.eclipse.aether.RepositorySystemSession; @@ -54,27 +55,94 @@ enum IssueLocality { * This method should be used in "early" phase of plugin execution, possibly even when plugin or mojo descriptor * does not exist yet. In turn, this method will not record extra information like plugin occurrence or declaration * location as those are not yet available. + * + * @since 4.1.0 */ void reportPluginValidationIssue( - IssueLocality locality, RepositorySystemSession session, Artifact pluginArtifact, String issue); + IssueLocality locality, RepositorySystemSession session, Artifact pluginArtifact, BuilderProblem problem); /** * Reports plugin issues applicable to the plugin as a whole. *

* This method will record extra information as well, like plugin occurrence or declaration location. + * + * @since 4.1.0 */ void reportPluginValidationIssue( - IssueLocality locality, MavenSession mavenSession, MojoDescriptor mojoDescriptor, String issue); + IssueLocality locality, MavenSession mavenSession, MojoDescriptor mojoDescriptor, BuilderProblem problem); /** * Reports plugin Mojo issues applicable to the Mojo itself. *

* This method will record extra information as well, like plugin occurrence or declaration location. + * + * @since 4.1.0 */ void reportPluginMojoValidationIssue( IssueLocality locality, MavenSession mavenSession, MojoDescriptor mojoDescriptor, Class mojoClass, - String issue); + BuilderProblem problem); + + // ---- Deprecated String-based adapters for external callers ---- + + /** + * @deprecated Use {@link #reportPluginValidationIssue(IssueLocality, RepositorySystemSession, Artifact, + * BuilderProblem)} instead. + */ + @Deprecated(since = "4.1.0", forRemoval = true) + default void reportPluginValidationIssue( + IssueLocality locality, RepositorySystemSession session, Artifact pluginArtifact, String issue) { + reportPluginValidationIssue( + locality, + session, + pluginArtifact, + BuilderProblem.builder() + .message(issue) + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:" + issue.hashCode()) + .build()); + } + + /** + * @deprecated Use {@link #reportPluginValidationIssue(IssueLocality, MavenSession, MojoDescriptor, + * BuilderProblem)} instead. + */ + @Deprecated(since = "4.1.0", forRemoval = true) + default void reportPluginValidationIssue( + IssueLocality locality, MavenSession mavenSession, MojoDescriptor mojoDescriptor, String issue) { + reportPluginValidationIssue( + locality, + mavenSession, + mojoDescriptor, + BuilderProblem.builder() + .message(issue) + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:" + issue.hashCode()) + .build()); + } + + /** + * @deprecated Use {@link #reportPluginMojoValidationIssue(IssueLocality, MavenSession, MojoDescriptor, Class, + * BuilderProblem)} instead. + */ + @Deprecated(since = "4.1.0", forRemoval = true) + default void reportPluginMojoValidationIssue( + IssueLocality locality, + MavenSession mavenSession, + MojoDescriptor mojoDescriptor, + Class mojoClass, + String issue) { + reportPluginMojoValidationIssue( + locality, + mavenSession, + mojoDescriptor, + mojoClass, + BuilderProblem.builder() + .message(issue) + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:" + issue.hashCode()) + .build()); + } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/AbstractMavenPluginParametersValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/AbstractMavenPluginParametersValidator.java index 9e7426b168ff..47a772809e37 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/AbstractMavenPluginParametersValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/AbstractMavenPluginParametersValidator.java @@ -18,6 +18,7 @@ */ package org.apache.maven.plugin.internal; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.PluginValidationManager; import org.apache.maven.plugin.descriptor.MojoDescriptor; @@ -97,6 +98,11 @@ protected boolean isIgnoredProperty(String strValue) { protected abstract String getParameterLogReason(Parameter parameter); + /** + * Returns the validation key prefix for this validator (e.g. "deprecated-param", "readonly-param"). + */ + protected abstract String getValidationKeyPrefix(); + protected String formatParameter(Parameter parameter) { StringBuilder stringBuilder = new StringBuilder() .append("Parameter '") @@ -112,4 +118,12 @@ protected String formatParameter(Parameter parameter) { return stringBuilder.toString(); } + + protected BuilderProblem buildParameterProblem(Parameter parameter) { + return BuilderProblem.builder() + .message(formatParameter(parameter)) + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:" + getValidationKeyPrefix() + ":" + parameter.getName()) + .build(); + } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java index 15f3d8df7b64..a95cf34fff83 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultMavenPluginManager.java @@ -51,6 +51,7 @@ import org.apache.maven.api.Service; import org.apache.maven.api.Session; import org.apache.maven.api.plugin.descriptor.Resolution; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.api.services.DependencyResolver; import org.apache.maven.api.services.DependencyResolverResult; import org.apache.maven.api.services.PathScopeRegistry; @@ -751,7 +752,13 @@ private T loadV3Mojo( session, mojoDescriptor, mojo.getClass(), - "Mojo implements `Contextualizable` interface from Plexus Container, which is EOL."); + BuilderProblem.builder() + .message( + "Mojo implements `Contextualizable` interface from Plexus Container, which is EOL.") + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:contextualizable") + .suggestion("Migrate from Contextualizable to javax.inject dependency injection") + .build()); } XmlNode dom = mojoExecution.getConfiguration() != null diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultPluginValidationManager.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultPluginValidationManager.java index 0bd14d1635d1..e445b2978e1f 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultPluginValidationManager.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DefaultPluginValidationManager.java @@ -18,6 +18,7 @@ */ package org.apache.maven.plugin.internal; +import javax.inject.Inject; import javax.inject.Named; import javax.inject.Singleton; @@ -39,9 +40,11 @@ import java.util.stream.Collectors; import org.apache.maven.api.Constants; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.eventspy.AbstractEventSpy; import org.apache.maven.execution.ExecutionEvent; import org.apache.maven.execution.MavenSession; +import org.apache.maven.internal.build.DefaultDiagnosticCollector; import org.apache.maven.model.InputLocation; import org.apache.maven.plugin.PluginValidationManager; import org.apache.maven.plugin.descriptor.MojoDescriptor; @@ -83,6 +86,13 @@ private enum ValidationReportLevel { private final Logger logger = LoggerFactory.getLogger(getClass()); + private final DefaultDiagnosticCollector diagnosticCollector; + + @Inject + DefaultPluginValidationManager(DefaultDiagnosticCollector diagnosticCollector) { + this.diagnosticCollector = diagnosticCollector; + } + @Override public void onEvent(Object event) { if (event instanceof ExecutionEvent executionEvent) { @@ -158,28 +168,30 @@ private void mayReportInline(RepositorySystemSession session, IssueLocality loca @Override public void reportPluginValidationIssue( - IssueLocality locality, RepositorySystemSession session, Artifact pluginArtifact, String issue) { + IssueLocality locality, RepositorySystemSession session, Artifact pluginArtifact, BuilderProblem problem) { String pluginKey = pluginKey(pluginArtifact); if (validationPluginExcludes(session).contains(pluginKey)) { return; } PluginValidationIssues pluginIssues = pluginIssues(session).computeIfAbsent(pluginKey, k -> new PluginValidationIssues()); - pluginIssues.reportPluginIssue(locality, null, issue); - mayReportInline(session, locality, issue); + pluginIssues.reportPluginIssue(locality, null, problem.getMessage()); + diagnosticCollector.report(problem); + mayReportInline(session, locality, problem.getMessage()); } @Override public void reportPluginValidationIssue( - IssueLocality locality, MavenSession mavenSession, MojoDescriptor mojoDescriptor, String issue) { + IssueLocality locality, MavenSession mavenSession, MojoDescriptor mojoDescriptor, BuilderProblem problem) { String pluginKey = pluginKey(mojoDescriptor); if (validationPluginExcludes(mavenSession.getRepositorySession()).contains(pluginKey)) { return; } PluginValidationIssues pluginIssues = pluginIssues(mavenSession.getRepositorySession()) .computeIfAbsent(pluginKey, k -> new PluginValidationIssues()); - pluginIssues.reportPluginIssue(locality, pluginDeclaration(mavenSession, mojoDescriptor), issue); - mayReportInline(mavenSession.getRepositorySession(), locality, issue); + pluginIssues.reportPluginIssue(locality, pluginDeclaration(mavenSession, mojoDescriptor), problem.getMessage()); + diagnosticCollector.report(problem); + mayReportInline(mavenSession.getRepositorySession(), locality, problem.getMessage()); } @Override @@ -188,7 +200,7 @@ public void reportPluginMojoValidationIssue( MavenSession mavenSession, MojoDescriptor mojoDescriptor, Class mojoClass, - String issue) { + BuilderProblem problem) { String pluginKey = pluginKey(mojoDescriptor); if (validationPluginExcludes(mavenSession.getRepositorySession()).contains(pluginKey)) { return; @@ -196,8 +208,12 @@ public void reportPluginMojoValidationIssue( PluginValidationIssues pluginIssues = pluginIssues(mavenSession.getRepositorySession()) .computeIfAbsent(pluginKey, k -> new PluginValidationIssues()); pluginIssues.reportPluginMojoIssue( - locality, pluginDeclaration(mavenSession, mojoDescriptor), mojoInfo(mojoDescriptor, mojoClass), issue); - mayReportInline(mavenSession.getRepositorySession(), locality, issue); + locality, + pluginDeclaration(mavenSession, mojoDescriptor), + mojoInfo(mojoDescriptor, mojoClass), + problem.getMessage()); + diagnosticCollector.report(problem); + mayReportInline(mavenSession.getRepositorySession(), locality, problem.getMessage()); } private void reportSessionCollectedValidationIssues(MavenSession mavenSession) { diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedCoreExpressionValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedCoreExpressionValidator.java index faaac5cf1c53..81029df570fb 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedCoreExpressionValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedCoreExpressionValidator.java @@ -63,6 +63,11 @@ protected String getParameterLogReason(Parameter parameter) { + DEPRECATED_CORE_PARAMETERS.get(parameter.getDefaultValue()); } + @Override + protected String getValidationKeyPrefix() { + return "deprecated-expr"; + } + @Override protected void doValidate( MavenSession mavenSession, @@ -76,9 +81,13 @@ protected void doValidate( mojoDescriptor.getParameters().stream() .filter(this::isDeprecated) - .map(this::formatParameter) - .forEach(m -> pluginValidationManager.reportPluginMojoValidationIssue( - PluginValidationManager.IssueLocality.EXTERNAL, mavenSession, mojoDescriptor, mojoClass, m)); + .map(this::buildParameterProblem) + .forEach(problem -> pluginValidationManager.reportPluginMojoValidationIssue( + PluginValidationManager.IssueLocality.EXTERNAL, + mavenSession, + mojoDescriptor, + mojoClass, + problem)); } private boolean isDeprecated(Parameter parameter) { diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedPluginValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedPluginValidator.java index a4d784c403dc..45c973fcd967 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedPluginValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/DeprecatedPluginValidator.java @@ -22,6 +22,7 @@ import javax.inject.Named; import javax.inject.Singleton; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.api.services.MessageBuilderFactory; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.PluginValidationManager; @@ -52,6 +53,11 @@ protected String getParameterLogReason(Parameter parameter) { return "is deprecated: " + parameter.getDeprecated(); } + @Override + protected String getValidationKeyPrefix() { + return "deprecated-param"; + } + @Override protected void doValidate( MavenSession mavenSession, @@ -65,7 +71,11 @@ protected void doValidate( mavenSession, mojoDescriptor, mojoClass, - logDeprecatedMojo(mojoDescriptor)); + BuilderProblem.builder() + .message(logDeprecatedMojo(mojoDescriptor)) + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:deprecated-goal:" + mojoDescriptor.getGoal()) + .build()); } if (mojoDescriptor.getParameters() != null) { @@ -92,7 +102,7 @@ private void checkParameter( mavenSession, mojoDescriptor, mojoClass, - formatParameter(parameter)); + buildParameterProblem(parameter)); } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven2DependenciesValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven2DependenciesValidator.java index d2f8dd89ce77..05b206b97c0b 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven2DependenciesValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven2DependenciesValidator.java @@ -25,6 +25,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.plugin.PluginValidationManager; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -64,7 +65,12 @@ protected void doValidate( PluginValidationManager.IssueLocality.EXTERNAL, session, pluginArtifact, - "Plugin is a Maven 2.x plugin, which will be not supported in Maven 4.x"); + BuilderProblem.builder() + .message("Plugin is a Maven 2.x plugin, which will be not supported in Maven 4.x") + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:maven2-plugin") + .suggestion("Upgrade to a Maven 3.x/4.x compatible version of this plugin") + .build()); } } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven3CompatDependenciesValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven3CompatDependenciesValidator.java index 0cf8fee6ad35..559d3c189828 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven3CompatDependenciesValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/Maven3CompatDependenciesValidator.java @@ -23,6 +23,7 @@ import javax.inject.Singleton; import org.apache.maven.api.DependencyScope; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.plugin.PluginValidationManager; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -55,7 +56,13 @@ protected void doValidate( PluginValidationManager.IssueLocality.EXTERNAL, session, pluginArtifact, - "Plugin depends on the deprecated Maven 2.x compatibility layer, which will be not supported in Maven 4.x"); + BuilderProblem.builder() + .message( + "Plugin depends on the deprecated Maven 2.x compatibility layer, which will be not supported in Maven 4.x") + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:maven-compat-dep") + .suggestion("Remove the maven-compat dependency from the plugin") + .build()); } } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenMixedDependenciesValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenMixedDependenciesValidator.java index 27003836d3fd..3ae05d3a100a 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenMixedDependenciesValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenMixedDependenciesValidator.java @@ -25,6 +25,7 @@ import java.util.Set; import java.util.stream.Collectors; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.plugin.PluginValidationManager; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -63,7 +64,12 @@ protected void doValidate( PluginValidationManager.IssueLocality.EXTERNAL, session, pluginArtifact, - "Plugin mixes multiple Maven versions: " + mavenVersions); + BuilderProblem.builder() + .message("Plugin mixes multiple Maven versions: " + mavenVersions) + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:mixed-maven-versions") + .suggestion("Align all Maven dependencies to a single version to avoid classloading issues") + .build()); } } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenScopeDependenciesValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenScopeDependenciesValidator.java index 460a91c6ec18..48f78c901102 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenScopeDependenciesValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/MavenScopeDependenciesValidator.java @@ -26,6 +26,7 @@ import java.util.stream.Collectors; import org.apache.maven.api.DependencyScope; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.plugin.PluginValidationManager; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -65,8 +66,15 @@ protected void doValidate( PluginValidationManager.IssueLocality.EXTERNAL, session, pluginArtifact, - "Plugin should declare Maven artifacts in `provided` scope. If the plugin already declares them in `provided` scope, update the maven-plugin-plugin to latest version. Artifacts found with wrong scope: " - + mavenArtifacts); + BuilderProblem.builder() + .message( + "Plugin should declare Maven artifacts in `provided` scope. If the plugin already declares them in `provided` scope, update the maven-plugin-plugin to latest version. Artifacts found with wrong scope: " + + mavenArtifacts) + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:wrong-scope") + .suggestion( + "Change Maven artifact dependencies to 'provided' scope or update maven-plugin-plugin") + .build()); } } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PlexusContainerDefaultDependenciesValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PlexusContainerDefaultDependenciesValidator.java index 31638139a3f7..ba15ab4d80cc 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PlexusContainerDefaultDependenciesValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/PlexusContainerDefaultDependenciesValidator.java @@ -22,6 +22,7 @@ import javax.inject.Named; import javax.inject.Singleton; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.plugin.PluginValidationManager; import org.eclipse.aether.RepositorySystemSession; import org.eclipse.aether.artifact.Artifact; @@ -55,7 +56,12 @@ protected void doValidate( PluginValidationManager.IssueLocality.EXTERNAL, session, pluginArtifact, - "Plugin depends on plexus-container-default, which is EOL"); + BuilderProblem.builder() + .message("Plugin depends on plexus-container-default, which is EOL") + .severity(BuilderProblem.Severity.WARNING) + .key("plugin-validation:plexus-container-eol") + .suggestion("Migrate from plexus-container-default to javax.inject / Eclipse Sisu") + .build()); } } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/ReadOnlyPluginParametersValidator.java b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/ReadOnlyPluginParametersValidator.java index 2989fb5166a2..69e4d9870aff 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/ReadOnlyPluginParametersValidator.java +++ b/impl/maven-core/src/main/java/org/apache/maven/plugin/internal/ReadOnlyPluginParametersValidator.java @@ -47,6 +47,11 @@ protected String getParameterLogReason(Parameter parameter) { return "is read-only, must not be used in configuration"; } + @Override + protected String getValidationKeyPrefix() { + return "readonly-param"; + } + @Override protected void doValidate( MavenSession mavenSession, @@ -79,7 +84,7 @@ private void checkParameter( mavenSession, mojoDescriptor, mojoClass, - formatParameter(parameter)); + buildParameterProblem(parameter)); } } } diff --git a/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java b/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java index 0be344384adf..bd5664d1eba2 100644 --- a/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java +++ b/impl/maven-core/src/main/java/org/apache/maven/project/collector/DefaultProjectsSelector.java @@ -26,7 +26,9 @@ import java.util.ArrayList; import java.util.List; +import org.apache.maven.api.services.BuilderProblem; import org.apache.maven.execution.MavenExecutionRequest; +import org.apache.maven.internal.build.DefaultDiagnosticCollector; import org.apache.maven.model.building.ModelProblem; import org.apache.maven.model.building.ModelProblemUtils; import org.apache.maven.project.MavenProject; @@ -45,10 +47,12 @@ public class DefaultProjectsSelector implements ProjectsSelector { private static final Logger LOGGER = LoggerFactory.getLogger(DefaultProjectsSelector.class); private final ProjectBuilder projectBuilder; + private final DefaultDiagnosticCollector diagnosticCollector; @Inject - public DefaultProjectsSelector(ProjectBuilder projectBuilder) { + public DefaultProjectsSelector(ProjectBuilder projectBuilder, DefaultDiagnosticCollector diagnosticCollector) { this.projectBuilder = projectBuilder; + this.diagnosticCollector = diagnosticCollector; } @Override @@ -83,6 +87,14 @@ public List selectProjects(List files, MavenExecutionRequest LOGGER.warn("{}{}", problem.getMessage(), ((loc != null && !loc.isEmpty()) ? " @ " + loc : "")); } } + + // Pipe structured problems directly to DiagnosticCollector so that + // source location and severity are preserved in the build report + // (instead of being lost to plain-text logging). The SLF4J hook + // excludes this logger to avoid double-counting. + for (ModelProblem problem : result.getProblems()) { + diagnosticCollector.report(toBuilderProblem(problem)); + } } } @@ -100,4 +112,26 @@ public List selectProjects(List files, MavenExecutionRequest return projects; } + + /** + * Converts a compat {@link ModelProblem} to the Maven 4 {@link BuilderProblem} API, + * preserving source, line, column, severity, and message. + */ + private static BuilderProblem toBuilderProblem(ModelProblem problem) { + BuilderProblem.Severity severity = + switch (problem.getSeverity()) { + case FATAL -> BuilderProblem.Severity.FATAL; + case ERROR -> BuilderProblem.Severity.ERROR; + default -> BuilderProblem.Severity.WARNING; + }; + return BuilderProblem.builder() + .source(problem.getSource()) + .lineNumber(problem.getLineNumber()) + .columnNumber(problem.getColumnNumber()) + .exception(problem.getException()) + .message(problem.getMessage()) + .severity(severity) + .key("model:" + problem.getMessage().hashCode()) + .build(); + } } diff --git a/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java b/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java index 72dad0b244d5..6982db4b97a0 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/graph/DefaultGraphBuilderTest.java @@ -101,7 +101,8 @@ class DefaultGraphBuilderTest { private final MavenSession session = mock(MavenSession.class); private final MavenExecutionRequest mavenExecutionRequest = mock(MavenExecutionRequest.class); - private final ProjectsSelector projectsSelector = new DefaultProjectsSelector(projectBuilder); + private final ProjectsSelector projectsSelector = new DefaultProjectsSelector( + projectBuilder, new org.apache.maven.internal.build.DefaultDiagnosticCollector()); // Not using mocks for these strategies - a mock would just copy the actual implementation. diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportCollectorTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportCollectorTest.java index 5733daf74d0a..2358d5d3a9d6 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportCollectorTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportCollectorTest.java @@ -35,7 +35,6 @@ import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenExecutionResult; import org.apache.maven.execution.MavenSession; -import org.apache.maven.impl.DefaultBuilderProblem; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; @@ -334,8 +333,12 @@ void testDiagnosticSuppressionWildcard() { // ---- Test helpers ---- private static BuilderProblem warning(String key, String message, String source) { - return new DefaultBuilderProblem( - source, -1, -1, null, message, BuilderProblem.Severity.WARNING, key, null, null); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.WARNING) + .key(key) + .build(); } private MavenProject createProject(String groupId, String artifactId, String version) { diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportIntegrationTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportIntegrationTest.java index e200cd06b6f8..10f2dc1ae78b 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportIntegrationTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportIntegrationTest.java @@ -35,7 +35,6 @@ import org.apache.maven.execution.MavenExecutionRequest; import org.apache.maven.execution.MavenExecutionResult; import org.apache.maven.execution.MavenSession; -import org.apache.maven.impl.DefaultBuilderProblem; import org.apache.maven.plugin.MojoExecution; import org.apache.maven.plugin.descriptor.MojoDescriptor; import org.apache.maven.plugin.descriptor.PluginDescriptor; @@ -72,21 +71,41 @@ void setUp() { } private static BuilderProblem warning(String key, String message, String source) { - return new DefaultBuilderProblem( - source, -1, -1, null, message, BuilderProblem.Severity.WARNING, key, null, null); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.WARNING) + .key(key) + .build(); } private static BuilderProblem warning(String key, String message, String source, String suggestion, String docUrl) { - return new DefaultBuilderProblem( - source, -1, -1, null, message, BuilderProblem.Severity.WARNING, key, suggestion, docUrl); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.WARNING) + .key(key) + .suggestion(suggestion) + .documentationUrl(docUrl) + .build(); } private static BuilderProblem info(String key, String message, String source) { - return new DefaultBuilderProblem(source, -1, -1, null, message, BuilderProblem.Severity.INFO, key, null, null); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.INFO) + .key(key) + .build(); } private static BuilderProblem error(String key, String message, String source) { - return new DefaultBuilderProblem(source, -1, -1, null, message, BuilderProblem.Severity.ERROR, key, null, null); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.ERROR) + .key(key) + .build(); } /** diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportJsonWriterTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportJsonWriterTest.java index 073072c4a539..831bb8a06115 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportJsonWriterTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/build/BuildReportJsonWriterTest.java @@ -30,7 +30,6 @@ import org.apache.maven.api.build.report.ModuleReport; import org.apache.maven.api.build.report.MojoReport; import org.apache.maven.api.services.BuilderProblem; -import org.apache.maven.impl.DefaultBuilderProblem; import org.junit.jupiter.api.Test; import static org.junit.jupiter.api.Assertions.assertEquals; @@ -334,27 +333,23 @@ void testNullMojoFields() { @Test void testProblemsSerialization() { - BuilderProblem p1 = new DefaultBuilderProblem( - "maven-compiler-plugin:3.15.0:compile", - 42, - 1, - null, - "source/target value 8 is deprecated", - BuilderProblem.Severity.WARNING, - "deprecated-source-target", - "Update to 11 or higher", - "https://example.com/docs/compiler"); - - BuilderProblem p2 = new DefaultBuilderProblem( - "maven-compiler-plugin", - -1, - -1, - null, - "3 errors found", - BuilderProblem.Severity.ERROR, - "compilation-failure", - null, - null); + BuilderProblem p1 = BuilderProblem.builder() + .source("maven-compiler-plugin:3.15.0:compile") + .lineNumber(42) + .columnNumber(1) + .message("source/target value 8 is deprecated") + .severity(BuilderProblem.Severity.WARNING) + .key("deprecated-source-target") + .suggestion("Update to 11 or higher") + .documentationUrl("https://example.com/docs/compiler") + .build(); + + BuilderProblem p2 = BuilderProblem.builder() + .source("maven-compiler-plugin") + .message("3 errors found") + .severity(BuilderProblem.Severity.ERROR) + .key("compilation-failure") + .build(); BuildReport report = new DefaultBuildReport( BuildStatus.FAILURE, diff --git a/impl/maven-core/src/test/java/org/apache/maven/internal/build/DefaultDiagnosticCollectorTest.java b/impl/maven-core/src/test/java/org/apache/maven/internal/build/DefaultDiagnosticCollectorTest.java index 3276c3c533cc..90dfeefe1457 100644 --- a/impl/maven-core/src/test/java/org/apache/maven/internal/build/DefaultDiagnosticCollectorTest.java +++ b/impl/maven-core/src/test/java/org/apache/maven/internal/build/DefaultDiagnosticCollectorTest.java @@ -27,7 +27,6 @@ import java.util.concurrent.Future; import org.apache.maven.api.services.BuilderProblem; -import org.apache.maven.impl.DefaultBuilderProblem; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; @@ -45,16 +44,30 @@ void setUp() { } private static BuilderProblem warning(String key, String message, String source) { - return new DefaultBuilderProblem( - source, -1, -1, null, message, BuilderProblem.Severity.WARNING, key, null, null); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.WARNING) + .key(key) + .build(); } private static BuilderProblem error(String key, String message, String source) { - return new DefaultBuilderProblem(source, -1, -1, null, message, BuilderProblem.Severity.ERROR, key, null, null); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.ERROR) + .key(key) + .build(); } private static BuilderProblem info(String key, String message, String source) { - return new DefaultBuilderProblem(source, -1, -1, null, message, BuilderProblem.Severity.INFO, key, null, null); + return BuilderProblem.builder() + .source(source) + .message(message) + .severity(BuilderProblem.Severity.INFO) + .key(key) + .build(); } @Test @@ -175,16 +188,16 @@ void testSummaryListIsUnmodifiable() { @Test void testFullProblemFields() { - BuilderProblem p = new DefaultBuilderProblem( - "maven-compiler-plugin:3.15.0:compile", - 42, - 15, - null, - "unchecked cast from Object to List", - BuilderProblem.Severity.WARNING, - "unchecked-cast", - "Add @SuppressWarnings(\"unchecked\") or use a type-safe alternative", - "https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html"); + BuilderProblem p = BuilderProblem.builder() + .source("maven-compiler-plugin:3.15.0:compile") + .lineNumber(42) + .columnNumber(15) + .message("unchecked cast from Object to List") + .severity(BuilderProblem.Severity.WARNING) + .key("unchecked-cast") + .suggestion("Add @SuppressWarnings(\"unchecked\") or use a type-safe alternative") + .documentationUrl("https://docs.oracle.com/javase/tutorial/java/generics/rawTypes.html") + .build(); collector.report(p);