diff --git a/CHANGES.md b/CHANGES.md index cd3bb03460..faf42c0cee 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,13 +10,13 @@ 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] -### Changes -* Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) - ### Added * `Jvm.Support` now accepts `-SNAPSHOT` versions, treated as the non`-SNAPSHOT`. ([#1583](https://github.com/diffplug/spotless/issues/1583)) +* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663)) ### Fixed * When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698)) +### Changes +* Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) ## [2.38.0] - 2023-04-06 ### Added diff --git a/README.md b/README.md index 52628574d9..aceb9c22e9 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ lib('npm.PrettierFormatterStep') +'{{yes}} | {{yes}} lib('npm.TsFmtFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('pom.SortPomStep') +'{{no}} | {{yes}} | {{no}} | {{no}} |', lib('python.BlackStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', +lib('rome.RomeStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', @@ -148,6 +149,7 @@ lib('yaml.JacksonYamlStep') +'{{yes}} | {{yes}} | [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`pom.SortPomStep`](lib/src/main/java/com/diffplug/spotless/pom/SortPomStep.java) | :white_large_square: | :+1: | :white_large_square: | :white_large_square: | | [`python.BlackStep`](lib/src/main/java/com/diffplug/spotless/python/BlackStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | +| [`rome.RomeStep`](lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`wtp.EclipseWtpFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/wtp/EclipseWtpFormatterStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | diff --git a/lib/src/main/java/com/diffplug/spotless/rome/Architecture.java b/lib/src/main/java/com/diffplug/spotless/rome/Architecture.java new file mode 100644 index 0000000000..64e0a94640 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rome/Architecture.java @@ -0,0 +1,69 @@ +/* + * 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. + * 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.rome; + +/** + * Enumeration of possible computer architectures. + */ +enum Architecture { + /** The arm64 architecture */ + ARM64, + /** Either x64 or x64_32 architecture */ + X64; + + /** + * Attempts to guess the architecture of the environment running the JVM. + * + * @return The best guess for the architecture. + */ + public static Architecture guess() { + var arch = System.getProperty("os.arch"); + var version = System.getProperty("os.version"); + + if (arch == null || arch.isBlank()) { + throw new IllegalStateException("No OS information is available, specify the Rome executable manually"); + } + + var msg = "Unsupported architecture " + arch + "/" + version + + ", specify the path to the Rome executable manually"; + + if (arch.equals("ppc64le")) { + throw new IllegalStateException(msg); + } + if (arch.equals("aarch64")) { + throw new IllegalStateException(msg); + } + if (arch.equals("s390x")) { + throw new IllegalStateException(msg); + } + if (arch.equals("ppc64")) { + throw new IllegalStateException(msg); + } + if (arch.equals("ppc")) { + throw new IllegalStateException(msg); + } + if (arch.equals("arm")) { + if (version.contains("v7")) { + throw new IllegalStateException(msg); + } + return ARM64; + } + if (arch.contains("64")) { + return X64; + } + throw new IllegalStateException(msg); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/rome/OS.java b/lib/src/main/java/com/diffplug/spotless/rome/OS.java new file mode 100644 index 0000000000..44c87d9ece --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rome/OS.java @@ -0,0 +1,56 @@ +/* + * 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. + * 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.rome; + +import java.util.Locale; + +/** + * Enumeration of possible computer operation systems. + */ +enum OS { + /** Any derivate of a Linux operation system. */ + LINUX, + /** The Macintosh operating system/ */ + MAC_OS, + /** The Microsoft Windows operating system. */ + WINDOWS,; + + /** + * Attempts to guess the OS of the environment running the JVM. + * + * @return The best guess for the architecture. + * @throws IllegalStateException When the OS is either unsupported or no + * information about the OS could be retrieved. + */ + public static OS guess() { + var osName = System.getProperty("os.name"); + if (osName == null || osName.isBlank()) { + throw new IllegalStateException("No OS information is available, specify the Rome executable manually"); + } + var osNameUpper = osName.toUpperCase(Locale.ROOT); + if (osNameUpper.contains("SUNOS") || osName.contains("AIX")) { + throw new IllegalStateException( + "Unsupported OS " + osName + ", specify the path to the Rome executable manually"); + } + if (osNameUpper.contains("WINDOWS")) { + return OS.WINDOWS; + } else if (osNameUpper.contains("MAC")) { + return OS.MAC_OS; + } else { + return OS.LINUX; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/rome/Platform.java b/lib/src/main/java/com/diffplug/spotless/rome/Platform.java new file mode 100644 index 0000000000..c50608830a --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rome/Platform.java @@ -0,0 +1,91 @@ +/* + * 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. + * 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.rome; + +/** + * Represents a platform where code is run, consisting of an operating system + * and an architecture. + */ +class Platform { + /** + * Attempts to guess the platform of the hosting environment running the JVM + * machine. + * + * @return The best guess for the current OS and architecture. + * @throws IllegalStateException When no OS information is available, or when + * the OS or architecture is unsupported. + */ + public static Platform guess() { + var os = OS.guess(); + var architecture = Architecture.guess(); + return new Platform(os, architecture); + } + + private final Architecture architecture; + + private final OS os; + + /** + * Creates a new Platform descriptor for the given OS and architecture. + * + * @param os Operating system of the platform. + * @param architecture Architecture of the platform. + */ + public Platform(OS os, Architecture architecture) { + this.os = os; + this.architecture = architecture; + } + + /** + * @return The architecture of this platform. + */ + public Architecture getArchitecture() { + return architecture; + } + + /** + * @return The operating system of this platform. + */ + public OS getOs() { + return os; + } + + /** + * @return Whether the operating system is Linux. + */ + public boolean isLinux() { + return os == OS.LINUX; + } + + /** + * @return Whether the operating system is Mac. + */ + public boolean isMac() { + return os == OS.MAC_OS; + } + + /** + * @return Whether the operating system is Windows. + */ + public boolean isWindows() { + return os == OS.WINDOWS; + } + + @Override + public String toString() { + return String.format("Platform[os=%s,architecture=%s]", os, architecture); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/rome/RomeExecutableDownloader.java b/lib/src/main/java/com/diffplug/spotless/rome/RomeExecutableDownloader.java new file mode 100644 index 0000000000..578916a54b --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rome/RomeExecutableDownloader.java @@ -0,0 +1,384 @@ +/* + * 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. + * 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.rome; + +import java.io.IOException; +import java.math.BigInteger; +import java.net.URI; +import java.net.http.HttpClient; +import java.net.http.HttpClient.Redirect; +import java.net.http.HttpRequest; +import java.net.http.HttpResponse.BodyHandlers; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.nio.file.StandardOpenOption; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.Locale; +import java.util.Objects; +import java.util.Optional; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +/** + * Downloader for the Rome executable: + * https://github.com/rome/tools. + */ +final class RomeExecutableDownloader { + private static final Logger logger = LoggerFactory.getLogger(RomeExecutableDownloader.class); + + /** + * The checksum algorithm to use for checking the integrity of downloaded files. + */ + private static final String CHECKSUM_ALGORITHM = "SHA-256"; + + /** + * The pattern for {@link String#format(String, Object...) String.format()} for + * the file name of a Rome executable for a certain version and architecure. The + * first parameter is the platform, the second is the OS, the third is the + * architecture. + */ + private static final String DOWNLOAD_FILE_PATTERN = "rome-%s-%s-%s"; + + /** + * The pattern for {@link String#format(String, Object...) String.format()} for + * the platform part of the Rome executable download URL. First parameter is the + * OS, second parameter the architecture, the third the file extension. + */ + private static final String PLATFORM_PATTERN = "%s-%s%s"; + + /** + * {@link OpenOption Open options} for reading an existing file without write + * access. + */ + private static final OpenOption[] READ_OPTIONS = {StandardOpenOption.READ}; + + /** + * The pattern for {@link String#format(String, Object...) String.format()} for + * the URL where the Rome executables can be downloaded. The first parameter is + * the version, the second parameter is the OS / platform. + */ + private static final String URL_PATTERN = "https://github.com/rome/tools/releases/download/cli%%2Fv%s/rome-%s"; + + /** + * {@link OpenOption Open options} for creating a new file, overwriting the + * existing file if present. + */ + private static final OpenOption[] WRITE_OPTIONS = {StandardOpenOption.CREATE, StandardOpenOption.TRUNCATE_EXISTING, + StandardOpenOption.WRITE}; + + private Path downloadDir; + + /** + * Creates a new downloader for the Rome executable. The executable files are + * stored in the given download directory. + * + * @param downloadDir Directory where + */ + public RomeExecutableDownloader(Path downloadDir) { + this.downloadDir = downloadDir; + } + + /** + * Downloads the Rome executable for the current platform from the network to + * the download directory. When the executable exists already, it is + * overwritten. + * + * @param version Desired Rome version. + * @return The path to the Rome executable. + * @throws IOException When the executable cannot be downloaded from + * the network or the file system could not be + * accessed. + * @throws InterruptedException When this thread was interrupted while + * downloading the file. + * @throws IllegalStateException When no information about the current OS and + * architecture could be obtained, or when the OS + * or architecture is not supported. + */ + public Path download(String version) throws IOException, InterruptedException { + var platform = Platform.guess(); + var url = getDownloadUrl(version, platform); + var executablePath = getExecutablePath(version, platform); + var checksumPath = getChecksumPath(executablePath); + var executableDir = executablePath.getParent(); + if (executableDir != null) { + Files.createDirectories(executableDir); + } + logger.info("Attempting to download Rome from '{}' to '{}'", url, executablePath); + var request = HttpRequest.newBuilder(URI.create(url)).GET().build(); + var handler = BodyHandlers.ofFile(executablePath, WRITE_OPTIONS); + var response = HttpClient.newBuilder().followRedirects(Redirect.NORMAL).build().send(request, handler); + if (response.statusCode() != 200) { + throw new IOException("Failed to download file from " + url + ", server returned " + response.statusCode()); + } + var downloadedFile = response.body(); + if (!Files.exists(downloadedFile) || Files.size(downloadedFile) == 0) { + throw new IOException("Failed to download file from " + url + ", file is empty or does not exist"); + } + writeChecksumFile(downloadedFile, checksumPath); + logger.debug("Rome was downloaded successfully to '{}'", downloadedFile); + return downloadedFile; + } + + /** + * Ensures that the Rome executable for the current platform exists in the + * download directory. When the executable does not exist in the download + * directory, an attempt is made to download the Rome executable from the + * network. When the executable exists already, no attempt to download it again + * is made. + * + * @param version Desired Rome version. + * @return The path to the Rome executable. + * @throws IOException When the executable cannot be downloaded from + * the network or the file system could not be + * accessed. + * @throws InterruptedException When this thread was interrupted while + * downloading the file. + * @throws IllegalStateException When no information about the current OS and + * architecture could be obtained, or when the OS + * or architecture is not supported. + */ + public Path ensureDownloaded(String version) throws IOException, InterruptedException { + var platform = Platform.guess(); + logger.debug("Ensuring that Rome for platform '{}' is downloaded", platform); + var existing = findDownloaded(version); + if (existing.isPresent()) { + logger.debug("Rome was already downloaded, using executable at '{}'", existing.get()); + return existing.get(); + } else { + logger.debug("Rome was not yet downloaded, attempting to download executable"); + return download(version); + } + } + + /** + * Attempts to find the Rome executable for the current platform in the download + * directory. No attempt is made to download the executable from the network. + * + * @param version Desired Rome version. + * @return The path to the Rome executable. + * @throws IOException When the executable does not exists in the + * download directory, or when the file system + * could not be accessed. + * @throws IllegalStateException When no information about the current OS and + * architecture could be obtained, or when the OS + * or architecture is not supported. + */ + public Optional findDownloaded(String version) throws IOException { + var platform = Platform.guess(); + var executablePath = getExecutablePath(version, platform); + logger.debug("Checking rome executable at {}", executablePath); + return checkFileWithChecksum(executablePath) ? Optional.ofNullable(executablePath) : Optional.empty(); + } + + /** + * Checks whether the given file exists and matches the checksum. The checksum + * must be contained in a file next to the file to check. + * + * @param filePath File to check. + * @return true if the file exists and matches the checksum, + * false otherwise. + */ + private boolean checkFileWithChecksum(Path filePath) { + if (!Files.exists(filePath)) { + logger.debug("File '{}' does not exist yet", filePath); + return false; + } + if (Files.isDirectory(filePath)) { + logger.debug("File '{}' exists, but is a directory", filePath); + return false; + } + var checksumPath = getChecksumPath(filePath); + if (!Files.exists(checksumPath)) { + logger.debug("File '{}' exists, but checksum file '{}' does not", filePath, checksumPath); + return false; + } + if (Files.isDirectory(checksumPath)) { + logger.debug("Checksum file '{}' exists, but is a directory", checksumPath); + return false; + } + try { + var actualChecksum = computeChecksum(filePath, CHECKSUM_ALGORITHM); + var expectedChecksum = readTextFile(checksumPath, StandardCharsets.ISO_8859_1); + logger.debug("Expected checksum: {}, actual checksum: {}", expectedChecksum, actualChecksum); + return Objects.equals(expectedChecksum, actualChecksum); + } catch (final IOException ignored) { + return false; + } + } + + /** + * Computes the checksum of the given file. + * + * @param file File to process. + * @param algorithm The checksum algorithm to use. + * @return The checksum of the given file. + * @throws IOException When the file does not exist or could not be read. + */ + private String computeChecksum(Path file, String algorithm) throws IOException { + var buffer = new byte[4192]; + try (var in = Files.newInputStream(file, READ_OPTIONS)) { + var digest = MessageDigest.getInstance(algorithm); + int result; + while ((result = in.read(buffer, 0, buffer.length)) != -1) { + digest.update(buffer, 0, result); + } + var bytes = digest.digest(); + return String.format("%0" + (bytes.length * 2) + "X", new BigInteger(1, bytes)); + } catch (NoSuchAlgorithmException e) { + throw new RuntimeException(e); + } + } + + /** + * Finds the code name for the given operating system used by the Rome + * executable download URL. + * + * @param os Desired operating system. + * @return Code name for the Rome download URL. + * @throws IOException When the given OS is not supported by Rome. + */ + private String getArchitectureCodeName(Architecture architecture) throws IOException { + switch (architecture) { + case ARM64: + return "arm64"; + case X64: + return "x64"; + default: + throw new IOException("Unsupported architecture: " + architecture); + } + } + + /** + * Derives a path for the file which contains the checksum of the given file. + * + * @param file A file for which to derive the checksum file path. + * @return The path with the checksum for the given file. + */ + private Path getChecksumPath(Path file) { + var parent = file.getParent(); + var base = parent != null ? parent : file; + var fileName = file.getFileName(); + var checksumName = fileName != null ? fileName.toString() + ".sha256" : "checksum.sha256"; + return base.resolve(checksumName); + } + + /** + * Finds the URL from which the Rome executable can be downloaded. + * + * @param version Desired Rome version. + * @param platform Desired platform. + * @return The URL for the Rome executable. + * @throws IOException When the platform is not supported by Rome. + */ + private String getDownloadUrl(String version, Platform platform) throws IOException { + var osCodeName = getOsCodeName(platform.getOs()); + var architectureCodeName = getArchitectureCodeName(platform.getArchitecture()); + var extension = getDownloadUrlExtension(platform.getOs()); + var platformString = String.format(PLATFORM_PATTERN, osCodeName, architectureCodeName, extension); + return String.format(URL_PATTERN, version, platformString); + } + + /** + * Finds the file extension of the Rome download URL for the given operating + * system. + * + * @param os Desired operating system. + * @return Extension for the Rome download URL. + * @throws IOException When the given OS is not supported by Rome. + */ + private String getDownloadUrlExtension(OS os) throws IOException { + switch (os) { + case LINUX: + return ""; + case MAC_OS: + return ""; + case WINDOWS: + return ".exe"; + default: + throw new IOException("Unsupported OS: " + os); + } + } + + /** + * Finds the path on the file system for the Rome executable with a given + * version and platform. + * + * @param version Desired Rome version. + * @param platform Desired platform. + * @return The path for the Rome executable. + */ + private Path getExecutablePath(String version, Platform platform) { + var os = platform.getOs().name().toLowerCase(Locale.ROOT); + var arch = platform.getArchitecture().name().toLowerCase(Locale.ROOT); + var fileName = String.format(DOWNLOAD_FILE_PATTERN, os, arch, version); + return downloadDir.resolve(fileName); + } + + /** + * Finds the code name for the given operating system used by the Rome + * executable download URL. + * + * @param os Desired operating system. + * @return Code name for the Rome download URL. + * @throws IOException When the given OS is not supported by Rome. + */ + private String getOsCodeName(OS os) throws IOException { + switch (os) { + case LINUX: + return "linux"; + case MAC_OS: + return "darwin"; + case WINDOWS: + return "win32"; + default: + throw new IOException("Unsupported OS: " + os); + } + } + + /** + * Reads a plain text file with the given encoding into a string. + * + * @param file File to read. + * @param charset Encoding to use. + * @return The contents of the file as a string. + * @throws IOException When the file could not be read. + */ + private String readTextFile(Path file, Charset charset) throws IOException { + try (var in = Files.newInputStream(file, READ_OPTIONS)) { + return new String(in.readAllBytes(), charset); + } + } + + /** + * Computes the checksum of the given file and writes it to the target checksum + * file, using the {@code ISO_8859_1} encoding. + * + * @param file + * @param checksumPath + * @throws IOException + */ + private void writeChecksumFile(Path file, Path checksumPath) throws IOException { + var checksum = computeChecksum(file, CHECKSUM_ALGORITHM); + try (var out = Files.newOutputStream(checksumPath, WRITE_OPTIONS)) { + out.write(checksum.getBytes(StandardCharsets.ISO_8859_1)); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java b/lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java new file mode 100644 index 0000000000..d6a3e62669 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/rome/RomeStep.java @@ -0,0 +1,475 @@ +/* + * 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. + * 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.rome; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.nio.charset.Charset; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.nio.file.attribute.PosixFilePermission; +import java.util.ArrayList; +import java.util.Collections; +import java.util.HashSet; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.ForeignExe; +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ProcessRunner; + +/** + * formatter step that formats JavaScript and TypeScript code with Rome: + * https://github.com/rome/tools. + * It delegates to the Rome executable. The Rome executable is downloaded from + * the network when no executable path is provided explicitly. + */ +public class RomeStep { + private static final Logger logger = LoggerFactory.getLogger(RomeStep.class); + + /** + * Path to the directory with the {@code rome.json} config file, can be + * null, in which case the defaults are used. + */ + private String configPath; + + /** + * The language (syntax) of the input files to format. When null or + * the empty string, the language is detected automatically from the file name. + * Currently the following languages are supported by Rome: + * + */ + private String language; + + /** + * Path to the Rome executable. Can be null, but either a path to + * the executable of a download directory and version must be given. The path + * must be either an absolute path, or a file name without path separators. If + * the latter, it is interpreted as a command in the user's path. + */ + private final String pathToExe; + + /** + * Absolute path to the download directory for storing the download Rome + * executable. Can be null, but either a path to the executable of + * a download directory and version must be given. + */ + private final String downloadDir; + + /** + * Version of Rome to download. Can be null, but either a path to + * the executable of a download directory and version must be given. + */ + private final String version; + + /** + * @return The name of this format step, i.e. {@code rome}. + */ + public static String name() { + return "rome"; + } + + /** + * Creates a Rome step that format code by downloading to the given Rome + * version. The executable is downloaded from the network. + * + * @param version Version of the Rome executable to download. + * @param downloadDir Directory where to place the downloaded executable. + * @return A new Rome step that download the executable from the network. + */ + public static RomeStep withExeDownload(String version, String downloadDir) { + return new RomeStep(version, null, downloadDir); + } + + /** + * Creates a Rome step that formats code by delegating to the Rome executable + * located at the given path. + * + * @param pathToExe Path to the Rome executable to use. + * @return A new Rome step that format with the given executable. + */ + public static RomeStep withExePath(String pathToExe) { + return new RomeStep(null, pathToExe, null); + } + + /** + * Attempts to add a POSIX permission to the given file, ignoring any errors. + * All existing permissions on the file are preserved and the new permission is + * added, if possible. + * + * @param file File or directory to which to add a permission. + * @param permission The POSIX permission to add. + */ + private static void attemptToAddPosixPermission(Path file, PosixFilePermission permission) { + try { + var newPermissions = new HashSet<>(Files.getPosixFilePermissions(file)); + newPermissions.add(permission); + Files.setPosixFilePermissions(file, newPermissions); + } catch (final Exception ignore) { + logger.debug("Unable to add POSIX permission '{}' to file '{}'", permission, file); + } + } + + /** + * Finds the default version for Rome when no version is specified explicitly. + * Over time this will become outdated -- people should always specify the + * version explicitly! + * + * @return The default version for Rome. + */ + private static String defaultVersion() { + return "12.0.0"; + } + + /** + * Attempts to make the given file executable. This is a best-effort attempt, + * any errors are swallowed. Depending on the OS, the file might still be + * executable even if this method fails. The user will get a descriptive error + * later when we attempt to execute the Rome executable. + * + * @param filePath Path to the file to make executable. + */ + private static void makeExecutable(String filePath) { + var exePath = Paths.get(filePath); + attemptToAddPosixPermission(exePath, PosixFilePermission.GROUP_EXECUTE); + attemptToAddPosixPermission(exePath, PosixFilePermission.OTHERS_EXECUTE); + attemptToAddPosixPermission(exePath, PosixFilePermission.OWNER_EXECUTE); + } + + /** + * Finds the absolute path of a command on the user's path. Uses {@code which} + * for Linux and {@code where} for Windows. + * + * @param name Name of the command to resolve. + * @return The absolute path of the command's executable. + * @throws IOException When the command could not be resolved. + * @throws InterruptedException When this thread was interrupted while waiting + * to the which command to finish. + */ + private static String resolveNameAgainstPath(String name) throws IOException, InterruptedException { + try (var runner = new ProcessRunner()) { + var cmdWhich = runner.shellWinUnix("where " + name, "which " + name); + if (cmdWhich.exitNotZero()) { + throw new IOException("Unable to find " + name + " on path via command " + cmdWhich); + } else { + return cmdWhich.assertExitZero(Charset.defaultCharset()).trim(); + } + } + } + + /** + * Checks the Rome config path. When the config path does not exist or when it + * does not contain a file named {@code rome.json}, an error is thrown. + */ + private static void validateRomeConfigPath(String configPath) { + if (configPath == null) { + return; + } + var path = Paths.get(configPath); + var config = path.resolve("rome.json"); + if (!Files.exists(path)) { + throw new IllegalArgumentException("Rome config directory does not exist: " + path); + } + if (!Files.exists(config)) { + throw new IllegalArgumentException("Rome config does not exist: " + config); + } + } + + /** + * Checks the Rome executable file. When the file does not exist, an error is + * thrown. + */ + private static void validateRomeExecutable(String resolvedPathToExe) { + if (!new File(resolvedPathToExe).isFile()) { + throw new IllegalArgumentException("Rome executable does not exist: " + resolvedPathToExe); + } + } + + /** + * Creates a new Rome step with the configuration from the given builder. + * + * @param builder Builder with the configuration to use. + */ + private RomeStep(String version, String pathToExe, String downloadDir) { + this.version = version != null && !version.isBlank() ? version : defaultVersion(); + this.pathToExe = pathToExe; + this.downloadDir = downloadDir; + } + + /** + * Creates a formatter step with the current configuration, which formats code + * by passing it to the Rome executable. + * + * @return A new formatter step for formatting with Rome. + */ + public FormatterStep create() { + return FormatterStep.createLazy(name(), this::createState, State::toFunc); + } + + /** + * Sets the path to the directory with the {@code rome.json} config file. When + * no config path is set, the default configuration is used. + * + * @param configPath Config path to use. Must point to a directory which contain + * a file named {@code rome.json}. + * @return This builder instance for chaining method calls. + */ + public RomeStep withConfigPath(String configPath) { + this.configPath = configPath; + return this; + } + + /** + * Sets the language of the files to format When no language is set, it is + * determined automatically from the file name. The following languages are + * currently supported by Rome. + * + * + * + * @param language The language of the files to format. + * @return This builder instance for chaining method calls. + */ + public RomeStep withLanguage(String language) { + this.language = language; + return this; + } + + /** + * Resolves the Rome executable, possibly downloading it from the network, and + * creates a new state instance with the resolved executable that can format + * code via Rome. + * + * @return The state instance for formatting code via Rome. + * @throws IOException When any file system or network operations + * failed, such as when the Rome executable could + * not be downloaded, or when the given executable + * does not exist. + * @throws InterruptedException When the Rome executable needs to be downloaded + * and this thread was interrupted while waiting + * for the download to complete. + */ + private State createState() throws IOException, InterruptedException { + var resolvedPathToExe = resolveExe(); + validateRomeExecutable(resolvedPathToExe); + validateRomeConfigPath(configPath); + logger.debug("Using Rome executable located at '{}'", resolvedPathToExe); + var exeSignature = FileSignature.signAsList(Collections.singleton(new File(resolvedPathToExe))); + makeExecutable(resolvedPathToExe); + return new State(resolvedPathToExe, exeSignature, configPath, language); + } + + /** + * Resolves the path to the Rome executable, given the configuration of this + * step. When the path to the Rome executable is given explicitly, that path is + * used as-is. Otherwise, at attempt is made to download the Rome executable for + * the configured version from the network, unless it was already downloaded and + * is available in the cache. + * + * @return The path to the resolved Rome executable. + * @throws IOException When any file system or network operations + * failed, such as when the Rome executable could + * not be downloaded. + * @throws InterruptedException When the Rome executable needs to be downloaded + * and this thread was interrupted while waiting + * for the download to complete. + */ + private String resolveExe() throws IOException, InterruptedException { + new ForeignExe(); + if (pathToExe != null) { + if (Paths.get(pathToExe).getNameCount() == 1) { + return resolveNameAgainstPath(pathToExe); + } else { + return pathToExe; + } + } else { + var downloader = new RomeExecutableDownloader(Paths.get(downloadDir)); + var downloaded = downloader.ensureDownloaded(version).toString(); + makeExecutable(downloaded); + return downloaded; + } + } + + /** + * The internal state used by the Rome formatter. A state instance is created + * when the spotless plugin for Maven or Gradle is executed, and reused for all + * formatting requests for different files. The lifetime of the instance ends + * when the Maven or Gradle plugin was successfully executed. + *

+ * The state encapsulated a particular executable. It is serializable for + * caching purposes. Spotless keeps a cache of which files need to be formatted. + * The cache is busted when the serialized form of a state instance changes. + */ + private static class State implements Serializable { + private static final long serialVersionUID = 6846790911054484379L; + + /** Path to the exe file */ + private final String pathToExe; + + /** The signature of the exe file, if any, used for caching. */ + @SuppressWarnings("unused") + private final FileSignature exeSignature; + + /** + * The optional path to the directory with the {@code rome.json} config file. + */ + private final String configPath; + + /** + * The language of the files to format. When null or the empty + * string, the language is detected from the file name. + */ + private final String language; + + /** + * Creates a new state for instance which can format code with the given Rome + * executable. + * + * @param exe Path to the Rome executable. + * @param exeSignature Signature (e.g. SHA-256 checksum) of the Rome executable. + * @param configPath Path to the optional directory with the {@code rome.json} + * config file, can be null, in which case the + * defaults are used. + */ + private State(String exe, FileSignature exeSignature, String configPath, String language) { + this.pathToExe = exe; + this.exeSignature = exeSignature; + this.configPath = configPath; + this.language = language; + } + + /** + * Builds the list of arguments for the command that executes Rome to format a + * piece of code passed via stdin. + * + * @param file File to format. + * @return The Rome command to use for formatting code. + */ + private String[] buildRomeCommand(File file) { + var fileName = resolveFileName(file); + var argList = new ArrayList(); + argList.add(pathToExe); + argList.add("format"); + argList.add("--stdin-file-path"); + argList.add(fileName); + if (configPath != null) { + argList.add("--config-path"); + argList.add(configPath); + } + return argList.toArray(String[]::new); + } + + /** + * Formats the given piece of code by delegating to the Rome executable. The + * code is passed to Rome via stdin, the file name is used by Rome only to + * determine the code syntax (e.g. JavaScript or TypeScript). + * + * @param runner Process runner for invoking the Rome executable. + * @param input Code to format. + * @param file File to format. + * @return The formatted code. + * @throws IOException When a file system error occurred while + * executing Rome. + * @throws InterruptedException When this thread was interrupted while waiting + * for Rome to finish formatting. + */ + private String format(ProcessRunner runner, String input, File file) throws IOException, InterruptedException { + var stdin = input.getBytes(StandardCharsets.UTF_8); + var args = buildRomeCommand(file); + if (logger.isDebugEnabled()) { + logger.debug("Running Rome comand to format code: '{}'", String.join(", ", args)); + } + return runner.exec(stdin, args).assertExitZero(StandardCharsets.UTF_8); + } + + /** + * The Rome executable currently does not have a parameter to specify the + * expected language / syntax. Rome always determined the language from the file + * extension. This method returns the file name for the desired language when a + * language was requested explicitly, or the file name of the input file for + * auto detection. + * + * @param file File to be formatted. + * @return The file name to pass to the Rome executable. + */ + private String resolveFileName(File file) { + var name = file.getName(); + if (language == null || language.isBlank()) { + return name; + } + var dot = name.lastIndexOf("."); + var ext = dot >= 0 ? name.substring(dot + 1) : name; + switch (language) { + case "js?": + return "jsx".equals(ext) || "js".equals(ext) || "mjs".equals(ext) || "cjs".equals(ext) ? name + : "file.js"; + case "ts?": + return "tsx".equals(ext) || "ts".equals(ext) || "mts".equals(ext) || "cts".equals(ext) ? name + : "file.js"; + case "js": + return "js".equals(ext) || "mjs".equals(ext) || "cjs".equals(ext) ? name : "file.js"; + case "jsx": + return "jsx".equals(ext) ? name : "file.jsx"; + case "ts": + return "ts".equals(ext) || "mts".equals(ext) || "cts".equals(ext) ? name : "file.ts"; + case "tsx": + return "tsx".equals(ext) ? name : "file.tsx"; + case "json": + return "json".equals(ext) ? name : "file.json"; + // so that we can support new languages such as css or yaml when Rome adds + // support for them without having to change the code + default: + return "file." + language; + } + } + + /** + * Creates a new formatter function for formatting a piece of code by delegating + * to the Rome executable. + * + * @return A formatter function for formatting code. + */ + private FormatterFunc.Closeable toFunc() { + var runner = new ProcessRunner(); + return FormatterFunc.Closeable.of(runner, this::format); + } + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index 0be3e2584d..5d68193da9 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,8 +3,10 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663)) ### Fixed -* Added `@DisableCachingByDefault` to `RegisterDependenciesTask`. +* Added `@DisableCachingByDefault` to `RegisterDependenciesTask`. ([#1666](https://github.com/diffplug/spotless/pull/1666)) * When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698)) ### Changes * Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index d84645e51d..c853f82a3f 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -63,8 +63,8 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [FreshMark](#freshmark) aka markdown - [Antlr4](#antlr4) ([antlr4formatter](#antlr4formatter)) - [SQL](#sql) ([dbeaver](#dbeaver), [prettier](#prettier)) - - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript)) - - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript)) + - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript), [Rome](#rome)) + - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript), [Rome](#rome)) - [JSON](#json) - [YAML](#yaml) - [Gherkin](#gherkin) @@ -75,6 +75,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - c, c++, c#, objective-c, protobuf, javascript, java - [eclipse web tools platform](#eclipse-web-tools-platform) - css, html, js, json, xml + - [Rome](#rome) ([binary detection](#rome-binary), [config file](#rome-configuration-file), [input language](#rome-input-language)) - **Language independent** - [Generic steps](#generic-steps) - [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history)) @@ -614,7 +615,8 @@ spotless { tsfmt() // has its own section below prettier() // has its own section below - eslint() // has its own section below + eslint() // has its own section below + rome() // has its own section below licenseHeader '/* (C) $YEAR */', '(import|const|declare|export|var) ' // or licenseHeaderFile // note the '(import|const|...' argument - this is a regex which identifies the top @@ -706,7 +708,8 @@ spotless { target 'src/**/*.js' // you have to set the target manually prettier() // has its own section below - eslint() // has its own section below + eslint() // has its own section below + rome() // has its own section below licenseHeader '/* (C) $YEAR */', 'REGEX_TO_DEFINE_TOP_OF_FILE' // or licenseHeaderFile } @@ -773,6 +776,7 @@ spotless { eclipseWtp('json') // see Eclipse web tools platform section gson() // has its own section below jackson() // has its own section below + rome() // has its own section below } } ``` @@ -1065,6 +1069,171 @@ Unlike Eclipse, Spotless WTP ignores per default external URIs in schema locatio external entities. To allow the access of external URIs, set the property `resolveExternalURI` to true. +## Rome + +[homepage](https://rome.tools/). [changelog](https://github.com/rome/tools/blob/main/CHANGELOG.md). Rome is a formatter that for the Frontend written in Rust, which has a native binary, +does not require Node.js and as such, is pretty fast. It can currently format +JavaScript, TypeScript, JSX, and JSON, and may support +[more frontend languages](https://docs.rome.tools/internals/language_support/) +such as CSS in the future. + +You can use rome in any language-specific format for supported languages, but +usually you will be creating a generic format. + +```gradle +spotless { + format 'styling', { + // you have to set the target manually + target 'src/*/webapp/**/*.js' + + // Download Rome from the network if not already downloaded, see below for more info + rome('12.0.0') + + // (optional) Path to the directory with the rome.json conig file + rome('12.0.0').configPath("path/config/dir") + + // (optional) Rome will auto detect the language based on the file extension. + // See below for possible values. + rome('12.0.0').language("js") + } +} +``` + +**Limitations:** +- The auto-discovery of config files (up the file tree) will not work when using + Rome within spotless. + +To apply Rome to more kinds of files with a different configuration, just add +more formats: + +```gradle +spotless { + format 'rome-js', { + target '**/*.js' + rome('12.0.0') + } + format 'rome-ts', { + target '**/*.ts' + rome('12.0.0') + } + format 'rome-json', { + target '**/*.json' + rome('12.0.0') + } +} +``` + +### Rome binary + +To format with Rome, spotless needs to find the Rome binary. By default, +spotless downloads the binary for the given version from the network. This +should be fine in most cases, but may not work e.g. when there is not connection +to the internet. + +To download the Rome binary from the network, just specify a version: + +```gradle +spotless { + format 'rome', { + target '**/*.js','**/*.ts','**/*.json' + rome('12.0.0') + } +} +``` + +Spotless uses a default version when you do not specfiy a version, but this +may change at any time, so we recommend that you always set the Rome version +you want to use. Optionally, you can also specify a directory for the downloaded +Rome binaries (defaults to `~/.m2/repository/com/diffplug/spotless/spotless-data/rome`): + +```gradle +spotless { + format 'rome', { + target '**/*.js','**/*.ts','**/*.json' + // Relative paths are resolved against the project's base directory + rome('12.0.0').downloadDir("${project.gradle.gradleUserHomeDir}/rome") + } +} +``` + +To use a fixed binary, omit the `version` and specify a `pathToExe`: + +```gradle +spotless { + format 'rome', { + target '**/*.js','**/*.ts','**/*.json' + rome().pathToExe("${project.buildDir.absolutePath}/bin/rome") + } +} +``` + +Absolute paths are used as-is. Relative paths are resolved against the project's +base directory. To use a pre-installed Rome binary on the user's path, specify +just a name without any slashes / backslashes: + +```gradle +spotless { + format 'rome', { + target '**/*.js','**/*.ts','**/*.json' + // Uses the "rome" command, which must be on the user's path. --> + rome().pathToExe('rome') + } +} +``` + +### Rome configuration file + +Rome is a biased formatter and linter without many options, but there are a few +basic options. Rome uses a file named [rome.json](https://docs.rome.tools/configuration/) +for its configuration. When none is specified, the default configuration from +Rome is used. To use a custom configuration: + +```gradle +spotless { + format 'rome', { + target '**/*.js','**/*.ts','**/*.json' + // Must point to the directory with the "rome.json" config file --> + // Relative paths are resolved against the project's base directory --> + rome('12.0.0').configPath('./config') + } +} +``` + +### Rome input language + +By default, Rome detects the language / syntax of the files to format +automatically from the file extension. This may fail if your source code files +have unusual extensions for some reason. If you are using the generic format, +you can force a certain language like this: + +```xml + + + + + src/**/typescript/**/*.mjson + + + + 12.0.0 + json + + + + + +``` + +The following languages are currently recognized: + +* `js` -- JavaScript +* `jsx` -- JavaScript + JSX (React) +* `js?` -- JavaScript, with or without JSX, depending on the file extension +* `ts` -- TypeScript +* `tsx` -- TypeScript + JSX (React) +* `ts?` -- TypeScript, with or without JSX, depending on the file extension +* `json` -- JSON + ## Generic steps [Prettier](#prettier), [eclipse wtp](#eclipse-web-tools-platform), and [license header](#license-header) are available in every format, and they each have their own section. As mentioned in the [quickstart](#quickstart), there are a variety of simple generic steps which are also available in every format, here are examples of these: diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle index 9a725dbf5d..021155abfe 100644 --- a/plugin-gradle/build.gradle +++ b/plugin-gradle/build.gradle @@ -25,6 +25,7 @@ dependencies { testImplementation "org.junit.jupiter:junit-jupiter:${VER_JUNIT}" testImplementation "org.assertj:assertj-core:${VER_ASSERTJ}" testImplementation "com.diffplug.durian:durian-testlib:${VER_DURIAN}" + testImplementation 'org.owasp.encoder:encoder:1.2.3' } apply from: rootProject.file('gradle/special-tests.gradle') 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 fd613e82e1..4fa74bc2ea 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 @@ -229,7 +229,7 @@ protected final FileCollection parseTarget(Object target) { private final FileCollection parseTargetIsExclude(Object target, boolean isExclude) { if (target instanceof Collection) { - return parseTargetsIsExclude(((Collection) target).toArray(), isExclude); + return parseTargetsIsExclude(((Collection) target).toArray(), isExclude); } else if (target instanceof FileCollection) { return (FileCollection) target; } else if (target instanceof String) { @@ -649,6 +649,58 @@ protected FormatterStep createStep() { } } + /** + * Generic Rome formatter step that detects the language of the input file from + * the file name. It should be specified as a formatter step for a generic + * format{ ... }. + */ + public class RomeGeneric extends RomeStepConfig { + @Nullable + String language; + + /** + * Creates a new Rome config that downloads the Rome executable for the given version from the network. + * @param version Rome version to use. The default version is used when null. + */ + public RomeGeneric(String version) { + super(getProject(), FormatExtension.this::replaceStep, version); + } + + /** + * Sets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently the following languages are supported by Rome: + *

+ * @param language The language of the files to format. + * @return This step for further configuration. + */ + public RomeGeneric language(String language) { + this.language = language; + replaceStep(); + return this; + } + + @Override + protected String getLanguage() { + return language; + } + + @Override + protected RomeGeneric getThis() { + return this; + } + } + /** Uses the default version of prettier. */ public PrettierConfig prettier() { return prettier(PrettierFormatterStep.defaultDevDependencies()); @@ -666,6 +718,22 @@ public PrettierConfig prettier(Map devDependencies) { return prettierConfig; } + /** + * Defaults to downloading the default Rome version from the network. To work + * offline, you can specify the path to the Rome executable via + * {@code rome().pathToExe(...)}. + */ + public RomeStepConfig rome() { + return rome(null); + } + + /** Downloads the given Rome version from the network. */ + public RomeStepConfig rome(String version) { + var romeConfig = new RomeGeneric(version); + addStep(romeConfig.createStep()); + return romeConfig; + } + /** Uses the default version of clang-format. */ public ClangFormatConfig clangFormat() { return clangFormat(ClangFormatStep.defaultVersion()); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java index e8a76166bc..76ad5650ca 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavascriptExtension.java @@ -138,9 +138,51 @@ public PrettierConfig prettier(Map devDependencies) { return prettierConfig; } + /** + * Defaults to downloading the default Rome version from the network. To work + * offline, you can specify the path to the Rome executable via + * {@code rome().pathToExe(...)}. + */ + public RomeJs rome() { + return rome(null); + } + + /** Downloads the given Rome version from the network. */ + public RomeJs rome(String version) { + var romeConfig = new RomeJs(version); + addStep(romeConfig.createStep()); + return romeConfig; + } + private static final String DEFAULT_PRETTIER_JS_PARSER = "babel"; private static final ImmutableList PRETTIER_JS_PARSERS = ImmutableList.of(DEFAULT_PRETTIER_JS_PARSER, "babel-flow", "flow"); + /** + * Rome formatter step for JavaScript. + */ + public class RomeJs extends RomeStepConfig { + + /** + * Creates a new Rome formatter step config for formatting JavaScript files. Unless + * overwritten, the given Rome version is downloaded from the network. + * + * @param version Rome version to use. + */ + public RomeJs(String version) { + super(getProject(), JavascriptExtension.this::replaceStep, version); + } + + @Override + protected String getLanguage() { + return "js?"; + } + + @Override + protected RomeJs getThis() { + return this; + } + } + /** * Overrides the parser to be set to a js parser. */ diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java index 39b158ce1e..510ac529e8 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JsonExtension.java @@ -55,6 +55,22 @@ public JacksonJsonGradleConfig jackson() { return new JacksonJsonGradleConfig(this); } + /** + * Defaults to downloading the default Rome version from the network. To work + * offline, you can specify the path to the Rome executable via + * {@code rome().pathToExe(...)}. + */ + public RomeJson rome() { + return rome(null); + } + + /** Downloads the given Rome version from the network. */ + public RomeJson rome(String version) { + var romeConfig = new RomeJson(version); + addStep(romeConfig.createStep()); + return romeConfig; + } + public class SimpleConfig { private int indent; @@ -145,4 +161,29 @@ protected final FormatterStep createStep() { return JacksonJsonStep.create(jacksonConfig, version, formatExtension.provisioner()); } } + + /** + * Rome formatter step for JSON. + */ + public class RomeJson extends RomeStepConfig { + /** + * Creates a new Rome formatter step config for formatting JSON files. Unless + * overwritten, the given Rome version is downloaded from the network. + * + * @param version Rome version to use. + */ + public RomeJson(String version) { + super(getProject(), JsonExtension.this::replaceStep, version); + } + + @Override + protected String getLanguage() { + return "json"; + } + + @Override + protected RomeJson getThis() { + return this; + } + } } diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RomeStepConfig.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RomeStepConfig.java new file mode 100644 index 0000000000..f739e922d2 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/RomeStepConfig.java @@ -0,0 +1,271 @@ +/* + * Copyright 2023 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.gradle.spotless; + +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.function.Consumer; + +import javax.annotation.Nullable; + +import org.gradle.api.Project; +import org.gradle.api.artifacts.repositories.MavenArtifactRepository; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.rome.RomeStep; + +public abstract class RomeStepConfig> { + /** + * Optional path to the directory with configuration file for Rome. The file + * must be named {@code rome.json}. When none is given, the default + * configuration is used. If this is a relative path, it is resolved against the + * project's base directory. + */ + @Nullable + private Object configPath; + + /** + * Optional directory where the downloaded Rome executable is placed. If this is + * a relative path, it is resolved against the project's base directory. + * Defaults to + * ~/.m2/repository/com/diffplug/spotless/spotless-data/rome. + */ + @Nullable + private Object downloadDir; + + /** + * Optional path to the Rome executable. Either a version or a + * pathToExe should be specified. When not given, an attempt is + * made to download the executable for the given version from the network. When + * given, the executable is used and the version parameter is + * ignored. + *

+ * When an absolute path is given, that path is used as-is. When a relative path + * is given, it is resolved against the project's base directory. When only a + * file name (i.e. without any slashes or back slash path separators such as + * {@code rome}) is given, this is interpreted as the name of a command with + * executable that is in your {@code path} environment variable. Use + * {@code ./executable-name} if you want to use an executable in the project's + * base directory. + */ + @Nullable + private Object pathToExe; + + /** + * A reference to the Gradle project for which spotless is executed. + */ + private final Project project; + + /** + * Replaces the current Rome formatter step with the given step. + */ + private final Consumer replaceStep; + + /** + * Rome version to download, applies only when no pathToExe is + * specified explicitly. Either a version or a + * pathToExe should be specified. When not given, a default known + * version is used. For stable builds, it is recommended that you always set the + * version explicitly. This parameter is ignored when you specify a + * pathToExe explicitly. + */ + @Nullable + private String version; + + protected RomeStepConfig(Project project, Consumer replaceStep, String version) { + this.project = requireNonNull(project); + this.replaceStep = requireNonNull(replaceStep); + this.version = version; + } + + /** + * Optional path to the directory with configuration file for Rome. The file + * must be named {@code rome.json}. When none is given, the default + * configuration is used. If this is a relative path, it is resolved against the + * project's base directory. + * + * @return This step for further configuration. + */ + public Self configPath(Object configPath) { + this.configPath = configPath; + replaceStep(); + return getThis(); + } + + /** + * Optional directory where the downloaded Rome executable is placed. If this is + * a relative path, it is resolved against the project's base directory. + * Defaults to + * ~/.m2/repository/com/diffplug/spotless/spotless-data/rome. + * + * @return This step for further configuration. + */ + public Self downloadDir(Object downloadDir) { + this.downloadDir = downloadDir; + replaceStep(); + return getThis(); + } + + /** + * Optional path to the Rome executable. Overwrites the configured version. No + * attempt is made to download the Rome executable from the network. + *

+ * When an absolute path is given, that path is used as-is. When a relative path + * is given, it is resolved against the project's base directory. When only a + * file name (i.e. without any slashes or back slash path separators such as + * {@code rome}) is given, this is interpreted as the name of a command with + * executable that is in your {@code path} environment variable. Use + * {@code ./executable-name} if you want to use an executable in the project's + * base directory. + * + * @return This step for further configuration. + */ + public Self pathToExe(Object pathToExe) { + this.pathToExe = pathToExe; + replaceStep(); + return getThis(); + } + + /** + * Creates a new formatter step that formats code by calling the Rome + * executable, using the current configuration. + * + * @return A new formatter step for the Rome formatter. + */ + protected FormatterStep createStep() { + var builder = newBuilder(); + if (configPath != null) { + var resolvedConfigPath = project.file(configPath); + builder.withConfigPath(resolvedConfigPath.toString()); + } + builder.withLanguage(getLanguage()); + return builder.create(); + } + + /** + * Gets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently the following languages are supported by Rome: + *

    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • json (JSON)
  • + *
+ * + * @return The language of the input files. + */ + protected abstract String getLanguage(); + + /** + * @return This Rome config instance. + */ + protected abstract Self getThis(); + + /** + * Creates a new Rome step and replaces the existing Rome step in the list of + * format steps. + */ + protected void replaceStep() { + replaceStep.accept(createStep()); + } + + /** + * Finds the data directory that can be used for storing shared data such as + * Rome executable globally. This is a directory in the local repository, e.g. + * ~/.m2/repository/com/diffplus/spotless/spotless-data. + * + * @return The directory for storing shared data. + */ + private File findDataDir() { + var currentRepo = project.getRepositories().stream() + .filter(r -> r instanceof MavenArtifactRepository) + .map(r -> (MavenArtifactRepository) r) + .filter(r -> "file".equals(r.getUrl().getScheme())) + .findAny().orElse(null); + // Temporarily add mavenLocal() repository to get its file URL + var localRepo = currentRepo != null ? (MavenArtifactRepository) currentRepo : project.getRepositories().mavenLocal(); + try { + // e.g. ~/.m2/repository/ + var repoPath = Path.of(localRepo.getUrl()); + var dataPath = repoPath.resolve("com").resolve("diffplug").resolve("spotless").resolve("spotless-data"); + return dataPath.toAbsolutePath().toFile(); + } finally { + // Remove mavenLocal() repository again if it was not part of the project + if (currentRepo == null) { + project.getRepositories().remove(localRepo); + } + } + } + + /** + * A new builder for configuring a Rome step that either downloads the Rome + * executable with the given version from the network, or uses the executable + * from the given path. + * + * @return A builder for a Rome step. + */ + private RomeStep newBuilder() { + if (pathToExe != null) { + var resolvedPathToExe = resolvePathToExe(); + return RomeStep.withExePath(resolvedPathToExe); + } else { + var downloadDir = resolveDownloadDir(); + return RomeStep.withExeDownload(version, downloadDir); + } + } + + /** + * Resolves the path to the Rome executable. When the path is only a file name, + * do not perform any resolution and interpret it as a command that must be on + * the user's path. Otherwise resolve the executable path against the project's + * base directory. + * + * @return The resolved path to the Rome executable. + */ + private String resolvePathToExe() { + var fileNameOnly = pathToExe instanceof String && Paths.get(pathToExe.toString()).getNameCount() == 1; + if (fileNameOnly) { + return pathToExe.toString(); + } else { + return project.file(pathToExe).toString(); + } + } + + /** + * Resolves the directory to use for storing downloaded Rome executable. When a + * {@link #downloadDir} is given, use that directory, resolved against the + * current project's directory. Otherwise, use the {@code Rome} sub folder in + * the shared data directory. + * + * @return The download directory for the Rome executable. + */ + private String resolveDownloadDir() { + if (downloadDir != null) { + return project.file(downloadDir).toString(); + } else { + return findDataDir().toPath().resolve("rome").toString(); + } + } +} diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java index 9f1d04abfd..ddb7fbfc10 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java @@ -229,6 +229,47 @@ protected EslintConfig eslintConfig() { } } + /** + * Defaults to downloading the default Rome version from the network. To work + * offline, you can specify the path to the Rome executable via + * {@code rome().pathToExe(...)}. + */ + public RomeTs rome() { + return rome(null); + } + + /** Downloads the given Rome version from the network. */ + public RomeTs rome(String version) { + var romeConfig = new RomeTs(version); + addStep(romeConfig.createStep()); + return romeConfig; + } + + /** + * Rome formatter step for TypeScript. + */ + public class RomeTs extends RomeStepConfig { + /** + * Creates a new Rome formatter step config for formatting TypeScript files. Unless + * overwritten, the given Rome version is downloaded from the network. + * + * @param version Rome version to use. + */ + public RomeTs(String version) { + super(getProject(), TypescriptExtension.this::replaceStep, version); + } + + @Override + protected String getLanguage() { + return "ts?"; + } + + @Override + protected RomeTs getThis() { + return this; + } + } + @Override protected void setupTask(SpotlessTask task) { if (target == null) { diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RomeIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RomeIntegrationTest.java new file mode 100644 index 0000000000..4464b1cec7 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/RomeIntegrationTest.java @@ -0,0 +1,343 @@ +/* + * Copyright 2023 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.gradle.spotless; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; + +import java.io.IOException; + +import org.junit.jupiter.api.Test; +import org.owasp.encoder.Encode; + +class RomeIntegrationTest extends GradleIntegrationHarness { + /** + * Tests that rome can be used as a generic formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asGenericStep() throws IOException { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + } + + /** + * Tests that rome can be used as a JavaScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJavaScriptStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " javascript {", + " target '**/*.js'", + " rome('12.0.0')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + } + + /** + * Tests that rome can be used as a JSON formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJsonStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " json {", + " target '**/*.json'", + " rome('12.0.0')", + " }", + "}"); + setFile("rome_test.json").toResource("rome/json/fileBefore.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.json").sameAsResource("rome/json/fileAfter.json"); + } + + /** + * Tests that rome can be used as a TypeScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asTypeScriptStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " typescript {", + " target '**/*.ts'", + " rome('12.0.0')", + " }", + "}"); + setFile("rome_test.ts").toResource("rome/ts/fileBefore.ts"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.ts").sameAsResource("rome/ts/fileAfter.ts"); + } + + /** + * Tests that the language can be specified for the generic format step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void canSetLanguageForGenericStep() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.nosj'", + " rome('12.0.0').language('json')", + " }", + "}"); + setFile("rome_test.nosj").toResource("rome/json/fileBefore.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.nosj").sameAsResource("rome/json/fileAfter.json"); + } + + /** + * Tests that an absolute config path can be specified. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathAbsolute() throws Exception { + var path = newFile("configs").getAbsolutePath(); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0').configPath('" + Encode.forJava(path) + "')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/longLineBefore.js"); + setFile("configs/rome.json").toResource("rome/config/line-width-120.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the rome.json config file can be + * specified. Uses a config file with a line width of 120. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth120() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0').configPath('configs')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/longLineBefore.js"); + setFile("configs/rome.json").toResource("rome/config/line-width-120.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the rome.json config file can be + * specified. Uses a config file with a line width of 80. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth80() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0').configPath('configs')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/longLineBefore.js"); + setFile("configs/rome.json").toResource("rome/config/line-width-80.json"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter80.js"); + } + + /** + * Tests that the download directory can be an absolute path. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirAbsolute() throws Exception { + var path = newFile("target/bin/rome").getAbsoluteFile().toString(); + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0').downloadDir('" + Encode.forJava(path) + "')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/rome").list().length); + } + + /** + * Tests that the download directory can be changed to a path relative to the + * project's base directory. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirRelative() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0').downloadDir('target/bin/rome')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/rome").list().length); + } + + /** + * Tests that the build fails when given Rome executable does not exist. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenExeNotFound() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0').pathToExe('rome/is/missing')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail(); + assertThat(spotlessApply.getOutput()).contains("Build failed with an exception"); + assertFile("rome_test.js").sameAsResource("rome/js/fileBefore.js"); + assertThat(spotlessApply.getOutput()).contains("Could not create task ':spotlessMyromeApply'"); + assertThat(spotlessApply.getOutput()).contains("Rome executable does not exist"); + } + + /** + * Tests that the build fails when the input file could not be parsed. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenNotParseable() throws Exception { + setFile("build.gradle").toLines( + "plugins {", + " id 'com.diffplug.spotless'", + "}", + "repositories { mavenCentral() }", + "spotless {", + " format 'myrome', {", + " target '**/*.js'", + " rome('12.0.0').language('json')", + " }", + "}"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + + var spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").buildAndFail(); + assertThat(spotlessApply.getOutput()).contains("spotlessMyrome FAILED"); + assertFile("rome_test.js").sameAsResource("rome/js/fileBefore.js"); + assertThat(spotlessApply.getOutput()).contains("Format with errors is disabled."); + assertThat(spotlessApply.getOutput()).contains("Step 'rome' found problem in 'rome_test.js'"); + } +} diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 4e7edddeef..993404f1fe 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,11 +3,13 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] -### Changes -* Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) +### Added +* Support Rome as a formatter for JavaScript and TypeScript code. Adds a new `rome` step to `javascript` and `typescript` formatter configurations. ([#1663](https://github.com/diffplug/spotless/pull/1663)) ### Fixed * `palantir` step now accepts a `style` parameter, which is documentation had already claimed to do. ([#1694](https://github.com/diffplug/spotless/pull/1694)) * When P2 download fails, indicate the responsible formatter. ([#1698](https://github.com/diffplug/spotless/issues/1698)) +### Changes +* Bump default sortpom version to latest `3.0.0` -> `3.2.1`. ([#1675](https://github.com/diffplug/spotless/pull/1675)) ## [2.36.0] - 2023-04-06 ### Added diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 505eafeedd..b11e2c164d 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -49,14 +49,15 @@ user@machine repo % mvn spotless:check - [Sql](#sql) ([dbeaver](#dbeaver)) - [Maven Pom](#maven-pom) ([sortPom](#sortpom)) - [Markdown](#markdown) ([flexmark](#flexmark)) - - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript)) - - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript)) + - [Typescript](#typescript) ([tsfmt](#tsfmt), [prettier](#prettier), [ESLint](#eslint-typescript), [Rome](#rome)) + - [Javascript](#javascript) ([prettier](#prettier), [ESLint](#eslint-javascript), [Rome](#rome)) - [JSON](#json) - [YAML](#yaml) - [Gherkin](#gherkin) - Multiple languages - [Prettier](#prettier) ([plugins](#prettier-plugins), [npm detection](#npm-detection), [`.npmrc` detection](#npmrc-detection), [caching `npm install` results](#caching-results-of-npm-install)) - [eclipse web tools platform](#eclipse-web-tools-platform) + - [Rome](#rome) ([binary detection](#rome-binary), [config file](#rome-configuration-file), [input language](#rome-input-language)) - **Language independent** - [Generic steps](#generic-steps) - [License header](#license-header) ([slurp year from git](#retroactively-slurp-years-from-git-history)) @@ -704,6 +705,7 @@ Currently, none of the available options can be configured yet. It uses only the + /* (C)$YEAR */ @@ -814,6 +816,7 @@ For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmr + /* (C)$YEAR */ @@ -890,6 +893,7 @@ For details, see the [npm detection](#npm-detection), [`.npmrc` detection](#npmr + ``` @@ -1220,6 +1224,155 @@ to true. +## Rome + +[homepage](https://rome.tools/). [changelog](https://github.com/rome/tools/blob/main/CHANGELOG.md). Rome is a formatter that for the Frontend written in Rust, which has a native binary, +does not require Node.js and as such, is pretty fast. It can currently format +JavaScript, TypeScript, JSX, and JSON, and may support +[more frontend languages](https://docs.rome.tools/internals/language_support/) +such as CSS in the future. + +You can use rome in any language-specific format for supported languages, but +usually you will be creating a generic format. + +```xml + + + + + src/**/typescript/**/*.ts + + + + + 12.0.0 + + + ${project.basedir}/path/to/config/dir + + + + ts + + + + + +``` + +**Limitations:** +- The auto-discovery of config files (up the file tree) will not work when using + Rome within spotless. + +To apply Rome to more kinds of files with a different configuration, just add +more formats + +```xml + + + src/**/*.ts + src/**/*.js + +``` + +### Rome binary + +To format with Rome, spotless needs to find the Rome binary. By default, +spotless downloads the binary for the given version from the network. This +should be fine in most cases, but may not work e.g. when there is not connection +to the internet. + +To download the Rome binary from the network, just specify a version: + +```xml + + 12.0.0 + +``` + +Spotless uses a default version when you do not specfiy a version, but this +may change at any time, so we recommend that you always set the Rome version +you want to use. Optionally, you can also specify a directory for the downloaded +Rome binaries (defaults to `~/.m2/repository/com/diffplug/spotless/spotless-data/rome`): + +```xml + + 12.0.0 + + ${user.home}/rome + +``` + +To use a fixed binary, omit the `version` and specify a `pathToExe`: + +```xml + + ${project.basedir}/bin/rome + +``` + +Absolute paths are used as-is. Relative paths are resolved against the project's +base directory. To use a pre-installed Rome binary on the user's path, specify +just a name without any slashes / backslashes: + + +```xml + + + rome + +``` + +### Rome configuration file + +Rome is a biased formatter and linter without many options, but there are a few +basic options. Rome uses a file named [rome.json](https://docs.rome.tools/configuration/) +for its configuration. When none is specified, the default configuration from +Rome is used. To use a custom configuration: + +```xml + + + + ${project.basedir} + +``` + +### Rome input language + +By default, Rome detects the language / syntax of the files to format +automatically from the file extension. This may fail if your source code files +have unusual extensions for some reason. If you are using the generic format, +you can force a certain language like this: + +```xml + + + + + src/**/typescript/**/*.mjson + + + + 12.0.0 + json + + + + + +``` + +The following languages are currently recognized: + +* `js` -- JavaScript +* `jsx` -- JavaScript + JSX (React) +* `js?` -- JavaScript, with or without JSX, depending on the file extension +* `ts` -- TypeScript +* `tsx` -- TypeScript + JSX (React) +* `ts?` -- TypeScript, with or without JSX, depending on the file extension +* `json` -- JSON + ## Generic steps [Prettier](#prettier), [eclipse wtp](#eclipse-web-tools-platform), and [license header](#license-header) are available in every format, and they each have their own section. As mentioned in the [quickstart](#quickstart), there are a variety of simple generic steps which are also available in every format, here are examples of these: diff --git a/plugin-maven/build.gradle b/plugin-maven/build.gradle index a8604c6ed9..dda98ad79a 100644 --- a/plugin-maven/build.gradle +++ b/plugin-maven/build.gradle @@ -50,6 +50,7 @@ dependencies { testImplementation "org.mockito:mockito-core:${VER_MOCKITO}" testImplementation "com.diffplug.durian:durian-io:${VER_DURIAN}" testImplementation 'com.github.spullara.mustache.java:compiler:0.9.10' + testImplementation 'org.owasp.encoder:encoder:1.2.3' testImplementation "org.apache.maven:maven-plugin-api:${VER_MAVEN_API}" testImplementation "org.eclipse.aether:aether-api:${VER_ECLIPSE_AETHER}" testImplementation "org.codehaus.plexus:plexus-resources:${VER_PLEXUS_RESOURCES}" diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java index 7ea998dc95..088c35a3d0 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/FileLocator.java @@ -1,5 +1,5 @@ /* - * Copyright 2016-2020 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. @@ -19,6 +19,10 @@ import static java.nio.charset.StandardCharsets.UTF_8; import java.io.File; +import java.net.URISyntaxException; +import java.nio.file.FileSystemNotFoundException; +import java.nio.file.Path; +import java.nio.file.Paths; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; @@ -34,17 +38,18 @@ public class FileLocator { static final String TMP_RESOURCE_FILE_PREFIX = "spotless-resource-"; private final ResourceManager resourceManager; - private final File baseDir, buildDir; + private final File baseDir, buildDir, dataDir; public FileLocator(ResourceManager resourceManager, File baseDir, File buildDir) { this.resourceManager = Objects.requireNonNull(resourceManager); this.baseDir = Objects.requireNonNull(baseDir); this.buildDir = Objects.requireNonNull(buildDir); + this.dataDir = findDataDir(); } /** - * If the given path is a local file returns it as such unchanged, - * otherwise extracts the given resource to a randomly-named file in the build folder. + * If the given path is a local file returns it as such unchanged, otherwise + * extracts the given resource to a randomly-named file in the build folder. */ public File locateFile(String path) { if (isNullOrEmpty(path)) { @@ -62,18 +67,42 @@ public File locateFile(String path) { } catch (ResourceNotFoundException e) { throw new RuntimeException("Unable to locate file with path: " + path, e); } catch (FileResourceCreationException e) { - throw new RuntimeException("Unable to create temporary file '" + outputFile + "' in the output directory", e); + throw new RuntimeException("Unable to create temporary file '" + outputFile + "' in the output directory", + e); } } + /** + * Finds the base directory of the Maven or Gradle project on which spotless is + * currently being executed. + * + * @return The base directory of the current Maven or Gradel project. + */ public File getBaseDir() { return baseDir; } + /** + * Finds the build directory (e.g. /target) of the Maven or Gradle + * project on which spotless is currently being executed. + * + * @return The project build directory of the current Maven or Gradle project. + */ public File getBuildDir() { return buildDir; } + /** + * Finds the data directory that can be used for storing shared data such as + * downloaded files globally. This is a directory in the local repository, e.g. + * ~/.m2/repository/com/diffplus/spotless/spotless-data. + * + * @return The directory for storing shared data. + */ + public File getDataDir() { + return dataDir; + } + private static String tmpOutputFileName(String path) { String extension = FileUtils.extension(path); byte[] pathHash = hash(path); @@ -91,4 +120,33 @@ private static byte[] hash(String value) { messageDigest.update(value.getBytes(UTF_8)); return messageDigest.digest(); } + + private static File findDataDir() { + try { + // JAR path is e.g. + // ~/.m2/repository/com/diffplug/spotless/spotless-plugin-maven/1.2.3/spotless-plugin-maven-1.2.3.jar + var codeSource = FileLocator.class.getProtectionDomain().getCodeSource(); + var location = codeSource != null ? codeSource.getLocation() : null; + var locationUri = location != null ? location.toURI() : null; + var jarPath = locationUri != null && "file".equals(locationUri.getScheme()) ? Path.of(locationUri) : null; + var parent1 = jarPath != null ? jarPath.getParent() : null; + var parent2 = parent1 != null ? parent1.getParent() : null; + var base = parent2 != null ? parent2.getParent() : null; + var sub = base != null ? base.resolve("spotless-data") : null; + if (sub != null) { + return sub.toAbsolutePath().toFile(); + } else { + return findUserHome(); + } + } catch (final SecurityException e) { + return findUserHome(); + } catch (final URISyntaxException | FileSystemNotFoundException | IllegalArgumentException e) { + throw new RuntimeException("Unable to determine data directory in local Maven repository", e); + } + } + + private static File findUserHome() { + var home = Paths.get(System.getenv("user.home")); + return home.resolve(".rome").toAbsolutePath().toFile(); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java index a696e13ffb..ed08718c65 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Format.java @@ -40,4 +40,8 @@ public String licenseHeaderDelimiter() { // do not specify a default delimiter return null; } + + public void addRome(Rome rome) { + addStepFactory(rome); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Rome.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Rome.java new file mode 100644 index 0000000000..62d9d3fdec --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Rome.java @@ -0,0 +1,55 @@ +/* + * 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. + * 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.generic; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.maven.rome.AbstractRome; + +/** + * Generic Rome formatter step that detects the language of the input file from + * the file name. It should be specified as a formatter step for a generic + * {@code }. + */ +public class Rome extends AbstractRome { + /** + * Gets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently the following languages are supported by Rome: + *
    + *
      + *
    • js (JavaScript)
    • + *
    • jsx (JavaScript + JSX)
    • + *
    • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
    • + *
    • ts (TypeScript)
    • + *
    • tsx (TypeScript + JSX)
    • + *
    • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
    • + *
    • json (JSON)
    • + *
    + *
+ * + * @return The language of the input files. + */ + @Parameter + private String language; + + @Override + protected String getLanguage() { + return language; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java index 31a5917e06..b254110486 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/Javascript.java @@ -41,4 +41,8 @@ public String licenseHeaderDelimiter() { public void addEslint(EslintJs eslint) { addStepFactory(eslint); } + + public void addRome(RomeJs rome) { + addStepFactory(rome); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/RomeJs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/RomeJs.java new file mode 100644 index 0000000000..60fb7077df --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/javascript/RomeJs.java @@ -0,0 +1,28 @@ +/* + * 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. + * 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.javascript; + +import com.diffplug.spotless.maven.rome.AbstractRome; + +/** + * Rome formatter step for JavaScript. + */ +public class RomeJs extends AbstractRome { + @Override + protected String getLanguage() { + return "js?"; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java index 5bb7c17b3a..adbb2b7883 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/Json.java @@ -50,4 +50,7 @@ public void addJackson(JacksonJson jackson) { addStepFactory(jackson); } + public void addRome(RomeJson rome) { + addStepFactory(rome); + } } diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/RomeJson.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/RomeJson.java new file mode 100644 index 0000000000..1cd044b759 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/json/RomeJson.java @@ -0,0 +1,28 @@ +/* + * 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. + * 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.json; + +import com.diffplug.spotless.maven.rome.AbstractRome; + +/** + * Rome formatter step for JSON. + */ +public class RomeJson extends AbstractRome { + @Override + protected String getLanguage() { + return "json"; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/rome/AbstractRome.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rome/AbstractRome.java new file mode 100644 index 0000000000..33567b0a20 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/rome/AbstractRome.java @@ -0,0 +1,186 @@ +/* + * 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. + * 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.rome; + +import java.nio.file.Paths; + +import org.apache.maven.plugins.annotations.Parameter; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; +import com.diffplug.spotless.rome.RomeStep; + +/** + * Factory for creating the Rome formatter step that can format format code in + * various types of language with Rome. Currently Rome support JavaScript, + * TypeScript, JSX, TSX, and JSON. See also + * https://github.com/rome/tools. + * It delegates to the Rome CLI executable. + */ +public abstract class AbstractRome implements FormatterStepFactory { + /** + * Optional path to the directory with configuration file for Rome. The file + * must be named {@code rome.json}. When none is given, the default + * configuration is used. If this is a relative path, it is resolved against the + * project's base directory. + */ + @Parameter + private String configPath; + + /** + * Optional directory where the downloaded Rome executable is placed. If this is + * a relative path, it is resolved against the project's base directory. + * Defaults to + * ~/.m2/repository/com/diffplug/spotless/spotless-data/rome. + *

+ * You can use an expression like ${user.home}/rome if you want to + * use the home directory, or ${project.build.directory if you want + * to use the target directory of the current project. + */ + @Parameter + private String downloadDir; + + /** + * Optional path to the Rome executable. Either a version or a + * pathToExe should be specified. When not given, an attempt is + * made to download the executable for the given version from the network. When + * given, the executable is used and the version parameter is + * ignored. + *

+ * When an absolute path is given, that path is used as-is. When a relative path + * is given, it is resolved against the project's base directory. When only a + * file name (i.e. without any slashes or back slash path separators such as + * {@code rome}) is given, this is interpreted as the name of a command with + * executable that is in your {@code path} environment variable. Use + * {@code ./executable-name} if you want to use an executable in the project's + * base directory. + */ + @Parameter + private String pathToExe; + + /** + * Rome version to download, applies only when no pathToExe is + * specified explicitly. Either a version or a + * pathToExe should be specified. When not given, a default known + * version is used. For stable builds, it is recommended that you always set the + * version explicitly. This parameter is ignored when you specify a + * pathToExe explicitly. + */ + @Parameter + private String version; + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + var builder = newBuilder(config); + if (configPath != null) { + var resolvedConfigFile = resolveConfigFile(config); + builder.withConfigPath(resolvedConfigFile); + } + if (getLanguage() != null) { + builder.withLanguage(getLanguage()); + } + return builder.create(); + } + + /** + * Gets the language (syntax) of the input files to format. When + * null or the empty string, the language is detected automatically + * from the file name. Currently the following languages are supported by Rome: + *

    + *
  • js (JavaScript)
  • + *
  • jsx (JavaScript + JSX)
  • + *
  • js? (JavaScript or JavaScript + JSX, depending on the file + * extension)
  • + *
  • ts (TypeScript)
  • + *
  • tsx (TypeScript + JSX)
  • + *
  • ts? (TypeScript or TypeScript + JSX, depending on the file + * extension)
  • + *
  • json (JSON)
  • + *
+ * + * @return The language of the input files. + */ + protected abstract String getLanguage(); + + /** + * A new builder for configuring a Rome step that either downloads the Rome + * executable with the given version from the network, or uses the executable + * from the given path. + * + * @param config Configuration from the Maven Mojo execution with details about + * the currently executed project. + * @return A builder for a Rome step. + */ + private RomeStep newBuilder(FormatterStepConfig config) { + if (pathToExe != null) { + var resolvedExePath = resolveExePath(config); + return RomeStep.withExePath(resolvedExePath); + } else { + var downloadDir = resolveDownloadDir(config); + return RomeStep.withExeDownload(version, downloadDir); + } + } + + /** + * Resolves the path to the configuration file for Rome. Relative paths are + * resolved against the project's base directory. + * + * @param config Configuration from the Maven Mojo execution with details about + * the currently executed project. + * @return The resolved path to the configuration file. + */ + private String resolveConfigFile(FormatterStepConfig config) { + return config.getFileLocator().getBaseDir().toPath().resolve(configPath).toAbsolutePath().toString(); + } + + /** + * Resolves the path to the Rome executable. When the path is only a file name, + * do not perform any resolution and interpret it as a command that must be on + * the user's path. Otherwise resolve the executable path against the project's + * base directory. + * + * @param config Configuration from the Maven Mojo execution with details about + * the currently executed project. + * @return The resolved path to the Rome executable. + */ + private String resolveExePath(FormatterStepConfig config) { + var path = Paths.get(pathToExe); + if (path.getNameCount() == 1) { + return path.toString(); + } else { + return config.getFileLocator().getBaseDir().toPath().resolve(path).toAbsolutePath().toString(); + } + } + + /** + * Resolves the directory to use for storing downloaded Rome executable. When a + * {@link #downloadDir} is given, use that directory, resolved against the + * current project's directory. Otherwise, use the {@code Rome} sub folder in + * the shared data directory. + * + * @param config Configuration for this step. + * @return The download directory for the Rome executable. + */ + private String resolveDownloadDir(FormatterStepConfig config) { + final var fileLocator = config.getFileLocator(); + if (downloadDir != null && !downloadDir.isBlank()) { + return fileLocator.getBaseDir().toPath().resolve(downloadDir).toAbsolutePath().toString(); + } else { + return fileLocator.getDataDir().toPath().resolve("rome").toString(); + } + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/RomeTs.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/RomeTs.java new file mode 100644 index 0000000000..ccee83744a --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/RomeTs.java @@ -0,0 +1,28 @@ +/* + * 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. + * 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.typescript; + +import com.diffplug.spotless.maven.rome.AbstractRome; + +/** + * Rome formatter step for TypeScript. + */ +public class RomeTs extends AbstractRome { + @Override + protected String getLanguage() { + return "ts?"; + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java index 6ba45ab719..ddae74db82 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/typescript/Typescript.java @@ -45,4 +45,8 @@ public void addTsfmt(Tsfmt tsfmt) { public void addEslint(EslintTs eslint) { addStepFactory(eslint); } + + public void addRome(RomeTs rome) { + addStepFactory(rome); + } } 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 216502bc07..398932dd19 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 @@ -154,6 +154,10 @@ protected void writePomWithPrettierSteps(String includes, String... steps) throw writePom(formats(groupWithSteps("format", including(includes), steps))); } + protected void writePomWithRomeSteps(String includes, String... steps) throws IOException { + writePom(formats(groupWithSteps("format", including(includes), steps))); + } + protected void writePomWithPrettierSteps(String[] plugins, String includes, String... steps) throws IOException { writePom(null, formats(groupWithSteps("format", including(includes), steps)), null, plugins); } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/rome/RomeMavenTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/rome/RomeMavenTest.java new file mode 100644 index 0000000000..7ca3f7d981 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/rome/RomeMavenTest.java @@ -0,0 +1,202 @@ +/* + * Copyright 2023 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.rome; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.owasp.encoder.Encode.forXml; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +class RomeMavenTest extends MavenIntegrationHarness { + /** + * Tests that rome can be used as a generic formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asGenericStep() throws Exception { + writePomWithRomeSteps("**/*.js", "12.0.0"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + } + + /** + * Tests that rome can be used as a JavaScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJavaScriptStep() throws Exception { + writePomWithJavascriptSteps("**/*.js", "12.0.0"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + } + + /** + * Tests that rome can be used as a JSON formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asJsonStep() throws Exception { + writePomWithJsonSteps("**/*.json", "12.0.0"); + setFile("rome_test.json").toResource("rome/json/fileBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.json").sameAsResource("rome/json/fileAfter.json"); + } + + /** + * Tests that rome can be used as a TypeScript formatting step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void asTypeScriptStep() throws Exception { + writePomWithTypescriptSteps("**/*.ts", "12.0.0"); + setFile("rome_test.ts").toResource("rome/ts/fileBefore.ts"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.ts").sameAsResource("rome/ts/fileAfter.ts"); + } + + /** + * Tests that the language can be specified for the generic format step. + * + * @throws Exception When a test failure occurs. + */ + @Test + void canSetLanguageForGenericStep() throws Exception { + writePomWithRomeSteps("**/*.nosj", "12.0.0json"); + setFile("rome_test.nosj").toResource("rome/json/fileBefore.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.nosj").sameAsResource("rome/json/fileAfter.json"); + } + + /** + * Tests that an absolute config path can be specified. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathAbsolute() throws Exception { + var path = newFile("configs").getAbsolutePath(); + writePomWithRomeSteps("**/*.js", + "12.0.0" + forXml(path) + ""); + setFile("rome_test.js").toResource("rome/js/longLineBefore.js"); + setFile("configs/rome.json").toResource("rome/config/line-width-120.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the rome.json config file can be + * specified. Uses a config file with a line width of 120. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth120() throws Exception { + writePomWithRomeSteps("**/*.js", "12.0.0configs"); + setFile("rome_test.js").toResource("rome/js/longLineBefore.js"); + setFile("configs/rome.json").toResource("rome/config/line-width-120.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter120.js"); + } + + /** + * Tests that a path to the directory with the rome.json config file can be + * specified. Uses a config file with a line width of 80. + * + * @throws Exception When a test failure occurs. + */ + @Test + void configPathLineWidth80() throws Exception { + writePomWithRomeSteps("**/*.js", "12.0.0configs"); + setFile("rome_test.js").toResource("rome/js/longLineBefore.js"); + setFile("configs/rome.json").toResource("rome/config/line-width-80.json"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.js").sameAsResource("rome/js/longLineAfter80.js"); + } + + /** + * Tests that the download directory can be an absolute path. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirAbsolute() throws Exception { + var path = newFile("target/bin/rome").getAbsoluteFile().toString(); + writePomWithRomeSteps("**/*.js", + "12.0.0" + forXml(path) + ""); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/rome").list().length); + } + + /** + * Tests that the download directory can be changed to a path relative to the + * project's base directory. + * + * @throws Exception When a test failure occurs. + */ + @Test + void downloadDirRelative() throws Exception { + writePomWithRomeSteps("**/*.js", + "12.0.0target/bin/rome"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + assertTrue(!newFile("target/bin/rome").exists() || newFile("target/bin/rome").list().length == 0); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("rome_test.js").sameAsResource("rome/js/fileAfter.js"); + assertEquals(2, newFile("target/bin/rome").list().length); + } + + /** + * Tests that the build fails when the input file could not be parsed. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenExeNotFound() throws Exception { + writePomWithRomeSteps("**/*.js", "12.0.0rome/is/missing"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + var result = mavenRunner().withArguments("spotless:apply").runHasError(); + assertFile("rome_test.js").sameAsResource("rome/js/fileBefore.js"); + assertThat(result.stdOutUtf8()).contains("Rome executable does not exist"); + } + + /** + * Tests that the build fails when the input file could not be parsed. + * + * @throws Exception When a test failure occurs. + */ + @Test + void failureWhenNotParseable() throws Exception { + writePomWithRomeSteps("**/*.js", "12.0.0json"); + setFile("rome_test.js").toResource("rome/js/fileBefore.js"); + var result = mavenRunner().withArguments("spotless:apply").runHasError(); + assertFile("rome_test.js").sameAsResource("rome/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 'rome' found problem in 'rome_test.js'"); + } +} diff --git a/testlib/src/main/resources/rome/config/line-width-120.json b/testlib/src/main/resources/rome/config/line-width-120.json new file mode 100644 index 0000000000..8f14afa3f8 --- /dev/null +++ b/testlib/src/main/resources/rome/config/line-width-120.json @@ -0,0 +1,11 @@ +{ + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 120, + "formatWithErrors": false + }, + "linter": { + "enabled": false + } + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/config/line-width-80.json b/testlib/src/main/resources/rome/config/line-width-80.json new file mode 100644 index 0000000000..5ec998bd97 --- /dev/null +++ b/testlib/src/main/resources/rome/config/line-width-80.json @@ -0,0 +1,11 @@ +{ + "formatter": { + "enabled": true, + "indentStyle": "tab", + "lineWidth": 80, + "formatWithErrors": false + }, + "linter": { + "enabled": false + } + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/js/fileAfter.cjs b/testlib/src/main/resources/rome/js/fileAfter.cjs new file mode 100644 index 0000000000..defc9c85eb --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileAfter.cjs @@ -0,0 +1,3 @@ +function foo(name = "World") { + return "Hello " + name; +} diff --git a/testlib/src/main/resources/rome/js/fileAfter.js b/testlib/src/main/resources/rome/js/fileAfter.js new file mode 100644 index 0000000000..defc9c85eb --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileAfter.js @@ -0,0 +1,3 @@ +function foo(name = "World") { + return "Hello " + name; +} diff --git a/testlib/src/main/resources/rome/js/fileAfter.jsx b/testlib/src/main/resources/rome/js/fileAfter.jsx new file mode 100644 index 0000000000..313aceb6ed --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileAfter.jsx @@ -0,0 +1,3 @@ +export function Panel(cfg = {}) { + return
{1 + 2}
; +} diff --git a/testlib/src/main/resources/rome/js/fileAfter.mjs b/testlib/src/main/resources/rome/js/fileAfter.mjs new file mode 100644 index 0000000000..defc9c85eb --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileAfter.mjs @@ -0,0 +1,3 @@ +function foo(name = "World") { + return "Hello " + name; +} diff --git a/testlib/src/main/resources/rome/js/fileBefore.cjs b/testlib/src/main/resources/rome/js/fileBefore.cjs new file mode 100644 index 0000000000..92539ba751 --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileBefore.cjs @@ -0,0 +1,3 @@ +function foo ( name="World"){ + return "Hello "+name ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/js/fileBefore.js b/testlib/src/main/resources/rome/js/fileBefore.js new file mode 100644 index 0000000000..92539ba751 --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileBefore.js @@ -0,0 +1,3 @@ +function foo ( name="World"){ + return "Hello "+name ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/js/fileBefore.jsx b/testlib/src/main/resources/rome/js/fileBefore.jsx new file mode 100644 index 0000000000..8e5d9834bc --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileBefore.jsx @@ -0,0 +1,4 @@ +export function Panel ( cfg={}){ + return (
{1+2}
+ ) ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/js/fileBefore.mjs b/testlib/src/main/resources/rome/js/fileBefore.mjs new file mode 100644 index 0000000000..92539ba751 --- /dev/null +++ b/testlib/src/main/resources/rome/js/fileBefore.mjs @@ -0,0 +1,3 @@ +function foo ( name="World"){ + return "Hello "+name ; + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/js/longLineAfter120.js b/testlib/src/main/resources/rome/js/longLineAfter120.js new file mode 100644 index 0000000000..68addc4b65 --- /dev/null +++ b/testlib/src/main/resources/rome/js/longLineAfter120.js @@ -0,0 +1 @@ +const x = ["Hello", "World", "How", "Are", "You", "Doing", "Today", "Such", "A", "Wondrous", "Sunshine"]; diff --git a/testlib/src/main/resources/rome/js/longLineAfter80.js b/testlib/src/main/resources/rome/js/longLineAfter80.js new file mode 100644 index 0000000000..dbdbd157e9 --- /dev/null +++ b/testlib/src/main/resources/rome/js/longLineAfter80.js @@ -0,0 +1,13 @@ +const x = [ + "Hello", + "World", + "How", + "Are", + "You", + "Doing", + "Today", + "Such", + "A", + "Wondrous", + "Sunshine", +]; diff --git a/testlib/src/main/resources/rome/js/longLineBefore.js b/testlib/src/main/resources/rome/js/longLineBefore.js new file mode 100644 index 0000000000..fd59e429c2 --- /dev/null +++ b/testlib/src/main/resources/rome/js/longLineBefore.js @@ -0,0 +1 @@ +const x = ["Hello", "World", "How", "Are", "You", "Doing", "Today", "Such", "A", "Wondrous", "Sunshine"]; \ No newline at end of file diff --git a/testlib/src/main/resources/rome/json/fileAfter.json b/testlib/src/main/resources/rome/json/fileAfter.json new file mode 100644 index 0000000000..468dac3297 --- /dev/null +++ b/testlib/src/main/resources/rome/json/fileAfter.json @@ -0,0 +1,5 @@ +{ + "a": [1, 2, 3], + "b": 9, + "c": null +} diff --git a/testlib/src/main/resources/rome/json/fileBefore.json b/testlib/src/main/resources/rome/json/fileBefore.json new file mode 100644 index 0000000000..77182284c7 --- /dev/null +++ b/testlib/src/main/resources/rome/json/fileBefore.json @@ -0,0 +1,7 @@ + { + "a":[1,2,3 + +], + "b":9, + "c" : null + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/ts/fileAfter.cts b/testlib/src/main/resources/rome/ts/fileAfter.cts new file mode 100644 index 0000000000..f854953234 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileAfter.cts @@ -0,0 +1,4 @@ +type Name = "World" | "Maven" | "Gradle"; +const foo = (name: Name = "World", v: T): string => { + return "Hello " + name; +}; diff --git a/testlib/src/main/resources/rome/ts/fileAfter.mts b/testlib/src/main/resources/rome/ts/fileAfter.mts new file mode 100644 index 0000000000..e6563e3030 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileAfter.mts @@ -0,0 +1,4 @@ +export type Name = "World" | "Maven" | "Gradle"; +export const foo = (name: Name = "World", v: T): string => { + return "Hello " + name; +}; diff --git a/testlib/src/main/resources/rome/ts/fileAfter.ts b/testlib/src/main/resources/rome/ts/fileAfter.ts new file mode 100644 index 0000000000..e6563e3030 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileAfter.ts @@ -0,0 +1,4 @@ +export type Name = "World" | "Maven" | "Gradle"; +export const foo = (name: Name = "World", v: T): string => { + return "Hello " + name; +}; diff --git a/testlib/src/main/resources/rome/ts/fileAfter.tsx b/testlib/src/main/resources/rome/ts/fileAfter.tsx new file mode 100644 index 0000000000..15ef316142 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileAfter.tsx @@ -0,0 +1,7 @@ +export interface Cfg { + classname: string; + message: T; +} +const Panel = (cfg: Cfg): JSX.Element => { + return
{String(cfg.message)}
; +}; diff --git a/testlib/src/main/resources/rome/ts/fileBefore.cts b/testlib/src/main/resources/rome/ts/fileBefore.cts new file mode 100644 index 0000000000..d4304287c0 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileBefore.cts @@ -0,0 +1,4 @@ +type Name = "World" | "Maven"|"Gradle"; +const foo = ( name: Name="World", v: T): string => { + return "Hello " + name; + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/ts/fileBefore.mts b/testlib/src/main/resources/rome/ts/fileBefore.mts new file mode 100644 index 0000000000..96837762a3 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileBefore.mts @@ -0,0 +1,5 @@ +export + type Name = "World" | "Maven"|"Gradle"; +export const foo = ( name: Name="World", v: T): string => { + return "Hello " + name; + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/ts/fileBefore.ts b/testlib/src/main/resources/rome/ts/fileBefore.ts new file mode 100644 index 0000000000..96837762a3 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileBefore.ts @@ -0,0 +1,5 @@ +export + type Name = "World" | "Maven"|"Gradle"; +export const foo = ( name: Name="World", v: T): string => { + return "Hello " + name; + } \ No newline at end of file diff --git a/testlib/src/main/resources/rome/ts/fileBefore.tsx b/testlib/src/main/resources/rome/ts/fileBefore.tsx new file mode 100644 index 0000000000..38f24f8440 --- /dev/null +++ b/testlib/src/main/resources/rome/ts/fileBefore.tsx @@ -0,0 +1,8 @@ +export interface Cfg{ +classname:string, +message:T, +} +const Panel = ( cfg:Cfg):JSX.Element =>{ + return (
{String(cfg.message)}
+ ) ; + } \ No newline at end of file diff --git a/testlib/src/test/java/com/diffplug/spotless/rome/RomeStepTest.java b/testlib/src/test/java/com/diffplug/spotless/rome/RomeStepTest.java new file mode 100644 index 0000000000..5c4801d50a --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/rome/RomeStepTest.java @@ -0,0 +1,284 @@ +/* + * Copyright 2023 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.rome; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Paths; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Nested; +import org.junit.jupiter.api.Test; + +import com.diffplug.common.base.StandardSystemProperty; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.StepHarnessWithFile; +import com.diffplug.spotless.ThrowingEx; + +class RomeStepTest extends ResourceHarness { + private static String downloadDir; + + @BeforeAll + static void createDownloadDir() throws IOException { + // We do not want to download Rome each time we execute a test + var userHome = Paths.get(StandardSystemProperty.USER_HOME.value()); + downloadDir = userHome.resolve(".gradle").resolve("rome-dl-test").toAbsolutePath().normalize().toString(); + } + + /** + * Tests that files can be formatted without setting the input language + * explicitly. + */ + @Nested + class AutoDetectLanguage { + /** + * Tests that a *.cjs file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectCjs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.cjs", "rome/js/fileAfter.cjs"); + } + + /** + * Tests that a *.cts file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectCts() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.cts", "rome/ts/fileAfter.cts"); + } + + /** + * Tests that a *.js file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectJs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.js", "rome/js/fileAfter.js"); + } + + /** + * Tests that a *.js file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectJson() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/json/fileBefore.json", "rome/json/fileAfter.json"); + } + + /** + * Tests that a *.jsx file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectJsx() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.jsx", "rome/js/fileAfter.jsx"); + } + + /** + * Tests that a *.mjs file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectMjs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.mjs", "rome/js/fileAfter.mjs"); + } + + /** + * Tests that a *.mts file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectMts() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.mts", "rome/ts/fileAfter.mts"); + } + + /** + * Tests that a *.ts file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectTs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.ts", "rome/ts/fileAfter.ts"); + } + + /** + * Tests that a *.tsx file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectTsx() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.tsx", "rome/ts/fileAfter.tsx"); + } + } + + @Nested + class ConfigFile { + /** + * Test formatting with the line width in the config file set to 120. + */ + @Test + void testLineWidth120() { + var path = createRomeConfig("rome/config/line-width-120.json"); + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withConfigPath(path).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/longLineBefore.js", "rome/js/longLineAfter120.js"); + } + + /** + * Test formatting with the line width in the config file set to 120. + */ + @Test + void testLineWidth80() { + var path = createRomeConfig("rome/config/line-width-80.json"); + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withConfigPath(path).create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/longLineBefore.js", "rome/js/longLineAfter80.js"); + } + + private String createRomeConfig(String name) { + var config = createTestFile(name).toPath(); + var dir = config.getParent(); + var rome = dir.resolve("rome.json"); + ThrowingEx.run(() -> Files.copy(config, rome)); + return dir.toString(); + } + } + + /** + * Tests that files can be formatted when setting the input language explicitly. + */ + @Nested + class ExplicitLanguage { + /** + * Tests that a *.cjs file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectCjs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("js").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.cjs", "rome/js/fileAfter.cjs"); + } + + /** + * Tests that a *.cts file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectCts() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("ts").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.cts", "rome/ts/fileAfter.cts"); + } + + /** + * Tests that a *.js file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectJs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("js").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.js", "rome/js/fileAfter.js"); + } + + /** + * Tests that a *.json file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectJson() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("json").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/json/fileBefore.json", "rome/json/fileAfter.json"); + } + + /** + * Tests that a *.jsx file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectJsx() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("jsx").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.jsx", "rome/js/fileAfter.jsx"); + } + + /** + * Tests that a *.mjs file can be formatted without setting the input language + * explicitly. + */ + @Test + void testAutoDetectMjs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("js").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/js/fileBefore.mjs", "rome/js/fileAfter.mjs"); + } + + /** + * Tests that a *.mts file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectMts() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("ts").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.mts", "rome/ts/fileAfter.mts"); + } + + /** + * Tests that a *.ts file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectTs() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("ts").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.ts", "rome/ts/fileAfter.ts"); + } + + /** + * Tests that a *.tsx file can be formatted when setting the input language + * explicitly. + */ + @Test + void testAutoDetectTsx() { + var step = RomeStep.withExeDownload("12.0.0", downloadDir.toString()).withLanguage("tsx").create(); + var stepHarness = StepHarnessWithFile.forStep(RomeStepTest.this, step); + stepHarness.testResource("rome/ts/fileBefore.tsx", "rome/ts/fileAfter.tsx"); + } + } +}