diff --git a/.circleci/config.yml b/.circleci/config.yml index 3b02e9d88d..adc2e92d01 100644 --- a/.circleci/config.yml +++ b/.circleci/config.yml @@ -126,21 +126,9 @@ jobs: shell: cmd.exe steps: - checkout - # install openjdk8 - - restore_cache: - key: choco2-ojdkbuild8 - run: name: install command: choco install ojdkbuild8 - - save_cache: - key: choco2-ojdkbuild8 - paths: - - ~\AppData\Local\Temp\chocolatey\ojdkbuild8 - # do the test - - restore_cache: - keys: - - gradle-deps-win2-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }} - - gradle-deps-win2- - run: name: gradlew check command: gradlew check --build-cache @@ -152,13 +140,15 @@ jobs: path: plugin-gradle/build/test-results/test - store_test_results: path: plugin-maven/build/test-results/test - - save_cache: - key: gradle-deps-win2-{{ checksum "build.gradle" }}-{{ checksum "gradle.properties" }} - paths: - - ~/.gradle/caches - - ~/.gradle/wrapper - - ~/.m2 - - ~/project/plugin-maven/build/localMavenRepository + - run: + name: gradlew npmTest + command: gradlew npmTest --build-cache + - store_test_results: + path: testlib/build/test-results/npm + - store_test_results: + path: plugin-maven/build/test-results/npm + - store_test_results: + path: plugin-gradle/build/test-results/npm changelog_print: << : *env_gradle steps: diff --git a/CHANGES.md b/CHANGES.md index a9d114a073..169012c595 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,12 @@ 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] +### Changed +* Nodejs-based formatters `prettier` and `tsfmt` now use native node instead of the J2V8 approach. ([#606](https://github.com/diffplug/spotless/pull/606)) + * This removes the dependency to the no-longer-maintained Linux/Windows/macOs variants of J2V8. + * This enables spotless to use the latest `prettier` versions (instead of being stuck at prettier version <= `1.19.0`) + * Bumped default versions, prettier `1.16.4` -> `2.0.5`, tslint `5.12.1` -> `6.1.2` + ## [1.34.0] - 2020-06-05 ### Added diff --git a/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java b/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java new file mode 100644 index 0000000000..163818d0e7 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/JsonEscaper.java @@ -0,0 +1,110 @@ +/* + * Copyright 2016-2020 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.npm; + +import static java.util.Objects.requireNonNull; + +/** + * Simple implementation on how to escape values when printing json. + * Implementation is partly based on https://github.com/stleary/JSON-java + */ +final class JsonEscaper { + private JsonEscaper() { + // no instance + } + + public static String jsonEscape(Object val) { + requireNonNull(val); + if (val instanceof JsonRawValue) { + return jsonEscape((JsonRawValue) val); + } + if (val instanceof String) { + return jsonEscape((String) val); + } + return val.toString(); + } + + private static String jsonEscape(JsonRawValue jsonRawValue) { + return jsonRawValue.getRawJson(); + } + + private static String jsonEscape(String unescaped) { + /** + * the following characters are reserved in JSON and must be properly escaped to be used in strings: + * + * Backspace is replaced with \b + * Form feed is replaced with \f + * Newline is replaced with \n + * Carriage return is replaced with \r + * Tab is replaced with \t + * Double quote is replaced with \" + * Backslash is replaced with \\ + * + * additionally we handle xhtml '</bla>' string + * and non-ascii chars + */ + StringBuilder escaped = new StringBuilder(); + escaped.append('"'); + char b; + char c = 0; + for (int i = 0; i < unescaped.length(); i++) { + b = c; + c = unescaped.charAt(i); + switch (c) { + case '\"': + escaped.append('\\').append('"'); + break; + case '\n': + escaped.append('\\').append('n'); + break; + case '\r': + escaped.append('\\').append('r'); + break; + case '\t': + escaped.append('\\').append('t'); + break; + case '\b': + escaped.append('\\').append('b'); + break; + case '\f': + escaped.append('\\').append('f'); + break; + case '\\': + escaped.append('\\').append('\\'); + break; + case '/': + if (b == '<') { + escaped.append('\\'); + } + escaped.append(c); + break; + default: + if (c < ' ' || (c >= '\u0080' && c < '\u00a0') + || (c >= '\u2000' && c < '\u2100')) { + escaped.append('\\').append('u'); + String hexString = Integer.toHexString(c); + escaped.append("0000", 0, 4 - hexString.length()); + escaped.append(hexString); + } else { + escaped.append(c); + } + } + } + escaped.append('"'); + return escaped.toString(); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java b/lib/src/main/java/com/diffplug/spotless/npm/JsonRawValue.java similarity index 58% rename from lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java rename to lib/src/main/java/com/diffplug/spotless/npm/JsonRawValue.java index c43f4963f3..332dfb7c17 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/JsonRawValue.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,27 +15,23 @@ */ package com.diffplug.spotless.npm; -class TsFmtResult { +import static java.util.Objects.requireNonNull; - private final String message; - private final Boolean error; - private final String formatted; - - TsFmtResult(String message, Boolean error, String formatted) { - this.message = message; - this.error = error; - this.formatted = formatted; - } +/** + * Wrapper class to signal the contained string must not be escaped when printing to json. + */ +class JsonRawValue { + private final String rawJson; - String getMessage() { - return message; + private JsonRawValue(String rawJson) { + this.rawJson = requireNonNull(rawJson); } - Boolean isError() { - return error; + static JsonRawValue wrap(String rawJson) { + return new JsonRawValue(rawJson); } - String getFormatted() { - return formatted; + public String getRawJson() { + return rawJson; } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java deleted file mode 100644 index 67c121baed..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java +++ /dev/null @@ -1,132 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import java.io.File; -import java.util.HashSet; -import java.util.Map; -import java.util.Objects; -import java.util.Set; - -import com.diffplug.spotless.ThrowingEx; - -class NodeJSWrapper extends ReflectiveObjectWrapper { - - public static final String V8_RUNTIME_CLASS = "com.eclipsesource.v8.V8"; - public static final String V8_VALUE_CLASS = "com.eclipsesource.v8.V8Value"; - - public static final String WRAPPED_CLASS = "com.eclipsesource.v8.NodeJS"; - - private static final Set<ClassLoader> alreadySetup = new HashSet<>(); - - public NodeJSWrapper(ClassLoader classLoader) { - super(Reflective.withClassLoader(classLoader), - reflective -> { - if (alreadySetup.add(classLoader)) { - // the bridge to node.js needs a .dll/.so/.dylib which gets loaded through System.load - // the problem is that when the JVM loads that DLL, it is bound to the specific classloader that called System.load - // no other classloaders have access to it, and if any other classloader tries to load it, you get an error - // - // ...but, you can copy that DLL as many times as you want, and each classloader can load its own copy of the DLL, and - // that is fine - // - // ...but the in order to do that, we have to manually load the DLL per classloader ourselves, which involves some - // especially hacky reflection into J2V8 *and* JVM internal - // - // so this is bad code, but it fixes our problem, and so far we don't have a better way... - - // here we get the name of the DLL within the jar, and we get our own copy of it on disk - String resource = (String) reflective.invokeStaticMethodPrivate("com.eclipsesource.v8.LibraryLoader", "computeLibraryFullName"); - File file = NodeJsGlobal.sharedLibs.nextDynamicLib(classLoader, resource); - - // ideally, we would call System.load, but the JVM does some tricky stuff to - // figure out who actually called this, and it realizes it was Reflective, which lives - // outside the J2V8 classloader, so System.load doesn't work. Soooo, we have to dig - // into JVM internals and manually tell it "this class from J2V8 called you" - Class<?> libraryLoaderClass = ThrowingEx.get(() -> classLoader.loadClass("com.eclipsesource.v8.LibraryLoader")); - reflective.invokeStaticMethodPrivate("java.lang.ClassLoader", "loadLibrary0", libraryLoaderClass, file); - - // and now we set the flag in J2V8 which says "the DLL is loaded, don't load it again" - reflective.staticFieldPrivate("com.eclipsesource.v8.V8", "nativeLibraryLoaded", true); - } - return reflective.invokeStaticMethod(WRAPPED_CLASS, "createNodeJS"); - }); - } - - public V8ObjectWrapper require(File npmModulePath) { - Objects.requireNonNull(npmModulePath); - Object v8Object = invoke("require", npmModulePath); - return new V8ObjectWrapper(reflective(), v8Object); - } - - public V8ObjectWrapper createNewObject() { - Object v8Object = reflective().invokeConstructor(V8ObjectWrapper.WRAPPED_CLASS, nodeJsRuntime()); - V8ObjectWrapper objectWrapper = new V8ObjectWrapper(reflective(), v8Object); - return objectWrapper; - } - - public V8ObjectWrapper createNewObject(Map<String, Object> values) { - Objects.requireNonNull(values); - V8ObjectWrapper obj = createNewObject(); - values.forEach(obj::add); - return obj; - } - - public V8ArrayWrapper createNewArray(Object... elements) { - final V8ArrayWrapper v8ArrayWrapper = this.createNewArray(); - for (Object element : elements) { - v8ArrayWrapper.push(element); - } - return v8ArrayWrapper; - } - - public V8ArrayWrapper createNewArray() { - Object v8Array = reflective().invokeConstructor(V8ArrayWrapper.WRAPPED_CLASS, nodeJsRuntime()); - V8ArrayWrapper arrayWrapper = new V8ArrayWrapper(reflective(), v8Array); - return arrayWrapper; - } - - public V8FunctionWrapper createNewFunction(V8FunctionWrapper.WrappedJavaCallback callback) { - Object v8Function = reflective().invokeConstructor(V8FunctionWrapper.WRAPPED_CLASS, - reflective().typed( - V8_RUNTIME_CLASS, - nodeJsRuntime()), - reflective().typed( - V8FunctionWrapper.CALLBACK_WRAPPED_CLASS, - V8FunctionWrapper.proxiedCallback(callback, reflective()))); - V8FunctionWrapper functionWrapper = new V8FunctionWrapper(reflective(), v8Function); - return functionWrapper; - } - - public void handleMessage() { - invoke("handleMessage"); - } - - private Object nodeJsRuntime() { - return invoke("getRuntime"); - } - - public Object v8NullValue(Object value) { - if (value == null) { - return reflective().staticField(V8_VALUE_CLASS, "NULL"); - } - return value; - } - - public boolean isV8NullValue(Object v8Object) { - return reflective().staticField(V8_VALUE_CLASS, "NULL") == v8Object; - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeJsGlobal.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeJsGlobal.java deleted file mode 100644 index ad2a5ff428..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/NodeJsGlobal.java +++ /dev/null @@ -1,93 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.io.InputStream; -import java.nio.file.Files; -import java.nio.file.Path; -import java.util.Optional; -import java.util.stream.IntStream; - -import com.diffplug.spotless.LineEnding; -import com.diffplug.spotless.ThrowingEx; - -/** Shared config acress the NodeJS steps. */ -public class NodeJsGlobal { - static SharedLibFolder sharedLibs; - - static { - sharedLibs = new SharedLibFolder( - ThrowingEx.get(() -> Files.createTempDirectory("spotless-nodejs"))); - sharedLibs.root.deleteOnExit(); - } - - /** - * All of the NodeJS steps need to extract a bridge DLL for node. By default this is - * a random location, but you can set it to be anywhere. - */ - public static void setSharedLibFolder(File sharedLibFolder) { - sharedLibs = new SharedLibFolder(sharedLibFolder.toPath()); - } - - static class SharedLibFolder { - private final File root; - - private SharedLibFolder(Path root) { - this.root = ThrowingEx.get(() -> root.toFile().getCanonicalFile()); - } - - static final int MAX_CLASSLOADERS_PER_CLEAN = 1_000; - - synchronized File nextDynamicLib(ClassLoader loader, String resource) { - // find a new unique file - Optional<File> nextLibOpt = IntStream.range(0, MAX_CLASSLOADERS_PER_CLEAN) - .mapToObj(i -> new File(root, i + "_" + resource)) - .filter(file -> !file.exists()) - .findFirst(); - if (!nextLibOpt.isPresent()) { - throw new IllegalArgumentException("Overflow, delete the spotless nodeJs cache: " + root); - } - File nextLib = nextLibOpt.get(); - // copy the dll to it - try { - Files.createDirectories(nextLib.getParentFile().toPath()); - try (FileOutputStream fileOut = new FileOutputStream(nextLib); - InputStream resourceIn = loader.loadClass("com.eclipsesource.v8.LibraryLoader").getResourceAsStream("/" + resource)) { - byte[] buf = new byte[0x1000]; - while (true) { - int r = resourceIn.read(buf); - if (r == -1) { - break; - } - fileOut.write(buf, 0, r); - } - } - } catch (IOException | ClassNotFoundException e) { - throw ThrowingEx.asRuntime(e); - } - // make sure it is executable (on unix) - if (LineEnding.PLATFORM_NATIVE.str().equals("\n")) { - ThrowingEx.run(() -> { - Runtime.getRuntime().exec(new String[]{"chmod", "755", nextLib.getAbsolutePath()}).waitFor(); //$NON-NLS-1$ - }); - } - return nextLib; - } - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java index 55d3683606..7fe3daf0b7 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,17 +17,22 @@ import java.io.Serializable; +import javax.annotation.Nonnull; + class NpmConfig implements Serializable { - private static final long serialVersionUID = -1866722789779160491L; + private static final long serialVersionUID = -7660089232952131272L; private final String packageJsonContent; private final String npmModule; - public NpmConfig(String packageJsonContent, String npmModule) { + private final String serveScriptContent; + + public NpmConfig(String packageJsonContent, String npmModule, String serveScriptContent) { this.packageJsonContent = packageJsonContent; this.npmModule = npmModule; + this.serveScriptContent = serveScriptContent; } public String getPackageJsonContent() { @@ -37,4 +42,9 @@ public String getPackageJsonContent() { public String getNpmModule() { return npmModule; } + + @Nonnull + public String getServeScriptContent() { + return serveScriptContent; + } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java index cea0984a2f..5be273621e 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -105,4 +105,15 @@ private static Supplier<Optional<File>> pathListFromEnvironment(String environme }; } + static String explainMessage() { + return "Spotless tries to find your npm executable automatically. It looks for npm in the following places:\n" + + "- An executable referenced by the java system property 'npm.exec' - if such a system property exists.\n" + + "- The environment variable 'NVM_BIN' - if such an environment variable exists.\n" + + "- The environment variable 'NVM_SYMLINK' - if such an environment variable exists.\n" + + "- The environment variable 'NODE_PATH' - if such an environment variable exists.\n" + + "- In your 'PATH' environment variable\n" + + "\n" + + "If autodiscovery fails for your system, try to set one of the environment variables correctly or\n" + + "try setting the system property 'npm.exec' in the build process to override autodiscovery."; + } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java index 8b041e3dcb..bcdd369e10 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -17,30 +17,31 @@ import static java.util.Objects.requireNonNull; -import java.io.ByteArrayOutputStream; import java.io.File; import java.io.IOException; -import java.io.InputStream; import java.io.Serializable; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; +import java.time.Duration; import java.util.Collections; import java.util.Iterator; import java.util.Map; import java.util.Map.Entry; import java.util.Optional; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; +import java.util.logging.Logger; import javax.annotation.Nullable; -import com.diffplug.spotless.*; +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.FormatterFunc; import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; abstract class NpmFormatterStepStateBase implements Serializable { - private static final long serialVersionUID = -5849375492831208496L; + private static final Logger logger = Logger.getLogger(NpmFormatterStepStateBase.class.getName()); - private final JarState jarState; + private static final long serialVersionUID = 1460749955865959948L; @SuppressWarnings("unused") private final FileSignature nodeModulesSignature; @@ -48,77 +49,71 @@ abstract class NpmFormatterStepStateBase implements Serializable { @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") public final transient File nodeModulesDir; + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + private final transient File npmExecutable; + private final NpmConfig npmConfig; private final String stepName; - protected NpmFormatterStepStateBase(String stepName, Provisioner provisioner, NpmConfig npmConfig, File buildDir, @Nullable File npm) throws IOException { + protected NpmFormatterStepStateBase(String stepName, NpmConfig npmConfig, File buildDir, @Nullable File npm) throws IOException { this.stepName = requireNonNull(stepName); this.npmConfig = requireNonNull(npmConfig); - this.jarState = JarState.from(j2v8MavenCoordinate(), requireNonNull(provisioner)); + this.npmExecutable = resolveNpm(npm); - this.nodeModulesDir = prepareNodeModules(buildDir, npm); + this.nodeModulesDir = prepareNodeServer(buildDir); this.nodeModulesSignature = FileSignature.signAsList(this.nodeModulesDir); } - private File prepareNodeModules(File buildDir, @Nullable File npm) throws IOException { + private File prepareNodeServer(File buildDir) throws IOException { File targetDir = new File(buildDir, "spotless-node-modules-" + stepName); - if (!targetDir.exists()) { - if (!targetDir.mkdirs()) { - throw new IOException("cannot create temp dir for node modules at " + targetDir); - } - } - File packageJsonFile = new File(targetDir, "package.json"); - Files.write(packageJsonFile.toPath(), this.npmConfig.getPackageJsonContent().getBytes(StandardCharsets.UTF_8)); - runNpmInstall(npm, targetDir); + NpmResourceHelper.assertDirectoryExists(targetDir); + NpmResourceHelper.writeUtf8StringToFile(targetDir, "package.json", this.npmConfig.getPackageJsonContent()); + NpmResourceHelper.writeUtf8StringToFile(targetDir, "serve.js", this.npmConfig.getServeScriptContent()); + runNpmInstall(targetDir); return targetDir; } - private void runNpmInstall(@Nullable File npm, File npmProjectDir) throws IOException { - Process npmInstall = new ProcessBuilder() - .inheritIO() - .directory(npmProjectDir) - .command(resolveNpm(npm).getAbsolutePath(), "install") - .start(); + private void runNpmInstall(File npmProjectDir) throws IOException { + new NpmProcess(npmProjectDir, this.npmExecutable).install(); + } + + protected ServerProcessInfo npmRunServer() throws ServerStartException { try { - if (npmInstall.waitFor() != 0) { - throw new IOException("Creating npm modules failed with exit code: " + npmInstall.exitValue()); + // The npm process will output the randomly selected port of the http server process to 'server.port' file + // so in order to be safe, remove such a file if it exists before starting. + final File serverPortFile = new File(this.nodeModulesDir, "server.port"); + NpmResourceHelper.deleteFileIfExists(serverPortFile); + // start the http server in node + Process server = new NpmProcess(this.nodeModulesDir, this.npmExecutable).start(); + + // await the readiness of the http server - wait for at most 60 seconds + try { + NpmResourceHelper.awaitReadableFile(serverPortFile, Duration.ofSeconds(60)); + } catch (TimeoutException timeoutException) { + // forcibly end the server process + try { + if (server.isAlive()) { + server.destroyForcibly(); + server.waitFor(); + } + } catch (Throwable t) { + // ignore + } + throw timeoutException; } - } catch (InterruptedException e) { - throw new IOException("Running npm install was interrupted.", e); + // read the server.port file for resulting port and remember the port for later formatting calls + String serverPort = NpmResourceHelper.readUtf8StringFromFile(serverPortFile).trim(); + return new ServerProcessInfo(server, serverPort, serverPortFile); + } catch (IOException | TimeoutException e) { + throw new ServerStartException(e); } } - private File resolveNpm(@Nullable File npm) { + private static File resolveNpm(@Nullable File npm) { return Optional.ofNullable(npm) .orElseGet(() -> NpmExecutableResolver.tryFind() - .orElseThrow(() -> new IllegalStateException("cannot automatically determine npm executable and none was specifically supplied!"))); - } - - protected NodeJSWrapper nodeJSWrapper() { - return new NodeJSWrapper(this.jarState.getClassLoader()); - } - - protected File nodeModulePath() { - return new File(new File(this.nodeModulesDir, "node_modules"), this.npmConfig.getNpmModule()); - } - - static String j2v8MavenCoordinate() { - return "com.eclipsesource.j2v8:j2v8_" + PlatformInfo.normalizedOSName() + "_" + PlatformInfo.normalizedArchName() + ":4.6.0"; - } - - protected static String readFileFromClasspath(Class<?> clazz, String name) { - ByteArrayOutputStream output = new ByteArrayOutputStream(); - try (InputStream input = clazz.getResourceAsStream(name)) { - byte[] buffer = new byte[1024]; - int numRead; - while ((numRead = input.read(buffer)) != -1) { - output.write(buffer, 0, numRead); - } - return output.toString(StandardCharsets.UTF_8.name()); - } catch (IOException e) { - throw ThrowingEx.asRuntime(e); - } + .orElseThrow(() -> new IllegalStateException("Can't automatically determine npm executable and none was specifically supplied!\n\n" + NpmExecutableResolver.explainMessage()))); } protected static String replaceDevDependencies(String template, Map<String, String> devDependencies) { @@ -147,4 +142,43 @@ private static String replacePlaceholders(String template, Map<String, String> r } public abstract FormatterFunc createFormatterFunc(); + + protected static class ServerProcessInfo implements AutoCloseable { + private final Process server; + private final String serverPort; + private final File serverPortFile; + + public ServerProcessInfo(Process server, String serverPort, File serverPortFile) { + this.server = server; + this.serverPort = serverPort; + this.serverPortFile = serverPortFile; + } + + public String getBaseUrl() { + return "http://127.0.0.1:" + this.serverPort; + } + + @Override + public void close() throws Exception { + try { + logger.fine("Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + ">"); + if (server.isAlive()) { + boolean ended = server.waitFor(5, TimeUnit.SECONDS); + if (!ended) { + logger.info("Force-Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + ">"); + server.destroyForcibly().waitFor(); + logger.fine("Force-Closing npm server in directory <" + serverPortFile.getParent() + "> and port <" + serverPort + "> -- Finished"); + } + } + } finally { + NpmResourceHelper.deleteFileIfExists(serverPortFile); + } + } + } + + protected static class ServerStartException extends RuntimeException { + public ServerStartException(Throwable cause) { + super(cause); + } + } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java new file mode 100644 index 0000000000..f2444b3313 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmProcess.java @@ -0,0 +1,89 @@ +/* + * Copyright 2016-2020 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.npm; + +import java.io.File; +import java.io.IOException; +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Collectors; + +class NpmProcess { + + private final File workingDir; + + private final File npmExecutable; + + NpmProcess(File workingDir, File npmExecutable) { + this.workingDir = workingDir; + this.npmExecutable = npmExecutable; + } + + void install() { + npmAwait("install", "--no-audit", "--no-package-lock"); + } + + Process start() { + return npm("start"); + } + + private void npmAwait(String... args) { + final Process npmProcess = npm(args); + + try { + if (npmProcess.waitFor() != 0) { + throw new NpmProcessException("Running npm command '" + commandLine(args) + "' failed with exit code: " + npmProcess.exitValue()); + } + } catch (InterruptedException e) { + throw new NpmProcessException("Running npm command '" + commandLine(args) + "' was interrupted.", e); + } + } + + private Process npm(String... args) { + List<String> processCommand = processCommand(args); + try { + return new ProcessBuilder() + .inheritIO() + .directory(this.workingDir) + .command(processCommand) + .start(); + } catch (IOException e) { + throw new NpmProcessException("Failed to launch npm command '" + commandLine(args) + "'.", e); + } + } + + private List<String> processCommand(String... args) { + List<String> command = new ArrayList<>(args.length + 1); + command.add(this.npmExecutable.getAbsolutePath()); + command.addAll(Arrays.asList(args)); + return command; + } + + private String commandLine(String... args) { + return "npm " + Arrays.stream(args).collect(Collectors.joining(" ")); + } + + static class NpmProcessException extends RuntimeException { + public NpmProcessException(String message) { + super(message); + } + + public NpmProcessException(String message, Throwable cause) { + super(message, cause); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java new file mode 100644 index 0000000000..3e5bcd68b4 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmResourceHelper.java @@ -0,0 +1,96 @@ +/* + * Copyright 2016-2020 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.npm; + +import java.io.*; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.time.Duration; +import java.util.concurrent.TimeoutException; + +import com.diffplug.spotless.ThrowingEx; + +final class NpmResourceHelper { + private NpmResourceHelper() { + // no instance required + } + + static void writeUtf8StringToFile(File targetDir, String fileName, String stringToWrite) throws IOException { + File packageJsonFile = new File(targetDir, fileName); + Files.write(packageJsonFile.toPath(), stringToWrite.getBytes(StandardCharsets.UTF_8)); + } + + static void writeUtf8StringToOutputStream(String stringToWrite, OutputStream outputStream) throws IOException { + final byte[] bytes = stringToWrite.getBytes(StandardCharsets.UTF_8); + outputStream.write(bytes); + } + + static void deleteFileIfExists(File file) throws IOException { + if (file.exists()) { + if (!file.delete()) { + throw new IOException("Failed to delete " + file); + } + } + } + + static String readUtf8StringFromClasspath(Class<?> clazz, String resourceName) { + try (InputStream input = clazz.getResourceAsStream(resourceName)) { + return readUtf8StringFromInputStream(input); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + static String readUtf8StringFromFile(File file) { + try { + return String.join("\n", Files.readAllLines(file.toPath())); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + static String readUtf8StringFromInputStream(InputStream input) { + try { + ByteArrayOutputStream output = new ByteArrayOutputStream(); + byte[] buffer = new byte[1024]; + int numRead; + while ((numRead = input.read(buffer)) != -1) { + output.write(buffer, 0, numRead); + } + return output.toString(StandardCharsets.UTF_8.name()); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + static void assertDirectoryExists(File directory) throws IOException { + if (!directory.exists()) { + if (!directory.mkdirs()) { + throw new IOException("cannot create temp dir for node modules at " + directory); + } + } + } + + static void awaitReadableFile(File file, Duration maxWaitTime) throws TimeoutException { + final long startedAt = System.currentTimeMillis(); + while (!file.exists() || !file.canRead()) { + // wait for at most maxWaitTime + if ((System.currentTimeMillis() - startedAt) > maxWaitTime.toMillis()) { + throw new TimeoutException("The file did not appear within " + maxWaitTime); + } + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java index a7161065ef..d9c470b5d9 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,6 @@ */ package com.diffplug.spotless.npm; -import static java.util.Arrays.asList; import static java.util.Objects.requireNonNull; import java.io.File; @@ -24,21 +23,26 @@ import java.util.Collections; import java.util.Map; import java.util.TreeMap; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterFunc.Closeable; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.ThrowingEx; public class PrettierFormatterStep { + private static final Logger logger = Logger.getLogger(PrettierFormatterStep.class.getName()); + public static final String NAME = "prettier-format"; public static final Map<String, String> defaultDevDependencies() { - return defaultDevDependenciesWithPrettier("1.16.4"); + return defaultDevDependenciesWithPrettier("2.0.5"); } public static final Map<String, String> defaultDevDependenciesWithPrettier(String version) { @@ -55,23 +59,23 @@ public static FormatterStep create(Map<String, String> devDependencies, Provisio requireNonNull(provisioner); requireNonNull(buildDir); return FormatterStep.createLazy(NAME, - () -> new State(NAME, devDependencies, provisioner, buildDir, npm, prettierConfig), + () -> new State(NAME, devDependencies, buildDir, npm, prettierConfig), State::createFormatterFunc); } public static class State extends NpmFormatterStepStateBase implements Serializable { - private static final long serialVersionUID = -3811104513825329168L; + private static final long serialVersionUID = -539537027004745812L; private final PrettierConfig prettierConfig; - State(String stepName, Map<String, String> devDependencies, Provisioner provisioner, File buildDir, @Nullable File npm, PrettierConfig prettierConfig) throws IOException { + State(String stepName, Map<String, String> devDependencies, File buildDir, @Nullable File npm, PrettierConfig prettierConfig) throws IOException { super(stepName, - provisioner, new NpmConfig( replaceDevDependencies( - readFileFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json"), + NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json"), new TreeMap<>(devDependencies)), - "prettier"), + "prettier", + NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-serve.js")), buildDir, npm); this.prettierConfig = requireNonNull(prettierConfig); @@ -80,84 +84,23 @@ public static class State extends NpmFormatterStepStateBase implements Serializa @Override @Nonnull public FormatterFunc createFormatterFunc() { - try { - final NodeJSWrapper nodeJSWrapper = nodeJSWrapper(); - final V8ObjectWrapper prettier = nodeJSWrapper.require(nodeModulePath()); - - @SuppressWarnings("unchecked") - final Map<String, Object>[] resolvedPrettierOptions = (Map<String, Object>[]) new Map[1]; - - if (this.prettierConfig.getPrettierConfigPath() != null) { - final Exception[] toThrow = new Exception[1]; - try ( - V8FunctionWrapper resolveConfigCallback = createResolveConfigFunction(nodeJSWrapper, resolvedPrettierOptions, toThrow); - V8ObjectWrapper resolveConfigOption = createResolveConfigOptionObj(nodeJSWrapper); - V8ArrayWrapper resolveConfigParams = createResolveConfigParamsArray(nodeJSWrapper, resolveConfigOption); - - V8ObjectWrapper promise = prettier.executeObjectFunction("resolveConfig", resolveConfigParams); - V8ArrayWrapper callbacks = nodeJSWrapper.createNewArray(resolveConfigCallback);) { - - promise.executeVoidFunction("then", callbacks); - executeResolution(nodeJSWrapper, resolvedPrettierOptions, toThrow); - } - } else { - resolvedPrettierOptions[0] = this.prettierConfig.getOptions(); - } - - final V8ObjectWrapper prettierConfig = nodeJSWrapper.createNewObject(resolvedPrettierOptions[0]); - - return FormatterFunc.Closeable.of(() -> { - asList(prettierConfig, prettier, nodeJSWrapper).forEach(ReflectiveObjectWrapper::release); - }, input -> { - try (V8ArrayWrapper formatParams = nodeJSWrapper.createNewArray(input, prettierConfig)) { - String result = prettier.executeStringFunction("format", formatParams); - return result; - } - }); + ServerProcessInfo prettierRestServer = npmRunServer(); + PrettierRestService restService = new PrettierRestService(prettierRestServer.getBaseUrl()); + String prettierConfigOptions = restService.resolveConfig(this.prettierConfig.getPrettierConfigPath(), this.prettierConfig.getOptions()); + return Closeable.of(() -> endServer(restService, prettierRestServer), input -> restService.format(input, prettierConfigOptions)); } catch (Exception e) { throw ThrowingEx.asRuntime(e); } } - private V8FunctionWrapper createResolveConfigFunction(NodeJSWrapper nodeJSWrapper, Map<String, Object>[] outputOptions, Exception[] toThrow) { - return nodeJSWrapper.createNewFunction((receiver, parameters) -> { - try { - try (final V8ObjectWrapper configOptions = parameters.getObject(0)) { - if (configOptions == null) { - toThrow[0] = new IllegalArgumentException("Cannot find or read config file " + this.prettierConfig.getPrettierConfigPath()); - } else { - Map<String, Object> resolvedOptions = new TreeMap<>(V8ObjectUtilsWrapper.toMap(configOptions)); - resolvedOptions.putAll(this.prettierConfig.getOptions()); - outputOptions[0] = resolvedOptions; - } - } - } catch (Exception e) { - toThrow[0] = e; - } - return receiver; - }); - } - - private V8ObjectWrapper createResolveConfigOptionObj(NodeJSWrapper nodeJSWrapper) { - return nodeJSWrapper.createNewObject() - .add("config", this.prettierConfig.getPrettierConfigPath().getAbsolutePath()); - } - - private V8ArrayWrapper createResolveConfigParamsArray(NodeJSWrapper nodeJSWrapper, V8ObjectWrapper resolveConfigOption) { - return nodeJSWrapper.createNewArray() - .pushNull() - .push(resolveConfigOption); - } - - private void executeResolution(NodeJSWrapper nodeJSWrapper, Map<String, Object>[] resolvedPrettierOptions, Exception[] toThrow) { - while (resolvedPrettierOptions[0] == null && toThrow[0] == null) { - nodeJSWrapper.handleMessage(); - } - - if (toThrow[0] != null) { - throw ThrowingEx.asRuntime(toThrow[0]); + private void endServer(PrettierRestService restService, ServerProcessInfo restServer) throws Exception { + try { + restService.shutdown(); + } catch (Throwable t) { + logger.log(Level.INFO, "Failed to request shutdown of rest service via api. Trying via process.", t); } + restServer.close(); } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java new file mode 100644 index 0000000000..ced08b013f --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierRestService.java @@ -0,0 +1,56 @@ +/* + * Copyright 2016-2020 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.npm; + +import java.io.File; +import java.util.LinkedHashMap; +import java.util.Map; + +public class PrettierRestService { + + private final SimpleRestClient restClient; + + PrettierRestService(String baseUrl) { + this.restClient = SimpleRestClient.forBaseUrl(baseUrl); + } + + public String resolveConfig(File prettierConfigPath, Map<String, Object> prettierConfigOptions) { + Map<String, Object> jsonProperties = new LinkedHashMap<>(); + if (prettierConfigPath != null) { + jsonProperties.put("prettier_config_path", prettierConfigPath.getAbsolutePath()); + } + if (prettierConfigOptions != null) { + jsonProperties.put("prettier_config_options", SimpleJsonWriter.of(prettierConfigOptions).toJsonRawValue()); + + } + return restClient.postJson("/prettier/config-options", jsonProperties); + } + + public String format(String fileContent, String configOptionsJsonString) { + Map<String, Object> jsonProperties = new LinkedHashMap<>(); + jsonProperties.put("file_content", fileContent); + if (configOptionsJsonString != null) { + jsonProperties.put("config_options", JsonRawValue.wrap(configOptionsJsonString)); + } + + return restClient.postJson("/prettier/format", jsonProperties); + } + + public String shutdown() { + return restClient.post("/shutdown"); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java b/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java deleted file mode 100644 index 3c65ea0492..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java +++ /dev/null @@ -1,302 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import static java.util.Objects.requireNonNull; - -import java.lang.reflect.*; -import java.util.Arrays; -import java.util.Objects; -import java.util.StringJoiner; - -import com.diffplug.spotless.ThrowingEx; - -import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; - -class Reflective { - private final ClassLoader classLoader; - - private Reflective(ClassLoader classLoader) { - this.classLoader = requireNonNull(classLoader); - } - - static Reflective withClassLoader(ClassLoader classLoader) { - return new Reflective(classLoader); - } - - Class<?> clazz(String className) { - try { - return this.classLoader.loadClass(className); - } catch (ClassNotFoundException e) { - throw new ReflectiveException(e); - } - } - - private Method staticMethod(String className, String methodName, Object... parameters) { - try { - final Class<?> clazz = clazz(className); - return clazz.getDeclaredMethod(methodName, types(parameters)); - } catch (NoSuchMethodException e) { - throw new ReflectiveException(e); - } - } - - Object invokeStaticMethod(String className, String methodName, Object... parameters) { - try { - Method m = staticMethod(className, methodName, parameters); - return m.invoke(m.getDeclaringClass(), parameters); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new ReflectiveException(e); - } - } - - @SuppressFBWarnings("DP_DO_INSIDE_DO_PRIVILEGED") - Object invokeStaticMethodPrivate(String className, String methodName, Object... parameters) { - try { - Method m = staticMethod(className, methodName, parameters); - m.setAccessible(true); - return m.invoke(m.getDeclaringClass(), parameters); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new ReflectiveException(e); - } - } - - private Class<?>[] types(TypedValue[] typedValues) { - return Arrays.stream(typedValues) - .map(TypedValue::getClazz) - .toArray(Class[]::new); - } - - Class<?>[] types(Object[] arguments) { - return Arrays.stream(arguments) - .map(Object::getClass) - .toArray(Class[]::new); - } - - Object invokeMethod(Object target, String methodName, Object... parameters) { - Method m = method(target, clazz(target), methodName, parameters); - try { - return m.invoke(target, parameters); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new ReflectiveException(e); - } - } - - Object invokeMethod(Object target, String methodName, TypedValue... parameters) { - Method m = method(target, clazz(target), methodName, parameters); - try { - return m.invoke(target, objects(parameters)); - } catch (IllegalAccessException | InvocationTargetException e) { - throw new ReflectiveException(e); - } - } - - private Method method(Object target, Class<?> clazz, String methodName, Object[] parameters) { - try { - final Method method = findMatchingMethod(clazz, methodName, parameters); - return method; - } catch (NoSuchMethodException e) { - if (clazz.getSuperclass() != null) { - return method(target, clazz.getSuperclass(), methodName, parameters); - } else { - throw new ReflectiveException("Could not find method " + methodName + " with parameters " + Arrays.toString(parameters) + " on object " + target, e); - } - } - } - - private Method method(Object target, Class<?> clazz, String methodName, TypedValue[] parameters) { - try { - final Method method = findMatchingMethod(clazz, methodName, parameters); - return method; - } catch (NoSuchMethodException e) { - if (clazz.getSuperclass() != null) { - return method(target, clazz.getSuperclass(), methodName, parameters); - } else { - throw new ReflectiveException("Could not find method " + methodName + " with parameters " + Arrays.toString(parameters) + " on object " + target, e); - } - } - } - - private Method findMatchingMethod(Class<?> clazz, String methodName, Object[] parameters) throws NoSuchMethodException { - final Class<?>[] origTypes = types(parameters); - try { - return clazz.getDeclaredMethod(methodName, origTypes); - } catch (NoSuchMethodException e) { - // try with primitives - final Class<?>[] primitives = autoUnbox(origTypes); - try { - return clazz.getDeclaredMethod(methodName, primitives); - } catch (NoSuchMethodException e1) { - // didn't work either - throw e; - } - } - } - - private Method findMatchingMethod(Class<?> clazz, String methodName, TypedValue[] parameters) throws NoSuchMethodException { - return clazz.getDeclaredMethod(methodName, types(parameters)); - } - - private Class<?>[] autoUnbox(Class<?>[] possiblyBoxed) { - return Arrays.stream(possiblyBoxed) - .map(clazz -> { - try { - return (Class<?>) this.staticField(clazz, "TYPE"); - } catch (ReflectiveException e) { - // no primitive type, just keeping current clazz - return clazz; - } - }).toArray(Class[]::new); - } - - private Class<?> clazz(Object target) { - return target.getClass(); - } - - Object invokeConstructor(String className, TypedValue... parameters) { - try { - final Constructor<?> constructor = constructor(className, parameters); - return constructor.newInstance(objects(parameters)); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new ReflectiveException(e); - } - } - - private Object[] objects(TypedValue[] parameters) { - return Arrays.stream(parameters) - .map(TypedValue::getObj) - .toArray(); - } - - Object invokeConstructor(String className, Object... parameters) { - try { - final Constructor<?> constructor = constructor(className, parameters); - return constructor.newInstance(parameters); - } catch (InstantiationException | IllegalAccessException | InvocationTargetException e) { - throw new ReflectiveException(e); - } - } - - private Constructor<?> constructor(String className, TypedValue[] parameters) { - try { - final Class<?> clazz = clazz(className); - final Constructor<?> constructor = clazz.getDeclaredConstructor(types(parameters)); - return constructor; - } catch (NoSuchMethodException e) { - throw new ReflectiveException(e); - } - } - - private Constructor<?> constructor(String className, Object[] parameters) { - try { - final Class<?> clazz = clazz(className); - final Constructor<?> constructor = clazz.getDeclaredConstructor(types(parameters)); - return constructor; - } catch (NoSuchMethodException e) { - throw new ReflectiveException(e); - } - } - - Object createDynamicProxy(InvocationHandler invocationHandler, String... interfaceNames) { - Class<?>[] clazzes = Arrays.stream(interfaceNames) - .map(this::clazz) - .toArray(Class[]::new); - return Proxy.newProxyInstance(this.classLoader, clazzes, invocationHandler); - } - - Object staticField(String className, String fieldName) { - final Class<?> clazz = clazz(className); - return staticField(clazz, fieldName); - } - - private Object staticField(Class<?> clazz, String fieldName) { - try { - return clazz.getDeclaredField(fieldName).get(clazz); - } catch (IllegalAccessException | NoSuchFieldException e) { - throw new ReflectiveException(e); - } - } - - @SuppressFBWarnings("DP_DO_INSIDE_DO_PRIVILEGED") - void staticFieldPrivate(String className, String fieldName, boolean newValue) { - Class<?> clazz = clazz(className); - try { - Field field = clazz.getDeclaredField(fieldName); - field.setAccessible(true); - field.setBoolean(null, newValue); - } catch (NoSuchFieldException | SecurityException | IllegalArgumentException | IllegalAccessException e) { - throw ThrowingEx.asRuntime(e); - } - } - - TypedValue typed(String className, Object obj) { - return new TypedValue(clazz(className), obj); - } - - public static class TypedValue { - private final Class<?> clazz; - private final Object obj; - - public TypedValue(Class<?> clazz, Object obj) { - this.clazz = requireNonNull(clazz); - this.obj = requireNonNull(obj); - } - - public Class<?> getClazz() { - return clazz; - } - - public Object getObj() { - return obj; - } - - @Override - public String toString() { - return new StringJoiner(", ", TypedValue.class.getSimpleName() + "[", "]") - .add("clazz=" + clazz) - .add("obj=" + obj) - .toString(); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (o == null || getClass() != o.getClass()) - return false; - TypedValue that = (TypedValue) o; - return Objects.equals(clazz, that.clazz) && - Objects.equals(obj, that.obj); - } - - @Override - public int hashCode() { - return Objects.hash(clazz, obj); - } - } - - public static class ReflectiveException extends RuntimeException { - private static final long serialVersionUID = -5764607170953013791L; - - public ReflectiveException(String message, Throwable cause) { - super(message, cause); - } - - public ReflectiveException(Throwable cause) { - super(cause); - } - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/ReflectiveObjectWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/ReflectiveObjectWrapper.java deleted file mode 100644 index 3dfffc8c94..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/ReflectiveObjectWrapper.java +++ /dev/null @@ -1,76 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import static java.util.Objects.requireNonNull; - -import java.util.Objects; -import java.util.function.Function; - -abstract class ReflectiveObjectWrapper implements AutoCloseable { - - private final Object wrappedObj; - private final Reflective reflective; - - public ReflectiveObjectWrapper(Reflective reflective, Object wrappedObj) { - this.reflective = requireNonNull(reflective); - this.wrappedObj = requireNonNull(wrappedObj); - } - - public ReflectiveObjectWrapper(Reflective reflective, Function<Reflective, Object> wrappedObjSupplier) { - this(reflective, wrappedObjSupplier.apply(reflective)); - } - - protected Reflective reflective() { - return this.reflective; - } - - protected Object wrappedObj() { - return this.wrappedObj; - } - - protected Object invoke(String methodName, Object... parameters) { - return reflective().invokeMethod(wrappedObj(), methodName, parameters); - } - - protected Object invoke(String methodName, Reflective.TypedValue... parameters) { - return reflective().invokeMethod(wrappedObj(), methodName, parameters); - } - - public void release() { - invoke("release"); - } - - @Override - public void close() throws Exception { - release(); - } - - @Override - public boolean equals(Object o) { - if (this == o) - return true; - if (!(o instanceof ReflectiveObjectWrapper)) - return false; - ReflectiveObjectWrapper that = (ReflectiveObjectWrapper) o; - return Objects.equals(wrappedObj, that.wrappedObj) && Objects.equals(getClass(), that.getClass()); - } - - @Override - public int hashCode() { - return Objects.hash(wrappedObj, getClass()); - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java b/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java index 8650f086c4..846f7f1cf3 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,7 +15,7 @@ */ package com.diffplug.spotless.npm; -import static java.util.Objects.requireNonNull; +import static com.diffplug.spotless.npm.JsonEscaper.jsonEscape; import java.io.File; import java.io.IOException; @@ -53,8 +53,8 @@ SimpleJsonWriter put(String name, Object value) { private void verifyValues(Map<String, ?> values) { if (values.values() .stream() - .anyMatch(val -> !(val instanceof String || val instanceof Number || val instanceof Boolean))) { - throw new IllegalArgumentException("Only values of type 'String', 'Number' and 'Boolean' are supported. You provided: " + values.values()); + .anyMatch(val -> !(val instanceof String || val instanceof JsonRawValue || val instanceof Number || val instanceof Boolean))) { + throw new IllegalArgumentException("Only values of type 'String', 'JsonRawValue', 'Number' and 'Boolean' are supported. You provided: " + values.values()); } } @@ -66,12 +66,8 @@ String toJsonString() { return "{\n" + valueString + "\n}"; } - private String jsonEscape(Object val) { - requireNonNull(val); - if (val instanceof String) { - return "\"" + val + "\""; - } - return val.toString(); + JsonRawValue toJsonRawValue() { + return JsonRawValue.wrap(toJsonString()); } void toJsonFile(File file) { @@ -91,4 +87,5 @@ void toJsonFile(File file) { public String toString() { return this.toJsonString(); } + } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java b/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java new file mode 100644 index 0000000000..5958135d09 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/SimpleRestClient.java @@ -0,0 +1,142 @@ +/* + * Copyright 2016-2020 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.npm; + +import static java.util.Objects.requireNonNull; + +import java.io.BufferedInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.net.HttpURLConnection; +import java.net.URL; +import java.util.Map; + +import javax.annotation.Nonnull; + +class SimpleRestClient { + private final String baseUrl; + + private SimpleRestClient(String baseUrl) { + this.baseUrl = requireNonNull(baseUrl); + } + + static SimpleRestClient forBaseUrl(String baseUrl) { + return new SimpleRestClient(baseUrl); + } + + String postJson(String endpoint, Map<String, Object> jsonParams) throws SimpleRestException { + final SimpleJsonWriter jsonWriter = SimpleJsonWriter.of(jsonParams); + final String jsonString = jsonWriter.toJsonString(); + + return postJson(endpoint, jsonString); + } + + String post(String endpoint) throws SimpleRestException { + return postJson(endpoint, (String) null); + } + + String postJson(String endpoint, String rawJson) throws SimpleRestException { + try { + URL url = new URL(this.baseUrl + endpoint); + HttpURLConnection con = (HttpURLConnection) url.openConnection(); + con.setConnectTimeout(60 * 1000); // one minute + con.setReadTimeout(2 * 60 * 1000); // two minutes - who knows how large those files can actually get + con.setRequestMethod("POST"); + con.setRequestProperty("Content-Type", "application/json"); + con.setDoOutput(true); + if (rawJson != null) { + try (OutputStream out = con.getOutputStream()) { + NpmResourceHelper.writeUtf8StringToOutputStream(rawJson, out); + out.flush(); + } + } + + int status = con.getResponseCode(); + + if (status != 200) { + throw new SimpleRestResponseException(status, readError(con), "Unexpected response status code at " + endpoint); + } + + String response = readResponse(con); + return response; + } catch (IOException e) { + throw new SimpleRestIOException(e); + } + } + + private String readError(HttpURLConnection con) throws IOException { + return readInputStream(con.getErrorStream()); + } + + private String readResponse(HttpURLConnection con) throws IOException { + return readInputStream(con.getInputStream()); + } + + private String readInputStream(InputStream inputStream) throws IOException { + try (BufferedInputStream input = new BufferedInputStream(inputStream)) { + return NpmResourceHelper.readUtf8StringFromInputStream(input); + } + } + + static abstract class SimpleRestException extends RuntimeException { + public SimpleRestException() {} + + public SimpleRestException(Throwable cause) { + super(cause); + } + } + + static class SimpleRestResponseException extends SimpleRestException { + private final int statusCode; + + private final String responseMessage; + + private final String exceptionMessage; + + public SimpleRestResponseException(int statusCode, String responseMessage, String exceptionmessage) { + this.statusCode = statusCode; + this.responseMessage = responseMessage; + this.exceptionMessage = exceptionmessage; + } + + @Nonnull + public int getStatusCode() { + return statusCode; + } + + @Nonnull + public String getResponseMessage() { + return responseMessage; + } + + @Nonnull + public String getExceptionMessage() { + return exceptionMessage; + } + + @Override + public String getMessage() { + return String.format("%s [HTTP %s] -- (%s)", getExceptionMessage(), getStatusCode(), getResponseMessage()); + } + } + + static class SimpleRestIOException extends SimpleRestException { + public SimpleRestIOException(Throwable cause) { + super(cause); + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java index 8180ce589f..e815ea1f63 100644 --- a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -15,23 +15,28 @@ */ package com.diffplug.spotless.npm; -import static java.util.Arrays.asList; import static java.util.Objects.requireNonNull; import java.io.File; import java.io.IOException; import java.io.Serializable; import java.util.*; +import java.util.logging.Level; +import java.util.logging.Logger; import javax.annotation.Nonnull; import javax.annotation.Nullable; import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterFunc.Closeable; import com.diffplug.spotless.FormatterStep; import com.diffplug.spotless.Provisioner; import com.diffplug.spotless.ThrowingEx; public class TsFmtFormatterStep { + + private static final Logger logger = Logger.getLogger(TsFmtFormatterStep.class.getName()); + public static final String NAME = "tsfmt-format"; @Deprecated @@ -43,7 +48,7 @@ public static FormatterStep create(Map<String, String> versions, Provisioner pro requireNonNull(provisioner); requireNonNull(buildDir); return FormatterStep.createLazy(NAME, - () -> new State(NAME, versions, provisioner, buildDir, npm, configFile, inlineTsFmtSettings), + () -> new State(NAME, versions, buildDir, npm, configFile, inlineTsFmtSettings), State::createFormatterFunc); } @@ -54,14 +59,14 @@ public static Map<String, String> defaultDevDependencies() { public static Map<String, String> defaultDevDependenciesWithTsFmt(String typescriptFormatter) { TreeMap<String, String> defaults = new TreeMap<>(); defaults.put("typescript-formatter", typescriptFormatter); - defaults.put("typescript", "3.3.3"); - defaults.put("tslint", "5.12.1"); + defaults.put("typescript", "3.9.5"); + defaults.put("tslint", "6.1.2"); return defaults; } public static class State extends NpmFormatterStepStateBase implements Serializable { - private static final long serialVersionUID = -3811104513825329168L; + private static final long serialVersionUID = -3789035117345809383L; private final TreeMap<String, Object> inlineTsFmtSettings; @@ -71,16 +76,16 @@ public static class State extends NpmFormatterStepStateBase implements Serializa private final TypedTsFmtConfigFile configFile; @Deprecated - public State(String stepName, Provisioner provisioner, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException { - this(stepName, defaultDevDependencies(), provisioner, buildDir, npm, configFile, inlineTsFmtSettings); + public State(String stepName, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException { + this(stepName, defaultDevDependencies(), buildDir, npm, configFile, inlineTsFmtSettings); } - public State(String stepName, Map<String, String> versions, Provisioner provisioner, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException { + public State(String stepName, Map<String, String> versions, File buildDir, @Nullable File npm, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map<String, Object> inlineTsFmtSettings) throws IOException { super(stepName, - provisioner, new NpmConfig( - replaceDevDependencies(readFileFromClasspath(TsFmtFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-package.json"), new TreeMap<>(versions)), - "typescript-formatter"), + replaceDevDependencies(NpmResourceHelper.readUtf8StringFromClasspath(TsFmtFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-package.json"), new TreeMap<>(versions)), + "typescript-formatter", + NpmResourceHelper.readUtf8StringFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-serve.js")), buildDir, npm); this.buildDir = requireNonNull(buildDir); @@ -91,69 +96,14 @@ public State(String stepName, Map<String, String> versions, Provisioner provisio @Override @Nonnull public FormatterFunc createFormatterFunc() { - - Map<String, Object> tsFmtOptions = unifyOptions(); - - final NodeJSWrapper nodeJSWrapper = nodeJSWrapper(); - final V8ObjectWrapper tsFmt = nodeJSWrapper.require(nodeModulePath()); - final V8ObjectWrapper formatterOptions = nodeJSWrapper.createNewObject(tsFmtOptions); - - final TsFmtResult[] tsFmtResult = new TsFmtResult[1]; - final Exception[] toThrow = new Exception[1]; - - V8FunctionWrapper formatResultCallback = createFormatResultCallback(nodeJSWrapper, tsFmtResult, toThrow); - - /* var result = { - fileName: fileName, - settings: formatSettings, - message: message, <-- string - error: error, <-- boolean - src: content, - dest: formattedCode, <-- result + try { + Map<String, Object> tsFmtOptions = unifyOptions(); + ServerProcessInfo tsfmtRestServer = npmRunServer(); + TsFmtRestService restService = new TsFmtRestService(tsfmtRestServer.getBaseUrl()); + return Closeable.of(() -> endServer(restService, tsfmtRestServer), input -> restService.format(input, tsFmtOptions)); + } catch (Exception e) { + throw ThrowingEx.asRuntime(e); } - */ - return FormatterFunc.Closeable.of(() -> { - asList(formatResultCallback, formatterOptions, tsFmt, nodeJSWrapper).forEach(ReflectiveObjectWrapper::release); - }, input -> { - tsFmtResult[0] = null; - - // function processString(fileName: string, content: string, opts: Options): Promise<Result> { - - try ( - V8ArrayWrapper processStringArgs = nodeJSWrapper.createNewArray("spotless-format-string.ts", input, formatterOptions); - V8ObjectWrapper promise = tsFmt.executeObjectFunction("processString", processStringArgs); - V8ArrayWrapper callbacks = nodeJSWrapper.createNewArray(formatResultCallback)) { - - promise.executeVoidFunction("then", callbacks); - - while (tsFmtResult[0] == null && toThrow[0] == null) { - nodeJSWrapper.handleMessage(); - } - - if (toThrow[0] != null) { - throw ThrowingEx.asRuntime(toThrow[0]); - } - - if (tsFmtResult[0] == null) { - throw new IllegalStateException("should never happen"); - } - if (tsFmtResult[0].isError()) { - throw new RuntimeException(tsFmtResult[0].getMessage()); - } - return tsFmtResult[0].getFormatted(); - } - }); - } - - private V8FunctionWrapper createFormatResultCallback(NodeJSWrapper nodeJSWrapper, TsFmtResult[] outputTsFmtResult, Exception[] toThrow) { - return nodeJSWrapper.createNewFunction((receiver, parameters) -> { - try (final V8ObjectWrapper result = parameters.getObject(0)) { - outputTsFmtResult[0] = new TsFmtResult(result.getString("message"), result.getBoolean("error"), result.getString("dest")); - } catch (Exception e) { - toThrow[0] = e; - } - return receiver; - }); } private Map<String, Object> unifyOptions() { @@ -169,5 +119,14 @@ private Map<String, Object> unifyOptions() { } return unified; } + + private void endServer(TsFmtRestService restService, ServerProcessInfo restServer) throws Exception { + try { + restService.shutdown(); + } catch (Throwable t) { + logger.log(Level.INFO, "Failed to request shutdown of rest service via api. Trying via process.", t); + } + restServer.close(); + } } } diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java new file mode 100644 index 0000000000..a47b608c36 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtRestService.java @@ -0,0 +1,43 @@ +/* + * Copyright 2016-2020 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.npm; + +import java.util.LinkedHashMap; +import java.util.Map; + +public class TsFmtRestService { + + private final SimpleRestClient restClient; + + TsFmtRestService(String baseUrl) { + this.restClient = SimpleRestClient.forBaseUrl(baseUrl); + } + + public String format(String fileContent, Map<String, Object> configOptions) { + Map<String, Object> jsonProperties = new LinkedHashMap<>(); + jsonProperties.put("file_content", fileContent); + if (configOptions != null && !configOptions.isEmpty()) { + jsonProperties.put("config_options", SimpleJsonWriter.of(configOptions).toJsonRawValue()); + } + + return restClient.postJson("/tsfmt/format", jsonProperties); + } + + public String shutdown() { + return restClient.post("/shutdown"); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java deleted file mode 100644 index 0d26df0b64..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java +++ /dev/null @@ -1,51 +0,0 @@ -/* - * Copyright 2016 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.npm; - -public class V8ArrayWrapper extends ReflectiveObjectWrapper { - - public static final String WRAPPED_CLASS = "com.eclipsesource.v8.V8Array"; - - public V8ArrayWrapper(Reflective reflective, Object v8Array) { - super(reflective, v8Array); - } - - public V8ArrayWrapper push(Object object) { - if (object instanceof ReflectiveObjectWrapper) { - ReflectiveObjectWrapper objectWrapper = (ReflectiveObjectWrapper) object; - object = objectWrapper.wrappedObj(); - } - if (reflective().clazz(NodeJSWrapper.V8_VALUE_CLASS).isAssignableFrom(object.getClass())) { - invoke("push", reflective().typed(NodeJSWrapper.V8_VALUE_CLASS, object)); - } else { - invoke("push", object); - } - return this; - } - - public V8ArrayWrapper pushNull() { - invoke("pushNull"); - return this; - } - - public V8ObjectWrapper getObject(Integer index) { - Object v8Object = invoke("getObject", index); - if (v8Object == null) { - return null; - } - return new V8ObjectWrapper(this.reflective(), v8Object); - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8FunctionWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8FunctionWrapper.java deleted file mode 100644 index 36a9804854..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/V8FunctionWrapper.java +++ /dev/null @@ -1,58 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import java.lang.reflect.Method; - -class V8FunctionWrapper extends ReflectiveObjectWrapper { - - public static final String WRAPPED_CLASS = "com.eclipsesource.v8.V8Function"; - public static final String CALLBACK_WRAPPED_CLASS = "com.eclipsesource.v8.JavaCallback"; - - public V8FunctionWrapper(Reflective reflective, Object v8Function) { - super(reflective, v8Function); - } - - public static Object proxiedCallback(WrappedJavaCallback callback, Reflective reflective) { - Object proxy = reflective.createDynamicProxy((proxyInstance, method, args) -> { - if (isCallbackFunction(reflective, method, args)) { - V8ObjectWrapper receiver = new V8ObjectWrapper(reflective, args[0]); - V8ArrayWrapper parameters = new V8ArrayWrapper(reflective, args[1]); - return callback.invoke(receiver, parameters); - } - return null; - }, CALLBACK_WRAPPED_CLASS); - return reflective.clazz(CALLBACK_WRAPPED_CLASS).cast(proxy); - } - - private static boolean isCallbackFunction(Reflective reflective, Method method, Object[] args) { - if (!"invoke".equals(method.getName())) { - return false; - } - final Class<?>[] types = reflective.types(args); - if (types.length != 2) { - return false; - } - - return V8ObjectWrapper.WRAPPED_CLASS.equals(types[0].getName()) && - V8ArrayWrapper.WRAPPED_CLASS.equals(types[1].getName()); - } - - @FunctionalInterface - public interface WrappedJavaCallback { - Object invoke(V8ObjectWrapper receiver, V8ArrayWrapper parameters); - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectUtilsWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectUtilsWrapper.java deleted file mode 100644 index af7448c748..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectUtilsWrapper.java +++ /dev/null @@ -1,35 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import static java.util.Objects.requireNonNull; - -import java.util.Map; - -class V8ObjectUtilsWrapper { - - public static final String WRAPPED_CLASS = "com.eclipsesource.v8.utils.V8ObjectUtils"; - - public static Map<String, ? super Object> toMap(final V8ObjectWrapper object) { - requireNonNull(object); - - final Reflective reflective = object.reflective(); - - @SuppressWarnings("unchecked") - final Map<String, ? super Object> map = (Map<String, ? super Object>) reflective.invokeStaticMethod(WRAPPED_CLASS, "toMap", object.wrappedObj()); - return map; - } -} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectWrapper.java deleted file mode 100644 index 917e25c222..0000000000 --- a/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectWrapper.java +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import java.util.Optional; - -class V8ObjectWrapper extends ReflectiveObjectWrapper { - - public static final String WRAPPED_CLASS = "com.eclipsesource.v8.V8Object"; - - public V8ObjectWrapper(Reflective reflective, Object v8Object) { - super(reflective, v8Object); - } - - public V8ObjectWrapper add(String name, Object value) { - invoke("add", name, value); - return this; - } - - public void executeVoidFunction(String functionName, V8ArrayWrapper params) { - invoke("executeVoidFunction", functionName, params.wrappedObj()); - } - - public V8ObjectWrapper executeObjectFunction(String functionName, V8ArrayWrapper params) { - Object returnV8Obj = invoke("executeObjectFunction", functionName, params.wrappedObj()); - return new V8ObjectWrapper(reflective(), returnV8Obj); - } - - public String executeStringFunction(String functionName, V8ArrayWrapper params) { - String returnValue = (String) invoke("executeStringFunction", functionName, params.wrappedObj()); - return returnValue; - } - - public String getString(String name) { - return (String) invoke("getString", name); - } - - public Optional<String> getOptionalString(String name) { - String result = null; - try { - result = getString(name); - } catch (RuntimeException e) { - // ignore - } - return Optional.ofNullable(result); - } - - public boolean getBoolean(String name) { - return (boolean) invoke("getBoolean", name); - } - - public Optional<Boolean> getOptionalBoolean(String name) { - Boolean result = null; - try { - result = getBoolean(name); - } catch (RuntimeException e) { - // ignore - } - return Optional.ofNullable(result); - } - - public int getInteger(String name) { - return (int) invoke("getInteger", name); - } - - public Optional<Integer> getOptionalInteger(String name) { - Integer result = null; - try { - result = getInteger(name); - } catch (RuntimeException e) { - // ignore - } - return Optional.ofNullable(result); - } - -} diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json index c46cf46a3d..113f20bc3f 100644 --- a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json +++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json @@ -1,8 +1,16 @@ { "name": "spotless-prettier-formatter-step", - "version": "1.0.0", + "version": "2.0.0", + "description": "Spotless formatter step for running prettier as a rest service.", + "repository": "https://github.com/diffplug/spotless", + "license": "Apache-2.0", + "scripts": { + "start": "node serve.js" + }, "devDependencies": { -${devDependencies} +${devDependencies}, + "express": "4.17.1", + "@moebius/http-graceful-shutdown": "1.1.0" }, "dependencies": {}, "engines": { diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js new file mode 100644 index 0000000000..351ef73f9a --- /dev/null +++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-serve.js @@ -0,0 +1,101 @@ +const GracefulShutdownManager = require("@moebius/http-graceful-shutdown").GracefulShutdownManager; +const express = require("express"); +const app = express(); + +app.use(express.json({ limit: "50mb" })); +const prettier = require("prettier"); + +const fs = require("fs"); + +var listener = app.listen(0, "127.0.0.1", () => { + console.log("Server running on port " + listener.address().port); + fs.writeFile("server.port.tmp", "" + listener.address().port, function(err) { + if (err) { + return console.log(err); + } else { + fs.rename("server.port.tmp", "server.port", function(err) { + if (err) { + return console.log(err); + } + }); // try to be as atomic as possible + } + }); +}); +const shutdownManager = new GracefulShutdownManager(listener); + +app.post("/shutdown", (req, res) => { + res.status(200).send("Shutting down"); + setTimeout(function() { + shutdownManager.terminate(() => console.log("graceful shutdown finished.")); + }, 200); +}); + +app.post("/prettier/config-options", (req, res) => { + var config_data = req.body; + var prettier_config_path = config_data.prettier_config_path; + var prettier_config_options = config_data.prettier_config_options || {}; + + if (prettier_config_path) { + prettier + .resolveConfig(undefined, { config: prettier_config_path }) + .then(options => { + var mergedConfigOptions = mergeConfigOptions(options, prettier_config_options); + res.set("Content-Type", "application/json") + res.json(mergedConfigOptions); + }) + .catch(reason => res.status(501).send("Exception while resolving config_file_path: " + reason)); + return; + } + res.set("Content-Type", "application/json") + res.json(prettier_config_options); +}); + +app.post("/prettier/format", (req, res) => { + var format_data = req.body; + + var formatted_file_content = ""; + try { + formatted_file_content = prettier.format(format_data.file_content, format_data.config_options); + } catch(err) { + res.status(501).send("Error while formatting: " + err); + return; + } + res.set("Content-Type", "text/plain"); + res.send(formatted_file_content); +}); + +var mergeConfigOptions = function(resolved_config_options, config_options) { + if (resolved_config_options !== undefined && config_options !== undefined) { + return extend(resolved_config_options, config_options); + } + if (resolved_config_options === undefined) { + return config_options; + } + if (config_options === undefined) { + return resolved_config_options; + } +}; + +var extend = function() { + // Variables + var extended = {}; + var i = 0; + var length = arguments.length; + + // Merge the object into the extended object + var merge = function(obj) { + for (var prop in obj) { + if (Object.prototype.hasOwnProperty.call(obj, prop)) { + extended[prop] = obj[prop]; + } + } + }; + + // Loop through each object and conduct a merge + for (; i < length; i++) { + var obj = arguments[i]; + merge(obj); + } + + return extended; +}; diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json index 5fddb1105f..d6e5eff3b2 100644 --- a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json +++ b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json @@ -1,8 +1,16 @@ { "name": "spotless-tsfmt-formatter-step", - "version": "1.0.0", + "version": "2.0.0", + "description": "Spotless formatter step for running tsfmt as a rest service.", + "repository": "https://github.com/diffplug/spotless", + "license": "Apache-2.0", + "scripts": { + "start": "node serve.js" + }, "devDependencies": { -${devDependencies} +${devDependencies}, + "express": "4.17.1", + "@moebius/http-graceful-shutdown": "1.1.0" }, "dependencies": {}, "engines": { diff --git a/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js new file mode 100644 index 0000000000..b25048d410 --- /dev/null +++ b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-serve.js @@ -0,0 +1,61 @@ +const GracefulShutdownManager = require("@moebius/http-graceful-shutdown").GracefulShutdownManager; +const express = require("express"); +const app = express(); +app.use(express.json({ limit: "50mb" })); + +const tsfmt = require("typescript-formatter"); + +const fs = require("fs"); + +var listener = app.listen(0, "127.0.0.1", () => { + console.log("Server running on port " + listener.address().port); + fs.writeFile("server.port.tmp", "" + listener.address().port, function(err) { + if (err) { + return console.log(err); + } else { + fs.rename("server.port.tmp", "server.port", function(err) { + if (err) { + return console.log(err); + } + }); // try to be as atomic as possible + } + }); +}); + +const shutdownManager = new GracefulShutdownManager(listener); + +app.post("/shutdown", (req, res) => { + res.status(200).send("Shutting down"); + setTimeout(function() { + shutdownManager.terminate(() => console.log("graceful shutdown finished.")); + }, 200); +}); + +app.post("/tsfmt/format", (req, res) => { + var format_data = req.body; + tsfmt.processString("spotless-format-string.ts", format_data.file_content, format_data.config_options).then(resultMap => { + /* + export interface ResultMap { + [fileName: string]: Result; + } + + export interface Result { + fileName: string; + settings: ts.FormatCodeSettings | null; + message: string; + error: boolean; + src: string; + dest: string; + } + */ + // result contains 'message' (String), 'error' (boolean), 'dest' (String) => formatted + if (resultMap.error !== undefined && resultMap.error) { + res.status(400).send(resultmap.message); + return; + } + res.set("Content-Type", "text/plain"); + res.send(resultMap.dest); + }).catch(reason => { + res.status(501).send(reason); + }); +}); diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index b1823ccd0f..c1dc8d12e8 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Changed +* Nodejs-based formatters `prettier` and `tsfmt` now use native node instead of the J2V8 approach. ([#606](https://github.com/diffplug/spotless/pull/606)) + * This removes the dependency to the no-longer-maintained Linux/Windows/macOs variants of J2V8. + * This enables spotless to use the latest `prettier` versions (instead of being stuck at prettier version <= `1.19.0`) + * Bumped default versions, prettier `1.16.4` -> `2.0.5`, tslint `5.12.1` -> `6.1.2` ## [4.3.0] - 2020-06-05 ### Deprecated diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 700b5c7822..d5acee50c6 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -138,13 +138,13 @@ See [ECLIPSE_SCREENSHOTS](../ECLIPSE_SCREENSHOTS.md) for screenshots that demons <a name="android"></a> ### Applying to Android Java source -Be sure to add `target '**/*.java'` otherwise spotless will not detect Java code inside Android modules. +Be sure to add `target 'src/*/java/**/*.java'` otherwise spotless will not detect Java code inside Android modules. ```gradle spotless { java { // ... - target '**/*.java' + target 'src/*/java/**/*.java' // ... } } @@ -409,7 +409,7 @@ spotless { } ``` -Spotless uses npm to install necessary packages locally. It runs tsfmt using [J2V8](https://github.com/eclipsesource/J2V8) internally after that. +Spotless uses npm to install necessary packages and run the `typescript-formatter` (`tsfmt`) package. <a name="prettier"></a> @@ -422,10 +422,10 @@ To use prettier, you first have to specify the files that you want it to apply t ```gradle spotless { format 'styling', { - target '**/*.css', '**/*.scss' + target 'src/*/webapp/**/*.css', 'src/*/webapp/**/*.scss', 'app/**/*.css', 'app/**/*.scss' // at least provide the parser to use - prettier().config(['parser': 'postcss']) + prettier().config(['parser': 'css']) // prettier('1.16.4') to specify specific version of prettier // prettier(['my-prettier-fork': '1.16.4']) to specify exactly which npm packages to use @@ -442,12 +442,10 @@ It is also possible to specify the config via file: ```gradle spotless { format 'styling', { - target '**/*.css', '**/*.scss' - + target 'src/*/webapp/**/*.css', 'src/*/webapp/**/*.scss', 'app/**/*.css', 'app/**/*.scss' prettier().configFile('/path-to/.prettierrc.yml') - // or provide both (config options take precedence over configFile options) - prettier().config(['parser': 'postcss']).configFile('path-to/.prettierrc.yml') + prettier().config(['parser': 'css']).configFile('path-to/.prettierrc.yml') } } ``` @@ -468,8 +466,27 @@ spotless { } ``` +<a name="prettier-plugins"></a> +### Using plugins for prettier + +Since spotless uses the actual npm prettier package behind the scenes, it is possible to use prettier with +[plugins](https://prettier.io/docs/en/plugins.html#official-plugins) or [community-plugins](https://www.npmjs.com/search?q=prettier-plugin) in order to support even more file types. + +```gradle +spotless { + format 'prettierJava', { + target 'src/*/java/**/*.java' + prettier(['prettier': '2.0.5', 'prettier-plugin-java': '0.8.0']).config(['parser': 'java', 'tabWidth': 4]) + } + format 'php', { + target 'src/**/*.php' + prettier(['prettier': '2.0.5', '@prettier/plugin-php': '0.14.2']).config(['parser': 'php', 'tabWidth': 3]) + } +} +``` + <a name="typescript-prettier"></a> -Prettier can also be applied from within the [typescript config block](#typescript-formatter): +### Note: Prettier can also be applied from within the [typescript config block](#typescript-formatter) ```gradle spotless { @@ -494,7 +511,7 @@ spotless { } ``` -Spotless uses npm to install necessary packages locally. It runs prettier using [J2V8](https://github.com/eclipsesource/J2V8) internally after that. +Spotless uses npm to install necessary packages and run the `prettier` (`tsfmt`) package. <a name="eclipse-wtp"></a> diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java index 7cbc45ffb7..35ad80ed3f 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtensionBase.java @@ -17,7 +17,6 @@ import static java.util.Objects.requireNonNull; -import java.io.File; import java.lang.reflect.Constructor; import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; @@ -32,7 +31,6 @@ import com.diffplug.common.base.Errors; import com.diffplug.spotless.LineEnding; -import com.diffplug.spotless.npm.NodeJsGlobal; public abstract class SpotlessExtensionBase { final Project project; @@ -54,8 +52,6 @@ public SpotlessExtensionBase(Project project) { if (registerDependenciesTask == null) { registerDependenciesTask = project.getRootProject().getTasks().create(RegisterDependenciesTask.TASK_NAME, RegisterDependenciesTask.class); registerDependenciesTask.setup(); - // set where the nodejs runtime will put its temp dlls - NodeJsGlobal.setSharedLibFolder(new File(project.getBuildDir(), "spotless-nodejs-cache")); } this.registerDependenciesTask = registerDependenciesTask; } diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java index a371761a53..a3955b476e 100644 --- a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java @@ -17,6 +17,8 @@ import java.io.IOException; +import org.assertj.core.api.Assertions; +import org.gradle.testkit.runner.BuildResult; import org.junit.Test; import org.junit.experimental.categories.Category; @@ -41,7 +43,8 @@ public void useInlineConfig() throws IOException { " }", "}"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); - gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean"); } @@ -60,8 +63,58 @@ public void useFileConfig() throws IOException { " }", "}"); setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); - gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean"); } + @Test + public void useJavaCommunityPlugin() throws IOException { + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "def prettierConfig = [:]", + "prettierConfig['tabWidth'] = 4", + "prettierConfig['parser'] = 'java'", + "def prettierPackages = [:]", + "prettierPackages['prettier'] = '2.0.5'", + "prettierPackages['prettier-plugin-java'] = '0.8.0'", + "spotless {", + " format 'java', {", + " target 'JavaTest.java'", + " prettier(prettierPackages).config(prettierConfig)", + " }", + "}"); + setFile("JavaTest.java").toResource("npm/prettier/plugins/java-test.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("JavaTest.java").sameAsResource("npm/prettier/plugins/java-test.clean"); + } + + @Test + public void usePhpCommunityPlugin() throws IOException { + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "def prettierConfig = [:]", + "prettierConfig['tabWidth'] = 3", + "prettierConfig['parser'] = 'php'", + "def prettierPackages = [:]", + "prettierPackages['prettier'] = '2.0.5'", + "prettierPackages['@prettier/plugin-php'] = '0.14.2'", + "spotless {", + " format 'php', {", + " target 'php-example.php'", + " prettier(prettierPackages).config(prettierConfig)", + " }", + "}"); + setFile("php-example.php").toResource("npm/prettier/plugins/php.dirty"); + final BuildResult spotlessApply = gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + Assertions.assertThat(spotlessApply.getOutput()).contains("BUILD SUCCESSFUL"); + assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); + } } diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index e2874d243b..258013a93b 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,11 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Changed +* Nodejs-based formatters `prettier` and `tsfmt` now use native node instead of the J2V8 approach. ([#606](https://github.com/diffplug/spotless/pull/606)) + * This removes the dependency to the no-longer-maintained Linux/Windows/macOs variants of J2V8. + * This enables spotless to use the latest `prettier` versions (instead of being stuck at prettier version <= `1.19.0`) + * Bumped default versions, prettier `1.16.4` -> `2.0.5`, tslint `5.12.1` -> `6.1.2` ### Fixed * `licenseHeader` is now more robust when parsing years from existing license headers. ([#593](https://github.com/diffplug/spotless/pull/593)) diff --git a/plugin-maven/README.md b/plugin-maven/README.md index 65223f648e..8e2e6dbab1 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -287,13 +287,21 @@ To use prettier, you first have to specify the files that you want it to apply t </includes> <prettier> - <!-- Specify either simple prettier version (1.19.0 is max supported, - which is also default) or whole devDependencies --> + <!-- Specify at most one of the following 3 configs: either 'prettierVersion' (2.0.5 is default), 'devDependencies' or 'devDependencyProperties' --> <prettierVersion>1.19.0</prettierVersion> <devDependencies> <prettier>1.19.0</prettier> </devDependencies> - + <devDependencyProperties> + <property> + <name>prettier</name> + <value>2.0.5</value> + </property> + <property> + <name>@prettier/plugin-php</name> <!-- this could not be written in the simpler to write 'devDependencies' element. --> + <value>0.14.2</value> + </property> + </devDependencyProperties> <!-- Specify config file and/or inline config --> <configFile>${basedir}/path/to/configfile</configFile> <config> @@ -315,6 +323,62 @@ Supported config file variants are documented on [prettier.io](https://prettier. To apply prettier to more kinds of files, just add more formats. +<a name="prettier-plugins"></a> +### Using plugins for prettier + +Since spotless uses the actual npm prettier package behind the scenes, it is possible to use prettier with +[plugins](https://prettier.io/docs/en/plugins.html#official-plugins) or [community-plugins](https://www.npmjs.com/search?q=prettier-plugin) in order to support even more file types. + +```xml +<configuration> + <formats> + <!-- prettier with java-plugin --> + <format> + <includes> + <include>src/*/java/**/*.java</include> + </includes> + + <prettier> + <devDependencies> + <prettier>2.0.5</prettier> + <prettier-plugin-java>0.8.0</prettier-plugin-java> + </devDependencies> + <config> + <tabWidth>4</tabWidth> + <parser>java</parser> + </config> + </prettier> + </format> + + <!-- prettier with php-plugin --> + <format> + <includes> + <include>src/**/*.php</include> + </includes> + + <prettier> + <!-- use the devDependencyProperties writing style when the property-names are not well-formed such as @prettier/plugin-php --> + <devDependencyProperties> + <property> + <name>prettier</name> + <value>2.0.5</value> + </property> + <property> + <name>@prettier/plugin-php</name> + <value>0.14.2</value> + </property> + </devDependencyProperties> + <config> + <tabWidth>3</tabWidth> + <parser>php</parser> + </config> + </prettier> + </format> + + </formats> +</configuration> +``` + ### Prerequisite: prettier requires a working NodeJS version Prettier, like tsfmt, is based on NodeJS, so to use it, a working NodeJS installation (especially npm) is required on the host running spotless. @@ -326,9 +390,7 @@ Spotless will try to auto-discover an npm installation. If that is not working f ... ``` -Spotless uses npm to install necessary packages locally. It runs prettier using [J2V8](https://github.com/eclipsesource/J2V8) internally after that. -Development for J2V8 for non android envs is stopped (for Windows since J2V8 4.6.0 and Unix 4.8.0), therefore Prettier is limited to <= v1.19.0 as newer versions -use ES6 feature and that needs a newer J2V8 version. +Spotless uses npm to install necessary packages and to run the prettier formatter after that. <a name="format"></a> diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java index 7c2e12eea5..cfd31d9294 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/generic/Prettier.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -16,8 +16,8 @@ package com.diffplug.spotless.maven.generic; import java.io.File; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; +import java.util.stream.Collectors; import org.apache.maven.plugins.annotations.Parameter; @@ -29,12 +29,17 @@ public class Prettier implements FormatterStepFactory { + public static final String ERROR_MESSAGE_ONLY_ONE_CONFIG = "must specify exactly one prettierVersion, devDependencies or devDependencyProperties"; + @Parameter private String prettierVersion; @Parameter private Map<String, String> devDependencies; + @Parameter + private Properties devDependencyProperties; + @Parameter private Map<String, String> config; @@ -48,17 +53,17 @@ public class Prettier implements FormatterStepFactory { public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { // check if config is only setup in one way - if (this.prettierVersion != null && this.devDependencies != null) { + if (moreThanOneNonNull(this.prettierVersion, this.devDependencies, this.devDependencyProperties)) { throw onlyOneConfig(); } - - // set dev dependencies if (devDependencies == null) { - if (prettierVersion == null || prettierVersion.isEmpty()) { - devDependencies = PrettierFormatterStep.defaultDevDependencies(); - } else { - devDependencies = PrettierFormatterStep.defaultDevDependenciesWithPrettier(prettierVersion); - } + devDependencies = PrettierFormatterStep.defaultDevDependencies(); // fallback + } + + if (prettierVersion != null && !prettierVersion.isEmpty()) { + this.devDependencies = PrettierFormatterStep.defaultDevDependenciesWithPrettier(prettierVersion); + } else if (devDependencyProperties != null) { + this.devDependencies = dependencyPropertiesAsMap(); } File npm = npmExecutable != null ? stepConfig.getFileLocator().locateFile(npmExecutable) : null; @@ -73,19 +78,20 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { Map<String, Object> configInline; if (config != null) { - configInline = new LinkedHashMap<>(); - // try to parse string values as integers or booleans - for (Map.Entry<String, String> e : config.entrySet()) { - try { - configInline.put(e.getKey(), Integer.parseInt(e.getValue())); - } catch (NumberFormatException ignore) { - try { - configInline.put(e.getKey(), Boolean.parseBoolean(e.getValue())); - } catch (IllegalArgumentException ignore2) { - configInline.put(e.getKey(), e.getValue()); - } - } - } + configInline = config.entrySet().stream() + .map(entry -> { + try { + Integer value = Integer.parseInt(entry.getValue()); + return new AbstractMap.SimpleEntry<>(entry.getKey(), value); + } catch (NumberFormatException ignore) { + // ignored + } + if (Boolean.TRUE.toString().equalsIgnoreCase(entry.getValue()) || Boolean.FALSE.toString().equalsIgnoreCase(entry.getValue())) { + return new AbstractMap.SimpleEntry<>(entry.getKey(), Boolean.parseBoolean(entry.getValue())); + } + return entry; + }) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) -> a, LinkedHashMap::new)); } else { configInline = null; } @@ -96,7 +102,21 @@ public FormatterStep newFormatterStep(FormatterStepConfig stepConfig) { return PrettierFormatterStep.create(devDependencies, stepConfig.getProvisioner(), buildDir, npm, prettierConfig); } + private boolean moreThanOneNonNull(Object... objects) { + return Arrays.stream(objects) + .filter(Objects::nonNull) + .filter(o -> !(o instanceof String) || !((String) o).isEmpty()) // if it is a string, it should not be empty + .count() > 1; + } + + private Map<String, String> dependencyPropertiesAsMap() { + return this.devDependencyProperties.stringPropertyNames() + .stream() + .map(name -> new AbstractMap.SimpleEntry<>(name, this.devDependencyProperties.getProperty(name))) + .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue)); + } + private static IllegalArgumentException onlyOneConfig() { - return new IllegalArgumentException("must specify exactly one configFile or config"); + return new IllegalArgumentException(ERROR_MESSAGE_ONLY_ONE_CONFIG); } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java index 772080ea31..971c04a343 100644 --- a/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/prettier/PrettierFormatStepTest.java @@ -25,6 +25,7 @@ import com.diffplug.spotless.category.NpmTest; import com.diffplug.spotless.maven.MavenIntegrationHarness; import com.diffplug.spotless.maven.MavenRunner; +import com.diffplug.spotless.maven.generic.Prettier; @Category(NpmTest.class) public class PrettierFormatStepTest extends MavenIntegrationHarness { @@ -93,6 +94,32 @@ public void unique_dependency_config() throws Exception { "</prettier>"); MavenRunner.Result result = mavenRunner().withArguments("spotless:apply").runHasError(); - assertThat(result.output()).contains("must specify exactly one configFile or config"); + assertThat(result.output()).contains(Prettier.ERROR_MESSAGE_ONLY_ONE_CONFIG); + } + + @Test + public void custom_plugin() throws Exception { + writePomWithFormatSteps( + "<includes><include>php-example.php</include></includes>", + "<prettier>", + " <devDependencyProperties>", + " <property>", + " <name>prettier</name>", + " <value>2.0.5</value>", + " </property>", + " <property>", + " <name>@prettier/plugin-php</name>", + " <value>0.14.2</value>", + " </property>", + " </devDependencyProperties>", + " <config>", + " <tabWidth>3</tabWidth>", + " <parser>php</parser>", + " </config>", + "</prettier>"); + + setFile("php-example.php").toResource("npm/prettier/plugins/php.dirty"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile("php-example.php").sameAsResource("npm/prettier/plugins/php.clean"); } } diff --git a/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml index 9919fdc796..1141920e4e 100644 --- a/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml +++ b/testlib/src/main/resources/npm/prettier/filetypes/css/.prettierrc.yml @@ -1 +1 @@ -parser: postcss +parser: css diff --git a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean index 86cbfca970..29002f6b05 100644 --- a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean +++ b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es5/javascript-es5.clean @@ -18,7 +18,7 @@ var numbers = [ 17, 18, 19, - 20 + 20, ]; var p = { @@ -26,7 +26,7 @@ var p = { last: "Pan", get fullName() { return this.first + " " + this.last; - } + }, }; var str = "Hello, world!"; diff --git a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean index 6737291a0a..d4e982d69f 100644 --- a/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean +++ b/testlib/src/main/resources/npm/prettier/filetypes/javascript-es6/javascript-es6.clean @@ -18,7 +18,7 @@ var numbers = [ 17, 18, 19, - 20 + 20, ]; const p = { @@ -26,7 +26,7 @@ const p = { last: "Pan", get fullName() { return this.first + " " + this.last; - } + }, }; const str = "Hello, world!"; diff --git a/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml index 9919fdc796..1141920e4e 100644 --- a/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml +++ b/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml @@ -1 +1 @@ -parser: postcss +parser: css diff --git a/testlib/src/main/resources/npm/prettier/plugins/java-test.clean b/testlib/src/main/resources/npm/prettier/plugins/java-test.clean new file mode 100644 index 0000000000..c45ffe5183 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/plugins/java-test.clean @@ -0,0 +1,37 @@ +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.function.Consumer; + +public class JavaTest { + private static final String NAME = "JavaTest"; + + private List<String> strings = new ArrayList<>(); + + public JavaTest(String... strings) { + this.strings.addAll(Arrays.asList(strings)); + } + + /** + * Join using char. + * @param joiner the char to use for joining. + * @return the joined string. + */ + public String join(char joiner) { + return String.join(joiner, strings); + } + + public void operateOn(Consumer<List<String>> consumer) { + // test comment + consumer.accept(strings); + } + + public static void main(String[] args) { + JavaTest javaTest = new JavaTest("1", "2", "3"); + System.out.println("joined: " + javaTest.join(',')); + StringBuilder builder = new StringBuilder(); + javaTest.operateOn( + strings -> builder.append(String.join("---", strings)) + ); + } +} diff --git a/testlib/src/main/resources/npm/prettier/plugins/java-test.dirty b/testlib/src/main/resources/npm/prettier/plugins/java-test.dirty new file mode 100644 index 0000000000..ebadbfe112 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/plugins/java-test.dirty @@ -0,0 +1,30 @@ +import java.util.ArrayList; + + +import java.util.Arrays;import java.util.List; +import java.util.function.Consumer; + +public class JavaTest { private static final String NAME = "JavaTest"; + + private List<String> strings = new ArrayList<>(); + + public JavaTest( String ... strings) { + this.strings .addAll(Arrays.asList(strings));; + } + +/** +* Join using char. +* @param joiner the char to use for joining. +* @return the joined string. +*/ + public String + join(char joiner ) { + return String.join(joiner, strings); + } + + public void operateOn( Consumer<List<String>> consumer) { +// test comment + consumer.accept( strings); + } + + public static void main(String[] args) {JavaTest javaTest = new JavaTest("1", "2", "3");System.out.println("joined: " + javaTest.join(',' ));StringBuilder builder = new StringBuilder();javaTest.operateOn((strings) -> builder.append(String.join("---", strings)));}} \ No newline at end of file diff --git a/testlib/src/main/resources/npm/prettier/plugins/php.clean b/testlib/src/main/resources/npm/prettier/plugins/php.clean new file mode 100644 index 0000000000..4c8710bbee --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/plugins/php.clean @@ -0,0 +1,500 @@ +<?php + +class Foo extends Bar implements Baz, Buzz +{ + public $test; + + function test() + { + return "test"; + } + + public function &passByReferenceTest() + { + $a = 1; + return $a; + } +} + +$test = new Foo(); + +abstract class ReallyReallyReallyLongClassName + extends AbstractModelFactoryResourceController + implements + TooMuchObjectOrientation, + ThisIsMadness +{ + // variable doc + public $test; + public $other = 1; + public static $staticTest = ['hi']; + static $cache; + protected static $_instance; + protected $fillable = ['title', 'requester_id', 'type', 'summary', 'proof']; + protected $fillable2 = [ + 'title', + 'description', + 'requester_id', + 'type', + 'summary', + 'proof', + ]; + protected $test = [ + //test + ]; + + /** + * test constructor + * + * @param \Test\Foo\Bar $test hi + * @param int $test_int test + * @param string $test_string test + * + * @return \Some\Test + */ + public function __construct($test, $test_int = null, $test_string = 'hi') + { + parent::__construct($test_int ?: 1); + $this->other = $test_string; + $this->current_version = $current_version ?: new Content_Version_Model(); + self::$staticTest = $test_int; + } + + public static function test_static_constructor( + $test, + $test_int, + $test_string + ) { + $model = new self($test, $test_int, $test_string); + $model = new self( + $really_really_really_really_really_really_really_really_long_array, + $test_int, + $test_string + ); + return $model; + } + + public function test_pass_by_reference(&$test) + { + $test + 1; + } + + /** + * This is a function + */ + private function hi($input) + { + $test = 1; + + //testing line spacing + $other_test = 2; + + $one_more_test = 3; + return $input . $this->test; + } + + public function reallyReallyReallyReallyReallyReallyReallyLongMethodName( + $input, + $otherInput = 1 + ) { + return true; + } + + // doc test + public static function testStaticFunction($input) + { + return self::$staticTest[0]; + } + + public function returnTypeTest(): string + { + return 'hi'; + } + + final public static function bar() + { + // Nothing + } + + abstract protected function zim(); + + public function method(iterable $iterable): array + { + // Parameter broadened and return type narrowed. + } + + public function method1() + { + return 'hi'; + } + + public function method2() + { + return 'hi'; + } + + public function method3() + { + return 'hi'; + } + + public function testReturn(?string $name): ?string + { + return $name; + } + + public function swap(&$left, &$right): void + { + if ($left === $right) { + return; + } + + $tmp = $left; + $left = $right; + $right = $tmp; + } + + public function test(object $obj): object + { + return new SplQueue(); + } + + public function longLongAnotherFunction( + string $foo, + string $bar, + int $baz + ): string { + return 'foo'; + } + + public function longLongAnotherFunctionOther( + string $foo, + string $bar, + int $baz + ) { + return 'foo'; + } + + public function testReturnTypeDeclaration(): object + { + return new SplQueue(); + } + + public function testReturnTypeDeclarationOther(): object + { + return new SplQueue(); + } +} + +$this->something->method( + $argument, + $this->more->stuff($this->even->more->things->complicatedMethod()) +); + +class A +{ +} + +$someVar = new ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongClassName(); + +class ClassName extends ParentClass implements \ArrayAccess, \Countable +{ + // constants, properties, methods +} + +class FooBar +{ + public $property; + public $property2; + public function method() + { + } + public function method2() + { + } +} + +class FooBarFoo +{ + public function fooBarBaz($x, $y, $z, $foo, $bar) + { + /* Comment */ + } +} + +class ClassName extends ParentClass implements InterfaceClass +{ +} + +class ClassName extends ParentClass implements + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 +{ +} + +class ClassName extends ParentClass implements + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 +{ +} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements + InterfaceClass +{ +} + +class ClassName + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 + implements InterfaceClass +{ +} + +class ClassName + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 + implements VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 +{ +} + +class ClassName + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 + implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 +{ +} + +class ClassName extends ParentClass implements + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1, + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 +{ +} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1, + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 +{ +} + +class ClassName + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 + implements + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1, + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 +{ +} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 + extends ParentClass + implements InterfaceClass +{ +} + +class Custom_Plugin_NotificationPlaceholderSource extends + Notification_Manager_DefaultPlaceholderSource +{ +} + +class field extends \models\base +{ + protected function pre_save($input, $fields) + { + $input['configs'] = json_encode( + array_merge( + $configs, + $field_type->process_field_config_from_user($input['definition']) + ) + ); + unset($input['definition']); + } +} + +class test +{ + public function test_method() + { + $customer = (object) ['name' => 'Bob']; + $job = (object) ['customer' => $customer]; + + return "The customer for that job, {$job->customer->name} has an error that shows up after the line gets waaaaay toooo long."; + } +} + +class EmptyClass +{ +} + +class EmptyClassWithComments +{ + /* Comment */ +} + +class MyClass implements MyOtherClass +{ +} + +class MyClass implements MyOtherClass, MyOtherClass1, MyOtherClass2 +{ +} + +class MyClass implements + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass +{ +} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyOtherClass +{ +} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + implements MyOtherClass +{ +} + +class MyClass implements + MyOtherClass, + MyOtherClass, + MyOtherOtherOtherClass, + MyOtherOtherOtherOtherClass +{ +} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + implements + MyOtherClass, + MyOtherClass, + MyOtherOtherOtherClass, + MyOtherOtherOtherOtherClass +{ +} + +class EmptyClass extends MyOtherClass +{ +} + +class EmptyClass extends + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass +{ +} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + extends EmptyClass +{ +} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass +{ +} + +class MyClass extends MyOtherClass implements MyI +{ +} + +class MyClass extends MyOtherClass implements MyI, MyII, MyIII +{ +} + +class MyClass extends MyOtherClass implements + VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass +{ +} + +class MyClass extends MyOtherClass implements + MyInterface, + MyOtherInterface, + MyOtherOtherInterface +{ +} + +class MyClass + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + implements MyI +{ +} + +class MyClass + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + implements + MyI, + MyII, + MyIII +{ +} + +class MyClass + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass +{ +} + +class MyClass + extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass + implements + MyI, + MyII, + MyIII +{ +} + +final class BaseClass +{ +} + +abstract class BaseClass +{ +} + +final class BaseClass extends MyOtherClass +{ +} + +abstract class BaseClass extends MyOtherClass +{ +} + +final class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass +{ +} + +abstract class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass +{ +} + +final class BaseClass extends + MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass1 +{ +} + +abstract class BaseClass extends + MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass1 +{ +} + +final class BaseClass extends MyOtherClass implements + MyInterface, + MyOtherInterface, + MyOtherOtherInterface +{ +} + +abstract class BaseClass extends MyOtherClass implements + MyInterface, + MyOtherInterface, + MyOtherOtherInterface +{ +} + +class User +{ + public int $id; + public string $name; + public ?string $b = 'foo'; + private Foo $prop; + protected static string $static = 'default'; + + public function __construct(int $id, string $name) + { + $this->id = $id; + $this->name = $name; + } +} diff --git a/testlib/src/main/resources/npm/prettier/plugins/php.dirty b/testlib/src/main/resources/npm/prettier/plugins/php.dirty new file mode 100644 index 0000000000..180e059c84 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/plugins/php.dirty @@ -0,0 +1,295 @@ +<?php + +class Foo extends Bar implements Baz, Buzz { + public $test; + + function test() { + return "test"; + } + + public function &passByReferenceTest() { + $a = 1; + return $a; + } +} + +$test = new Foo(); + +abstract class ReallyReallyReallyLongClassName extends AbstractModelFactoryResourceController implements TooMuchObjectOrientation, ThisIsMadness { + // variable doc + public $test; + public $other = 1; + public static $staticTest = ['hi']; + static $cache; + protected static $_instance; + protected $fillable = ['title', 'requester_id', 'type', 'summary', 'proof']; + protected $fillable2 = ['title', 'description', 'requester_id', 'type', 'summary', 'proof']; + protected $test = [ + //test + ]; + + /** + * test constructor + * + * @param \Test\Foo\Bar $test hi + * @param int $test_int test + * @param string $test_string test + * + * @return \Some\Test + */ + public function __construct($test, $test_int = null, $test_string = 'hi') { + parent::__construct($test_int ?: 1); + $this->other = $test_string; + $this->current_version = $current_version ?: new Content_Version_Model(); + self::$staticTest = $test_int; + } + + public static function test_static_constructor($test, $test_int, $test_string) { + $model = new self($test, $test_int, $test_string); + $model = new self($really_really_really_really_really_really_really_really_long_array, $test_int, $test_string); + return $model; + } + + public function test_pass_by_reference(&$test) + { + $test + 1; + } + + /** + * This is a function + */ + private function hi($input) { + $test = 1; + + //testing line spacing + $other_test = 2; + + + $one_more_test = 3; + return $input . $this->test; + + } + + public function reallyReallyReallyReallyReallyReallyReallyLongMethodName($input, $otherInput = 1) { + return true; + } + + // doc test + public static function testStaticFunction($input) { + return self::$staticTest[0]; + } + + public function returnTypeTest() : string + { + return 'hi'; + } + + final public static function bar() + { + // Nothing + } + + abstract protected function zim(); + + public function method(iterable $iterable): array { + // Parameter broadened and return type narrowed. + } + + public function method1() { return 'hi'; } + + public function method2() { + return 'hi'; } + + public function method3() + { return 'hi'; } + + public function testReturn(?string $name): ?string + { + return $name; + } + + public function swap(&$left, &$right): void + { + if ($left === $right) { + return; + } + + $tmp = $left; + $left = $right; + $right = $tmp; + } + + public function test(object $obj): object + { + return new SplQueue(); + } + + public function longLongAnotherFunction( + string $foo, + string $bar, + int $baz + ): string { + return 'foo'; + } + + public function longLongAnotherFunctionOther( + string $foo, + string $bar, + int $baz + ) { + return 'foo'; + } + + public function testReturnTypeDeclaration() : object + { + return new SplQueue(); + } + + public function testReturnTypeDeclarationOther() + : + object + { + return new SplQueue(); + } +} + +$this->something->method($argument, $this->more->stuff($this->even->more->things->complicatedMethod())); + +class A {} + +$someVar = new ReaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaalyLongClassName(); + +class ClassName + +extends ParentClass + +implements \ArrayAccess, \Countable + +{ + + // constants, properties, methods + +} + +class FooBar { public $property; public $property2; public function method() {} public function method2() {} } + +class FooBarFoo +{ + public function fooBarBaz ( $x,$y ,$z, $foo , $bar ) { /* Comment */ } +} + +class ClassName extends ParentClass implements InterfaceClass {} + +class ClassName extends ParentClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {} + +class ClassName extends ParentClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements InterfaceClass {} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements InterfaceClass {} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 {} + +class ClassName extends ParentClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 {} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 {} + +class ClassName extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1,VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName2, VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName3 {} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongFileName1 extends ParentClass implements InterfaceClass {} + +class Custom_Plugin_NotificationPlaceholderSource extends Notification_Manager_DefaultPlaceholderSource {} + +class field extends \models\base +{ + protected function pre_save( $input, $fields ) { + $input['configs'] = json_encode( array_merge( $configs, $field_type->process_field_config_from_user( $input['definition'] ) ) ); + unset( $input['definition'] ); + } +} + +class test { + public function test_method() { + $customer = (object) [ 'name' => 'Bob' ]; + $job = (object) [ 'customer' => $customer ]; + + return "The customer for that job, {$job->customer->name} has an error that shows up after the line gets waaaaay toooo long."; + } +} + +class EmptyClass {} + +class EmptyClassWithComments { /* Comment */ } + +class MyClass implements MyOtherClass {} + +class MyClass implements MyOtherClass, MyOtherClass1, MyOtherClass2 {} + +class MyClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyOtherClass {} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyOtherClass {} + +class MyClass implements MyOtherClass, MyOtherClass, MyOtherOtherOtherClass, MyOtherOtherOtherOtherClass {} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyOtherClass, MyOtherClass, MyOtherOtherOtherClass, MyOtherOtherOtherOtherClass {} + +class EmptyClass extends MyOtherClass {} + +class EmptyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass extends EmptyClass {} + +class VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {} + +class MyClass extends MyOtherClass implements MyI {} + +class MyClass extends MyOtherClass implements MyI, MyII, MyIII {} + +class MyClass extends MyOtherClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {} + +class MyClass extends MyOtherClass implements MyInterface, MyOtherInterface, MyOtherOtherInterface {} + +class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyI {} + +class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyI, MyII, MyIII {} + +class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass {} + +class MyClass extends VeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryVeryLongMyClass implements MyI, MyII, MyIII {} + +final class BaseClass {} + +abstract class BaseClass {} + +final class BaseClass extends MyOtherClass {} + +abstract class BaseClass extends MyOtherClass {} + +final class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass {} + +abstract class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass {} + +final class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVeryVeryLongClass1 {} + +abstract class BaseClass extends MyOtherVeryVeryVeryVeVeryVeryVeryVVeryLongClass1 {} + +final class BaseClass extends MyOtherClass implements MyInterface, MyOtherInterface, MyOtherOtherInterface {} + +abstract class BaseClass extends MyOtherClass implements MyInterface, MyOtherInterface, MyOtherOtherInterface {} + +class User { + public int $id; + public string $name; + public ?string $b = 'foo'; + private Foo $prop; + protected static string $static = 'default'; + + public function __construct(int $id, string $name) { + $this->id = $id; + $this->name = $name; + } +} \ No newline at end of file diff --git a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java index 65c6e61989..2b0d2a4937 100644 --- a/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/generic/LicenseHeaderStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/NodeJsNativeDoubleLoadTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/NodeJsNativeDoubleLoadTest.java deleted file mode 100644 index 33ec041cf2..0000000000 --- a/testlib/src/test/java/com/diffplug/spotless/npm/NodeJsNativeDoubleLoadTest.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Copyright 2016 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.npm; - -import java.util.Optional; - -import org.assertj.core.api.Assertions; -import org.junit.Test; - -import com.diffplug.common.collect.ImmutableMap; -import com.diffplug.spotless.JarState; -import com.diffplug.spotless.TestProvisioner; - -public class NodeJsNativeDoubleLoadTest { - @Test - public void inMultipleClassLoaders() throws Exception { - JarState state = JarState.from(NpmFormatterStepStateBase.j2v8MavenCoordinate(), TestProvisioner.mavenCentral()); - ClassLoader loader1 = state.getClassLoader(1); - ClassLoader loader2 = state.getClassLoader(2); - createAndTestWrapper(loader1); - createAndTestWrapper(loader2); - } - - @Test - public void multipleTimesInOneClassLoader() throws Exception { - JarState state = JarState.from(NpmFormatterStepStateBase.j2v8MavenCoordinate(), TestProvisioner.mavenCentral()); - ClassLoader loader3 = state.getClassLoader(3); - createAndTestWrapper(loader3); - createAndTestWrapper(loader3); - } - - private void createAndTestWrapper(ClassLoader loader) throws Exception { - try (NodeJSWrapper node = new NodeJSWrapper(loader)) { - V8ObjectWrapper object = node.createNewObject(ImmutableMap.of("a", 1)); - Optional<Integer> value = object.getOptionalInteger("a"); - Assertions.assertThat(value).hasValue(1); - object.release(); - } - } -} diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java index 796f6dcc43..fa15b186ad 100644 --- a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java +++ b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java @@ -1,5 +1,5 @@ /* - * Copyright 2016 DiffPlug + * Copyright 2016-2020 DiffPlug * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -41,7 +41,7 @@ public static class PrettierFormattingOfFileTypesIsWorking extends NpmFormatterS @Parameterized.Parameters(name = "{index}: prettier can be applied to {0}") public static Iterable<String> formattingConfigFiles() { - return Arrays.asList("typescript", "json", "javascript-es5", "javascript-es6", "css", "scss", "markdown", "yaml"); + return Arrays.asList("html", "typescript", "json", "javascript-es5", "javascript-es6", "css", "scss", "markdown", "yaml"); } @Test @@ -86,6 +86,22 @@ public void parserInferenceIsWorking() throws Exception { stepHarness.testResource(dirtyFile, cleanFile); } } + + @Test + public void verifyPrettierErrorMessageIsRelayed() throws Exception { + FormatterStep formatterStep = PrettierFormatterStep.create( + PrettierFormatterStep.defaultDevDependenciesWithPrettier("2.0.5"), + TestProvisioner.mavenCentral(), + buildDir(), + npmExecutable(), + new PrettierConfig(null, ImmutableMap.of("parser", "postcss"))); + try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) { + stepHarness.testException("npm/prettier/filetypes/scss/scss.dirty", exception -> { + exception.hasMessageContaining("HTTP 501"); + exception.hasMessageContaining("Couldn't resolve parser \"postcss\""); + }); + } + } } @Category(NpmTest.class)