From 56b6eab417aa183cc64e31f03048b7b1bf0e0f57 Mon Sep 17 00:00:00 2001 From: Nicolas PETERS Date: Sat, 2 Nov 2024 18:06:05 +0100 Subject: [PATCH] java21:reworks all the packaging --- palantir-java-format/build.gradle | 117 +++++++++++- .../java/java21/Java21InputAstVisitor.java | 170 ++++++++++++++++++ .../java21/Java21PreviewInputAstVisitor.java | 31 ++++ .../palantir/javaformat/java/Formatter.java | 27 ++- .../javaformat/java/FileBasedTests.java | 1 + .../javaformat/java/FileBased21Tests.java | 142 +++++++++++++++ .../java/FormatterIntegration21Test.java | 138 ++++++++++++++ .../javaformat/java/testdata21/I964.input | 23 +++ .../javaformat/java/testdata21/I964.output | 19 ++ .../java/FileBased21PreviewTests.java | 142 +++++++++++++++ .../FormatterIntegration21PreviewTest.java | 139 ++++++++++++++ .../java/testdata21Preview/I963.input | 15 ++ .../java/testdata21Preview/I963.output | 11 ++ 13 files changed, 969 insertions(+), 6 deletions(-) create mode 100644 palantir-java-format/src/java21/java/com/palantir/javaformat/java/java21/Java21InputAstVisitor.java create mode 100644 palantir-java-format/src/java21Preview/java/com/palantir/javaformat/java/java21/Java21PreviewInputAstVisitor.java create mode 100644 palantir-java-format/src/test21/java/com/palantir/javaformat/java/FileBased21Tests.java create mode 100644 palantir-java-format/src/test21/java/com/palantir/javaformat/java/FormatterIntegration21Test.java create mode 100644 palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.input create mode 100644 palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.output create mode 100644 palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FileBased21PreviewTests.java create mode 100644 palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FormatterIntegration21PreviewTest.java create mode 100644 palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.input create mode 100644 palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.output diff --git a/palantir-java-format/build.gradle b/palantir-java-format/build.gradle index bef1b8918..d700a5d0c 100644 --- a/palantir-java-format/build.gradle +++ b/palantir-java-format/build.gradle @@ -49,7 +49,7 @@ tasks.withType(JavaCompile).configureEach { options.compilerArgs += jvmArgList if (JavaVersion.current() < JavaVersion.VERSION_14) { - excludes = ['**/Java14InputAstVisitor.java'] + excludes = ['**/Java14InputAstVisitor.java', '**/Java21InputAstVisitor.java'] } } @@ -62,7 +62,7 @@ tasks.withType(Javadoc).configureEach { options.optionFiles << file('../gradle/javadoc.options') if (JavaVersion.current() < JavaVersion.VERSION_14) { - exclude '**/Java14InputAstVisitor.java' + excludes = ['**/Java14InputAstVisitor.java', '**/Java21InputAstVisitor.java'] } } @@ -77,15 +77,122 @@ tasks.named("test") { systemProperty 'junit.jupiter.execution.parallel.mode.default', 'concurrent' } -// necessary to compile Java14InputAstVisitor -idea { - module.languageLevel = new org.gradle.plugins.ide.idea.model.IdeaLanguageLevel(14) + +def target21Compiler = javaToolchains.compilerFor { + it.languageVersion = JavaLanguageVersion.of(21) +} + +def target21Launcher = javaToolchains.launcherFor { + it.languageVersion = JavaLanguageVersion.of(21) +} + + +sourceSets.create("java21") { +} + +sourceSets.create("test21") { +} + +tasks.named("compileJava21Java", JavaCompile) { + it.javaCompiler = target21Compiler + it.sourceCompatibility = 21 + it.targetCompatibility = 21 + it.source = sourceSets.named("java21").get().java + it.classpath = project.objects.fileCollection().from( + sourceSets.named("main").get().output, + sourceSets.named("main").get().compileClasspath, + sourceSets.named("java21").get().compileClasspath + ) +} + +tasks.named("compileTest21Java", JavaCompile) { + it.javaCompiler = target21Compiler + it.sourceCompatibility = 21 + it.targetCompatibility = 21 + it.source = sourceSets.named("test21").get().java + it.classpath = sourceSets.named("test").get().runtimeClasspath +} + +tasks.register("testJava21", Test) { + + it.group = "verification" + it.javaLauncher.set(target21Launcher.get()) + + useJUnitPlatform() + + it.testClassesDirs = project.objects.fileCollection().from( + sourceSets.named("test21").get().output.classesDirs + ) + it.classpath = project.objects.fileCollection().from( + sourceSets.named("test21").get().output, + sourceSets.named("java21").get().output, + sourceSets.named("test").get().runtimeClasspath + ) +} + + +sourceSets.create("java21Preview") { + java.srcDirs(project.layout.projectDirectory.dir("src/java21Preview/java")) +} + +sourceSets.create("test21Preview") { + java.srcDirs(project.layout.projectDirectory.dir("src/test21Preview/java")) } +tasks.named("compileJava21PreviewJava", JavaCompile) { + it.javaCompiler = target21Compiler + it.sourceCompatibility = 21 + it.targetCompatibility = 21 + options.compilerArgs += '--enable-preview' + it.source = sourceSets.named("java21Preview").get().java + it.classpath = objects.fileCollection().from( + sourceSets.named("main").get().output, + sourceSets.named("java21").get().output, + sourceSets.named("main").get().compileClasspath, + sourceSets.named("java21Preview").get().compileClasspath + + ) +} + +tasks.named("compileTest21PreviewJava", JavaCompile) { + it.javaCompiler = target21Compiler + it.sourceCompatibility = 21 + it.targetCompatibility = 21 + options.compilerArgs += '--enable-preview' + it.source = sourceSets.named("test21Preview").get().java + it.classpath = sourceSets.named("test").get().runtimeClasspath +} + +tasks.register("testJava21Preview", Test) { + + it.group = "verification" + it.javaLauncher.set(target21Launcher.get()) + jvmArgs += '--enable-preview' + useJUnitPlatform() + + it.testClassesDirs = project.objects.fileCollection().from( + sourceSets.named("test21Preview").get().output.classesDirs + ) + it.classpath = project.objects.fileCollection().from( + sourceSets.named("test21Preview").get().output, + sourceSets.named("java21Preview").get().output, + sourceSets.named("java21").get().output, + sourceSets.named("test").get().runtimeClasspath + ) +} + +tasks.check { dependsOn(test,testJava21,testJava21Preview) } + // This block may be replaced by BaselineExportsExtension exports // once https://github.com/gradle/gradle/issues/18824 is resolved. tasks.named("jar", Jar) { manifest { attributes('Add-Exports': exports.join(' ')) } + it.from( + sourceSets.named("main").get().output, + sourceSets.named("java21").get().output, + sourceSets.named("java21Preview").get().output + ) + } diff --git a/palantir-java-format/src/java21/java/com/palantir/javaformat/java/java21/Java21InputAstVisitor.java b/palantir-java-format/src/java21/java/com/palantir/javaformat/java/java21/Java21InputAstVisitor.java new file mode 100644 index 000000000..9b7b7794d --- /dev/null +++ b/palantir-java-format/src/java21/java/com/palantir/javaformat/java/java21/Java21InputAstVisitor.java @@ -0,0 +1,170 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java.java21; + +import com.google.common.collect.Iterables; +import com.palantir.javaformat.OpsBuilder; +import com.palantir.javaformat.java.JavaInputAstVisitor; +import com.palantir.javaformat.java.java14.Java14InputAstVisitor; +import com.sun.source.tree.BlockTree; +import com.sun.source.tree.CaseTree; +import com.sun.source.tree.ConstantCaseLabelTree; +import com.sun.source.tree.DeconstructionPatternTree; +import com.sun.source.tree.ParenthesizedTree; +import com.sun.source.tree.PatternCaseLabelTree; +import com.sun.source.tree.PatternTree; +import com.sun.source.tree.Tree; +import com.sun.source.tree.Tree.Kind; +import java.lang.reflect.Method; +import java.util.List; + +/** + * Extends {@link Java14InputAstVisitor} with support for AST nodes that were added or modified for Java 14. + */ +public class Java21InputAstVisitor extends Java14InputAstVisitor { + private static final Method CASE_TREE_GET_LABELS2 = maybeGetMethodPrivate(CaseTree.class, "getLabels"); + + private static Method maybeGetMethodPrivate(Class c, String name) { + try { + return c.getMethod(name); + } catch (ReflectiveOperationException e) { + return null; + } + } + + private static Object invokePrivate(Method m, Object target) { + try { + return m.invoke(target); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e.getMessage(), e); + } + } + + public Java21InputAstVisitor(OpsBuilder builder, int indentMultiplier) { + super(builder, indentMultiplier); + } + + @Override + public Void visitParenthesized(ParenthesizedTree node, Void unused) { + token("("); + Void v = scan(node.getExpression(), unused); + token(")"); + return v; + } + + @Override + public Void visitDeconstructionPattern(DeconstructionPatternTree node, Void unused) { + + Void r = scan(node.getDeconstructor(), unused); + token("("); + boolean firstInRow = true; + for (PatternTree item : node.getNestedPatterns()) { + if (!firstInRow) { + token(","); + builder.breakToFill(" "); + } + scan(item, null); + firstInRow = false; + } + token(")"); + return r; + } + + @Override + public Void visitConstantCaseLabel(ConstantCaseLabelTree node, Void unused) { + return scan(node.getConstantExpression(), unused); + } + + @Override + public Void visitCase(CaseTree node, Void unused) { + sync(node); + markForPartialFormat(); + builder.forcedBreak(); + List labels; + boolean isDefault; + if (CASE_TREE_GET_LABELS2 != null) { + labels = (List) invokePrivate(CASE_TREE_GET_LABELS2, node); + isDefault = labels.size() == 1 + && Iterables.getOnlyElement(labels).getKind().name().equals("DEFAULT_CASE_LABEL"); + } else { + labels = node.getExpressions(); + isDefault = labels.isEmpty(); + } + if (isDefault) { + token("default", plusTwo); + } else { + token("case", plusTwo); + builder.space(); + builder.open(labels.size() > 1 ? plusFour : JavaInputAstVisitor.ZERO); + boolean first = true; + for (Tree expression : labels) { + if (!first) { + token(","); + builder.breakOp(" "); + } + scan(expression, null); + first = false; + } + builder.close(); + } + switch (node.getCaseKind()) { + case STATEMENT: + token(":"); + boolean isBlock = node.getStatements().size() == 1 + && node.getStatements().get(0).getKind() == Kind.BLOCK; + builder.open(isBlock ? JavaInputAstVisitor.ZERO : plusTwo); + if (isBlock) { + builder.space(); + } + visitStatements(node.getStatements(), isBlock); + builder.close(); + break; + case RULE: + if (node.getGuard() != null) { + builder.space(); + token("when"); + builder.space(); + scan(node.getGuard(), null); + } + + builder.space(); + token("-"); + token(">"); + + builder.space(); + if (node.getBody().getKind() == Kind.BLOCK) { + // Explicit call with {@link CollapseEmptyOrNot.YES} to handle empty case blocks. + visitBlock( + (BlockTree) node.getBody(), + CollapseEmptyOrNot.YES, + AllowLeadingBlankLine.NO, + AllowTrailingBlankLine.NO); + } else { + scan(node.getBody(), null); + } + builder.guessToken(";"); + break; + default: + throw new IllegalArgumentException(node.getCaseKind().name()); + } + return null; + } + + public Void visitPatternCaseLabel(PatternCaseLabelTree node, Void p) { + return scan(node.getPattern(), p); + } +} diff --git a/palantir-java-format/src/java21Preview/java/com/palantir/javaformat/java/java21/Java21PreviewInputAstVisitor.java b/palantir-java-format/src/java21Preview/java/com/palantir/javaformat/java/java21/Java21PreviewInputAstVisitor.java new file mode 100644 index 000000000..53307a5cd --- /dev/null +++ b/palantir-java-format/src/java21Preview/java/com/palantir/javaformat/java/java21/Java21PreviewInputAstVisitor.java @@ -0,0 +1,31 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java.java21; + +import com.palantir.javaformat.OpsBuilder; +import com.sun.source.tree.AnyPatternTree; + +public class Java21PreviewInputAstVisitor extends Java21InputAstVisitor { + + public Java21PreviewInputAstVisitor(OpsBuilder builder, int indentMultiplier) { + super(builder, indentMultiplier); + } + + public Void visitAnyPattern(AnyPatternTree node, Void p) { + token("_"); + return p; + } +} diff --git a/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java b/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java index bf07ca2d5..8b001c2a8 100644 --- a/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java +++ b/palantir-java-format/src/main/java/com/palantir/javaformat/java/Formatter.java @@ -45,6 +45,7 @@ import com.sun.tools.javac.util.Log; import com.sun.tools.javac.util.Options; import java.io.IOException; +import java.lang.management.ManagementFactory; import java.net.URI; import java.util.Collection; import javax.tools.Diagnostic; @@ -131,7 +132,26 @@ static JavaOutput format( OpsBuilder opsBuilder = new OpsBuilder(javaInput); JavaInputAstVisitor visitor; - if (getRuntimeVersion() >= 14) { + + if (getRuntimeVersion() >= 21 && isEnabledPriew()) { + try { + visitor = Class.forName("com.palantir.javaformat.java.java21.Java21PreviewInputAstVisitor") + .asSubclass(JavaInputAstVisitor.class) + .getConstructor(OpsBuilder.class, int.class) + .newInstance(opsBuilder, options.indentationMultiplier()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e.getMessage(), e); + } + } else if (getRuntimeVersion() >= 21) { + try { + visitor = Class.forName("com.palantir.javaformat.java.java21.Java21InputAstVisitor") + .asSubclass(JavaInputAstVisitor.class) + .getConstructor(OpsBuilder.class, int.class) + .newInstance(opsBuilder, options.indentationMultiplier()); + } catch (ReflectiveOperationException e) { + throw new RuntimeException(e.getMessage(), e); + } + } else if (getRuntimeVersion() >= 14) { try { visitor = Class.forName("com.palantir.javaformat.java.java14.Java14InputAstVisitor") .asSubclass(JavaInputAstVisitor.class) @@ -206,6 +226,11 @@ static int getRuntimeVersion() { return Runtime.version().feature(); } + @VisibleForTesting + static boolean isEnabledPriew() { + return ManagementFactory.getRuntimeMXBean().getInputArguments().contains("--enable-preview"); + } + static boolean errorDiagnostic(Diagnostic input) { if (input.getKind() != Diagnostic.Kind.ERROR) { return false; diff --git a/palantir-java-format/src/test/java/com/palantir/javaformat/java/FileBasedTests.java b/palantir-java-format/src/test/java/com/palantir/javaformat/java/FileBasedTests.java index cf8b6af34..21b7cd9ab 100644 --- a/palantir-java-format/src/test/java/com/palantir/javaformat/java/FileBasedTests.java +++ b/palantir-java-format/src/test/java/com/palantir/javaformat/java/FileBasedTests.java @@ -50,6 +50,7 @@ public final class FileBasedTests { .putAll(15, "I603") .putAll(16, "I588") .putAll(17, "I683", "I684", "I696") + // .putAll(21, "I683", "I684", "I696") .build(); private final Class testClass; diff --git a/palantir-java-format/src/test21/java/com/palantir/javaformat/java/FileBased21Tests.java b/palantir-java-format/src/test21/java/com/palantir/javaformat/java/FileBased21Tests.java new file mode 100644 index 000000000..e4920c055 --- /dev/null +++ b/palantir-java-format/src/test21/java/com/palantir/javaformat/java/FileBased21Tests.java @@ -0,0 +1,142 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java; + +import static com.google.common.collect.MoreCollectors.toOptional; +import static com.google.common.io.Files.getFileExtension; +import static com.google.common.io.Files.getNameWithoutExtension; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.io.CharStreams; +import com.google.common.reflect.ClassPath; +import com.google.common.reflect.ClassPath.ResourceInfo; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +public final class FileBased21Tests { + // Test files that are only used when run with a minimum Java version + private static final ImmutableMultimap VERSIONED_TESTS = + ImmutableMultimap.builder() + // .putAll(21, "I959", "I960", "I961", "I962") + .putAll(21, "I964", "I965") + .build(); + + private final Class testClass; + /** The path prefix for all tests if loaded as resources. */ + private final Path resourcePrefix; + /** Where to output test outputs when recreating. */ + private final Path fullTestPath; + + public FileBased21Tests(Class testClass) { + this(testClass, testClass.getSimpleName()); + } + + public FileBased21Tests(Class testClass, String testDirName) { + this.resourcePrefix = + Paths.get(testClass.getPackage().getName().replace('.', '/')).resolve(testDirName); + this.testClass = testClass; + this.fullTestPath = Paths.get("src/test21/resources").resolve(resourcePrefix); + System.out.println(this.fullTestPath.normalize().toAbsolutePath().toString()); + } + + public static void assumeJavaVersionForTest(String testName) { + Optional maybeJavaVersion = + VERSIONED_TESTS.inverse().get(testName).stream().collect(toOptional()); + maybeJavaVersion.ifPresent(version -> Assumptions.assumeTrue( + Runtime.version().feature() >= version, String.format("Not running on jdk %d or later", version))); + } + + public List paramsAsNameInputOutput() throws IOException { + ClassLoader classLoader = testClass.getClassLoader(); + Map inputs = new TreeMap<>(); + Map outputs = new TreeMap<>(); + for (ResourceInfo resourceInfo : ClassPath.from(classLoader).getResources()) { + String resourceName = resourceInfo.getResourceName(); + Path resourceNamePath = Paths.get(resourceName); + if (resourceNamePath.startsWith(resourcePrefix)) { + Path subPath = resourcePrefix.relativize(resourceNamePath); + assertThat(subPath.getNameCount()) + .describedAs("bad testdata file names") + .isEqualTo(1); + String baseName = getNameWithoutExtension(subPath.getFileName().toString()); + String extension = getFileExtension(subPath.getFileName().toString()); + String contents; + try (InputStream stream = testClass.getClassLoader().getResourceAsStream(resourceName)) { + contents = CharStreams.toString(new InputStreamReader(stream, UTF_8)); + } + switch (extension) { + case "input": + inputs.put(baseName, contents); + break; + case "output": + outputs.put(baseName, contents); + break; + default: + } + } + } + List testInputs = new ArrayList<>(); + if (!isRecreate()) { + assertThat(outputs).describedAs("unmatched inputs and outputs").hasSameSizeAs(inputs); + } + for (Map.Entry entry : inputs.entrySet()) { + String fileName = entry.getKey(); + String input = inputs.get(fileName); + + String expectedOutput; + if (isRecreate()) { + expectedOutput = null; + } else { + assertThat(outputs).describedAs("unmatched input").containsKey(fileName); + expectedOutput = outputs.get(fileName); + } + testInputs.add(new Object[] {fileName, input, expectedOutput}); + } + return testInputs; + } + + public static boolean isRecreate() { + return Boolean.getBoolean("recreate"); + } + + private Path getOutputTestPath(String testName) { + return fullTestPath.resolve(testName + ".output"); + } + + public void writeFormatterOutput(String testName, String output) { + try (BufferedWriter writer = Files.newBufferedWriter(getOutputTestPath(testName))) { + writer.append(output); + } catch (IOException e) { + throw new RuntimeException("Couldn't recreate test output for " + testName, e); + } + } +} diff --git a/palantir-java-format/src/test21/java/com/palantir/javaformat/java/FormatterIntegration21Test.java b/palantir-java-format/src/test21/java/com/palantir/javaformat/java/FormatterIntegration21Test.java new file mode 100644 index 000000000..d179bceec --- /dev/null +++ b/palantir-java-format/src/test21/java/com/palantir/javaformat/java/FormatterIntegration21Test.java @@ -0,0 +1,138 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java; + +import static com.palantir.javaformat.java.FileBased21Tests.assumeJavaVersionForTest; +import static com.palantir.javaformat.java.FileBased21Tests.isRecreate; +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.javaformat.Newlines; +import com.palantir.javaformat.jupiter.ParameterizedClass; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@ExtendWith(ParameterizedClass.class) +@Execution(ExecutionMode.CONCURRENT) +public class FormatterIntegration21Test { + + private static FileBased21Tests tests = new FileBased21Tests(FormatterIntegration21Test.class, "testdata21"); + + @ParameterizedClass.Parameters(name = "{0}") + public static List data() throws IOException { + return tests.paramsAsNameInputOutput(); + } + + private final String name; + private final String input; + private final String expected; + private final String separator; + + public FormatterIntegration21Test(String name, String input, String expected) { + this.name = name; + this.input = input; + this.expected = expected; + this.separator = isRecreate() ? null : Newlines.getLineEnding(expected); + } + + /** + * If enabled, then {@link DebugRenderer} will produce an output at {@link DebugRenderer#getOutputFile()}. This can + * then be viewed in a browser by running the following once (after which it will auto-reload whenever the debug + * output file changes): + * + *
+     * cd debugger
+     * yarn start
+     * 
+ * + *

Warning: don't turn this on for all tests. The debugger will always write to the same file. + */ + private static boolean isDebugMode() { + return Boolean.getBoolean("debugOutput"); + } + + @TestTemplate + public void format() { + assumeJavaVersionForTest(name); + try { + String output = createFormatter().formatSource(input); + Files.writeString(Path.of("/tmp/" + name + ".output"), output); + if (isRecreate()) { + tests.writeFormatterOutput(name, output); + return; + } + assertThat(output).describedAs("bad output for " + name).isEqualTo(expected); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Formatter createFormatter() { + return new Formatter( + JavaFormatterOptions.builder() + .style(JavaFormatterOptions.Style.PALANTIR) + .build(), + isDebugMode()); + } + + // @TestTemplate + public void idempotentLF() { + assumeJavaVersionForTest(name); + Assumptions.assumeFalse(isRecreate(), "Not running when recreating test outputs"); + try { + String mangled = expected.replace(separator, "\n"); + String output = createFormatter().formatSource(mangled); + assertThat(output).describedAs("bad output for " + name).isEqualTo(mangled); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } + } + + // @TestTemplate + public void idempotentCR() { + assumeJavaVersionForTest(name); + Assumptions.assumeFalse(isRecreate(), "Not running when recreating test outputs"); + try { + String mangled = expected.replace(separator, "\r"); + String output = createFormatter().formatSource(mangled); + assertThat(output).describedAs("bad output for " + name).isEqualTo(mangled); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } + } + + @TestTemplate + public void idempotentCRLF() { + assumeJavaVersionForTest(name); + Assumptions.assumeFalse(isRecreate(), "Not running when recreating test outputs"); + try { + String mangled = expected.replace(separator, "\r\n"); + String output = createFormatter().formatSource(mangled); + assertThat(output).describedAs("bad output for " + name).isEqualTo(mangled); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } + } +} diff --git a/palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.input b/palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.input new file mode 100644 index 000000000..707d8688d --- /dev/null +++ b/palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.input @@ -0,0 +1,23 @@ + +public class I964 { + + record Point(int x, int y) {} + + public static void printSum(Object obj) { + if (obj instanceof Point p) { + int x = p.x(); + int y = p.y(); + System.out.println(x+y); + } + } + + public void JEP440(Object obj){ + + if (obj instanceof Point(int x, int y)) { + System.out.println(x+y); + } + + } + + +} \ No newline at end of file diff --git a/palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.output b/palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.output new file mode 100644 index 000000000..c7a98a98d --- /dev/null +++ b/palantir-java-format/src/test21/resources/com/palantir/javaformat/java/testdata21/I964.output @@ -0,0 +1,19 @@ +public class I964 { + + record Point(int x, int y) {} + + public static void printSum(Object obj) { + if (obj instanceof Point p) { + int x = p.x(); + int y = p.y(); + System.out.println(x + y); + } + } + + public void JEP440(Object obj) { + + if (obj instanceof Point(int x, int y)) { + System.out.println(x + y); + } + } +} diff --git a/palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FileBased21PreviewTests.java b/palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FileBased21PreviewTests.java new file mode 100644 index 000000000..8e85232f7 --- /dev/null +++ b/palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FileBased21PreviewTests.java @@ -0,0 +1,142 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java; + +import static com.google.common.collect.MoreCollectors.toOptional; +import static com.google.common.io.Files.getFileExtension; +import static com.google.common.io.Files.getNameWithoutExtension; +import static java.nio.charset.StandardCharsets.UTF_8; +import static org.assertj.core.api.Assertions.assertThat; + +import com.google.common.collect.ImmutableMultimap; +import com.google.common.io.CharStreams; +import com.google.common.reflect.ClassPath; +import com.google.common.reflect.ClassPath.ResourceInfo; +import java.io.BufferedWriter; +import java.io.IOException; +import java.io.InputStream; +import java.io.InputStreamReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.TreeMap; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@Execution(ExecutionMode.CONCURRENT) +public final class FileBased21PreviewTests { + // Test files that are only used when run with a minimum Java version + private static final ImmutableMultimap VERSIONED_TESTS = + ImmutableMultimap.builder() + // .putAll(21, "I959", "I960", "I961", "I962") + .putAll(21, "I964", "I965") + .build(); + + private final Class testClass; + /** The path prefix for all tests if loaded as resources. */ + private final Path resourcePrefix; + /** Where to output test outputs when recreating. */ + private final Path fullTestPath; + + public FileBased21PreviewTests(Class testClass) { + this(testClass, testClass.getSimpleName()); + } + + public FileBased21PreviewTests(Class testClass, String testDirName) { + this.resourcePrefix = + Paths.get(testClass.getPackage().getName().replace('.', '/')).resolve(testDirName); + this.testClass = testClass; + this.fullTestPath = Paths.get("src/test21Preview/resources").resolve(resourcePrefix); + System.out.println(this.fullTestPath.normalize().toAbsolutePath().toString()); + } + + public static void assumeJavaVersionForTest(String testName) { + Optional maybeJavaVersion = + VERSIONED_TESTS.inverse().get(testName).stream().collect(toOptional()); + maybeJavaVersion.ifPresent(version -> Assumptions.assumeTrue( + Runtime.version().feature() >= version, String.format("Not running on jdk %d or later", version))); + } + + public List paramsAsNameInputOutput() throws IOException { + ClassLoader classLoader = testClass.getClassLoader(); + Map inputs = new TreeMap<>(); + Map outputs = new TreeMap<>(); + for (ResourceInfo resourceInfo : ClassPath.from(classLoader).getResources()) { + String resourceName = resourceInfo.getResourceName(); + Path resourceNamePath = Paths.get(resourceName); + if (resourceNamePath.startsWith(resourcePrefix)) { + Path subPath = resourcePrefix.relativize(resourceNamePath); + assertThat(subPath.getNameCount()) + .describedAs("bad testdata file names") + .isEqualTo(1); + String baseName = getNameWithoutExtension(subPath.getFileName().toString()); + String extension = getFileExtension(subPath.getFileName().toString()); + String contents; + try (InputStream stream = testClass.getClassLoader().getResourceAsStream(resourceName)) { + contents = CharStreams.toString(new InputStreamReader(stream, UTF_8)); + } + switch (extension) { + case "input": + inputs.put(baseName, contents); + break; + case "output": + outputs.put(baseName, contents); + break; + default: + } + } + } + List testInputs = new ArrayList<>(); + if (!isRecreate()) { + assertThat(outputs).describedAs("unmatched inputs and outputs").hasSameSizeAs(inputs); + } + for (Map.Entry entry : inputs.entrySet()) { + String fileName = entry.getKey(); + String input = inputs.get(fileName); + + String expectedOutput; + if (isRecreate()) { + expectedOutput = null; + } else { + assertThat(outputs).describedAs("unmatched input").containsKey(fileName); + expectedOutput = outputs.get(fileName); + } + testInputs.add(new Object[] {fileName, input, expectedOutput}); + } + return testInputs; + } + + public static boolean isRecreate() { + return Boolean.getBoolean("recreate"); + } + + private Path getOutputTestPath(String testName) { + return fullTestPath.resolve(testName + ".output"); + } + + public void writeFormatterOutput(String testName, String output) { + try (BufferedWriter writer = Files.newBufferedWriter(getOutputTestPath(testName))) { + writer.append(output); + } catch (IOException e) { + throw new RuntimeException("Couldn't recreate test output for " + testName, e); + } + } +} diff --git a/palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FormatterIntegration21PreviewTest.java b/palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FormatterIntegration21PreviewTest.java new file mode 100644 index 000000000..135782391 --- /dev/null +++ b/palantir-java-format/src/test21Preview/java/com/palantir/javaformat/java/FormatterIntegration21PreviewTest.java @@ -0,0 +1,139 @@ +/* + * (c) Copyright 2024 Palantir Technologies Inc. All rights reserved. + * + * 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.palantir.javaformat.java; + +import static com.palantir.javaformat.java.FileBased21PreviewTests.assumeJavaVersionForTest; +import static com.palantir.javaformat.java.FileBased21PreviewTests.isRecreate; +import static org.assertj.core.api.Assertions.assertThat; + +import com.palantir.javaformat.Newlines; +import com.palantir.javaformat.jupiter.ParameterizedClass; +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import org.junit.jupiter.api.Assumptions; +import org.junit.jupiter.api.TestTemplate; +import org.junit.jupiter.api.extension.ExtendWith; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +@ExtendWith(ParameterizedClass.class) +@Execution(ExecutionMode.CONCURRENT) +public class FormatterIntegration21PreviewTest { + + private static FileBased21PreviewTests tests = + new FileBased21PreviewTests(FormatterIntegration21PreviewTest.class, "testdata21Preview"); + + @ParameterizedClass.Parameters(name = "{0}") + public static List data() throws IOException { + return tests.paramsAsNameInputOutput(); + } + + private final String name; + private final String input; + private final String expected; + private final String separator; + + public FormatterIntegration21PreviewTest(String name, String input, String expected) { + this.name = name; + this.input = input; + this.expected = expected; + this.separator = isRecreate() ? null : Newlines.getLineEnding(expected); + } + + /** + * If enabled, then {@link DebugRenderer} will produce an output at {@link DebugRenderer#getOutputFile()}. This can + * then be viewed in a browser by running the following once (after which it will auto-reload whenever the debug + * output file changes): + * + *

+     * cd debugger
+     * yarn start
+     * 
+ * + *

Warning: don't turn this on for all tests. The debugger will always write to the same file. + */ + private static boolean isDebugMode() { + return Boolean.getBoolean("debugOutput"); + } + + @TestTemplate + public void format() { + assumeJavaVersionForTest(name); + try { + String output = createFormatter().formatSource(input); + Files.writeString(Path.of("/tmp/" + name + ".output"), output); + if (isRecreate()) { + tests.writeFormatterOutput(name, output); + return; + } + assertThat(output).describedAs("bad output for " + name).isEqualTo(expected); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + private static Formatter createFormatter() { + return new Formatter( + JavaFormatterOptions.builder() + .style(JavaFormatterOptions.Style.PALANTIR) + .build(), + isDebugMode()); + } + + // @TestTemplate + public void idempotentLF() { + assumeJavaVersionForTest(name); + Assumptions.assumeFalse(isRecreate(), "Not running when recreating test outputs"); + try { + String mangled = expected.replace(separator, "\n"); + String output = createFormatter().formatSource(mangled); + assertThat(output).describedAs("bad output for " + name).isEqualTo(mangled); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } + } + + // @TestTemplate + public void idempotentCR() { + assumeJavaVersionForTest(name); + Assumptions.assumeFalse(isRecreate(), "Not running when recreating test outputs"); + try { + String mangled = expected.replace(separator, "\r"); + String output = createFormatter().formatSource(mangled); + assertThat(output).describedAs("bad output for " + name).isEqualTo(mangled); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } + } + + @TestTemplate + public void idempotentCRLF() { + assumeJavaVersionForTest(name); + Assumptions.assumeFalse(isRecreate(), "Not running when recreating test outputs"); + try { + String mangled = expected.replace(separator, "\r\n"); + String output = createFormatter().formatSource(mangled); + assertThat(output).describedAs("bad output for " + name).isEqualTo(mangled); + } catch (FormatterException e) { + throw new RuntimeException(String.format("Formatter crashed on %s", name), e); + } + } +} diff --git a/palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.input b/palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.input new file mode 100644 index 000000000..f3b236629 --- /dev/null +++ b/palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.input @@ -0,0 +1,15 @@ + +public class I964 { + + record Point(int x, int y) {} + + public void JEP395(Object obj){ + + if (obj instanceof Point(int x, _)) { + System.out.println(x); + } + + } + + +} \ No newline at end of file diff --git a/palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.output b/palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.output new file mode 100644 index 000000000..fcd944784 --- /dev/null +++ b/palantir-java-format/src/test21Preview/resources/com/palantir/javaformat/java/testdata21Preview/I963.output @@ -0,0 +1,11 @@ +public class I964 { + + record Point(int x, int y) {} + + public void JEP395(Object obj) { + + if (obj instanceof Point(int x, _)) { + System.out.println(x); + } + } +}