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/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..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. @@ -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/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/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-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)) 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-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(); } } 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 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? 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..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,8 @@ 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; import com.diffplug.spotless.maven.antlr4.Antlr4; @@ -213,6 +215,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,8 +225,21 @@ public abstract class AbstractSpotlessMojo extends AbstractMojo { @Parameter(defaultValue = "false") protected boolean m2eEnableForIncrementalBuild; + protected List getLintSuppressions() { + return lintSuppressions; + } + 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; protected AbstractSpotlessMojo() { @@ -378,7 +396,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() { 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..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. @@ -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..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 @@ -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. @@ -17,13 +17,16 @@ 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; 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; /** @@ -60,15 +63,39 @@ protected void process(String name, Iterable files, Formatter formatter, U } try { - DirtyState dirtyState = DirtyState.of(formatter, file); - if (!dirtyState.isClean() && !dirtyState.didNotConverge()) { + LintState lintState = super.calculateLintState(formatter, file); + 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, any lints should fail the build (matching Gradle behavior) + if (hasUnsuppressedLints) { + 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()) { + String relativePath = LintSuppression.relativizeAsUnix(baseDir, file); + message.append("\n ").append(relativePath).append(":"); + lint.addWarningMessageTo(message, stepName, true); + } + } + message.append("\n Resolve these lints or suppress with ``"); + 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 116a24dcf4..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 @@ -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. @@ -27,8 +27,8 @@ 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.extra.integration.DiffMessageFormatter; import com.diffplug.spotless.maven.incremental.UpToDateChecker; @@ -68,6 +68,7 @@ protected void process(String name, Iterable files, Formatter formatter, U 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 +79,16 @@ 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); + LintState lintState = super.calculateLintState(formatter, file); + 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); @@ -104,11 +112,20 @@ protected void process(String name, Iterable files, Formatter formatter, U } 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))); } } } 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..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 @@ -29,7 +29,11 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; -import java.util.*; +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; import org.junit.jupiter.api.BeforeAll; @@ -41,6 +45,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 +353,24 @@ protected static String[] formats(String[]... formats) { .toArray(String[]::new); return formats(formatsArray); } + + private static final String ERROR_PREFIX = "[ERROR] "; + + 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) + .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); + + 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}"); + String sanitizeWindowsPathSep = sanitizeUserHome.replace('\\', '/'); + return Selfie.expectSelfie(sanitizeWindowsPathSep); + } } 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..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 @@ -247,9 +247,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: [${user.home}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 new file mode 100644 index 0000000000..1fe1c59fc6 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/LintSuppressionTest.java @@ -0,0 +1,169 @@ +/* + * 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"); + + 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 + 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 + 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); + } +} 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..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 @@ -27,7 +27,15 @@ 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/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; - } -}