From d245591cffaeb48b3881645b49a88468e4910519 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 11:24:22 -0700 Subject: [PATCH 01/20] First take. --- .../integration/DiffMessageFormatter.java | 2 +- .../spotless/maven/AbstractSpotlessMojo.java | 29 ++++++++++++++-- .../spotless/maven/FormatterConfig.java | 9 ++++- .../spotless/maven/SpotlessApplyMojo.java | 34 ++++++++++++++++--- .../spotless/maven/SpotlessCheckMojo.java | 33 +++++++++++++++--- 5 files changed, 93 insertions(+), 14 deletions(-) diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java index a0d056bc0a..d28c969bcc 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java @@ -292,7 +292,7 @@ private static Map.Entry diffWhitespaceLineEndings(String dirty } private static int getLineOfFirstDifference(EditList edits) { - return edits.stream().mapToInt(Edit::getBeginA).min().getAsInt(); + return edits.stream().mapToInt(Edit::getBeginA).min().orElse(0); } private static final CharMatcher NEWLINE_MATCHER = CharMatcher.is('\n'); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index e5a8004f97..886e844640 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -54,6 +54,7 @@ import com.diffplug.spotless.Formatter; import com.diffplug.spotless.Jvm; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.generic.LicenseHeaderStep; import com.diffplug.spotless.maven.antlr4.Antlr4; @@ -213,6 +214,9 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Parameter private UpToDateChecking upToDateChecking = UpToDateChecking.enabled(); + @Parameter + private List lintSuppressions = new ArrayList<>(); + /** * If set to {@code true} will also run on incremental builds (i.e. within Eclipse with m2e). * Otherwise this goal is skipped in incremental builds and only runs on full builds. @@ -220,7 +224,11 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Parameter(defaultValue = "false") protected boolean m2eEnableForIncrementalBuild; - protected abstract void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException; + protected List getLintSuppressions() { + return lintSuppressions; + } + + protected abstract void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, FormatterConfig config) throws MojoExecutionException; private static final int MINIMUM_JRE = 11; @@ -253,7 +261,7 @@ public final void execute() throws MojoExecutionException { for (FormatterFactory factory : formattersHolder.openFormatters.keySet()) { Formatter formatter = formattersHolder.openFormatters.get(factory); Iterable files = formattersHolder.factoryToFiles.get(factory).get(); - process(formattersHolder.nameFor(factory), files, formatter, upToDateChecker); + process(formattersHolder.nameFor(factory), files, formatter, upToDateChecker, config); } } catch (PluginException e) { throw e.asMojoExecutionException(); @@ -378,7 +386,7 @@ private FormatterConfig getFormatterConfig() { FileLocator fileLocator = getFileLocator(); final Optional optionalRatchetFrom = Optional.ofNullable(this.ratchetFrom) .filter(ratchet -> !RATCHETFROM_NONE.equals(ratchet)); - return new FormatterConfig(baseDir, encoding, lineEndings, optionalRatchetFrom, provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory)); + return new FormatterConfig(baseDir, encoding, lineEndings, optionalRatchetFrom, provisioner, fileLocator, formatterStepFactories, Optional.ofNullable(setLicenseHeaderYearsFromGitHistory), lintSuppressions); } private FileLocator getFileLocator() { @@ -416,4 +424,19 @@ private UpToDateChecker createUpToDateChecker(Iterable formatters) { } return UpToDateChecker.wrapWithBuildContext(checker, buildContext); } + + /** + * Returns the relative path between root and dest, or null if dest is not a + * child of root. + */ + static String relativize(File root, File dest) { + String rootPath = root.getAbsolutePath(); + String destPath = dest.getAbsolutePath(); + if (!destPath.startsWith(rootPath)) { + return null; + } else { + String relativized = destPath.substring(rootPath.length()); + return relativized.startsWith("/") || relativized.startsWith("\\") ? relativized.substring(1) : relativized; + } + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java index 52f2a871f3..d1c8f25eaf 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java @@ -22,6 +22,7 @@ import java.util.Optional; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.Provisioner; public class FormatterConfig { @@ -33,9 +34,10 @@ public class FormatterConfig { private final FileLocator fileLocator; private final List globalStepFactories; private final Optional spotlessSetLicenseHeaderYearsFromGitHistory; + private final List lintSuppressions; public FormatterConfig(File baseDir, String encoding, LineEnding lineEndings, Optional ratchetFrom, Provisioner provisioner, - FileLocator fileLocator, List globalStepFactories, Optional spotlessSetLicenseHeaderYearsFromGitHistory) { + FileLocator fileLocator, List globalStepFactories, Optional spotlessSetLicenseHeaderYearsFromGitHistory, List lintSuppressions) { this.encoding = encoding; this.lineEndings = lineEndings; this.ratchetFrom = ratchetFrom; @@ -43,6 +45,7 @@ public FormatterConfig(File baseDir, String encoding, LineEnding lineEndings, Op this.fileLocator = fileLocator; this.globalStepFactories = globalStepFactories; this.spotlessSetLicenseHeaderYearsFromGitHistory = spotlessSetLicenseHeaderYearsFromGitHistory; + this.lintSuppressions = lintSuppressions; } public String getEncoding() { @@ -72,4 +75,8 @@ public Optional getSpotlessSetLicenseHeaderYearsFromGitHistory() { public FileLocator getFileLocator() { return fileLocator; } + + public List getLintSuppressions() { + return unmodifiableList(lintSuppressions); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index 608dca0ef8..225986bc07 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -17,6 +17,9 @@ import java.io.File; import java.io.IOException; +import java.util.List; + +import com.diffplug.spotless.LintState; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; @@ -42,7 +45,7 @@ public class SpotlessApplyMojo extends AbstractSpotlessMojo { private boolean spotlessIdeHookUseStdOut; @Override - protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, FormatterConfig config) throws MojoExecutionException { if (isIdeHook()) { IdeHook.performHook(files, formatter, spotlessIdeHook, spotlessIdeHookUseStdIn, spotlessIdeHookUseStdOut); return; @@ -60,15 +63,38 @@ protected void process(String name, Iterable files, Formatter formatter, U } try { - DirtyState dirtyState = DirtyState.of(formatter, file); - if (!dirtyState.isClean() && !dirtyState.didNotConverge()) { + String relativePath = relativize(baseDir, file); + if (relativePath == null) { + // File is not within baseDir, use absolute path as fallback + relativePath = file.getAbsolutePath(); + } + LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, config.getLintSuppressions()); + boolean hasDirtyState = !lintState.getDirtyState().isClean() && !lintState.getDirtyState().didNotConverge(); + boolean hasUnsuppressedLints = lintState.isHasLints(); + + if (hasDirtyState) { getLog().info(String.format("clean file: %s", file)); - dirtyState.writeCanonicalTo(file); + lintState.getDirtyState().writeCanonicalTo(file); buildContext.refresh(file); counter.cleaned(); } else { counter.checkedButAlreadyClean(); } + + // In apply mode, lints are usually warnings, but fatal errors should still fail the build + if (hasUnsuppressedLints) { + // Check if any lint represents a fatal error (like parsing failures) + boolean hasFatalError = lintState.getLintsByStep(formatter).values().stream() + .flatMap(List::stream) + .anyMatch(lint -> lint.getShortCode().contains("Exception")); + + if (hasFatalError) { + String stepName = lintState.getLintsByStep(formatter).keySet().iterator().next(); + throw new MojoExecutionException(String.format("Unable to format file %s%nStep '%s' found problem in '%s':%n%s", file, stepName, file.getName(), lintState.asStringDetailed(file, formatter))); + } else { + getLog().warn(String.format("File %s has lint issues that cannot be auto-fixed:%n%s", file, lintState.asStringDetailed(file, formatter))); + } + } } catch (IOException | RuntimeException e) { throw new MojoExecutionException("Unable to format file " + file, e); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java index 116a24dcf4..61cfe484e5 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java @@ -21,6 +21,8 @@ import java.util.List; import java.util.Map; +import com.diffplug.spotless.LintState; + import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; @@ -64,10 +66,11 @@ public int getSeverity() { private MessageSeverity m2eIncrementalBuildMessageSeverity; @Override - protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, FormatterConfig config) throws MojoExecutionException { ImpactedFilesTracker counter = new ImpactedFilesTracker(); List problemFiles = new ArrayList<>(); + List> lintProblems = new ArrayList<>(); for (File file : files) { if (upToDateChecker.isUpToDate(file.toPath())) { counter.skippedAsCleanCache(); @@ -78,9 +81,21 @@ protected void process(String name, Iterable files, Formatter formatter, U } buildContext.removeMessages(file); try { - DirtyState dirtyState = DirtyState.of(formatter, file); - if (!dirtyState.isClean() && !dirtyState.didNotConverge()) { - problemFiles.add(file); + String relativePath = relativize(baseDir, file); + if (relativePath == null) { + // File is not within baseDir, use absolute path as fallback + relativePath = file.getAbsolutePath(); + } + LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, config.getLintSuppressions()); + boolean hasDirtyState = !lintState.getDirtyState().isClean() && !lintState.getDirtyState().didNotConverge(); + boolean hasUnsuppressedLints = lintState.isHasLints(); + + if (hasDirtyState || hasUnsuppressedLints) { + if (hasUnsuppressedLints) { + lintProblems.add(Map.entry(file, lintState)); + } else { + problemFiles.add(file); + } if (buildContext.isIncremental()) { Map.Entry diffEntry = DiffMessageFormatter.diff(baseDir.toPath(), formatter, file); buildContext.addMessage(file, diffEntry.getKey() + 1, 0, INCREMENTAL_MESSAGE_PREFIX + diffEntry.getValue(), m2eIncrementalBuildMessageSeverity.getSeverity(), null); @@ -103,7 +118,15 @@ protected void process(String name, Iterable files, Formatter formatter, U getLog().debug(String.format("Spotless.%s has no target files. Examine your ``: https://github.com/diffplug/spotless/tree/main/plugin-maven#quickstart", name)); } - if (!problemFiles.isEmpty()) { + if (!lintProblems.isEmpty()) { + // If we have lint problems, prioritize showing them with detailed messages + Map.Entry firstLintProblem = lintProblems.get(0); + File file = firstLintProblem.getKey(); + LintState lintState = firstLintProblem.getValue(); + String stepName = lintState.getLintsByStep(formatter).keySet().iterator().next(); + throw new MojoExecutionException(String.format("Unable to format file %s%nStep '%s' found problem in '%s':%n%s", + file, stepName, file.getName(), lintState.asStringDetailed(file, formatter))); + } else if (!problemFiles.isEmpty()) { throw new MojoExecutionException(DiffMessageFormatter.builder() .runToFix("Run 'mvn spotless:apply' to fix these violations.") .formatter(baseDir.toPath(), formatter) From b2eb9ac837ec0a8c256419ab74ef6c3ce56bdb5c Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 11:34:11 -0700 Subject: [PATCH 02/20] Minor refactor to improve. --- .../com/diffplug/spotless/maven/AbstractSpotlessMojo.java | 4 ++-- .../java/com/diffplug/spotless/maven/SpotlessApplyMojo.java | 5 +++-- .../java/com/diffplug/spotless/maven/SpotlessCheckMojo.java | 5 +++-- 3 files changed, 8 insertions(+), 6 deletions(-) diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index 886e844640..9395d1d0ff 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -228,7 +228,7 @@ protected List getLintSuppressions() { return lintSuppressions; } - protected abstract void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, FormatterConfig config) throws MojoExecutionException; + protected abstract void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, List lintSuppressions) throws MojoExecutionException; private static final int MINIMUM_JRE = 11; @@ -261,7 +261,7 @@ public final void execute() throws MojoExecutionException { for (FormatterFactory factory : formattersHolder.openFormatters.keySet()) { Formatter formatter = formattersHolder.openFormatters.get(factory); Iterable files = formattersHolder.factoryToFiles.get(factory).get(); - process(formattersHolder.nameFor(factory), files, formatter, upToDateChecker, config); + process(formattersHolder.nameFor(factory), files, formatter, upToDateChecker, getLintSuppressions()); } } catch (PluginException e) { throw e.asMojoExecutionException(); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index 225986bc07..9a4702aea4 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -20,6 +20,7 @@ import java.util.List; import com.diffplug.spotless.LintState; +import com.diffplug.spotless.LintSuppression; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; @@ -45,7 +46,7 @@ public class SpotlessApplyMojo extends AbstractSpotlessMojo { private boolean spotlessIdeHookUseStdOut; @Override - protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, FormatterConfig config) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, List lintSuppressions) throws MojoExecutionException { if (isIdeHook()) { IdeHook.performHook(files, formatter, spotlessIdeHook, spotlessIdeHookUseStdIn, spotlessIdeHookUseStdOut); return; @@ -68,7 +69,7 @@ protected void process(String name, Iterable files, Formatter formatter, U // File is not within baseDir, use absolute path as fallback relativePath = file.getAbsolutePath(); } - LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, config.getLintSuppressions()); + LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, lintSuppressions); boolean hasDirtyState = !lintState.getDirtyState().isClean() && !lintState.getDirtyState().didNotConverge(); boolean hasUnsuppressedLints = lintState.isHasLints(); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java index 61cfe484e5..ee101585c5 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java @@ -22,6 +22,7 @@ import java.util.Map; import com.diffplug.spotless.LintState; +import com.diffplug.spotless.LintSuppression; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; @@ -66,7 +67,7 @@ public int getSeverity() { private MessageSeverity m2eIncrementalBuildMessageSeverity; @Override - protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, FormatterConfig config) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, List lintSuppressions) throws MojoExecutionException { ImpactedFilesTracker counter = new ImpactedFilesTracker(); List problemFiles = new ArrayList<>(); @@ -86,7 +87,7 @@ protected void process(String name, Iterable files, Formatter formatter, U // File is not within baseDir, use absolute path as fallback relativePath = file.getAbsolutePath(); } - LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, config.getLintSuppressions()); + LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, lintSuppressions); boolean hasDirtyState = !lintState.getDirtyState().isClean() && !lintState.getDirtyState().didNotConverge(); boolean hasUnsuppressedLints = lintState.isHasLints(); From ec80f7fee87597fbaadae7694d3e41363cdc4954 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 12:06:12 -0700 Subject: [PATCH 03/20] spotless --- .../spotless/extra/integration/DiffMessageFormatter.java | 2 +- .../java/com/diffplug/spotless/maven/FormatterConfig.java | 2 +- .../com/diffplug/spotless/maven/SpotlessApplyMojo.java | 8 +++----- .../com/diffplug/spotless/maven/SpotlessCheckMojo.java | 8 +++----- 4 files changed, 8 insertions(+), 12 deletions(-) diff --git a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java index d28c969bcc..c02c2f2987 100644 --- a/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java +++ b/lib-extra/src/main/java/com/diffplug/spotless/extra/integration/DiffMessageFormatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java index d1c8f25eaf..842d4da68e 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FormatterConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index 9a4702aea4..f8b3b84486 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -19,15 +19,13 @@ import java.io.IOException; import java.util.List; -import com.diffplug.spotless.LintState; -import com.diffplug.spotless.LintSuppression; - import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; -import com.diffplug.spotless.DirtyState; import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.LintState; +import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.maven.incremental.UpToDateChecker; /** diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java index ee101585c5..00cfe6e580 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -21,17 +21,15 @@ import java.util.List; import java.util.Map; -import com.diffplug.spotless.LintState; -import com.diffplug.spotless.LintSuppression; - import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.sonatype.plexus.build.incremental.BuildContext; -import com.diffplug.spotless.DirtyState; import com.diffplug.spotless.Formatter; +import com.diffplug.spotless.LintState; +import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.extra.integration.DiffMessageFormatter; import com.diffplug.spotless.maven.incremental.UpToDateChecker; From 74b9cf2c0ba45d72339fbdecc2942292c48ad6bf Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 12:06:16 -0700 Subject: [PATCH 04/20] Add a test. --- .../maven/java/LintSuppressionTest.java | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java new file mode 100644 index 0000000000..37e3ead86f --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java @@ -0,0 +1,164 @@ +/* + * Copyright 2025 DiffPlug + * + * Licensed 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 com.diffplug.spotless.maven.java; + +import static org.assertj.core.api.Assertions.assertThat; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +class LintSuppressionTest extends MavenIntegrationHarness { + + @Test + void testNoSuppressionFailsOnWildcardImports() throws Exception { + writePomWithJavaSteps(""); + + String path = "src/main/java/TestFile.java"; + setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + + var result = mavenRunner().withArguments("spotless:check").runHasError(); + assertThat(result.stdOutUtf8()).contains("Unable to format file"); + assertThat(result.stdOutUtf8()).contains("Step 'removeWildcardImports' found problem"); + assertThat(result.stdOutUtf8()).contains("Do not use wildcard imports"); + } + + @Test + void testSuppressByFilePath() throws Exception { + writePomWithLintSuppressions( + "", + "", + " ", + " src/main/java/TestFile1.java", + " *", + " *", + " ", + ""); + + String suppressedFile = "src/main/java/TestFile1.java"; + String unsuppressedFile = "src/main/java/TestFile2.java"; + + setFile(suppressedFile).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + setFile(unsuppressedFile).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + + var result = mavenRunner().withArguments("spotless:check").runHasError(); + assertThat(result.stdOutUtf8()).contains("TestFile2.java"); + assertThat(result.stdOutUtf8()).doesNotContain("TestFile1.java"); + } + + @Test + void testSuppressByStep() throws Exception { + writePomWithLintSuppressions( + "", + "", + " ", + " *", + " removeWildcardImports", + " *", + " ", + ""); + + String path = "src/main/java/TestFile.java"; + setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + + // Should succeed because we suppressed the entire step + mavenRunner().withArguments("spotless:check").runNoError(); + } + + @Test + void testSuppressByShortCode() throws Exception { + // Use wildcard to suppress all shortCodes - this tests the shortCode suppression mechanism + writePomWithLintSuppressions( + "", + "", + " ", + " *", + " *", + " *", + " ", + ""); + + String path = "src/main/java/TestFile.java"; + setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + + // Should succeed because we suppressed all error codes + mavenRunner().withArguments("spotless:check").runNoError(); + } + + @Test + void testMultipleSuppressionsWork() throws Exception { + writePomWithLintSuppressions( + "", + "", + " ", + " src/main/java/TestFile1.java", + " *", + " *", + " ", + " ", + " src/main/java/TestFile2.java", + " *", + " *", + " ", + ""); + + String file1 = "src/main/java/TestFile1.java"; + String file2 = "src/main/java/TestFile2.java"; + String file3 = "src/main/java/TestFile3.java"; + + setFile(file1).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + setFile(file2).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + setFile(file3).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); + + var result = mavenRunner().withArguments("spotless:check").runHasError(); + assertThat(result.stdOutUtf8()).contains("TestFile3.java"); + assertThat(result.stdOutUtf8()).doesNotContain("TestFile1.java"); + assertThat(result.stdOutUtf8()).doesNotContain("TestFile2.java"); + } + + /** + * Helper method to write POM with both Java steps and lint suppressions configuration + */ + private void writePomWithLintSuppressions(String... stepsAndSuppressions) throws IOException { + // Separate java steps from lint suppressions + StringBuilder javaSteps = new StringBuilder(); + StringBuilder globalConfig = new StringBuilder(); + + boolean inSuppressions = false; + for (String line : stepsAndSuppressions) { + if (line.startsWith("")) { + inSuppressions = true; + globalConfig.append(line); + } else if (line.startsWith("")) { + inSuppressions = false; + globalConfig.append(line); + } else if (inSuppressions) { + globalConfig.append(line); + } else { + // This is a java step + javaSteps.append(line); + } + } + + // Create the configuration + String javaGroup = "" + javaSteps.toString() + ""; + String fullConfiguration = javaGroup + globalConfig.toString(); + + writePom(fullConfiguration); + } +} From 5d2213ef985474240b4c778b1cca662730d8d6e5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 12:06:21 -0700 Subject: [PATCH 05/20] update the README. --- plugin-maven/README.md | 58 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 58 insertions(+) diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 7efd7c3157..3410712c79 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -1944,6 +1944,64 @@ By default, `spotless:check` is bound to the `verify` phase. You might want to - set `-Dspotless.check.skip=true` at the command line - set `spotless.check.skip` to `true` in the `` section of the `pom.xml` +### Suppressing lint errors + +Sometimes Spotless will encounter lint errors that can't be auto-fixed. For example, if you run `mvn spotless:check`, you might see: + +``` +[ERROR] Unable to format file src/main/java/com/example/App.java +[ERROR] Step 'removeWildcardImports' found problem in 'App.java': +[ERROR] Do not use wildcard imports +``` + +To suppress these lints, you can use the `` configuration: + +```xml + + com.diffplug.spotless + spotless-maven-plugin + ${spotless.version} + + + + + + + src/main/java/com/example/App.java + removeWildcardImports + * + + + + +``` + +Each `` can match by: +- `` - file path (supports wildcards like `*`) +- `` - step name (supports wildcards like `*`) +- `` - specific error code (supports wildcards like `*`) + +You can suppress multiple patterns: + +```xml + + + + src/main/java/com/example/legacy/* + removeWildcardImports + * + + + + * + removeWildcardImports + * + + +``` + +Spotless is primarily a formatter, _not_ a linter. In our opinion, a linter is just a broken formatter. But formatters do break sometimes, and representing these failures as lints that can be suppressed is more useful than just giving up. + ## How do I preview what `mvn spotless:apply` will do? From e0d193336595949cf27b418cf6a2556dcf5e8cb5 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 12:51:46 -0700 Subject: [PATCH 06/20] Update changelog. --- plugin-maven/CHANGES.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index a3b6f64d72..b472f34421 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -17,6 +17,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * Bump default `palantir-java-format` version to latest `2.57.0` -> `2.71.0`. ### Fixed * Fix `spaceBeforeSeparator` in Jackson formatter. ([#2103](https://github.com/diffplug/spotless/pull/2103)) +### Added +* `` API ([#2309](https://github.com/diffplug/spotless/issues/2309)) ## [2.46.1] - 2025-07-21 ### Fixed From 695137ee6acfc50f6e0477d9191aea10bc7b6343 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 14:08:50 -0700 Subject: [PATCH 07/20] Bring maven in-line with gradle. --- .../spotless/maven/SpotlessApplyMojo.java | 27 +++++++++++-------- .../spotless/maven/SpotlessCheckMojo.java | 19 ++++++------- 2 files changed, 26 insertions(+), 20 deletions(-) diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index f8b3b84486..ab1f14db9e 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -80,19 +80,24 @@ protected void process(String name, Iterable files, Formatter formatter, U counter.checkedButAlreadyClean(); } - // In apply mode, lints are usually warnings, but fatal errors should still fail the build + // In apply mode, any lints should fail the build (matching Gradle behavior) if (hasUnsuppressedLints) { - // Check if any lint represents a fatal error (like parsing failures) - boolean hasFatalError = lintState.getLintsByStep(formatter).values().stream() - .flatMap(List::stream) - .anyMatch(lint -> lint.getShortCode().contains("Exception")); - - if (hasFatalError) { - String stepName = lintState.getLintsByStep(formatter).keySet().iterator().next(); - throw new MojoExecutionException(String.format("Unable to format file %s%nStep '%s' found problem in '%s':%n%s", file, stepName, file.getName(), lintState.asStringDetailed(file, formatter))); - } else { - getLog().warn(String.format("File %s has lint issues that cannot be auto-fixed:%n%s", file, lintState.asStringDetailed(file, formatter))); + int lintCount = lintState.getLintsByStep(formatter).values().stream() + .mapToInt(List::size) + .sum(); + StringBuilder message = new StringBuilder(); + message.append("There were ").append(lintCount).append(" lint error(s), they must be fixed or suppressed."); + + // Build lint messages in Gradle format (using relative path, not just filename) + for (Map.Entry> stepEntry : lintState.getLintsByStep(formatter).entrySet()) { + String stepName = stepEntry.getKey(); + for (com.diffplug.spotless.Lint lint : stepEntry.getValue()) { + message.append("\n ").append(relativePath).append(":"); + lint.addWarningMessageTo(message, stepName, true); + } } + message.append("\n Resolve these lints or suppress with `suppressLintsFor`"); + throw new MojoExecutionException(message.toString()); } } catch (IOException | RuntimeException e) { throw new MojoExecutionException("Unable to format file " + file, e); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java index 00cfe6e580..363891c702 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java @@ -117,20 +117,21 @@ protected void process(String name, Iterable files, Formatter formatter, U getLog().debug(String.format("Spotless.%s has no target files. Examine your ``: https://github.com/diffplug/spotless/tree/main/plugin-maven#quickstart", name)); } - if (!lintProblems.isEmpty()) { - // If we have lint problems, prioritize showing them with detailed messages - Map.Entry firstLintProblem = lintProblems.get(0); - File file = firstLintProblem.getKey(); - LintState lintState = firstLintProblem.getValue(); - String stepName = lintState.getLintsByStep(formatter).keySet().iterator().next(); - throw new MojoExecutionException(String.format("Unable to format file %s%nStep '%s' found problem in '%s':%n%s", - file, stepName, file.getName(), lintState.asStringDetailed(file, formatter))); - } else if (!problemFiles.isEmpty()) { + if (!problemFiles.isEmpty()) { + // Prioritize formatting violations first (matching Gradle behavior) throw new MojoExecutionException(DiffMessageFormatter.builder() .runToFix("Run 'mvn spotless:apply' to fix these violations.") .formatter(baseDir.toPath(), formatter) .problemFiles(problemFiles) .getMessage()); + } else if (!lintProblems.isEmpty()) { + // Show lints only if there are no formatting violations + Map.Entry firstLintProblem = lintProblems.get(0); + File file = firstLintProblem.getKey(); + LintState lintState = firstLintProblem.getValue(); + String stepName = lintState.getLintsByStep(formatter).keySet().iterator().next(); + throw new MojoExecutionException(String.format("Unable to format file %s%nStep '%s' found problem in '%s':%n%s", + file, stepName, file.getName(), lintState.asStringOneLine(file, formatter))); } } } From 2d0cfe5020c407c3e2a2ea9dfdd22e993f040607 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 15:08:15 -0700 Subject: [PATCH 08/20] spotless. --- .../java/com/diffplug/spotless/maven/SpotlessApplyMojo.java | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index ab1f14db9e..1637323ab3 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -18,6 +18,7 @@ import java.io.File; import java.io.IOException; import java.util.List; +import java.util.Map; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugins.annotations.Mojo; @@ -83,8 +84,8 @@ protected void process(String name, Iterable files, Formatter formatter, U // In apply mode, any lints should fail the build (matching Gradle behavior) if (hasUnsuppressedLints) { int lintCount = lintState.getLintsByStep(formatter).values().stream() - .mapToInt(List::size) - .sum(); + .mapToInt(List::size) + .sum(); StringBuilder message = new StringBuilder(); message.append("There were ").append(lintCount).append(" lint error(s), they must be fixed or suppressed."); From 4352a36d09499da14a1e2f1bfb62644052cb1e5f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 16:07:43 -0700 Subject: [PATCH 09/20] Since we're fully Java 17 now we can use triple-quote literals now. --- .../src/main/java/selfie/SelfieSettings.java | 26 ------------------- 1 file changed, 26 deletions(-) delete mode 100644 testlib/src/main/java/selfie/SelfieSettings.java diff --git a/testlib/src/main/java/selfie/SelfieSettings.java b/testlib/src/main/java/selfie/SelfieSettings.java deleted file mode 100644 index 4a60c15dd0..0000000000 --- a/testlib/src/main/java/selfie/SelfieSettings.java +++ /dev/null @@ -1,26 +0,0 @@ -/* - * Copyright 2024 DiffPlug - * - * Licensed 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 selfie; - -import com.diffplug.selfie.junit5.SelfieSettingsAPI; - -/** https://selfie.dev/jvm/get-started#quickstart */ -public class SelfieSettings extends SelfieSettingsAPI { - @Override - public boolean getJavaDontUseTripleQuoteLiterals() { - return true; - } -} From 1098702ff0e81d5de80cce9810e4b2a073d2533f Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 16:20:19 -0700 Subject: [PATCH 10/20] Use `MavenSelfie` to assert linting error messages. --- .../spotless/maven/SpotlessApplyMojo.java | 2 +- .../spotless/maven/biome/BiomeMavenTest.java | 9 +++-- .../java/RemoveWildcardImportsStepTest.java | 17 +++++++- .../src/test/java/selfie/MavenSelfie.java | 40 +++++++++++++++++++ 4 files changed, 63 insertions(+), 5 deletions(-) create mode 100644 plugin-maven/src/test/java/selfie/MavenSelfie.java diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index 1637323ab3..27fc1308fb 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -97,7 +97,7 @@ protected void process(String name, Iterable files, Formatter formatter, U lint.addWarningMessageTo(message, stepName, true); } } - message.append("\n Resolve these lints or suppress with `suppressLintsFor`"); + message.append("\n Resolve these lints or suppress with ``"); throw new MojoExecutionException(message.toString()); } } catch (IOException | RuntimeException e) { diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java index 4197b623dc..b2c6d921b1 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java @@ -19,6 +19,7 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.owasp.encoder.Encode.forXml; +import static selfie.MavenSelfie.expectSelfieErrorMsg; import java.io.File; @@ -247,9 +248,11 @@ void failureWhenNotParseable() throws Exception { setFile("biome_test.js").toResource("biome/js/fileBefore.js"); var result = mavenRunner().withArguments("spotless:apply").runHasError(); assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); - assertThat(result.stdOutUtf8()).contains("Format with errors is disabled."); - assertThat(result.stdOutUtf8()).contains("Unable to format file"); - assertThat(result.stdOutUtf8()).contains("Step 'biome' found problem in 'biome_test.js'"); + expectSelfieErrorMsg(result).toBe(""" + Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:apply (default-cli) on project spotless-maven-plugin-tests: There were 1 lint error(s), they must be fixed or suppressed. + biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException) > arguments: [/Users/ntwigg/.m2/repository/com/diffplug/spotless/spotless-data/biome/biome-mac_os-arm64-1.2.0, format, --stdin-file-path, file.json] (...) + Resolve these lints or suppress with `` + """); } /** diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java index 4d4ade94d0..713e1f9eb2 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java @@ -15,6 +15,8 @@ */ package com.diffplug.spotless.maven.java; +import static selfie.MavenSelfie.expectSelfieErrorMsg; + import org.junit.jupiter.api.Test; import com.diffplug.spotless.maven.MavenIntegrationHarness; @@ -27,7 +29,20 @@ void testRemoveWildcardImports() throws Exception { String path = "src/main/java/test.java"; setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); - mavenRunner().withArguments("spotless:apply").runNoError(); + expectSelfieErrorMsg(mavenRunner().withArguments("spotless:apply").runHasError()).toBe(""" + Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:apply (default-cli) on project spotless-maven-plugin-tests: There were 5 lint error(s), they must be fixed or suppressed. + src/main/java/test.java:L1 removeWildcardImports(import java.util.*; + ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L2 removeWildcardImports(import static java.util.Collections.*; + ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L5 removeWildcardImports(import io.quarkus.maven.dependency.*; + ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L6 removeWildcardImports(import static io.quarkus.vertx.web.Route.HttpMethod.*; + ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L7 removeWildcardImports(import static org.springframework.web.reactive.function.BodyInserters.*; + ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + Resolve these lints or suppress with `` + """); assertFile(path).sameAsResource("java/removewildcardimports/JavaCodeWildcardsFormatted.test"); } } diff --git a/plugin-maven/src/test/java/selfie/MavenSelfie.java b/plugin-maven/src/test/java/selfie/MavenSelfie.java new file mode 100644 index 0000000000..0d652c5471 --- /dev/null +++ b/plugin-maven/src/test/java/selfie/MavenSelfie.java @@ -0,0 +1,40 @@ +/* + * Copyright 2025 DiffPlug + * + * Licensed 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 selfie; + +import java.util.stream.Collectors; + +import com.diffplug.selfie.Selfie; +import com.diffplug.selfie.StringSelfie; +import com.diffplug.spotless.ProcessRunner; + +public class MavenSelfie { + private static final String ERROR_PREFIX = "[ERROR] "; + + public static StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { + String concatenatedError = result.stdOutUtf8().lines() + .map(line -> line.startsWith(ERROR_PREFIX) ? line.substring(ERROR_PREFIX.length()) : null) + .filter(line -> line != null) + .collect(Collectors.joining("\n")); + + String sanitizedVersion = concatenatedError.replaceFirst("com\\.diffplug\\.spotless:spotless-maven-plugin:([^:]+):", "com.diffplug.spotless:spotless-maven-plugin:VERSION:"); + + int help1 = sanitizedVersion.indexOf("-> [Help 1]"); + String trimTrailingString = sanitizedVersion.substring(0, help1); + + return Selfie.expectSelfie(trimTrailingString); + } +} From 6a734f6d563dc7a31db010c0b9664417cf3e7979 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 16:22:14 -0700 Subject: [PATCH 11/20] Slightly better error messages for ReplaceRegexStep. --- .../spotless/generic/ReplaceRegexStep.java | 7 ++++++- .../maven/java/RemoveWildcardImportsStepTest.java | 15 +++++---------- 2 files changed, 11 insertions(+), 11 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/generic/ReplaceRegexStep.java b/lib/src/main/java/com/diffplug/spotless/generic/ReplaceRegexStep.java index 7d68a14c61..33a4e4c12e 100644 --- a/lib/src/main/java/com/diffplug/spotless/generic/ReplaceRegexStep.java +++ b/lib/src/main/java/com/diffplug/spotless/generic/ReplaceRegexStep.java @@ -90,7 +90,12 @@ public List lint(String raw, File file) { var matcher = regex.matcher(raw); while (matcher.find()) { int line = 1 + (int) raw.codePoints().limit(matcher.start()).filter(c -> c == '\n').count(); - lints.add(Lint.atLine(line, matcher.group(0), lintDetail)); + String errorCode = matcher.group(0).trim(); + int firstNewline = errorCode.indexOf("\n"); + if (firstNewline != -1) { + errorCode = errorCode.substring(0, firstNewline); + } + lints.add(Lint.atLine(line, errorCode, lintDetail)); } return lints; } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java index 713e1f9eb2..d0d1d263d0 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java @@ -31,16 +31,11 @@ void testRemoveWildcardImports() throws Exception { setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); expectSelfieErrorMsg(mavenRunner().withArguments("spotless:apply").runHasError()).toBe(""" Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:apply (default-cli) on project spotless-maven-plugin-tests: There were 5 lint error(s), they must be fixed or suppressed. - src/main/java/test.java:L1 removeWildcardImports(import java.util.*; - ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this - src/main/java/test.java:L2 removeWildcardImports(import static java.util.Collections.*; - ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this - src/main/java/test.java:L5 removeWildcardImports(import io.quarkus.maven.dependency.*; - ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this - src/main/java/test.java:L6 removeWildcardImports(import static io.quarkus.vertx.web.Route.HttpMethod.*; - ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this - src/main/java/test.java:L7 removeWildcardImports(import static org.springframework.web.reactive.function.BodyInserters.*; - ) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L1 removeWildcardImports(import java.util.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L2 removeWildcardImports(import static java.util.Collections.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L5 removeWildcardImports(import io.quarkus.maven.dependency.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L6 removeWildcardImports(import static io.quarkus.vertx.web.Route.HttpMethod.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + src/main/java/test.java:L7 removeWildcardImports(import static org.springframework.web.reactive.function.BodyInserters.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this Resolve these lints or suppress with `` """); assertFile(path).sameAsResource("java/removewildcardimports/JavaCodeWildcardsFormatted.test"); From cff607b19e9e06196b9b092dd1df56b0217526ef Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 16:24:07 -0700 Subject: [PATCH 12/20] Move the Maven selfie harnessing somewhere more natural. --- .../maven/MavenIntegrationHarness.java | 20 +++++++++- .../spotless/maven/biome/BiomeMavenTest.java | 1 - .../java/RemoveWildcardImportsStepTest.java | 2 - .../src/test/java/selfie/MavenSelfie.java | 40 ------------------- 4 files changed, 19 insertions(+), 44 deletions(-) delete mode 100644 plugin-maven/src/test/java/selfie/MavenSelfie.java diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java index f3e1f30355..b5fb9c0b90 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java @@ -29,7 +29,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +import java.util.stream.Collectors; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.BeforeAll; @@ -41,6 +41,8 @@ import com.diffplug.common.base.Unhandled; import com.diffplug.common.io.Resources; +import com.diffplug.selfie.Selfie; +import com.diffplug.selfie.StringSelfie; import com.diffplug.spotless.Jvm; import com.diffplug.spotless.ProcessRunner; import com.diffplug.spotless.ResourceHarness; @@ -347,4 +349,20 @@ protected static String[] formats(String[]... formats) { .toArray(String[]::new); return formats(formatsArray); } + + private static final String ERROR_PREFIX = "[ERROR] "; + + protected static StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { + String concatenatedError = result.stdOutUtf8().lines() + .map(line -> line.startsWith(ERROR_PREFIX) ? line.substring(ERROR_PREFIX.length()) : null) + .filter(line -> line != null) + .collect(Collectors.joining("\n")); + + String sanitizedVersion = concatenatedError.replaceFirst("com\\.diffplug\\.spotless:spotless-maven-plugin:([^:]+):", "com.diffplug.spotless:spotless-maven-plugin:VERSION:"); + + int help1 = sanitizedVersion.indexOf("-> [Help 1]"); + String trimTrailingString = sanitizedVersion.substring(0, help1); + + return Selfie.expectSelfie(trimTrailingString); + } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java index b2c6d921b1..b87e95f367 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java @@ -19,7 +19,6 @@ import static org.junit.jupiter.api.Assertions.assertEquals; import static org.junit.jupiter.api.Assertions.assertTrue; import static org.owasp.encoder.Encode.forXml; -import static selfie.MavenSelfie.expectSelfieErrorMsg; import java.io.File; diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java index d0d1d263d0..a801f7f735 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/RemoveWildcardImportsStepTest.java @@ -15,8 +15,6 @@ */ package com.diffplug.spotless.maven.java; -import static selfie.MavenSelfie.expectSelfieErrorMsg; - import org.junit.jupiter.api.Test; import com.diffplug.spotless.maven.MavenIntegrationHarness; diff --git a/plugin-maven/src/test/java/selfie/MavenSelfie.java b/plugin-maven/src/test/java/selfie/MavenSelfie.java deleted file mode 100644 index 0d652c5471..0000000000 --- a/plugin-maven/src/test/java/selfie/MavenSelfie.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Copyright 2025 DiffPlug - * - * Licensed 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 selfie; - -import java.util.stream.Collectors; - -import com.diffplug.selfie.Selfie; -import com.diffplug.selfie.StringSelfie; -import com.diffplug.spotless.ProcessRunner; - -public class MavenSelfie { - private static final String ERROR_PREFIX = "[ERROR] "; - - public static StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { - String concatenatedError = result.stdOutUtf8().lines() - .map(line -> line.startsWith(ERROR_PREFIX) ? line.substring(ERROR_PREFIX.length()) : null) - .filter(line -> line != null) - .collect(Collectors.joining("\n")); - - String sanitizedVersion = concatenatedError.replaceFirst("com\\.diffplug\\.spotless:spotless-maven-plugin:([^:]+):", "com.diffplug.spotless:spotless-maven-plugin:VERSION:"); - - int help1 = sanitizedVersion.indexOf("-> [Help 1]"); - String trimTrailingString = sanitizedVersion.substring(0, help1); - - return Selfie.expectSelfie(trimTrailingString); - } -} From a7a1df7179ac538c286f8edff9cbb5435275f939 Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 16:26:44 -0700 Subject: [PATCH 13/20] Use the harness a bit in `LintSuppressionTest`. --- .../spotless/maven/MavenIntegrationHarness.java | 4 ++++ .../spotless/maven/java/LintSuppressionTest.java | 13 +++++++++---- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java index b5fb9c0b90..2d50dc9b52 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java @@ -29,6 +29,10 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.Arrays; +import java.util.HashMap; +import java.util.List; +import java.util.Map; import java.util.stream.Collectors; import org.junit.jupiter.api.AfterAll; diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java index 37e3ead86f..4f22c441f9 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java @@ -32,10 +32,15 @@ void testNoSuppressionFailsOnWildcardImports() throws Exception { String path = "src/main/java/TestFile.java"; setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); - var result = mavenRunner().withArguments("spotless:check").runHasError(); - assertThat(result.stdOutUtf8()).contains("Unable to format file"); - assertThat(result.stdOutUtf8()).contains("Step 'removeWildcardImports' found problem"); - assertThat(result.stdOutUtf8()).contains("Do not use wildcard imports"); + expectSelfieErrorMsg(mavenRunner().withArguments("spotless:check").runHasError()).toBe(""" + Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:check (default-cli) on project spotless-maven-plugin-tests: Unable to format file /private/var/folders/8p/37qyn_6506sd85y1jbl91dbw0000gn/T/junit-7771631643861333109/src/main/java/TestFile.java + Step 'removeWildcardImports' found problem in 'TestFile.java': + TestFile.java:L1 removeWildcardImports(import java.util.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + TestFile.java:L2 removeWildcardImports(import static java.util.Collections.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + TestFile.java:L5 removeWildcardImports(import io.quarkus.maven.dependency.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + TestFile.java:L6 removeWildcardImports(import static io.quarkus.vertx.web.Route.HttpMethod.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + TestFile.java:L7 removeWildcardImports(import static org.springframework.web.reactive.function.BodyInserters.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this + """); } @Test From 4818acf034eea78feb33b70a6518048dd5ec623a Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 16:53:48 -0700 Subject: [PATCH 14/20] Sanitize system-dependent pieces of the maven log snapshots. --- .../diffplug/spotless/maven/MavenIntegrationHarness.java | 6 ++++-- .../com/diffplug/spotless/maven/biome/BiomeMavenTest.java | 2 +- .../diffplug/spotless/maven/java/LintSuppressionTest.java | 2 +- 3 files changed, 6 insertions(+), 4 deletions(-) diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java index 2d50dc9b52..ce22a9f664 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java @@ -356,7 +356,7 @@ protected static String[] formats(String[]... formats) { private static final String ERROR_PREFIX = "[ERROR] "; - protected static StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { + protected StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { String concatenatedError = result.stdOutUtf8().lines() .map(line -> line.startsWith(ERROR_PREFIX) ? line.substring(ERROR_PREFIX.length()) : null) .filter(line -> line != null) @@ -367,6 +367,8 @@ protected static StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) int help1 = sanitizedVersion.indexOf("-> [Help 1]"); String trimTrailingString = sanitizedVersion.substring(0, help1); - return Selfie.expectSelfie(trimTrailingString); + String sanitizeBiomeNative = trimTrailingString.replaceAll("spotless-data/biome/biome-(.+),", "biome-exe"); + String sanitizeFilePath = sanitizeBiomeNative.replaceAll(rootFolder().getAbsolutePath(), "PROJECT_DIR"); + return Selfie.expectSelfie(sanitizeFilePath); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java index b87e95f367..6020bf15c3 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java @@ -249,7 +249,7 @@ void failureWhenNotParseable() throws Exception { assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); expectSelfieErrorMsg(result).toBe(""" Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:apply (default-cli) on project spotless-maven-plugin-tests: There were 1 lint error(s), they must be fixed or suppressed. - biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException) > arguments: [/Users/ntwigg/.m2/repository/com/diffplug/spotless/spotless-data/biome/biome-mac_os-arm64-1.2.0, format, --stdin-file-path, file.json] (...) + biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException) > arguments: [/Users/ntwigg/.m2/repository/com/diffplug/spotless/biome-exe file.json] (...) Resolve these lints or suppress with `` """); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java index 4f22c441f9..8a0ae16d4f 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java @@ -33,7 +33,7 @@ void testNoSuppressionFailsOnWildcardImports() throws Exception { setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); expectSelfieErrorMsg(mavenRunner().withArguments("spotless:check").runHasError()).toBe(""" - Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:check (default-cli) on project spotless-maven-plugin-tests: Unable to format file /private/var/folders/8p/37qyn_6506sd85y1jbl91dbw0000gn/T/junit-7771631643861333109/src/main/java/TestFile.java + Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:check (default-cli) on project spotless-maven-plugin-tests: Unable to format file PROJECT_DIR/src/main/java/TestFile.java Step 'removeWildcardImports' found problem in 'TestFile.java': TestFile.java:L1 removeWildcardImports(import java.util.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this TestFile.java:L2 removeWildcardImports(import static java.util.Collections.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this From bf1d60aae113f9b4de6cf630f3212e2fe2cc548d Mon Sep 17 00:00:00 2001 From: Ned Twigg Date: Tue, 23 Sep 2025 19:56:58 -0700 Subject: [PATCH 15/20] More path sanitizing. --- .../com/diffplug/spotless/maven/MavenIntegrationHarness.java | 5 +++-- .../com/diffplug/spotless/maven/biome/BiomeMavenTest.java | 2 +- .../diffplug/spotless/maven/java/LintSuppressionTest.java | 2 +- 3 files changed, 5 insertions(+), 4 deletions(-) diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java index ce22a9f664..364696a50a 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java @@ -368,7 +368,8 @@ protected StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { String trimTrailingString = sanitizedVersion.substring(0, help1); String sanitizeBiomeNative = trimTrailingString.replaceAll("spotless-data/biome/biome-(.+),", "biome-exe"); - String sanitizeFilePath = sanitizeBiomeNative.replaceAll(rootFolder().getAbsolutePath(), "PROJECT_DIR"); - return Selfie.expectSelfie(sanitizeFilePath); + String sanitizeFilePath = sanitizeBiomeNative.replace(rootFolder().getAbsolutePath(), "${PROJECT_DIR}"); + String sanitizeUserHome = sanitizeFilePath.replace(System.getProperty("user.home"), "${user.home}"); + return Selfie.expectSelfie(sanitizeUserHome); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java index 6020bf15c3..cc74611f5e 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java @@ -249,7 +249,7 @@ void failureWhenNotParseable() throws Exception { assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); expectSelfieErrorMsg(result).toBe(""" Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:apply (default-cli) on project spotless-maven-plugin-tests: There were 1 lint error(s), they must be fixed or suppressed. - biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException) > arguments: [/Users/ntwigg/.m2/repository/com/diffplug/spotless/biome-exe file.json] (...) + biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException) > arguments: [${user.home}/.m2/repository/com/diffplug/spotless/biome-exe file.json] (...) Resolve these lints or suppress with `` """); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java index 8a0ae16d4f..1fe1c59fc6 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java @@ -33,7 +33,7 @@ void testNoSuppressionFailsOnWildcardImports() throws Exception { setFile(path).toResource("java/removewildcardimports/JavaCodeWildcardsUnformatted.test"); expectSelfieErrorMsg(mavenRunner().withArguments("spotless:check").runHasError()).toBe(""" - Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:check (default-cli) on project spotless-maven-plugin-tests: Unable to format file PROJECT_DIR/src/main/java/TestFile.java + Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:check (default-cli) on project spotless-maven-plugin-tests: Unable to format file ${PROJECT_DIR}/src/main/java/TestFile.java Step 'removeWildcardImports' found problem in 'TestFile.java': TestFile.java:L1 removeWildcardImports(import java.util.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this TestFile.java:L2 removeWildcardImports(import static java.util.Collections.*;) Do not use wildcard imports (e.g. java.util.*) - replace with specific class imports (e.g. java.util.List) as 'spotlessApply' cannot auto-fix this From aedb7c08e2f83f8dff18a2d8ec2e737c3b39e146 Mon Sep 17 00:00:00 2001 From: EdgarTwigg Date: Tue, 23 Sep 2025 20:49:55 -0700 Subject: [PATCH 16/20] Some windows-specific sanitization. --- .../com/diffplug/spotless/maven/MavenIntegrationHarness.java | 2 +- .../java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java index 364696a50a..b815ce9418 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java @@ -367,7 +367,7 @@ protected StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { int help1 = sanitizedVersion.indexOf("-> [Help 1]"); String trimTrailingString = sanitizedVersion.substring(0, help1); - String sanitizeBiomeNative = trimTrailingString.replaceAll("spotless-data/biome/biome-(.+),", "biome-exe"); + String sanitizeBiomeNative = trimTrailingString.replaceAll("[/|\\\\].m2(.*)[/|\\\\]biome\\-(.+),", "biome-exe"); String sanitizeFilePath = sanitizeBiomeNative.replace(rootFolder().getAbsolutePath(), "${PROJECT_DIR}"); String sanitizeUserHome = sanitizeFilePath.replace(System.getProperty("user.home"), "${user.home}"); return Selfie.expectSelfie(sanitizeUserHome); diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java index cc74611f5e..895511bbce 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/biome/BiomeMavenTest.java @@ -249,7 +249,7 @@ void failureWhenNotParseable() throws Exception { assertFile("biome_test.js").sameAsResource("biome/js/fileBefore.js"); expectSelfieErrorMsg(result).toBe(""" Failed to execute goal com.diffplug.spotless:spotless-maven-plugin:VERSION:apply (default-cli) on project spotless-maven-plugin-tests: There were 1 lint error(s), they must be fixed or suppressed. - biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException) > arguments: [${user.home}/.m2/repository/com/diffplug/spotless/biome-exe file.json] (...) + biome_test.js:LINE_UNDEFINED biome(java.lang.RuntimeException) > arguments: [${user.home}biome-exe file.json] (...) Resolve these lints or suppress with `` """); } From da53172fbaf81a48dd7d3d88bf954bf3f644dd07 Mon Sep 17 00:00:00 2001 From: EdgarTwigg Date: Tue, 23 Sep 2025 21:23:32 -0700 Subject: [PATCH 17/20] Make sure that lint suppression path-filtering is always done using unix paths. --- .../diffplug/spotless/LintSuppression.java | 24 ++++++++++++++- .../gradle/spotless/FormatExtension.java | 17 +---------- .../spotless/maven/AbstractSpotlessMojo.java | 29 ++++++++----------- .../spotless/maven/SpotlessApplyMojo.java | 10 ++----- .../spotless/maven/SpotlessCheckMojo.java | 10 ++----- 5 files changed, 41 insertions(+), 49 deletions(-) diff --git a/lib/src/main/java/com/diffplug/spotless/LintSuppression.java b/lib/src/main/java/com/diffplug/spotless/LintSuppression.java index d84107bd3a..bf4d410a04 100644 --- a/lib/src/main/java/com/diffplug/spotless/LintSuppression.java +++ b/lib/src/main/java/com/diffplug/spotless/LintSuppression.java @@ -1,5 +1,5 @@ /* - * Copyright 2024 DiffPlug + * Copyright 2024-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,11 @@ */ package com.diffplug.spotless; +import java.io.File; import java.util.Objects; +import javax.annotation.Nullable; + public class LintSuppression implements java.io.Serializable { private static final long serialVersionUID = 1L; @@ -30,6 +33,9 @@ public String getPath() { } public void setPath(String path) { + if (path.indexOf('\\') != -1) { + throw new IllegalArgumentException("Path must use only unix style path separator `/`, this was " + path); + } this.path = Objects.requireNonNull(path); } @@ -90,4 +96,20 @@ public String toString() { ", code='" + shortCode + '\'' + '}'; } + + /** + * Returns the relative path between root and dest, or null if dest is not a + * child of root. Guaranteed to only have unix-separators. + */ + public static @Nullable String relativizeAsUnix(File root, File dest) { + String rootPath = root.getAbsolutePath(); + String destPath = dest.getAbsolutePath(); + if (!destPath.startsWith(rootPath)) { + return null; + } else { + String relativized = destPath.substring(rootPath.length()); + String unixified = relativized.replace('\\', '/'); + return unixified.startsWith("/") ? unixified.substring(1) : unixified; + } + } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 09ba60fcaa..869999c071 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -347,27 +347,12 @@ private final FileCollection parseTargetIsExclude(Object target, boolean isExclu } private static void relativizeIfSubdir(List relativePaths, File root, File dest) { - String relativized = relativize(root, dest); + String relativized = LintSuppression.relativizeAsUnix(root, dest); if (relativized != null) { relativePaths.add(relativized); } } - /** - * Returns the relative path between root and dest, or null if dest is not a - * child of root. - */ - static @Nullable String relativize(File root, File dest) { - String rootPath = root.getAbsolutePath(); - String destPath = dest.getAbsolutePath(); - if (!destPath.startsWith(rootPath)) { - return null; - } else { - String relativized = destPath.substring(rootPath.length()); - return relativized.startsWith("/") || relativized.startsWith("\\") ? relativized.substring(1) : relativized; - } - } - /** The steps that need to be added. */ protected final List steps = new ArrayList<>(); diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java index 9395d1d0ff..1ddda39c65 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/AbstractSpotlessMojo.java @@ -54,6 +54,7 @@ import com.diffplug.spotless.Formatter; import com.diffplug.spotless.Jvm; import com.diffplug.spotless.LineEnding; +import com.diffplug.spotless.LintState; import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.generic.LicenseHeaderStep; @@ -228,7 +229,16 @@ protected List getLintSuppressions() { return lintSuppressions; } - protected abstract void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, List lintSuppressions) throws MojoExecutionException; + protected abstract void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException; + + protected LintState calculateLintState(Formatter formatter, File file) throws IOException { + String relativePath = LintSuppression.relativizeAsUnix(baseDir, file); + if (relativePath == null) { + // File is not within baseDir, use absolute path as fallback + relativePath = file.getAbsolutePath(); + } + return LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, lintSuppressions); + } private static final int MINIMUM_JRE = 11; @@ -261,7 +271,7 @@ public final void execute() throws MojoExecutionException { for (FormatterFactory factory : formattersHolder.openFormatters.keySet()) { Formatter formatter = formattersHolder.openFormatters.get(factory); Iterable files = formattersHolder.factoryToFiles.get(factory).get(); - process(formattersHolder.nameFor(factory), files, formatter, upToDateChecker, getLintSuppressions()); + process(formattersHolder.nameFor(factory), files, formatter, upToDateChecker); } } catch (PluginException e) { throw e.asMojoExecutionException(); @@ -424,19 +434,4 @@ private UpToDateChecker createUpToDateChecker(Iterable formatters) { } return UpToDateChecker.wrapWithBuildContext(checker, buildContext); } - - /** - * Returns the relative path between root and dest, or null if dest is not a - * child of root. - */ - static String relativize(File root, File dest) { - String rootPath = root.getAbsolutePath(); - String destPath = dest.getAbsolutePath(); - if (!destPath.startsWith(rootPath)) { - return null; - } else { - String relativized = destPath.substring(rootPath.length()); - return relativized.startsWith("/") || relativized.startsWith("\\") ? relativized.substring(1) : relativized; - } - } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java index 27fc1308fb..829b89a38a 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessApplyMojo.java @@ -45,7 +45,7 @@ public class SpotlessApplyMojo extends AbstractSpotlessMojo { private boolean spotlessIdeHookUseStdOut; @Override - protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, List lintSuppressions) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { if (isIdeHook()) { IdeHook.performHook(files, formatter, spotlessIdeHook, spotlessIdeHookUseStdIn, spotlessIdeHookUseStdOut); return; @@ -63,12 +63,7 @@ protected void process(String name, Iterable files, Formatter formatter, U } try { - String relativePath = relativize(baseDir, file); - if (relativePath == null) { - // File is not within baseDir, use absolute path as fallback - relativePath = file.getAbsolutePath(); - } - LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, lintSuppressions); + LintState lintState = super.calculateLintState(formatter, file); boolean hasDirtyState = !lintState.getDirtyState().isClean() && !lintState.getDirtyState().didNotConverge(); boolean hasUnsuppressedLints = lintState.isHasLints(); @@ -93,6 +88,7 @@ protected void process(String name, Iterable files, Formatter formatter, U for (Map.Entry> stepEntry : lintState.getLintsByStep(formatter).entrySet()) { String stepName = stepEntry.getKey(); for (com.diffplug.spotless.Lint lint : stepEntry.getValue()) { + String relativePath = LintSuppression.relativizeAsUnix(baseDir, file); message.append("\n ").append(relativePath).append(":"); lint.addWarningMessageTo(message, stepName, true); } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java index 363891c702..2c318b9a95 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/SpotlessCheckMojo.java @@ -29,7 +29,6 @@ import com.diffplug.spotless.Formatter; import com.diffplug.spotless.LintState; -import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.extra.integration.DiffMessageFormatter; import com.diffplug.spotless.maven.incremental.UpToDateChecker; @@ -65,7 +64,7 @@ public int getSeverity() { private MessageSeverity m2eIncrementalBuildMessageSeverity; @Override - protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker, List lintSuppressions) throws MojoExecutionException { + protected void process(String name, Iterable files, Formatter formatter, UpToDateChecker upToDateChecker) throws MojoExecutionException { ImpactedFilesTracker counter = new ImpactedFilesTracker(); List problemFiles = new ArrayList<>(); @@ -80,12 +79,7 @@ protected void process(String name, Iterable files, Formatter formatter, U } buildContext.removeMessages(file); try { - String relativePath = relativize(baseDir, file); - if (relativePath == null) { - // File is not within baseDir, use absolute path as fallback - relativePath = file.getAbsolutePath(); - } - LintState lintState = LintState.of(formatter, file).withRemovedSuppressions(formatter, relativePath, lintSuppressions); + LintState lintState = super.calculateLintState(formatter, file); boolean hasDirtyState = !lintState.getDirtyState().isClean() && !lintState.getDirtyState().didNotConverge(); boolean hasUnsuppressedLints = lintState.isHasLints(); From 479fca87abb2bc19df040a3a4ac1f3f2b4717cc3 Mon Sep 17 00:00:00 2001 From: EdgarTwigg Date: Tue, 23 Sep 2025 21:23:49 -0700 Subject: [PATCH 18/20] More windows-specific path sanitization. --- .../com/diffplug/spotless/maven/MavenIntegrationHarness.java | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java index b815ce9418..e63f75f561 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/MavenIntegrationHarness.java @@ -370,6 +370,7 @@ protected StringSelfie expectSelfieErrorMsg(ProcessRunner.Result result) { String sanitizeBiomeNative = trimTrailingString.replaceAll("[/|\\\\].m2(.*)[/|\\\\]biome\\-(.+),", "biome-exe"); String sanitizeFilePath = sanitizeBiomeNative.replace(rootFolder().getAbsolutePath(), "${PROJECT_DIR}"); String sanitizeUserHome = sanitizeFilePath.replace(System.getProperty("user.home"), "${user.home}"); - return Selfie.expectSelfie(sanitizeUserHome); + String sanitizeWindowsPathSep = sanitizeUserHome.replace('\\', '/'); + return Selfie.expectSelfie(sanitizeWindowsPathSep); } } From bb7f61a8508b69b7e24a28f42106d34faf0cb106 Mon Sep 17 00:00:00 2001 From: EdgarTwigg Date: Tue, 23 Sep 2025 21:29:46 -0700 Subject: [PATCH 19/20] Missed some spots. --- .../com/diffplug/gradle/spotless/SpotlessTaskImpl.java | 5 +++-- .../java/com/diffplug/gradle/spotless/FileTreeTest.java | 7 +++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java index 7241f018d0..d3b96bf6e3 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessTaskImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2024 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -45,6 +45,7 @@ import com.diffplug.spotless.Formatter; import com.diffplug.spotless.Lint; import com.diffplug.spotless.LintState; +import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.extra.GitRatchet; @CacheableTask @@ -101,7 +102,7 @@ public void performAction(InputChanges inputs) throws Exception { for (FileChange fileChange : inputs.getFileChanges(target)) { File input = fileChange.getFile(); File projectDir = getProjectDir().get().getAsFile(); - String relativePath = FormatExtension.relativize(projectDir, input); + String relativePath = LintSuppression.relativizeAsUnix(projectDir, input); if (relativePath == null) { throw new IllegalArgumentException(StringPrinter.buildString(printer -> { printer.println("Spotless error! All target files must be within the project dir."); diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FileTreeTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FileTreeTest.java index bd9800a6c1..0a6cb2c466 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FileTreeTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/FileTreeTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2025 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,8 +15,6 @@ */ package com.diffplug.gradle.spotless; -import static com.diffplug.gradle.spotless.FormatExtension.relativize; - import java.io.File; import java.io.IOException; @@ -26,6 +24,7 @@ import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; +import com.diffplug.spotless.LintSuppression; import com.diffplug.spotless.ResourceHarness; import com.diffplug.spotless.TestProvisioner; @@ -51,7 +50,7 @@ void absolutePathDoesntWork() throws IOException { void relativePathDoes() throws IOException { File someFile = setFile("someFolder/someFile").toContent(""); File someFolder = someFile.getParentFile(); - fileTree.exclude(relativize(rootFolder(), someFolder)); + fileTree.exclude(LintSuppression.relativizeAsUnix(rootFolder(), someFolder)); Assertions.assertThat(fileTree).containsExactlyInAnyOrder(); } } From 9e246d5b8d7c7a4725a0a470caff4652bdc8583c Mon Sep 17 00:00:00 2001 From: EdgarTwigg Date: Tue, 23 Sep 2025 21:48:59 -0700 Subject: [PATCH 20/20] Update changelog of other projects affected by unix/windows path separators we uncovered & resolved here. --- CHANGES.md | 1 + plugin-gradle/CHANGES.md | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGES.md b/CHANGES.md index a7f17fca6d..38b36ca7b4 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -26,6 +26,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * `GitPrePushHookInstaller` uses a lock to run gracefully if it is called many times in parallel. ([#2570](https://github.com/diffplug/spotless/pull/2570)) ### Added * Add a `lint` mode to `ReplaceRegexStep` ([#2571](https://github.com/diffplug/spotless/pull/2571)) +* `LintSuppression` now enforces unix-style paths in its `setPath` and `relativizeAsUnix` methods. ([#2629](https://github.com/diffplug/spotless/pull/2629)) ## [3.3.1] - 2025-07-21 ### Fixed diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index f069f9e758..1833db9955 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -6,6 +6,7 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( ### Changed * **BREAKING** Bump the required Gradle to `7.3` and required Java to `17`. ([#2375](https://github.com/diffplug/spotless/issues/2375), [#2540](https://github.com/diffplug/spotless/pull/2540)) * **BREAKING** `spotlessInstallGitPrePushHook` task is now installed only on the root project. ([#2570](https://github.com/diffplug/spotless/pull/2570)) +* **BREAKING** `LintSuppression` now enforces unix-style paths in its `setPath` method. ([#2629](https://github.com/diffplug/spotless/pull/2629)) * Running `spotlessCheck` with violations unilaterally produces the error message `Run './gradlew spotlessApply' to fix these violations`. ([#2592](https://github.com/diffplug/spotless/issues/2592)) * Bump JGit from `6.10.1` to `7.3.0` ([#2257](https://github.com/diffplug/spotless/pull/2257)) * Adds support for worktrees (fixes [#1765](https://github.com/diffplug/spotless/issues/1765)) @@ -16,7 +17,6 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format ( * **BREAKING** use `TrailingCommaManagementStrategy` enum instead of `manageTrailingCommas` boolean configuration option * Bump default `ktlint` version to latest `1.5.0` -> `1.7.1`. ([#2555](https://github.com/diffplug/spotless/pull/2555)) * Bump default `palantir-java-format` version to latest `2.57.0` -> `2.71.0`. - ### Fixed * Respect system gitconfig when performing git operations ([#2404](https://github.com/diffplug/spotless/issues/2404)) * Fix `spaceBeforeSeparator` in Jackson formatter. ([#2103](https://github.com/diffplug/spotless/pull/2103))