diff --git a/CHANGES.md b/CHANGES.md index 22e568f566..2af0ab7090 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `Formatter` now has a field `public static final File NO_FILE_SENTINEL` which can be used to pass string content to a Formatter or FormatterStep when there is no actual File to format. ([#1525](https://github.com/diffplug/spotless/pull/1525)) ## [2.33.0] - 2023-01-26 ### Added diff --git a/gradle.properties b/gradle.properties index 5702d21bdd..d99bdac6ce 100644 --- a/gradle.properties +++ b/gradle.properties @@ -28,4 +28,4 @@ VER_DURIAN=1.2.0 VER_JGIT=5.13.1.202206130422-r VER_JUNIT=5.9.2 VER_ASSERTJ=3.24.2 -VER_MOCKITO=4.11.0 \ No newline at end of file +VER_MOCKITO=4.11.0 diff --git a/lib/src/main/java/com/diffplug/spotless/Formatter.java b/lib/src/main/java/com/diffplug/spotless/Formatter.java index 038e77fa64..3470a3dc69 100644 --- a/lib/src/main/java/com/diffplug/spotless/Formatter.java +++ b/lib/src/main/java/com/diffplug/spotless/Formatter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -237,8 +237,13 @@ public String compute(String unix, File file) { unix = LineEnding.toUnix(formatted); } } catch (Throwable e) { - String relativePath = rootDir.relativize(file.toPath()).toString(); - exceptionPolicy.handleError(e, step, relativePath); + if (file == NO_FILE_SENTINEL) { + exceptionPolicy.handleError(e, step, ""); + } else { + // Path may be forged from a different FileSystem than Filesystem.default + String relativePath = rootDir.relativize(rootDir.getFileSystem().getPath(file.getPath())).toString(); + exceptionPolicy.handleError(e, step, relativePath); + } } } return unix; @@ -284,4 +289,7 @@ public void close() { } } } + + /** This Sentinel reference may be used to pass string content to a Formatter or FormatterStep when there is no actual File to format */ + public static final File NO_FILE_SENTINEL = new File("NO_FILE_SENTINEL"); } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java index 48a8e810ee..14b407a2eb 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterFunc.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -127,7 +127,7 @@ public String apply(String unix, File file) throws Exception { @Override public String apply(String unix) throws Exception { - return apply(unix, FormatterStepImpl.SENTINEL); + return apply(unix, Formatter.NO_FILE_SENTINEL); } }; } @@ -156,7 +156,7 @@ default String apply(String unix, File file) throws Exception { @Override default String apply(String unix) throws Exception { - return apply(unix, FormatterStepImpl.SENTINEL); + return apply(unix, Formatter.NO_FILE_SENTINEL); } } } diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java index 5729f676b2..ce09f68450 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2021 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -37,8 +37,8 @@ public interface FormatterStep extends Serializable { * @param rawUnix * the content to format, guaranteed to have unix-style newlines ('\n'); never null * @param file - * the file which {@code rawUnix} was obtained from; never null. Pass an empty file using - * {@code new File("")} if and only if no file is actually associated with {@code rawUnix} + * the file which {@code rawUnix} was obtained from; never null. Pass the reference + * {@link Formatter#NO_FILE_SENTINEL} if and only if no file is actually associated with {@code rawUnix} * @return the formatted content, guaranteed to only have unix-style newlines; may return null * if the formatter step doesn't have any changes to make * @throws Exception if the formatter step experiences a problem diff --git a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java b/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java index 7663145f51..17b62e75c6 100644 --- a/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java +++ b/lib/src/main/java/com/diffplug/spotless/FormatterStepImpl.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -116,11 +116,8 @@ protected String format(Integer state, String rawUnix, File file) throws Excepti } } - /** A dummy SENTINEL file. */ - static final File SENTINEL = new File(""); - static void checkNotSentinel(File file) { - if (file == SENTINEL) { + if (file == Formatter.NO_FILE_SENTINEL) { throw new IllegalArgumentException("This step requires the underlying file. If this is a test, use StepHarnessWithFile"); } } diff --git a/testlib/build.gradle b/testlib/build.gradle index 639df8a2ab..afa6a67a91 100644 --- a/testlib/build.gradle +++ b/testlib/build.gradle @@ -12,6 +12,7 @@ dependencies { api "com.diffplug.durian:durian-testlib:${VER_DURIAN}" api "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" api "org.assertj:assertj-core:${VER_ASSERTJ}" + api "org.mockito:mockito-core:$VER_MOCKITO" implementation "com.diffplug.durian:durian-io:${VER_DURIAN}" implementation "com.diffplug.durian:durian-collect:${VER_DURIAN}" diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java index 71e2a663b5..c611cc738a 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java @@ -88,7 +88,7 @@ public AbstractStringAssert testResourceExceptionMsg(String resourceBefore) { public AbstractStringAssert testExceptionMsg(String before) { try { - formatter.compute(LineEnding.toUnix(before), FormatterStepImpl.SENTINEL); + formatter.compute(LineEnding.toUnix(before), Formatter.NO_FILE_SENTINEL); throw new SecurityException("Expected exception"); } catch (Throwable e) { if (e instanceof SecurityException) { diff --git a/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java b/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java index 06b8e64d31..314507bd8d 100644 --- a/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/FormatterTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2022 DiffPlug + * Copyright 2016-2023 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,15 +15,20 @@ */ package com.diffplug.spotless; +import java.io.File; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; +import java.nio.file.FileSystem; +import java.nio.file.FileSystems; import java.nio.file.Path; import java.nio.file.Paths; import java.util.ArrayList; +import java.util.Collections; import java.util.List; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; +import org.mockito.Mockito; import com.diffplug.common.base.StandardSystemProperty; import com.diffplug.spotless.generic.EndWithNewlineStep; @@ -89,4 +94,95 @@ protected Formatter create() { } }.testEquals(); } + + // new File("") as filePath is known to fail + @Test + public void testExceptionWithEmptyPath() throws Exception { + LineEnding.Policy lineEndingsPolicy = LineEnding.UNIX.createPolicy(); + Charset encoding = StandardCharsets.UTF_8; + FormatExceptionPolicy exceptionPolicy = FormatExceptionPolicy.failOnlyOnError(); + + Path rootDir = Paths.get(StandardSystemProperty.USER_DIR.value()); + + FormatterStep step = Mockito.mock(FormatterStep.class); + Mockito.when(step.getName()).thenReturn("someFailingStep"); + Mockito.when(step.format(Mockito.anyString(), Mockito.any(File.class))).thenThrow(new IllegalArgumentException("someReason")); + List steps = Collections.singletonList(step); + + Formatter formatter = Formatter.builder() + .lineEndingsPolicy(lineEndingsPolicy) + .encoding(encoding) + .rootDir(rootDir) + .steps(steps) + .exceptionPolicy(exceptionPolicy) + .build(); + + Assertions.assertThrows(IllegalArgumentException.class, () -> formatter.compute("someFileContent", new File(""))); + } + + // If there is no File actually holding the content, one may rely on Formatter.NO_FILE_ON_DISK + @Test + public void testExceptionWithSentinelNoFileOnDisk() throws Exception { + LineEnding.Policy lineEndingsPolicy = LineEnding.UNIX.createPolicy(); + Charset encoding = StandardCharsets.UTF_8; + FormatExceptionPolicy exceptionPolicy = FormatExceptionPolicy.failOnlyOnError(); + + Path rootDir = Paths.get(StandardSystemProperty.USER_DIR.value()); + + FormatterStep step = Mockito.mock(FormatterStep.class); + Mockito.when(step.getName()).thenReturn("someFailingStep"); + Mockito.when(step.format(Mockito.anyString(), Mockito.any(File.class))).thenThrow(new IllegalArgumentException("someReason")); + List steps = Collections.singletonList(step); + + Formatter formatter = Formatter.builder() + .lineEndingsPolicy(lineEndingsPolicy) + .encoding(encoding) + .rootDir(rootDir) + .steps(steps) + .exceptionPolicy(exceptionPolicy) + .build(); + + formatter.compute("someFileContent", Formatter.NO_FILE_SENTINEL); + } + + // rootDir may be a path not from the default FileSystem + @Test + public void testExceptionWithRootDirIsNotFileSystem() throws Exception { + LineEnding.Policy lineEndingsPolicy = LineEnding.UNIX.createPolicy(); + Charset encoding = StandardCharsets.UTF_8; + FormatExceptionPolicy exceptionPolicy = FormatExceptionPolicy.failOnlyOnError(); + + Path rootDir = Mockito.mock(Path.class); + FileSystem customFileSystem = Mockito.mock(FileSystem.class); + Mockito.when(rootDir.getFileSystem()).thenReturn(customFileSystem); + + Path pathFromFile = Mockito.mock(Path.class); + Mockito.when(customFileSystem.getPath(Mockito.anyString())).thenReturn(pathFromFile); + + Path relativized = Mockito.mock(Path.class); + Mockito.when(rootDir.relativize(Mockito.any(Path.class))).then(invok -> { + Path filePath = invok.getArgument(0); + if (filePath.getFileSystem() == FileSystems.getDefault()) { + throw new IllegalArgumentException("Can not relativize through different FileSystems"); + } + + return relativized; + }); + + FormatterStep step = Mockito.mock(FormatterStep.class); + Mockito.when(step.getName()).thenReturn("someFailingStep"); + Mockito.when(step.format(Mockito.anyString(), Mockito.any(File.class))).thenThrow(new IllegalArgumentException("someReason")); + List steps = Collections.singletonList(step); + + Formatter formatter = Formatter.builder() + .lineEndingsPolicy(lineEndingsPolicy) + .encoding(encoding) + .rootDir(rootDir) + .steps(steps) + .exceptionPolicy(exceptionPolicy) + .build(); + + formatter.compute("someFileContent", new File("/some/folder/some.file")); + } + }