diff --git a/.ci/ci.sh b/.ci/ci.sh index 075a3abb43..15b6f70870 100755 --- a/.ci/ci.sh +++ b/.ci/ci.sh @@ -2,6 +2,7 @@ # Do the Gradle build ./gradlew build || exit 1 +./gradlew npmTest || exit 1 if [ "$TRAVIS_REPO_SLUG" == "diffplug/spotless" ] && [ "$TRAVIS_PULL_REQUEST" == "false" ] && [ "$TRAVIS_BRANCH" == "master" ]; then # Make sure that all pom are up-to-date diff --git a/.travis.yml b/.travis.yml index 5de9f5b2c5..267295353f 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,6 +1,10 @@ language: java jdk: -- oraclejdk8 + - oraclejdk8 +env: + - NODE_VERSION="6.10.2" +before_install: + - nvm install $NODE_VERSION install: true script: - ./.ci/ci.sh diff --git a/README.md b/README.md index 17c0600dfa..8f7ac6288e 100644 --- a/README.md +++ b/README.md @@ -12,7 +12,7 @@ output = [ [![License Apache](https://img.shields.io/badge/license-apache-brightgreen.svg)](https://tldrlegal.com/license/apache-license-2.0-(apache-2.0)) -Spotless can format <java | kotlin | scala | sql | groovy | markdown | license headers | anything> using <gradle | maven | anything>. +Spotless can format <java | kotlin | scala | sql | groovy | javascript | flow | typeScript | css | scss | less | jsx | vue | graphql | json | yaml | markdown | license headers | anything> using <gradle | maven | anything>. - [Spotless for Gradle](plugin-gradle) - [Spotless for Maven](plugin-maven) @@ -46,6 +46,8 @@ lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} extra('java.EclipseFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{no}} |', lib('markdown.FreshMarkStep') +'{{yes}} | {{no}} | {{no}} |', +lib('npm.PrettierFormatterStep') +'{{yes}} | {{no}} | {{no}} |', +lib('npm.TsFmtFormatterStep') +'{{yes}} | {{no}} | {{no}} |', lib('scala.ScalaFmtStep') +'{{yes}} | {{yes}} | {{no}} |', lib('sql.DBeaverSQLFormatterStep') +'{{yes}} | {{no}} | {{no}} |', extra('wtp.WtpEclipseFormatterStep') +'{{yes}} | {{yes}} | {{no}} |', @@ -71,6 +73,8 @@ extra('wtp.WtpEclipseFormatterStep') +'{{yes}} | {{yes}} | [`java.EclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseFormatterStep.java) | :+1: | :+1: | :white_large_square: | | [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :white_large_square: | | [`markdown.FreshMarkStep`](lib/src/main/java/com/diffplug/spotless/markdown/FreshMarkStep.java) | :+1: | :white_large_square: | :white_large_square: | +| [`npm.PrettierFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java) | :+1: | :white_large_square: | :white_large_square: | +| [`npm.TsFmtFormatterStep`](lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`scala.ScalaFmtStep`](lib/src/main/java/com/diffplug/spotless/scala/ScalaFmtStep.java) | :+1: | :+1: | :white_large_square: | | [`sql.DBeaverSQLFormatterStep`](lib/src/main/java/com/diffplug/spotless/sql/DBeaverSQLFormatterStep.java) | :+1: | :white_large_square: | :white_large_square: | | [`wtp.WtpEclipseFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/wtp/WtpEclipseFormatterStep.java) | :+1: | :+1: | :white_large_square: | diff --git a/lib-extra/build.gradle b/lib-extra/build.gradle index bc5bcee8cc..45a754cded 100644 --- a/lib-extra/build.gradle +++ b/lib-extra/build.gradle @@ -23,4 +23,3 @@ dependencies { // we'll hold the core lib to a high standard spotbugs { reportLevel = 'low' } // low|medium|high (low = sensitive to even minor mistakes) - diff --git a/lib/src/main/java/com/diffplug/spotless/npm/BlacklistedOptionException.java b/lib/src/main/java/com/diffplug/spotless/npm/BlacklistedOptionException.java new file mode 100644 index 0000000000..3704f487dc --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/BlacklistedOptionException.java @@ -0,0 +1,24 @@ +/* + * 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; + +class BlacklistedOptionException extends RuntimeException { + private static final long serialVersionUID = -5876348893394153811L; + + public BlacklistedOptionException(String blacklistedOption) { + super("The config option '" + blacklistedOption + "' is not supported."); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java new file mode 100644 index 0000000000..bdb535591a --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NodeJSWrapper.java @@ -0,0 +1,106 @@ +/* + * 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.Map; +import java.util.Objects; +import java.util.concurrent.atomic.AtomicBoolean; + +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 AtomicBoolean flagsSet = new AtomicBoolean(false); + + public NodeJSWrapper(ClassLoader classLoader) { + super(Reflective.withClassLoader(classLoader), + reflective -> { + final boolean firstRun = flagsSet.compareAndSet(false, true); + if (firstRun) { + reflective.invokeStaticMethod(V8_RUNTIME_CLASS, "setFlags", "-color=false"); // required to run prettier on windows + } + 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 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/NpmConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java new file mode 100644 index 0000000000..55d3683606 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmConfig.java @@ -0,0 +1,40 @@ +/* + * 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.Serializable; + +class NpmConfig implements Serializable { + + private static final long serialVersionUID = -1866722789779160491L; + + private final String packageJsonContent; + + private final String npmModule; + + public NpmConfig(String packageJsonContent, String npmModule) { + this.packageJsonContent = packageJsonContent; + this.npmModule = npmModule; + } + + public String getPackageJsonContent() { + return packageJsonContent; + } + + public String getNpmModule() { + return npmModule; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java new file mode 100644 index 0000000000..cea0984a2f --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmExecutableResolver.java @@ -0,0 +1,108 @@ +/* + * 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 com.diffplug.spotless.npm.PlatformInfo.OS.WINDOWS; + +import java.io.File; +import java.util.Arrays; +import java.util.Optional; +import java.util.function.Supplier; +import java.util.stream.Stream; + +/** + * Utility class to resolve an npm binary to be used by npm-based steps. + * Tries to find an npm executable in the following order: + *
    + *
  1. from System-Property {@code npm.exec} (unverified)
  2. + *
  3. from Environment-Properties in the following order:
  4. + *
      + *
    1. from NVM_BIN environment variable, if available
    2. + *
    3. from NVM_SYMLINK environment variable, if available
    4. + *
    5. from NODE_PATH environment variable, if available
    6. + *
    7. fallback: PATH environment variable
    8. + *
    + *
+ */ +class NpmExecutableResolver { + + private NpmExecutableResolver() { + // no instance + } + + static String npmExecutableName() { + String npmName = "npm"; + if (PlatformInfo.normalizedOS() == WINDOWS) { + npmName += ".cmd"; + } + return npmName; + } + + static Supplier> systemProperty() { + return () -> Optional.ofNullable(System.getProperty("npm.exec")) + .map(File::new); + } + + static Supplier> environmentNvmBin() { + return () -> Optional.ofNullable(System.getenv("NVM_BIN")) + .map(File::new) + .map(binDir -> new File(binDir, npmExecutableName())) + .filter(File::exists) + .filter(File::canExecute); + } + + static Supplier> environmentNvmSymlink() { + return pathListFromEnvironment("NVM_SYMLINK"); + } + + static Supplier> environmentNodepath() { + return pathListFromEnvironment("NODE_PATH"); + } + + static Supplier> environmentPath() { + return pathListFromEnvironment("PATH"); + } + + static Optional tryFind() { + return Stream.of(systemProperty(), + environmentNvmBin(), + environmentNvmSymlink(), + environmentNodepath(), + environmentPath()) + .map(Supplier::get) + .filter(Optional::isPresent) + .map(Optional::get) + .findFirst(); + } + + private static Supplier> pathListFromEnvironment(String environmentPathListName) { + return () -> { + String pathList = System.getenv(environmentPathListName); + if (pathList != null) { + return Arrays.stream(pathList.split(System.getProperty("path.separator", ":"))) + .map(File::new) + .map(dir -> dir.getName().equalsIgnoreCase("node_modules") ? dir.getParentFile() : dir) + .map(dir -> new File(dir, npmExecutableName())) + .filter(File::exists) + .filter(File::canExecute) + .findFirst(); + + } + return Optional.empty(); + }; + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java new file mode 100644 index 0000000000..1fc056fa0d --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/NpmFormatterStepStateBase.java @@ -0,0 +1,121 @@ +/* + * 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.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.util.Optional; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.*; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +abstract class NpmFormatterStepStateBase implements Serializable { + + private static final long serialVersionUID = -5849375492831208496L; + + private final JarState jarState; + + @SuppressWarnings("unused") + private final FileSignature nodeModulesSignature; + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + public final transient File nodeModulesDir; + + private final NpmConfig npmConfig; + + private final String stepName; + + protected NpmFormatterStepStateBase(String stepName, Provisioner provisioner, 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.nodeModulesDir = prepareNodeModules(buildDir, npm); + this.nodeModulesSignature = FileSignature.signAsList(this.nodeModulesDir); + } + + private File prepareNodeModules(File buildDir, @Nullable File npm) 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); + 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(); + try { + if (npmInstall.waitFor() != 0) { + throw new IOException("Creating npm modules failed with exit code: " + npmInstall.exitValue()); + } + } catch (InterruptedException e) { + throw new IOException("Running npm install was interrupted.", e); + } + } + + private 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()); // TODO (simschla, 02.08.18): cache this instance + } + + protected File nodeModulePath() { + return new File(new File(this.nodeModulesDir, "node_modules"), this.npmConfig.getNpmModule()); + } + + private 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); + } + } + + public abstract FormatterFunc createFormatterFunc(); +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PlatformInfo.java b/lib/src/main/java/com/diffplug/spotless/npm/PlatformInfo.java new file mode 100644 index 0000000000..3ae624d7c0 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/PlatformInfo.java @@ -0,0 +1,78 @@ +/* + * 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.Locale; + +class PlatformInfo { + private PlatformInfo() { + // no instance + } + + static OS normalizedOS() { + final String osNameProperty = System.getProperty("os.name"); + if (osNameProperty == null) { + throw new RuntimeException("No info about OS available, cannot decide which implementation of j2v8 to use"); + } + final String normalizedOsName = osNameProperty.toLowerCase(Locale.ROOT); + if (normalizedOsName.contains("win")) { + return OS.WINDOWS; + } + if (normalizedOsName.contains("mac")) { + return OS.MACOS; + } + if (normalizedOsName.contains("nix") || normalizedOsName.contains("nux") || normalizedOsName.contains("aix")) { + return OS.LINUX; + } + throw new RuntimeException("Cannot handle os " + osNameProperty); + } + + static String normalizedOSName() { + return normalizedOS().normalizedOsName(); + } + + static String normalizedArchName() { + final String osArchProperty = System.getProperty("os.arch"); + if (osArchProperty == null) { + throw new RuntimeException("No info about ARCH available, cannot decide which implementation of j2v8 to use"); + } + final String normalizedOsArch = osArchProperty.toLowerCase(Locale.ROOT); + + if (normalizedOsArch.contains("64")) { + return "x86_64"; + } + if (normalizedOsArch.contains("x86") || normalizedOsArch.contains("32")) { + return "x86"; + } + throw new RuntimeException("Cannot handle arch " + osArchProperty); + } + + enum OS { + WINDOWS("win32"), MACOS("macosx"), LINUX("linux"); + + private final String normalizedOsName; + + OS(String normalizedOsName) { + this.normalizedOsName = requireNonNull(normalizedOsName); + } + + public String normalizedOsName() { + return normalizedOsName; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java new file mode 100644 index 0000000000..328cb25a4e --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierConfig.java @@ -0,0 +1,62 @@ +/* + * 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.IOException; +import java.io.Serializable; +import java.util.Map; +import java.util.TreeMap; + +import javax.annotation.Nullable; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.ThrowingEx; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class PrettierConfig implements Serializable { + + private static final long serialVersionUID = -8709340269833126583L; + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + @Nullable + private final transient File prettierConfigPath; + + @SuppressWarnings("unused") + private final FileSignature prettierConfigPathSignature; + + private final TreeMap options; + + public PrettierConfig(@Nullable File prettierConfigPath, @Nullable Map options) { + try { + this.prettierConfigPath = prettierConfigPath; + this.prettierConfigPathSignature = prettierConfigPath != null ? FileSignature.signAsList(this.prettierConfigPath) : FileSignature.signAsList(); + this.options = options == null ? new TreeMap<>() : new TreeMap<>(options); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + @Nullable + public File getPrettierConfigPath() { + return prettierConfigPath; + } + + public Map getOptions() { + return new TreeMap<>(this.options); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java new file mode 100644 index 0000000000..56842ef413 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/PrettierFormatterStep.java @@ -0,0 +1,147 @@ +/* + * 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.Arrays.asList; +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.Map; +import java.util.TreeMap; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.ThrowingEx; + +public class PrettierFormatterStep { + + public static final String NAME = "prettier-format"; + + public static FormatterStep create(Provisioner provisioner, File buildDir, @Nullable File npm, PrettierConfig prettierConfig) { + requireNonNull(provisioner); + requireNonNull(buildDir); + return FormatterStep.createLazy(NAME, + () -> new State(NAME, provisioner, buildDir, npm, prettierConfig), + State::createFormatterFunc); + } + + public static class State extends NpmFormatterStepStateBase implements Serializable { + + private static final long serialVersionUID = -3811104513825329168L; + private final PrettierConfig prettierConfig; + + State(String stepName, Provisioner provisioner, File buildDir, @Nullable File npm, PrettierConfig prettierConfig) throws IOException { + super(stepName, + provisioner, + new NpmConfig( + readFileFromClasspath(PrettierFormatterStep.class, "/com/diffplug/spotless/npm/prettier-package.json"), + "prettier"), + buildDir, + npm); + this.prettierConfig = requireNonNull(prettierConfig); + } + + @Override + @Nonnull + public FormatterFunc createFormatterFunc() { + + try { + final NodeJSWrapper nodeJSWrapper = nodeJSWrapper(); + final V8ObjectWrapper prettier = nodeJSWrapper.require(nodeModulePath()); + + @SuppressWarnings("unchecked") + final Map[] resolvedPrettierOptions = (Map[]) 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; + } + }); + } catch (Exception e) { + throw ThrowingEx.asRuntime(e); + } + } + + private V8FunctionWrapper createResolveConfigFunction(NodeJSWrapper nodeJSWrapper, Map[] 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 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[] resolvedPrettierOptions, Exception[] toThrow) { + while (resolvedPrettierOptions[0] == null && toThrow[0] == null) { + nodeJSWrapper.handleMessage(); + } + + if (toThrow[0] != null) { + throw ThrowingEx.asRuntime(toThrow[0]); + } + } + + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java b/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java new file mode 100644 index 0000000000..e1076672b9 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/Reflective.java @@ -0,0 +1,275 @@ +/* + * 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; + +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); + } + } + + 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); + } + } + + 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 new file mode 100644 index 0000000000..3dfffc8c94 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/ReflectiveObjectWrapper.java @@ -0,0 +1,76 @@ +/* + * 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 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 new file mode 100644 index 0000000000..8650f086c4 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/SimpleJsonWriter.java @@ -0,0 +1,94 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.stream.Collectors; + +import com.diffplug.spotless.ThrowingEx; + +public class SimpleJsonWriter { + + private final LinkedHashMap valueMap = new LinkedHashMap<>(); + + public static SimpleJsonWriter of(Map values) { + SimpleJsonWriter writer = new SimpleJsonWriter(); + writer.putAll(values); + return writer; + } + + SimpleJsonWriter putAll(Map values) { + verifyValues(values); + this.valueMap.putAll(values); + return this; + } + + SimpleJsonWriter put(String name, Object value) { + verifyValues(Collections.singletonMap(name, value)); + this.valueMap.put(name, value); + return this; + } + + private void verifyValues(Map 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()); + } + } + + String toJsonString() { + final String valueString = valueMap.entrySet() + .stream() + .map(entry -> " " + jsonEscape(entry.getKey()) + ": " + jsonEscape(entry.getValue())) + .collect(Collectors.joining(",\n")); + return "{\n" + valueString + "\n}"; + } + + private String jsonEscape(Object val) { + requireNonNull(val); + if (val instanceof String) { + return "\"" + val + "\""; + } + return val.toString(); + } + + void toJsonFile(File file) { + if (!file.getParentFile().exists()) { + if (!file.getParentFile().mkdirs()) { + throw new RuntimeException("Cannot write to file"); + } + } + try { + Files.write(file.toPath(), toJsonString().getBytes(StandardCharsets.UTF_8)); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + @Override + public String toString() { + return this.toJsonString(); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsConfigFileType.java b/lib/src/main/java/com/diffplug/spotless/npm/TsConfigFileType.java new file mode 100644 index 0000000000..19251f7a76 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsConfigFileType.java @@ -0,0 +1,29 @@ +/* + * 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.Arrays; + +public enum TsConfigFileType { + TSCONFIG, TSLINT, VSCODE, TSFMT; + + public static TsConfigFileType forNameIgnoreCase(String name) { + return Arrays.stream(values()) + .filter(type -> type.name().equalsIgnoreCase(name)) + .findFirst() + .orElseThrow(() -> new IllegalArgumentException("Config file type " + name + " is not supported. Supported values (case is ignored): " + Arrays.toString(values()))); + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java new file mode 100644 index 0000000000..52d7a31d2b --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtFormatterStep.java @@ -0,0 +1,156 @@ +/* + * 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.Arrays.asList; +import static java.util.Objects.requireNonNull; + +import java.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.*; + +import javax.annotation.Nonnull; +import javax.annotation.Nullable; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.Provisioner; +import com.diffplug.spotless.ThrowingEx; + +public class TsFmtFormatterStep { + + public static final String NAME = "tsfmt-format"; + + public static FormatterStep create(Provisioner provisioner, File buildDir, @Nullable File npm, File baseDir, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map inlineTsFmtSettings) { + requireNonNull(provisioner); + requireNonNull(buildDir); + requireNonNull(baseDir); + return FormatterStep.createLazy(NAME, + () -> new State(NAME, provisioner, buildDir, npm, baseDir, configFile, inlineTsFmtSettings), + State::createFormatterFunc); + } + + public static class State extends NpmFormatterStepStateBase implements Serializable { + + private static final long serialVersionUID = -3811104513825329168L; + + private final TreeMap inlineTsFmtSettings; + + private final File buildDir; + + @Nullable + private final TypedTsFmtConfigFile configFile; + + private final File baseDir; + + public State(String stepName, Provisioner provisioner, File buildDir, @Nullable File npm, File baseDir, @Nullable TypedTsFmtConfigFile configFile, @Nullable Map inlineTsFmtSettings) throws IOException { + super(stepName, + provisioner, + new NpmConfig( + readFileFromClasspath(TsFmtFormatterStep.class, "/com/diffplug/spotless/npm/tsfmt-package.json"), + "typescript-formatter"), + buildDir, + npm); + this.buildDir = requireNonNull(buildDir); + this.baseDir = requireNonNull(baseDir); + this.configFile = configFile; + this.inlineTsFmtSettings = inlineTsFmtSettings == null ? new TreeMap<>() : new TreeMap<>(inlineTsFmtSettings); + } + + @Override + @Nonnull + public FormatterFunc createFormatterFunc() { + + Map 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 + } + */ + 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 { + + 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 unifyOptions() { + Map unified = new HashMap<>(); + if (!this.inlineTsFmtSettings.isEmpty()) { + File targetFile = new File(this.buildDir, "inline-tsfmt.json"); + SimpleJsonWriter.of(this.inlineTsFmtSettings).toJsonFile(targetFile); + unified.put("tsfmt", true); + unified.put("tsfmtFile", targetFile.getAbsolutePath()); + } else if (this.configFile != null) { + unified.put(this.configFile.configFileEnabledOptionName(), Boolean.TRUE); + unified.put(this.configFile.configFileOptionName(), this.configFile.absolutePath()); + } + return unified; + } + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java new file mode 100644 index 0000000000..c43f4963f3 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/TsFmtResult.java @@ -0,0 +1,41 @@ +/* + * 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; + +class TsFmtResult { + + 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; + } + + String getMessage() { + return message; + } + + Boolean isError() { + return error; + } + + String getFormatted() { + return formatted; + } +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/TypedTsFmtConfigFile.java b/lib/src/main/java/com/diffplug/spotless/npm/TypedTsFmtConfigFile.java new file mode 100644 index 0000000000..75257711f6 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/TypedTsFmtConfigFile.java @@ -0,0 +1,76 @@ +/* + * 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.io.File; +import java.io.IOException; +import java.io.Serializable; +import java.util.Locale; + +import com.diffplug.spotless.FileSignature; +import com.diffplug.spotless.ThrowingEx; + +import edu.umd.cs.findbugs.annotations.SuppressFBWarnings; + +public class TypedTsFmtConfigFile implements Serializable { + + private static final long serialVersionUID = -4442310349275775501L; + + private final TsConfigFileType configFileType; + + private final File configFile; + + @SuppressFBWarnings("SE_TRANSIENT_FIELD_NOT_RESTORED") + @SuppressWarnings("unused") + private final FileSignature configFileSignature; + + public TypedTsFmtConfigFile(TsConfigFileType configFileType, File configFile) { + this.configFileType = requireNonNull(configFileType); + this.configFile = requireNonNull(configFile); + try { + this.configFileSignature = FileSignature.signAsList(configFile); + } catch (IOException e) { + throw ThrowingEx.asRuntime(e); + } + } + + TsConfigFileType configFileType() { + return configFileType; + } + + File configFile() { + return configFile; + } + + String configFileEnabledOptionName() { + return this.configFileType.name().toLowerCase(Locale.ROOT); + } + + String configFileOptionName() { + return this.configFileEnabledOptionName() + "File"; + } + + String absolutePath() { + return this.configFile.getAbsolutePath(); + } + + static TypedTsFmtConfigFile named(String name, File file) { + return new TypedTsFmtConfigFile(TsConfigFileType.forNameIgnoreCase(name), file); + } + +} diff --git a/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java b/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java new file mode 100644 index 0000000000..0d26df0b64 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/V8ArrayWrapper.java @@ -0,0 +1,51 @@ +/* + * 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 new file mode 100644 index 0000000000..36a9804854 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/V8FunctionWrapper.java @@ -0,0 +1,58 @@ +/* + * 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 new file mode 100644 index 0000000000..af7448c748 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectUtilsWrapper.java @@ -0,0 +1,35 @@ +/* + * 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 toMap(final V8ObjectWrapper object) { + requireNonNull(object); + + final Reflective reflective = object.reflective(); + + @SuppressWarnings("unchecked") + final Map map = (Map) 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 new file mode 100644 index 0000000000..917e25c222 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/npm/V8ObjectWrapper.java @@ -0,0 +1,89 @@ +/* + * 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 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 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 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 new file mode 100644 index 0000000000..160c71bc3d --- /dev/null +++ b/lib/src/main/resources/com/diffplug/spotless/npm/prettier-package.json @@ -0,0 +1,11 @@ +{ + "name": "spotless-prettier-formatter-step", + "version": "1.0.0", + "devDependencies": { + "prettier": "1.13.4" + }, + "dependencies": {}, + "engines": { + "node": ">=6" + } +} 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 new file mode 100644 index 0000000000..139ee30820 --- /dev/null +++ b/lib/src/main/resources/com/diffplug/spotless/npm/tsfmt-package.json @@ -0,0 +1,13 @@ +{ + "name": "spotless-tsfmt-formatter-step", + "version": "1.0.0", + "devDependencies": { + "typescript-formatter": "7.2.2", + "typescript": "2.9.2", + "tslint": "5.1.0" + }, + "dependencies": {}, + "engines": { + "node": ">= 4.2.0" + } +} diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index 16fca28c84..5867f87202 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -86,6 +86,8 @@ Spotless can check and apply formatting to any plain-text file, using simple rul * [ktlint](https://github.com/shyiko/ktlint) * [scalafmt](https://github.com/olafurpg/scalafmt) * [DBeaver sql format](https://dbeaver.jkiss.org/) +* [Prettier: An opinionated code formatter](https://prettier.io) +* [TypeScript Formatter (tsfmt)](https://github.com/vvakame/typescript-formatter) * Any user-defined function which takes an unformatted string and outputs a formatted version. Contributions are welcome, see [the contributing guide](../CONTRIBUTING.md) for development info. @@ -344,6 +346,146 @@ Use Eclipse to define the *XML editor preferences* (see [Eclipse documentation]( The Eclipse WTP formatter supports DTD/XSD restrictions on white spaces. For XSD/DTD lookup, relative and absolute XSD/DTD URIs are supported. Furthermore a user catalog can be configured using the `userCatalog` property key. Add the property to the preference file or add an additional preference or properties files as an additional argument to the `configFile`. + + +## Applying to Typescript source + +To use tsfmt, you first have to specify the files that you want it to apply to. +Then you specify `tsfmt()`, and optionally how you want to apply it. + +By default, all typescript source sets will be formatted. To change this, +set the `target` parameter as described in the [Custom rules](#custom) section. + +```gradle +spotless { + typescript { + // using existing config files + tsfmt().tslintFile('/path/to/repo/tslint.json') + } +} +``` +Supported config file types are `tsconfigFile`, `tslintFile`, `vscodeFile` and `tsfmtFile`. They are corresponding to the respective +[tsfmt-parameters](https://github.com/vvakame/typescript-formatter/blob/7764258ad42ac65071399840d1b8701868510ca7/lib/index.ts#L27L34). + +*Please note:* +The auto-discovery of config files (up the file tree) will not work when using tsfmt within spotless, + hence you are required to provide resolvable file paths for config files. + +... or alternatively provide the configuration inline ... + +```gradle +spotless { + typescript { + // custom file-set + target 'src/main/resources/**/*.ts' + // provide config inline + tsfmt().config(['indentSize': 1, 'convertTabsToSpaces': true]) + } +} +``` + +See [tsfmt's default config settings](https://github.com/vvakame/typescript-formatter/blob/7764258ad42ac65071399840d1b8701868510ca7/lib/utils.ts#L11L32) for what is available. + +... and it is also possible to apply `prettier()` instead of `tsfmt()` as formatter. For details see the section about [prettier](#typescript-prettier). + +### Prerequisite: tsfmt requires a working NodeJS version + +tsfmt is based on NodeJS, so to use it, a working NodeJS installation (especially npm) is required on the host running spotless. +Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm binary to use. + +```gradle +spotless { + typescript { + tsfmt().npmExecutable('/usr/bin/npm').config(...) + } +} +``` + +Spotless uses npm to install necessary packages locally. It runs tsfmt using [J2V8](https://github.com/eclipsesource/J2V8) internally after that. + + + +## Applying [Prettier](https://prettier.io) to javascript | flow | typeScript | css | scss | less | jsx | graphQL | yaml | etc. + +Prettier is a formatter that can format [multiple file types](https://prettier.io/docs/en/language-support.html). + +To use prettier, you first have to specify the files that you want it to apply to. Then you specify prettier, and how you want to apply it. + +```gradle +spotless { + format 'styling', { + target '**/*.css', '**/*.scss' + + // at least provide the parser to use + prettier().config(['parser': 'postcss']) + + // or provide a typical filename + prettier().config(['filepath': 'style.scss']) + } +} +``` + +Supported config options are documented on [prettier.io](https://prettier.io/docs/en/options.html). + +It is also possible to specify the config via file: + +```gradle +spotless { + format 'styling', { + target '**/*.css', '**/*.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') + } +} +``` + +Supported config file variants are documented on [prettier.io](https://prettier.io/docs/en/configuration.html). +*Please note:* +- The auto-discovery of config files (up the file tree) will not work when using prettier within spotless. +- Prettier's override syntax is not supported when using prettier within spotless. + +To apply prettier to more kinds of files, just add more formats + +```gradle +spotless { + format 'javascript', { + target 'src/main/resources/**/*.js' + prettier().config(['filepath': 'file.js']) + } +} +``` + + +Prettier can also be applied from within the [typescript config block](#typescript-formatter): + +```gradle +spotless { + typescript { + // no parser or filepath needed + // -> will default to 'typescript' parser when used in the typescript block + prettier() + } +} +``` + +### 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. +Spotless will try to auto-discover an npm installation. If that is not working for you, it is possible to directly configure the npm binary to use. + +```gradle +spotless { + format 'javascript', { + prettier().npmExecutable('/usr/bin/npm').config(...) + } +} +``` + +Spotless uses npm to install necessary packages locally. It runs prettier using [J2V8](https://github.com/eclipsesource/J2V8) internally after that. + ## License header options @@ -386,7 +528,6 @@ spotless { } ``` - ## Custom rules diff --git a/plugin-gradle/build.gradle b/plugin-gradle/build.gradle index ef13f70de5..1f94ecda91 100644 --- a/plugin-gradle/build.gradle +++ b/plugin-gradle/build.gradle @@ -41,6 +41,10 @@ task spotlessApply(type: JavaExec) { } test { testLogging.showStandardStreams = true } +test { useJUnit { excludeCategories 'com.diffplug.spotless.category.NpmTest' } } + +task npmTest(type: Test) { useJUnit { includeCategories 'com.diffplug.spotless.category.NpmTest' } } + ////////////////////////// // GRADLE PLUGIN PORTAL // ////////////////////////// diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java index 7071dbd6ee..e19e88e356 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/FormatExtension.java @@ -20,12 +20,7 @@ import java.io.File; import java.io.Serializable; import java.nio.charset.Charset; -import java.util.ArrayList; -import java.util.Arrays; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.Random; +import java.util.*; import java.util.stream.Stream; import javax.annotation.Nullable; @@ -46,6 +41,7 @@ import com.diffplug.spotless.generic.ReplaceRegexStep; import com.diffplug.spotless.generic.ReplaceStep; import com.diffplug.spotless.generic.TrimTrailingWhitespaceStep; +import com.diffplug.spotless.npm.PrettierFormatterStep; import groovy.lang.Closure; @@ -430,6 +426,63 @@ public LicenseHeaderConfig licenseHeaderFile(Object licenseHeaderFile, String de return config; } + public abstract class NpmStepConfig> { + @Nullable + protected Object npmFile; + + @SuppressWarnings("unchecked") + public T npmExecutable(final Object npmFile) { + this.npmFile = npmFile; + replaceStep(createStep()); + return (T) this; + } + + File npmFileOrNull() { + return npmFile != null ? getProject().file(npmFile) : null; + } + + abstract FormatterStep createStep(); + + } + + public class PrettierConfig extends NpmStepConfig { + + @Nullable + protected Object prettierConfigFile; + + @Nullable + protected Map prettierConfig; + + public PrettierConfig configFile(final Object prettierConfigFile) { + this.prettierConfigFile = prettierConfigFile; + replaceStep(createStep()); + return this; + } + + public PrettierConfig config(final Map prettierConfig) { + this.prettierConfig = new TreeMap<>(prettierConfig); + replaceStep(createStep()); + return this; + } + + FormatterStep createStep() { + final Project project = getProject(); + return PrettierFormatterStep.create( + GradleProvisioner.fromProject(project), + project.getBuildDir(), + npmFileOrNull(), + new com.diffplug.spotless.npm.PrettierConfig( + this.prettierConfigFile != null ? project.file(this.prettierConfigFile) : null, + this.prettierConfig)); + } + } + + public PrettierConfig prettier() { + final PrettierConfig prettierConfig = new PrettierConfig(); + addStep(prettierConfig.createStep()); + return prettierConfig; + } + /** Sets up a format task according to the values in this extension. */ protected void setupTask(SpotlessTask task) { task.setPaddedCell(paddedCell); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java index 0b8409710c..354d84f3a1 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/SpotlessExtension.java @@ -128,6 +128,11 @@ public void cpp(Action closure) { configure(CppExtension.NAME, CppExtension.class, closure); } + /** Configures the special typescript-specific extension for typescript files. */ + public void typescript(Action closure) { + configure(TypescriptExtension.NAME, TypescriptExtension.class, closure); + } + /** Configures a custom extension. */ public void format(String name, Action closure) { requireNonNull(name, "name"); diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java new file mode 100644 index 0000000000..b20a4e34c1 --- /dev/null +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/TypescriptExtension.java @@ -0,0 +1,141 @@ +/* + * 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.gradle.spotless; + +import static java.util.Objects.requireNonNull; + +import java.util.Collections; +import java.util.Map; +import java.util.TreeMap; + +import javax.annotation.Nullable; + +import org.gradle.api.Project; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.npm.TsConfigFileType; +import com.diffplug.spotless.npm.TsFmtFormatterStep; +import com.diffplug.spotless.npm.TypedTsFmtConfigFile; + +public class TypescriptExtension extends FormatExtension { + + static final String NAME = "typescript"; + + public TypescriptExtension(SpotlessExtension root) { + super(root); + } + + public TypescriptFormatExtension tsfmt() { + TypescriptFormatExtension tsfmt = new TypescriptFormatExtension(); + addStep(tsfmt.createStep()); + return tsfmt; + } + + public class TypescriptFormatExtension extends NpmStepConfig { + + private Map config = Collections.emptyMap(); + + @Nullable + TsConfigFileType configFileType = null; + + @Nullable + Object configFilePath = null; + + public void config(final Map config) { + this.config = new TreeMap<>(requireNonNull(config)); + replaceStep(createStep()); + } + + public void tsconfigFile(final Object path) { + configFile(TsConfigFileType.TSCONFIG, path); + } + + public void tslintFile(final Object path) { + configFile(TsConfigFileType.TSLINT, path); + } + + public void vscodeFile(final Object path) { + configFile(TsConfigFileType.VSCODE, path); + } + + public void tsfmtFile(final Object path) { + configFile(TsConfigFileType.TSFMT, path); + } + + private void configFile(TsConfigFileType filetype, Object path) { + this.configFileType = requireNonNull(filetype); + this.configFilePath = requireNonNull(path); + replaceStep(createStep()); + } + + public FormatterStep createStep() { + final Project project = getProject(); + + return TsFmtFormatterStep.create( + GradleProvisioner.fromProject(project), + project.getBuildDir(), + npmFileOrNull(), + project.getProjectDir(), + typedConfigFile(), + config); + } + + private TypedTsFmtConfigFile typedConfigFile() { + if (this.configFileType != null && this.configFilePath != null) { + return new TypedTsFmtConfigFile(this.configFileType, getProject().file(this.configFilePath)); + } + return null; + } + } + + @Override + public PrettierConfig prettier() { + PrettierConfig prettierConfig = new TypescriptPrettierConfig(); + addStep(prettierConfig.createStep()); + return prettierConfig; + } + + /** + * Overrides the parser to be set to typescript, no matter what the user's config says. + */ + public class TypescriptPrettierConfig extends PrettierConfig { + @Override + FormatterStep createStep() { + fixParserToTypescript(); + return super.createStep(); + } + + private void fixParserToTypescript() { + if (this.prettierConfig == null) { + this.prettierConfig = Collections.singletonMap("parser", "typescript"); + } else { + final Object replaced = this.prettierConfig.put("parser", "typescript"); + if (replaced != null) { + getProject().getLogger().warn("overriding parser option to 'typescript'. Was set to '{}'", replaced); + } + } + } + } + + @Override + protected void setupTask(SpotlessTask task) { + // defaults to all typescript files + if (target == null) { + target = parseTarget("**/*.ts"); + } + super.setupTask(task); + } +} 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 new file mode 100644 index 0000000000..7a0d374e49 --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/PrettierIntegrationTest.java @@ -0,0 +1,67 @@ +/* + * 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.gradle.spotless; + +import java.io.IOException; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import com.diffplug.spotless.category.NpmTest; + +@Category(NpmTest.class) +public class PrettierIntegrationTest extends GradleIntegrationTest { + @Test + public void useInlineConfig() throws IOException { + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "def prettierConfig = [:]", + "prettierConfig['printWidth'] = 50", + "prettierConfig['parser'] = 'typescript'", + "spotless {", + " format 'mytypescript', {", + " target 'test.ts'", + " prettier().config(prettierConfig)", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean"); + } + + @Test + public void useFileConfig() throws IOException { + setFile(".prettierrc.yml").toResource("npm/prettier/config/.prettierrc.yml"); + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "spotless {", + " format 'mytypescript', {", + " target 'test.ts'", + " prettier().configFile('.prettierrc.yml')", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/config/typescript.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/prettier/config/typescript.configfile.clean"); + } + +} diff --git a/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TypescriptExtensionTest.java b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TypescriptExtensionTest.java new file mode 100644 index 0000000000..6904b62b9c --- /dev/null +++ b/plugin-gradle/src/test/java/com/diffplug/gradle/spotless/TypescriptExtensionTest.java @@ -0,0 +1,88 @@ +/* + * 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.gradle.spotless; + +import java.io.IOException; + +import org.junit.Test; +import org.junit.experimental.categories.Category; + +import com.diffplug.spotless.category.NpmTest; + +@Category(NpmTest.class) +public class TypescriptExtensionTest extends GradleIntegrationTest { + @Test + public void useTsfmtInlineConfig() throws IOException { + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "def tsfmtconfig = [:]", + "tsfmtconfig['indentSize'] = 1", + "tsfmtconfig['convertTabsToSpaces'] = true", + "spotless {", + " typescript {", + " target 'test.ts'", + " tsfmt().config(tsfmtconfig)", + " }", + "}"); + setFile("test.ts").toResource("npm/tsfmt/tsfmt/tsfmt.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/tsfmt/tsfmt/tsfmt.clean"); + } + + @Test + public void useTsfmtFileConfig() throws IOException { + setFile("tsfmt.json").toLines( + "{", + " \"indentSize\": 1,", + " \"convertTabsToSpaces\": true", + "}"); + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "spotless {", + " typescript {", + " target 'test.ts'", + " tsfmt().tsfmtFile('tsfmt.json')", + " }", + "}"); + setFile("test.ts").toResource("npm/tsfmt/tsfmt/tsfmt.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/tsfmt/tsfmt/tsfmt.clean"); + } + + @Test + public void usePrettier() throws IOException { + setFile("build.gradle").toLines( + "buildscript { repositories { mavenCentral() } }", + "plugins {", + " id 'com.diffplug.gradle.spotless'", + "}", + "spotless {", + " typescript {", + " target 'test.ts'", + " prettier()", + " }", + "}"); + setFile("test.ts").toResource("npm/prettier/filetypes/typescript/typescript.dirty"); + gradleRunner().withArguments("--stacktrace", "spotlessApply").build(); + assertFile("test.ts").sameAsResource("npm/prettier/filetypes/typescript/typescript.clean"); + } +} diff --git a/testlib/build.gradle b/testlib/build.gradle index 8aa81bd9a9..752d26f0eb 100644 --- a/testlib/build.gradle +++ b/testlib/build.gradle @@ -15,3 +15,6 @@ dependencies { // we'll hold the testlib to a low standard (prize brevity) spotbugs { reportLevel = 'high' } // low|medium|high (low = sensitive to even minor mistakes) +test { useJUnit { excludeCategories 'com.diffplug.spotless.category.NpmTest' } } + +task npmTest(type: Test) { useJUnit { includeCategories 'com.diffplug.spotless.category.NpmTest' } } diff --git a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java index 9d90b24a59..a119ea3608 100644 --- a/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/ResourceHarness.java @@ -86,6 +86,15 @@ protected File newFile(String subpath) throws IOException { return new File(rootFolder(), subpath); } + /** Creates and returns a new child-folder of the root folder. */ + protected File newFolder(String subpath) throws IOException { + File targetDir = newFile(subpath); + if (!targetDir.mkdir()) { + throw new IOException("Failed to create " + targetDir); + } + return targetDir; + } + protected String read(String path) throws IOException { return read(newFile(path).toPath(), StandardCharsets.UTF_8); } diff --git a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java index 096b7ec1b3..a21c0f55f1 100644 --- a/testlib/src/main/java/com/diffplug/spotless/StepHarness.java +++ b/testlib/src/main/java/com/diffplug/spotless/StepHarness.java @@ -24,7 +24,7 @@ import org.junit.Assert; /** An api for adding test cases. */ -public class StepHarness { +public class StepHarness implements AutoCloseable { private final FormatterFunc formatter; /** Creates a StepHarness around the given FormatterFunc. */ @@ -37,12 +37,20 @@ public static StepHarness forStep(FormatterStep step) { // We don't care if an individual FormatterStep is misbehaving on line-endings, because // Formatter fixes that. No reason to care in tests either. It's likely to pop up when // running tests on Windows from time-to-time - return new StepHarness(input -> LineEnding.toUnix(step.format(input, new File("")))); + return new StepHarness(FormatterFunc.Closeable.of( + () -> { + if (step instanceof FormatterStepImpl.Standard) { + ((FormatterStepImpl.Standard) step).cleanupFormatterFunc(); + } + }, + input -> LineEnding.toUnix(step.format(input, new File(""))))); } /** Creates a harness for testing a formatter whose steps don't depend on the file. */ public static StepHarness forFormatter(Formatter formatter) { - return new StepHarness(input -> formatter.compute(input, new File(""))); + return new StepHarness(FormatterFunc.Closeable.of( + formatter::close, + input -> formatter.compute(input, new File("")))); } /** Asserts that the given element is transformed as expected, and that the result is idempotent. */ @@ -84,4 +92,11 @@ public StepHarness testException(String resourceBefore, Consumer Nested blockquotes +> +> > inner +> > +> > > even more inner + +- list +- list2 + - sublist sublist + * 2 + - 3 +- list3 + - sublist2 + - sublist2 + +Text text text +text text + +text2. diff --git a/testlib/src/main/resources/npm/prettier/filetypes/markdown/markdown.dirty b/testlib/src/main/resources/npm/prettier/filetypes/markdown/markdown.dirty new file mode 100644 index 0000000000..6a0c12c119 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/markdown/markdown.dirty @@ -0,0 +1,20 @@ +# Title + +> Nested blockquotes +>> inner +>> > even more inner + + ++ list ++ list2 + - sublist sublist + * 2 + + 3 ++ list3 + - sublist2 + - sublist2 + +Text text text + text text + +text2. diff --git a/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml new file mode 100644 index 0000000000..9919fdc796 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/scss/.prettierrc.yml @@ -0,0 +1 @@ +parser: postcss diff --git a/testlib/src/main/resources/npm/prettier/filetypes/scss/scss.clean b/testlib/src/main/resources/npm/prettier/filetypes/scss/scss.clean new file mode 100644 index 0000000000..2692f4221e --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/scss/scss.clean @@ -0,0 +1,35 @@ +p, +#content, +.basic-text, +.custom-long-class-name, +.second-long-class-name, +.third-long-class-name, +.any-class { + font-size: 12 px; + padding: 0px 0px 0px 0px; +} + +div.custom-layout { + div.layout-element { + padding-left: 5px; + padding-right: 5px; + } +} + +@mixin transform($property) { + -webkit-transform: $property; + -ms-transform: $property; + transform: $property; +} + +.box { + @include transform(rotate(30deg)); +} + +.almost-fullwidth { + width: calc(100%- 20px); +} + +.calculated-width { + width: 300px / 960px * 100%; +} diff --git a/testlib/src/main/resources/npm/prettier/filetypes/scss/scss.dirty b/testlib/src/main/resources/npm/prettier/filetypes/scss/scss.dirty new file mode 100644 index 0000000000..79b963af75 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/scss/scss.dirty @@ -0,0 +1,34 @@ + + + +p, +#content,.basic-text,.custom-long-class-name,.second-long-class-name,.third-long-class-name,.any-class{ +font-size:12 px; + padding: 0px 0px 0px 0px; +} + + +div.custom-layout {div.layout-element { +padding-left:5px; padding-right:5px +}} + +@mixin transform($property) { -webkit-transform: $property; + -ms-transform: $property; + transform: $property; +} + + + + +.box { @include transform(rotate(30deg)); } + + +.almost-fullwidth { +width:calc(100%- 20px) +} + + + +.calculated-width { +width: 300px / 960px * 100% +} diff --git a/testlib/src/main/resources/npm/prettier/filetypes/typescript/.prettierrc.yml b/testlib/src/main/resources/npm/prettier/filetypes/typescript/.prettierrc.yml new file mode 100644 index 0000000000..dc710caa92 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/typescript/.prettierrc.yml @@ -0,0 +1 @@ +parser: typescript diff --git a/testlib/src/main/resources/npm/prettier/filetypes/typescript/typescript.clean b/testlib/src/main/resources/npm/prettier/filetypes/typescript/typescript.clean new file mode 100644 index 0000000000..c8d17e7233 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/typescript/typescript.clean @@ -0,0 +1,7 @@ +class Sample { + hello(word = "world") { + return "Hello, " + word; + } +} + +var s = new Sample(); diff --git a/testlib/src/main/resources/npm/prettier/filetypes/typescript/typescript.dirty b/testlib/src/main/resources/npm/prettier/filetypes/typescript/typescript.dirty new file mode 100644 index 0000000000..e2329f4f00 --- /dev/null +++ b/testlib/src/main/resources/npm/prettier/filetypes/typescript/typescript.dirty @@ -0,0 +1,5 @@ +class Sample { +hello(word="world"){return "Hello, " + word;} +} + +var s=new Sample(); diff --git a/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.clean b/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.clean new file mode 100644 index 0000000000..48296fcf7e --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.clean @@ -0,0 +1,6 @@ +class Sample { + hello(word: string = "world"): string { return "Hello, " + word; } +} + +var s: Sample = new Sample(); +if (s === s) { console.log(s.hello()); } diff --git a/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.dirty b/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.dirty new file mode 100644 index 0000000000..8ee345f189 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.dirty @@ -0,0 +1,6 @@ +class Sample { +hello(word:string="world"):string{return "Hello, " + word;} +} + +var s:Sample=new Sample(); +if(s===s){console.log(s.hello());} diff --git a/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.json b/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.json new file mode 100644 index 0000000000..4b233e423e --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tsconfig/tsconfig.json @@ -0,0 +1,4 @@ +{ + // comment + /* comment */ +} diff --git a/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.clean b/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.clean new file mode 100644 index 0000000000..822424e9c3 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.clean @@ -0,0 +1,5 @@ +class Sample { + hello(word = "world") { return "Hello, " + word; } +} + +var s = new Sample(); diff --git a/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.dirty b/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.dirty new file mode 100644 index 0000000000..e2329f4f00 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.dirty @@ -0,0 +1,5 @@ +class Sample { +hello(word="world"){return "Hello, " + word;} +} + +var s=new Sample(); diff --git a/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.json b/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.json new file mode 100644 index 0000000000..52c343f843 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tsfmt/tsfmt.json @@ -0,0 +1,4 @@ +{ + "indentSize": 1, + "convertTabsToSpaces": true +} diff --git a/testlib/src/main/resources/npm/tsfmt/tslint/tslint.clean b/testlib/src/main/resources/npm/tsfmt/tslint/tslint.clean new file mode 100644 index 0000000000..48296fcf7e --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tslint/tslint.clean @@ -0,0 +1,6 @@ +class Sample { + hello(word: string = "world"): string { return "Hello, " + word; } +} + +var s: Sample = new Sample(); +if (s === s) { console.log(s.hello()); } diff --git a/testlib/src/main/resources/npm/tsfmt/tslint/tslint.dirty b/testlib/src/main/resources/npm/tsfmt/tslint/tslint.dirty new file mode 100644 index 0000000000..8ee345f189 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tslint/tslint.dirty @@ -0,0 +1,6 @@ +class Sample { +hello(word:string="world"):string{return "Hello, " + word;} +} + +var s:Sample=new Sample(); +if(s===s){console.log(s.hello());} diff --git a/testlib/src/main/resources/npm/tsfmt/tslint/tslint.json b/testlib/src/main/resources/npm/tsfmt/tslint/tslint.json new file mode 100644 index 0000000000..4688d5f1e6 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/tslint/tslint.json @@ -0,0 +1,83 @@ +{ + "rules": { + "ban": [true, + ["_", "extend"], + ["_", "isNull"], + ["_", "isDefined"] + ], + "class-name": true, + "comment-format": [true, + "check-space", + "check-lowercase" + ], + "curly": true, + "eofline": true, + "forin": true, + "indent": [true, 4], + "interface-name": true, + "jsdoc-format": true, + "label-position": true, + "label-undefined": true, + "max-line-length": [true, 140], + "no-arg": true, + "no-bitwise": true, + "no-console": [true, + "debug", + "info", + "time", + "timeEnd", + "trace" + ], + "no-construct": true, + "no-debugger": true, + "no-duplicate-key": true, + "no-duplicate-variable": true, + "no-empty": true, + "no-eval": true, + "no-string-literal": true, + "trailing-comma": [true, { + "singleline": "never", + "multiline": "always" + }], + "no-trailing-whitespace": true, + "no-unused-expression": true, + "no-unused-variable": true, + "no-unreachable": true, + "no-use-before-declare": true, + "one-line": [true, + "check-open-brace", + "check-catch", + "check-else", + "check-whitespace" + ], + "quotemark": [true, "double"], + "radix": true, + "semicolon": true, + "triple-equals": [true, "allow-null-check"], + "typedef": [true, + "callSignature", + "catchClause", + "indexSignature", + "parameter", + "propertySignature", + "variableDeclarator" + ], + "typedef-whitespace": [true, + ["callSignature", "noSpace"], + ["catchClause", "noSpace"], + ["indexSignature", "space"] + ], + "use-strict": [true, + "check-module", + "check-function" + ], + "variable-name": false, + "whitespace": [true, + "check-branch", + "check-decl", + "check-operator", + "check-separator", + "check-type" + ] + } +} diff --git a/testlib/src/main/resources/npm/tsfmt/vscode/vscode.clean b/testlib/src/main/resources/npm/tsfmt/vscode/vscode.clean new file mode 100644 index 0000000000..50cc165af1 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/vscode/vscode.clean @@ -0,0 +1,7 @@ +class Sample +{ + hello ( word: string = "world" ): string { return "Hello, " + word; } +} + +var s: Sample = new Sample(); +if ( s === s ) { console.log( s.hello() ); } diff --git a/testlib/src/main/resources/npm/tsfmt/vscode/vscode.dirty b/testlib/src/main/resources/npm/tsfmt/vscode/vscode.dirty new file mode 100644 index 0000000000..8ee345f189 --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/vscode/vscode.dirty @@ -0,0 +1,6 @@ +class Sample { +hello(word:string="world"):string{return "Hello, " + word;} +} + +var s:Sample=new Sample(); +if(s===s){console.log(s.hello());} diff --git a/testlib/src/main/resources/npm/tsfmt/vscode/vscode.json b/testlib/src/main/resources/npm/tsfmt/vscode/vscode.json new file mode 100644 index 0000000000..b931d84a6d --- /dev/null +++ b/testlib/src/main/resources/npm/tsfmt/vscode/vscode.json @@ -0,0 +1,19 @@ +{ + // comment + "typescript.format.enable": true, + "typescript.format.insertSpaceAfterCommaDelimiter": true, + "typescript.format.insertSpaceAfterConstructor": true, + "typescript.format.insertSpaceAfterSemicolonInForStatements": true, + "typescript.format.insertSpaceBeforeAndAfterBinaryOperators": true, + "typescript.format.insertSpaceAfterKeywordsInControlFlowStatements": true, + "typescript.format.insertSpaceAfterFunctionKeywordForAnonymousFunctions": true, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyParenthesis": true, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBrackets": true, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingNonemptyBraces": true, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingTemplateStringBraces": true, + "typescript.format.insertSpaceAfterOpeningAndBeforeClosingJsxExpressionBraces": true, + "typescript.format.insertSpaceAfterTypeAssertion": true, + "typescript.format.insertSpaceBeforeFunctionParenthesis": true, + "typescript.format.placeOpenBraceOnNewLineForFunctions": true, + "typescript.format.placeOpenBraceOnNewLineForControlBlocks": true +} diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/NpmFormatterStepCommonTests.java b/testlib/src/test/java/com/diffplug/spotless/npm/NpmFormatterStepCommonTests.java new file mode 100644 index 0000000000..79b58ab5a9 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/npm/NpmFormatterStepCommonTests.java @@ -0,0 +1,37 @@ +/* + * 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.IOException; + +import com.diffplug.spotless.ResourceHarness; + +public abstract class NpmFormatterStepCommonTests extends ResourceHarness { + + protected File npmExecutable() { + return NpmExecutableResolver.tryFind().orElseThrow(() -> new IllegalStateException("cannot detect node binary")); + } + + private File buildDir = null; + + protected File buildDir() throws IOException { + if (this.buildDir == null) { + this.buildDir = newFolder("build-dir"); + } + return this.buildDir; + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java new file mode 100644 index 0000000000..508a49a28a --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/npm/PrettierFormatterStepTest.java @@ -0,0 +1,126 @@ +/* + * 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.Arrays; + +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import com.diffplug.common.collect.ImmutableMap; +import com.diffplug.spotless.*; +import com.diffplug.spotless.category.NpmTest; + +@Category(NpmTest.class) +@RunWith(Enclosed.class) +public class PrettierFormatterStepTest { + + @Category(NpmTest.class) + @RunWith(Parameterized.class) + public static class PrettierFormattingOfFileTypesIsWorking extends NpmFormatterStepCommonTests { + + @Parameterized.Parameter + public String fileType; + + @Parameterized.Parameters(name = "{index}: prettier can be applied to {0}") + public static Iterable formattingConfigFiles() { + return Arrays.asList("typescript", "json", "javascript-es5", "javascript-es6", "css", "scss", "markdown"); + } + + @Test + public void formattingUsingConfigFile() throws Exception { + String filedir = "npm/prettier/filetypes/" + fileType + "/"; + + final File prettierRc = createTestFile(filedir + ".prettierrc.yml"); + final String dirtyFile = filedir + fileType + ".dirty"; + final String cleanFile = filedir + fileType + ".clean"; + + final FormatterStep formatterStep = PrettierFormatterStep.create( + TestProvisioner.mavenCentral(), + buildDir(), + npmExecutable(), + new PrettierConfig(prettierRc, null)); + + try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) { + stepHarness.testResource(dirtyFile, cleanFile); + } + } + } + + @Category(NpmTest.class) + public static class SpecificPrettierFormatterStepTests extends NpmFormatterStepCommonTests { + + @Test + public void parserInferenceIsWorking() throws Exception { + String filedir = "npm/prettier/filetypes/json/"; + + final String dirtyFile = filedir + "json.dirty"; + final String cleanFile = filedir + "json.clean"; + + final FormatterStep formatterStep = PrettierFormatterStep.create( + TestProvisioner.mavenCentral(), + buildDir(), + npmExecutable(), + new PrettierConfig(null, ImmutableMap.of("filepath", "anyname.json"))); // should select parser based on this name + + try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) { + stepHarness.testResource(dirtyFile, cleanFile); + } + } + } + + @Category(NpmTest.class) + public static class PrettierFormattingOptionsAreWorking extends NpmFormatterStepCommonTests { + + private static final String FILEDIR = "npm/prettier/config/"; + + public void runFormatTest(PrettierConfig config, String cleanFileNameSuffix) throws Exception { + + final String dirtyFile = FILEDIR + "typescript.dirty"; + final String cleanFile = FILEDIR + "typescript." + cleanFileNameSuffix + ".clean"; + + final FormatterStep formatterStep = PrettierFormatterStep.create( + TestProvisioner.mavenCentral(), + buildDir(), + npmExecutable(), + config); // should select parser based on this name + + try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) { + stepHarness.testResource(dirtyFile, cleanFile); + } + } + + @Test + public void defaultsAreApplied() throws Exception { + runFormatTest(new PrettierConfig(null, ImmutableMap.of("parser", "typescript")), "defaults"); + } + + @Test + public void configFileOptionsAreApplied() throws Exception { + runFormatTest(new PrettierConfig(createTestFile(FILEDIR + ".prettierrc.yml"), null), "configfile"); + } + + @Test + public void configFileOptionsCanBeOverriden() throws Exception { + runFormatTest(new PrettierConfig(createTestFile(FILEDIR + ".prettierrc.yml"), ImmutableMap.of("printWidth", 300)), "override"); + } + + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/SimpleJsonWriterTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/SimpleJsonWriterTest.java new file mode 100644 index 0000000000..787bba49ff --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/npm/SimpleJsonWriterTest.java @@ -0,0 +1,75 @@ +/* + * 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 org.hamcrest.CoreMatchers.equalTo; +import static org.junit.Assert.*; + +import java.io.File; +import java.io.IOException; + +import org.junit.Test; + +import com.diffplug.common.collect.ImmutableMap; +import com.diffplug.spotless.ResourceHarness; + +public class SimpleJsonWriterTest extends ResourceHarness { + + private SimpleJsonWriter jsonWriter = new SimpleJsonWriter(); + + @Test + public void itWritesAValidEmptyObject() { + assertThat(jsonWriter.toJsonString().replaceAll("\\s", ""), equalTo("{}")); + } + + @Test + public void itWritesABooleanProperty() { + jsonWriter.put("mybool", true); + assertThat(jsonWriter.toJsonString(), equalTo("{\n \"mybool\": true\n}")); + } + + @Test + public void itWritesAStringProperty() { + jsonWriter.put("mystring", "stringvalue"); + assertThat(jsonWriter.toJsonString(), equalTo("{\n \"mystring\": \"stringvalue\"\n}")); + } + + @Test + public void itWritesAnInteger() { + jsonWriter.put("myint", 7); + assertThat(jsonWriter.toJsonString(), equalTo("{\n \"myint\": 7\n}")); + } + + @Test(expected = IllegalArgumentException.class) + public void itFailsOnUnsupportedObject() { + jsonWriter.put("anyobj", new Object()); + fail("should not be accepted"); + } + + @Test + public void itHandlesSeveralOptionsSimultaneously() { + jsonWriter.putAll(ImmutableMap.of("mystring", "stringvalue", "intvalue", 1)); + assertThat(jsonWriter.toJsonString(), equalTo("{\n \"mystring\": \"stringvalue\",\n \"intvalue\": 1\n}")); + } + + @Test + public void itWritesToFile() throws IOException { + jsonWriter.put("mystring", "stringvalue"); + final File file = newFile("target.json"); + jsonWriter.toJsonFile(file); + assertFile(file).hasContent("{\n \"mystring\": \"stringvalue\"\n}"); + } +} diff --git a/testlib/src/test/java/com/diffplug/spotless/npm/TsFmtFormatterStepTest.java b/testlib/src/test/java/com/diffplug/spotless/npm/TsFmtFormatterStepTest.java new file mode 100644 index 0000000000..773f355ecf --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/npm/TsFmtFormatterStepTest.java @@ -0,0 +1,96 @@ +/* + * 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.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.util.Arrays; +import java.util.Collections; + +import org.junit.Test; +import org.junit.experimental.categories.Category; +import org.junit.experimental.runners.Enclosed; +import org.junit.runner.RunWith; +import org.junit.runners.Parameterized; + +import com.diffplug.common.collect.ImmutableMap; +import com.diffplug.spotless.*; +import com.diffplug.spotless.category.NpmTest; + +@Category(NpmTest.class) +@RunWith(Enclosed.class) +public class TsFmtFormatterStepTest { + + @Category(NpmTest.class) + @RunWith(Parameterized.class) + public static class TsFmtUsingVariousFormattingFilesTest extends NpmFormatterStepCommonTests { + @Parameterized.Parameter + public String formattingConfigFile; + + @Parameterized.Parameters(name = "{index}: formatting using {0} is working") + public static Iterable formattingConfigFiles() { + return Arrays.asList("vscode/vscode.json", "tslint/tslint.json", "tsfmt/tsfmt.json", "tsconfig/tsconfig.json"); + } + + @Test + public void formattingUsingConfigFile() throws Exception { + String configFileName = formattingConfigFile.substring(formattingConfigFile.lastIndexOf('/') >= 0 ? formattingConfigFile.lastIndexOf('/') + 1 : 0); + String configFileNameWithoutExtension = configFileName.substring(0, configFileName.lastIndexOf('.')); + String filedir = "npm/tsfmt/" + configFileNameWithoutExtension + "/"; + + final File configFile = createTestFile(filedir + configFileName); + final String dirtyFile = filedir + configFileNameWithoutExtension + ".dirty"; + final String cleanFile = filedir + configFileNameWithoutExtension + ".clean"; + + // some config options expect to see at least one file in the baseDir, so let's write one there + Files.write(new File(configFile.getParentFile(), configFileNameWithoutExtension + ".ts").toPath(), getTestResource(dirtyFile).getBytes(StandardCharsets.UTF_8)); + + final FormatterStep formatterStep = TsFmtFormatterStep.create( + TestProvisioner.mavenCentral(), + buildDir(), + npmExecutable(), + configFile.getParentFile(), + TypedTsFmtConfigFile.named(configFileNameWithoutExtension, configFile), + Collections.emptyMap()); + + try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) { + stepHarness.testResource(dirtyFile, cleanFile); + } + } + } + + @Category(NpmTest.class) + public static class TsFmtUsingInlineConfigTest extends NpmFormatterStepCommonTests { + @Test + public void formattingUsingInlineConfigWorks() throws Exception { + + final ImmutableMap inlineConfig = ImmutableMap.of("indentSize", 1, "convertTabsToSpaces", true); + + final FormatterStep formatterStep = TsFmtFormatterStep.create( + TestProvisioner.mavenCentral(), + buildDir(), + npmExecutable(), + buildDir().getAbsoluteFile(), + null, + inlineConfig); + + try (StepHarness stepHarness = StepHarness.forStep(formatterStep)) { + stepHarness.testResource("npm/tsfmt/tsfmt/tsfmt.dirty", "npm/tsfmt/tsfmt/tsfmt.clean"); + } + } + } +}