diff --git a/CHANGES.md b/CHANGES.md index 580b23b9fb..f61bebaa50 100644 --- a/CHANGES.md +++ b/CHANGES.md @@ -10,6 +10,8 @@ This document is intended for Spotless developers. We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `formatAnnotations()` step to correct formatting of Java type annotations. It puts type annotations on the same line as the type that they qualify. Run it after a Java formatting step, such as `googleJavaFormat()`. ([#1275](https://github.com/diffplug/spotless/pull/1275)) ## [2.29.0] - 2022-08-23 ### Added diff --git a/README.md b/README.md index f95ea7a7ca..fb1a3670e6 100644 --- a/README.md +++ b/README.md @@ -61,6 +61,7 @@ lib('java.ImportOrderStep') +'{{yes}} | {{yes}} lib('java.PalantirJavaFormatStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('java.RemoveUnusedImportsStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', extra('java.EclipseJdtFormatterStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', +lib('java.FormatAnnotationsStep') +'{{yes}} | {{yes}} | {{no}} | {{no}} |', lib('json.gson.GsonStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', lib('json.JsonSimpleStep') +'{{yes}} | {{no}} | {{no}} | {{no}} |', lib('kotlin.KtLintStep') +'{{yes}} | {{yes}} | {{yes}} | {{no}} |', @@ -104,6 +105,7 @@ extra('wtp.EclipseWtpFormatterStep') +'{{yes}} | {{yes}} | [`java.PalantirJavaFormatStep`](lib/src/main/java/com/diffplug/spotless/java/PalantirJavaFormatStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`java.RemoveUnusedImportsStep`](lib/src/main/java/com/diffplug/spotless/java/RemoveUnusedImportsStep.java) | :+1: | :+1: | :+1: | :white_large_square: | | [`java.EclipseJdtFormatterStep`](lib-extra/src/main/java/com/diffplug/spotless/extra/java/EclipseJdtFormatterStep.java) | :+1: | :+1: | :+1: | :white_large_square: | +| [`java.FormatAnnotationsStep`](lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java) | :+1: | :+1: | :white_large_square: | :white_large_square: | | [`json.gson.GsonStep`](lib/src/main/java/com/diffplug/spotless/json/gson/GsonStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`json.JsonSimpleStep`](lib/src/main/java/com/diffplug/spotless/json/JsonSimpleStep.java) | :+1: | :white_large_square: | :white_large_square: | :white_large_square: | | [`kotlin.KtLintStep`](lib/src/main/java/com/diffplug/spotless/kotlin/KtLintStep.java) | :+1: | :+1: | :+1: | :white_large_square: | diff --git a/gradle/spotless.gradle b/gradle/spotless.gradle index 96037b21a3..677630ac69 100644 --- a/gradle/spotless.gradle +++ b/gradle/spotless.gradle @@ -22,6 +22,7 @@ spotless { eclipse().configFile rootProject.file('gradle/spotless.eclipseformat.xml') trimTrailingWhitespace() removeUnusedImports() + // TODO: formatAnnotations() custom 'noInternalDeps', noInternalDepsClosure } } diff --git a/lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java b/lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java new file mode 100644 index 0000000000..2d01e305a0 --- /dev/null +++ b/lib/src/main/java/com/diffplug/spotless/java/FormatAnnotationsStep.java @@ -0,0 +1,474 @@ +/* + * Copyright 2022 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.java; + +import java.io.Serializable; +import java.util.Arrays; +import java.util.Collections; +import java.util.HashSet; +import java.util.List; +import java.util.Set; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +import com.diffplug.spotless.FormatterFunc; +import com.diffplug.spotless.FormatterStep; + +/** + * Some formatters put every annotation on its own line + * -- even type annotations, which should be on the same line as the type they qualify. + * This class corrects the formatting. + * This is useful as a postprocessing step after a Java formatter that is not cognizant of type annotations. + + *

+ * Note: A type annotation is an annotation that is meta-annotated with {@code @Target({ElementType.TYPE_USE})}. + */ +public final class FormatAnnotationsStep { + + /** + * Simple names of type annotations. + * A type annotation is an annotation that is meta-annotated with @Target({ElementType.TYPE_USE}). + * A type annotation should be formatted on the same line as the type it qualifies. + */ + private static final List defaultTypeAnnotations = + // Use simple names because Spotless has no access to the + // fully-qualified names or the definitions of the type qualifiers. + Arrays.asList( + // Type annotations from the Checker Framework and all + // the tools it supports: FindBugs, JetBrains (IntelliJ), + // Eclipse, NetBeans, Spring, JML, Android, etc. + "A", + "ACCBottom", + "Acceleration", + "ACCTop", + "AinferBottom", + "AlwaysSafe", + "Angle", + "AnnoWithStringArg", + "Area", + "ArrayLen", + "ArrayLenRange", + "ArrayWithoutPackage", + "AwtAlphaCompositingRule", + "AwtColorSpace", + "AwtCursorType", + "AwtFlowLayout", + "B", + "BinaryName", + "BinaryNameInUnnamedPackage", + "BinaryNameOrPrimitiveType", + "BinaryNameWithoutPackage", + "BoolVal", + "Bottom", + "BottomQualifier", + "BottomThis", + "BottomVal", + "C", + "CalledMethods", + "CalledMethodsBottom", + "CalledMethodsPredicate", + "CalledMethodsTop", + "CanonicalName", + "CanonicalNameAndBinaryName", + "CanonicalNameOrEmpty", + "CanonicalNameOrPrimitiveType", + "CCBottom", + "CCTop", + "cd", + "ClassBound", + "ClassGetName", + "ClassGetSimpleName", + "ClassVal", + "ClassValBottom", + "CompilerMessageKey", + "CompilerMessageKeyBottom", + "Constant", + "Critical", + "Current", + "D", + "DefaultType", + "degrees", + "Det", + "DotSeparatedIdentifiers", + "DotSeparatedIdentifiersOrPrimitiveType", + "DoubleVal", + "E", + "Encrypted", + "EnhancedRegex", + "EnumVal", + "Even", + "F", + "FBCBottom", + "FEBottom", + "FEBot", + "Fenum", + "FenumBottom", + "FenumTop", + "FETop", + "FieldDescriptor", + "FieldDescriptorForPrimitive", + "FieldDescriptorForPrimitiveOrArrayInUnnamedPackage", + "FieldDescriptorWithoutPackage", + "FlowExp", + "Force", + "Format", + "FormatBottom", + "FqBinaryName", + "Frequency", + "FullyQualifiedName", + "g", + "GTENegativeOne", + "GuardedBy", + "GuardedByBottom", + "GuardedByUnknown", + "GuardSatisfied", + "h", + "H1Bot", + "H1Invalid", + "H1Poly", + "H1S1", + "H1S2", + "H1Top", + "H2Bot", + "H2Poly", + "H2S1", + "H2S2", + "H2Top", + "Hz", + "I18nFormat", + "I18nFormatBottom", + "I18nFormatFor", + "I18nInvalidFormat", + "I18nUnknownFormat", + "Identifier", + "IdentifierOrArray", + "IdentifierOrPrimitiveType", + "ImplicitAnno", + "IndexFor", + "IndexOrHigh", + "IndexOrLow", + "Initialized", + "InitializedFields", + "InitializedFieldsBottom", + "InitializedFieldsPredicate", + "InternalForm", + "Interned", + "InternedDistinct", + "IntRange", + "IntVal", + "InvalidFormat", + "K", + "KeyFor", + "KeyForBottom", + "KeyForType", + "kg", + "kHz", + "km", + "km2", + "km3", + "kmPERh", + "kN", + "LbTop", + "LB_TOP", + "LeakedToResult", + "Length", + "LengthOf", + "LessThan", + "LessThanBottom", + "LessThanUnknown", + "LocalizableKey", + "LocalizableKeyBottom", + "Localized", + "LowerBoundBottom", + "LowerBoundUnknown", + "LTEqLengthOf", + "LTLengthOf", + "LTOMLengthOf", + "Luminance", + "m", + "m2", + "m3", + "Mass", + "MatchesRegex", + "MaybeAliased", + "MaybeDerivedFromConstant", + "MaybePresent", + "MaybeThis", + "MethodDescriptor", + "MethodVal", + "MethodValBottom", + "min", + "MinLen", + "mm", + "mm2", + "mm3", + "mol", + "MonotonicNonNull", + "MonotonicNonNullType", + "MonotonicOdd", + "mPERs", + "mPERs2", + "MustCall", + "MustCallAlias", + "MustCallUnknown", + "N", + "NegativeIndexFor", + "NewObject", + "NonConstant", + "NonDet", + "NonLeaked", + "NonNegative", + "NonNull", + "NonNullType", + "NonRaw", + "NotCalledMethods", + "NotNull", + "NotQualifier", + "NTDBottom", + "NTDMiddle", + "NTDSide", + "NTDTop", + "Nullable", + "NullableType", + "Odd", + "OptionalBottom", + "OrderNonDet", + "Parent", + "PatternA", + "PatternAB", + "PatternAC", + "PatternB", + "PatternBC", + "PatternBottomFull", + "PatternBottomPartial", + "PatternC", + "PatternUnknown", + "Poly", + "PolyAll", + "PolyConstant", + "PolyDet", + "PolyEncrypted", + "PolyFenum", + "PolyIndex", + "PolyInitializedFields", + "PolyInterned", + "PolyKeyFor", + "PolyLength", + "PolyLowerBound", + "PolyMustCall", + "PolyNull", + "PolyNullType", + "PolyPresent", + "PolyRaw", + "PolyReflection", + "PolyRegex", + "PolySameLen", + "PolySignature", + "PolySigned", + "PolyTainted", + "PolyTestAccumulation", + "PolyTypeDeclDefault", + "PolyUI", + "PolyUnit", + "PolyUpperBound", + "PolyValue", + "PolyVariableNameDefault", + "Positive", + "Present", + "PrimitiveType", + "PropertyKey", + "PropertyKeyBottom", + "PurityUnqualified", + "Qualifier", + "radians", + "Raw", + "ReflectBottom", + "Regex", + "RegexBottom", + "RegexNNGroups", + "ReportUnqualified", + "s", + "SameLen", + "SameLenBottom", + "SameLenUnknown", + "SearchIndexBottom", + "SearchIndexFor", + "SearchIndexUnknown", + "Sibling1", + "Sibling2", + "SiblingWithFields", + "SignatureBottom", + "Signed", + "SignednessBottom", + "SignednessGlb", + "SignedPositive", + "SignedPositiveFromUnsigned", + "Speed", + "StringVal", + "SubQual", + "Substance", + "SubstringIndexBottom", + "SubstringIndexFor", + "SubstringIndexUnknown", + "SuperQual", + "SwingBoxOrientation", + "SwingCompassDirection", + "SwingElementOrientation", + "SwingHorizontalOrientation", + "SwingSplitPaneOrientation", + "SwingTextOrientation", + "SwingTitleJustification", + "SwingTitlePosition", + "SwingVerticalOrientation", + "t", + "Tainted", + "Temperature", + "TestAccumulation", + "TestAccumulationBottom", + "TestAccumulationPredicate", + "This", + "Time", + "Top", + "TypeDeclDefaultBottom", + "TypeDeclDefaultMiddle", + "TypeDeclDefaultTop", + "UbTop", + "UB_TOP", + "UI", + "UnderInitialization", + "Unique", + "UnitsBottom", + "UnknownClass", + "UnknownCompilerMessageKey", + "UnknownFormat", + "UnknownInitialization", + "UnknownInterned", + "UnknownKeyFor", + "UnknownLocalizableKey", + "UnknownLocalized", + "UnknownMethod", + "UnknownPropertyKey", + "UnknownRegex", + "UnknownSignedness", + "UnknownThis", + "UnknownUnits", + "UnknownVal", + "Unsigned", + "Untainted", + "UpperBoundBottom", + "UpperBoundLiteral", + "UpperBoundUnknown", + "Value", + "VariableNameDefaultBottom", + "VariableNameDefaultMiddle", + "VariableNameDefaultTop", + "Volume", + "WholeProgramInferenceBottom" + // TODO: Add type annotations from other tools here. + + ); + + static final String NAME = "No line break between type annotation and type"; + + public static FormatterStep create() { + return create(Collections.emptyList(), Collections.emptyList()); + } + + public static FormatterStep create(List addedTypeAnnotations, List removedTypeAnnotations) { + return FormatterStep.create(NAME, new State(addedTypeAnnotations, removedTypeAnnotations), State::toFormatter); + } + + private FormatAnnotationsStep() {} + + // TODO: Read from a local .type-annotations file. + private static final class State implements Serializable { + private static final long serialVersionUID = 1L; + + private final Set typeAnnotations = new HashSet<>(defaultTypeAnnotations); + + // group 1 is the basename of the annotation. + private static final String annoNoArgRegex = "@(?:[A-Za-z_][A-Za-z0-9_.]*\\.)?([A-Za-z_][A-Za-z0-9_]*)"; + private static final Pattern annoNoArgPattern = Pattern.compile(annoNoArgRegex); + // 3 non-empty cases: () (".*") (.*) + private static final String annoArgRegex = "(?:\\(\\)|\\(\"[^\"]*\"\\)|\\([^\")][^)]*\\))?"; + // group 1 is the basename of the annotation. + private static final String annoRegex = annoNoArgRegex + annoArgRegex; + private static final String trailingAnnoRegex = annoRegex + "$"; + private static final Pattern trailingAnnoPattern = Pattern.compile(trailingAnnoRegex); + + // Heuristic: matches if the line might be within a //, /*, or Javadoc comment. + private static final Pattern withinCommentPattern = Pattern.compile("//|/\\*(?!.*/*/)|^[ \t]*\\*[ \t]"); + // Don't move an annotation to the start of a comment line. + private static final Pattern startsWithCommentPattern = Pattern.compile("^[ \t]*(//|/\\*$|/\\*|void\\b)"); + + /** + * @param addedTypeAnnotations simple names to add to Spotless's default list + * @param removedTypeAnnotations simple names to remove from Spotless's default list + */ + State(List addedTypeAnnotations, List removedTypeAnnotations) { + typeAnnotations.addAll(addedTypeAnnotations); + typeAnnotations.removeAll(removedTypeAnnotations); + } + + FormatterFunc toFormatter() { + return unixStr -> fixupTypeAnnotations(unixStr); + } + + /** + * Removes line break between type annotations and the following type. + * + * @param the text of a Java file + * @return corrected text of the Java file + */ + String fixupTypeAnnotations(String unixStr) { + // Each element of `lines` ends with a newline. + String[] lines = unixStr.split("((?<=\n))"); + for (int i = 0; i < lines.length - 1; i++) { + String line = lines[i]; + if (endsWithTypeAnnotation(line)) { + String nextLine = lines[i + 1]; + if (startsWithCommentPattern.matcher(nextLine).find()) { + continue; + } + lines[i] = ""; + lines[i + 1] = line.replaceAll("\\s+$", "") + " " + nextLine.replaceAll("^\\s+", ""); + } + } + return String.join("", lines); + } + + /** + * Returns true if the line ends with a type annotation. + * FormatAnnotationsStep fixes such formatting. + */ + boolean endsWithTypeAnnotation(String unixLine) { + // Remove trailing newline. + String line = unixLine.replaceAll("\\s+$", ""); + Matcher m = trailingAnnoPattern.matcher(line); + if (!m.find()) { + return false; + } + String preceding = line.substring(0, m.start()); + String basename = m.group(1); + + if (withinCommentPattern.matcher(preceding).find()) { + return false; + } + + return typeAnnotations.contains(basename); + } + } +} diff --git a/plugin-gradle/CHANGES.md b/plugin-gradle/CHANGES.md index bd866fb6a5..5003de1aba 100644 --- a/plugin-gradle/CHANGES.md +++ b/plugin-gradle/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `3.27.0`). ## [Unreleased] +### Added +* `formatAnnotations()` step to correct formatting of Java type annotations. It puts type annotations on the same line as the type that they qualify. Run it after a Java formatting step, such as `googleJavaFormat()`. ([#1275](https://github.com/diffplug/spotless/pull/1275)) ## [6.10.0] - 2022-08-23 ### Added diff --git a/plugin-gradle/README.md b/plugin-gradle/README.md index a9fcfbfcf6..a6d05daa9d 100644 --- a/plugin-gradle/README.md +++ b/plugin-gradle/README.md @@ -60,7 +60,7 @@ Spotless supports all of Gradle's built-in performance features (incremental bui - [**Quickstart**](#quickstart) - [Requirements](#requirements) - **Languages** - - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format)) + - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [clang-format](#clang-format), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) @@ -117,6 +117,8 @@ spotless { // apply a specific flavor of google-java-format googleJavaFormat('1.8').aosp().reflowLongStrings() + // fix formatting of type annotations + formatAnnotations() // make sure every file has the following copyright header. // optionally, Spotless can set copyright years by digging // through git history (see "license" section below) @@ -157,10 +159,13 @@ spotless { removeUnusedImports() - googleJavaFormat() // has its own section below - eclipse() // has its own section below - prettier() // has its own section below - clangFormat() // has its own section below + // Choose one of these formatters. + googleJavaFormat() // has its own section below + eclipse() // has its own section below + prettier() // has its own section below + clangFormat() // has its own section below + + formatAnnotations() // fixes formatting of type annotations, see below licenseHeader '/* (C) $YEAR */' // or licenseHeaderFile } @@ -190,21 +195,6 @@ spotless { googleJavaFormat('1.8').aosp().reflowLongStrings().groupArtifact('com.google.googlejavaformat:google-java-format') ``` -**⚠️ Note on using Google Java Format with Java 16+** - -Using Java 16+ with Google Java Format 1.10.0 [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) to the running JDK. -These Flags can be provided using the `gradle.properties` file (See [documentation](https://docs.gradle.org/current/userguide/build_environment.html)). - -For example the following file under `gradle.properties` will run gradle with the required flags: -``` -org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). - ### palantir-java-format [homepage](https://github.com/palantir/palantir-java-format). [changelog](https://github.com/palantir/palantir-java-format/releases). @@ -216,21 +206,6 @@ spotless { palantirJavaFormat('2.9.0') ``` -**⚠️ Note on using Palantir Java Format with Java 16+** - -Using Java 16+ with Palantir Java Format [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) on the running JDK. -These Flags can be provided using the `gradle.properties` file (See [documentation](https://docs.gradle.org/current/userguide/build_environment.html)). - -For example the following file under `gradle.properties` will run gradle with the required flags: -``` -org.gradle.jvmargs=--add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED \ - --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). - ### eclipse jdt [homepage](https://www.eclipse.org/downloads/packages/). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter). See [here](../ECLIPSE_SCREENSHOTS.md) for screenshots that demonstrate how to get and install the config file mentioned below. @@ -244,6 +219,50 @@ spotless { ``` +### formatAnnotations + +Type annotations should be on the same line as the type that they qualify. + +```java + @Override + @Deprecated + @Nullable @Interned String s; +``` + +However, some tools format them incorrectly, like this: + +```java + @Override + @Deprecated + @Nullable + @Interned + String s; +``` + +To fix the incorrect formatting, add the `formatAnnotations()` rule after a Java formatter. For example: + +```gradle +spotless { + java { + googleJavaFormat() + formatAnnotations() + } +} +``` + +This does not re-order annotations, it just removes incorrect newlines. + +A type annotation is an annotation that is meta-annotated with `@Target({ElementType.TYPE_USE})`. +Spotless has a default list of well-known type annotations. +You can use `addTypeAnnotation()` and `removeTypeAnnotation()` to override its defaults: + +```gradle + formatAnnotations().addTypeAnnotation("Empty").addTypeAnnotation("NonEmpty").removeTypeAnnotation("Localized") +``` + +You can make a pull request to add new annotations to Spotless's default list. + + ## Groovy 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 94ae136429..de0313eab4 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 @@ -691,6 +691,7 @@ public void withinBlocks(String name, String open, String close, Action diff --git a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java index ee7590da88..0f5926ec66 100644 --- a/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java +++ b/plugin-gradle/src/main/java/com/diffplug/gradle/spotless/JavaExtension.java @@ -18,6 +18,8 @@ import static com.diffplug.gradle.spotless.PluginGradlePreconditions.requireElementsNonNull; import java.io.File; +import java.util.ArrayList; +import java.util.List; import java.util.Objects; import javax.inject.Inject; @@ -32,6 +34,7 @@ import com.diffplug.spotless.extra.EclipseBasedStepBuilder; import com.diffplug.spotless.extra.java.EclipseJdtFormatterStep; import com.diffplug.spotless.generic.LicenseHeaderStep; +import com.diffplug.spotless.java.FormatAnnotationsStep; import com.diffplug.spotless.java.GoogleJavaFormatStep; import com.diffplug.spotless.java.ImportOrderStep; import com.diffplug.spotless.java.PalantirJavaFormatStep; @@ -231,6 +234,42 @@ public void configFile(Object... configFiles) { } + /** Removes newlines between type annotations and types. */ + public FormatAnnotationsConfig formatAnnotations() { + return new FormatAnnotationsConfig(); + } + + public class FormatAnnotationsConfig { + /** Annotations in addition to those in the default list. */ + final List addedTypeAnnotations = new ArrayList<>(); + /** Annotations that the user doesn't want treated as type annotations. */ + final List removedTypeAnnotations = new ArrayList<>(); + + FormatAnnotationsConfig() { + addStep(createStep()); + } + + public FormatAnnotationsConfig addTypeAnnotation(String simpleName) { + Objects.requireNonNull(simpleName); + addedTypeAnnotations.add(simpleName); + replaceStep(createStep()); + return this; + } + + public FormatAnnotationsConfig removeTypeAnnotation(String simpleName) { + Objects.requireNonNull(simpleName); + removedTypeAnnotations.add(simpleName); + replaceStep(createStep()); + return this; + } + + private FormatterStep createStep() { + return FormatAnnotationsStep.create( + addedTypeAnnotations, + removedTypeAnnotations); + } + } + /** If the user hasn't specified the files yet, we'll assume he/she means all of the java files. */ @Override protected void setupTask(SpotlessTask task) { diff --git a/plugin-maven/CHANGES.md b/plugin-maven/CHANGES.md index 3b6986c5db..fd371233f2 100644 --- a/plugin-maven/CHANGES.md +++ b/plugin-maven/CHANGES.md @@ -3,6 +3,8 @@ We adhere to the [keepachangelog](https://keepachangelog.com/en/1.0.0/) format (starting after version `1.27.0`). ## [Unreleased] +### Added +* `formatAnnotations` step to correct formatting of Java type annotations. It puts type annotations on the same line as the type that they qualify. Run it after a Java formatting step, such as `googleJavaFormat`. ([#1275](https://github.com/diffplug/spotless/pull/1275)) ## [2.25.0] - 2022-08-23 ### Added diff --git a/plugin-maven/README.md b/plugin-maven/README.md index f59d00d47a..d80773f3f1 100644 --- a/plugin-maven/README.md +++ b/plugin-maven/README.md @@ -24,7 +24,7 @@ output = [ output = prefixDelimiterReplace(input, 'https://{{org}}.github.io/{{name}}/javadoc/spotless-plugin-maven/', '/', versionLast) --> -Spotless is a general-purpose formatting plugin. It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. Plugin requires a version of Maven higher or equal to 3.1.0. +Spotless is a general-purpose formatting plugin used by [4,000 projects on GitHub (August 2020)](https://github.com/search?l=gradle&q=spotless&type=Code). It is completely à la carte, but also includes powerful "batteries-included" if you opt-in. Plugin requires a version of Maven higher or equal to 3.1.0. To people who use your build, it looks like this: @@ -47,7 +47,7 @@ user@machine repo % mvn spotless:check - [Requirements](#requirements) - [Binding to maven phase](#binding-to-maven-phase) - **Languages** - - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format)) + - [Java](#java) ([google-java-format](#google-java-format), [eclipse jdt](#eclipse-jdt), [prettier](#prettier), [palantir-java-format](#palantir-java-format), [formatAnnotations](#formatAnnotations)) - [Groovy](#groovy) ([eclipse groovy](#eclipse-groovy)) - [Kotlin](#kotlin) ([ktfmt](#ktfmt), [ktlint](#ktlint), [diktat](#diktat), [prettier](#prettier)) - [Scala](#scala) ([scalafmt](#scalafmt)) @@ -191,6 +191,8 @@ any other maven phase (i.e. compile) then it can be configured as below; + + /* (C)$YEAR */ @@ -212,17 +214,6 @@ any other maven phase (i.e. compile) then it can be configured as below; ``` -**⚠️ Note on using Google Java Format with Java 16+** - -Using Java 16+ with Google Java Format 1.10.0 [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) to the running JDK. -These Flags can be provided using `MAVEN_OPTS` environment variable or using the `./mvn/jvm.config` file (See [documentation](https://maven.apache.org/configure.html#mvn-jvm-config-file)). - -For example the following file under `.mvn/jvm.config` will run maven with the required flags: -``` ---add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). - ### palantir-java-format [homepage](https://github.com/palantir/palantir-java-format). [changelog](https://github.com/palantir/palantir-java-format/releases). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/PalantirJavaFormat.java). @@ -233,17 +224,6 @@ This is a workaround to a [pending issue](https://github.com/diffplug/spotless/i ``` -**⚠️ Note on using Palantir Java Format with Java 16+** - -Using Java 16+ with Palantir Java Format [requires additional flags](https://github.com/google/google-java-format/releases/tag/v1.10.0) on the running JDK. -These Flags can be provided using `MAVEN_OPTS` environment variable or using the `./mvn/jvm.config` file (See [documentation](https://maven.apache.org/configure.html#mvn-jvm-config-file)). - -For example the following file under `.mvn/jvm.config` will run maven with the required flags: -``` ---add-exports jdk.compiler/com.sun.tools.javac.api=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.file=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.parser=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.tree=ALL-UNNAMED --add-exports jdk.compiler/com.sun.tools.javac.util=ALL-UNNAMED -``` -This is a workaround to a [pending issue](https://github.com/diffplug/spotless/issues/834). - ### eclipse jdt [homepage](https://www.eclipse.org/downloads/packages/). [compatible versions](https://github.com/diffplug/spotless/tree/main/lib-extra/src/main/resources/com/diffplug/spotless/extra/eclipse_jdt_formatter). [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Eclipse.java). See [here](../ECLIPSE_SCREENSHOTS.md) for screenshots that demonstrate how to get and install the config file mentioned below. @@ -255,6 +235,42 @@ This is a workaround to a [pending issue](https://github.com/diffplug/spotless/i ``` +### formatAnnotations + +Type annotations should be on the same line as the type that they qualify. + +```java + @Override + @Deprecated + @Nullable @Interned String s; +``` + +However, some tools format them incorrectly, like this: + +```java + @Override + @Deprecated + @Nullable + @Interned + String s; +``` + +To fix the incorrect formatting, add the `formatAnnotations` rule after a Java formatter. For example: + +```XML + + +``` + +This does not re-order annotations, it just removes incorrect newlines. + +A type annotation is an annotation that is meta-annotated with `@Target({ElementType.TYPE_USE})`. +Because Spotless cannot necessarily examine the annotation definition, it uses a hard-coded +list of well-known type annotations. You can make a pull request to add new ones. +In the future there will be mechanisms to add/remove annotations from the list. +These mechanisms already exist for the Gradle plugin. + + ## Groovy [code](https://github.com/diffplug/spotless/blob/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy/Groovy.java). [available steps](https://github.com/diffplug/spotless/tree/main/plugin-maven/src/main/java/com/diffplug/spotless/maven/groovy). diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/FormatAnnotations.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/FormatAnnotations.java new file mode 100644 index 0000000000..ac60af0a62 --- /dev/null +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/FormatAnnotations.java @@ -0,0 +1,32 @@ +/* + * Copyright 2022 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.maven.java; + +import java.util.Collections; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.java.FormatAnnotationsStep; +import com.diffplug.spotless.maven.FormatterStepConfig; +import com.diffplug.spotless.maven.FormatterStepFactory; + +public class FormatAnnotations implements FormatterStepFactory { + + @Override + public FormatterStep newFormatterStep(FormatterStepConfig config) { + // TODO: Permit customization in Maven build files. + return FormatAnnotationsStep.create(Collections.emptyList(), Collections.emptyList()); + } +} diff --git a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java index 819179ebba..4bd018d53c 100644 --- a/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java +++ b/plugin-maven/src/main/java/com/diffplug/spotless/maven/java/Java.java @@ -61,4 +61,8 @@ public void addPalantirJavaFormat(PalantirJavaFormat palantirJavaFormat) { public void addRemoveUnusedImports(RemoveUnusedImports removeUnusedImports) { addStepFactory(removeUnusedImports); } + + public void addFormatAnnotations(FormatAnnotations formatAnnotations) { + addStepFactory(formatAnnotations); + } } diff --git a/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/FormatAnnotationsStepTest.java b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/FormatAnnotationsStepTest.java new file mode 100644 index 0000000000..8560b73fd4 --- /dev/null +++ b/plugin-maven/src/test/java/com/diffplug/spotless/maven/java/FormatAnnotationsStepTest.java @@ -0,0 +1,33 @@ +/* + * Copyright 2022 DiffPlug + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package com.diffplug.spotless.maven.java; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.maven.MavenIntegrationHarness; + +class FormatAnnotationsStepTest extends MavenIntegrationHarness { + + @Test + void testFormatAnnotations() throws Exception { + writePomWithJavaSteps(""); + + String path = "src/main/java/test.java"; + setFile(path).toResource("java/formatannotations/FormatAnnotationsTestInput.test"); + mavenRunner().withArguments("spotless:apply").runNoError(); + assertFile(path).sameAsResource("java/formatannotations/FormatAnnotationsTestOutput.test"); + } +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveInput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveInput.test new file mode 100644 index 0000000000..e822a8cc41 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveInput.test @@ -0,0 +1,11 @@ +class FormatAnnotationsAddRemove { + + @Empty + String e; + + @NonEmpty + String ne; + + @Localized + String localized; +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveOutput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveOutput.test new file mode 100644 index 0000000000..355528bd88 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsAddRemoveOutput.test @@ -0,0 +1,9 @@ +class FormatAnnotationsAddRemove { + + @Empty String e; + + @NonEmpty String ne; + + @Localized + String localized; +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsInput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsInput.test new file mode 100644 index 0000000000..a744b072a1 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsInput.test @@ -0,0 +1,31 @@ +class FormatAnnotationsInComments { + + // Here is a comment + @Interned + String m1() {} + + // Here is another comment + String m2() {} + + /** + * Here is a misformatted type annotation within a Javadoc comment. + * + * @Nullable + * String s; + */ + + @Nullable + @Interned + String m3(/* Don't get confused by other comments on the line with the type */) {} + + @Nullable + @Interned + String m3() {} // Still not confused + + /* + code snippets in regular comments do get re-formatted + + @Nullable + String s; + */ +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsOutput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsOutput.test new file mode 100644 index 0000000000..be65af24be --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsInCommentsOutput.test @@ -0,0 +1,25 @@ +class FormatAnnotationsInComments { + + // Here is a comment + @Interned String m1() {} + + // Here is another comment + String m2() {} + + /** + * Here is a misformatted type annotation within a Javadoc comment. + * + * @Nullable + * String s; + */ + + @Nullable @Interned String m3(/* Don't get confused by other comments on the line with the type */) {} + + @Nullable @Interned String m3() {} // Still not confused + + /* + code snippets in regular comments do get re-formatted + + @Nullable String s; + */ +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestInput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestInput.test new file mode 100644 index 0000000000..fb711451d1 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestInput.test @@ -0,0 +1,77 @@ +class FormatAnnotationsTest { + + public @Nullable + String s0 = null; + + @Deprecated + public @Nullable + String m0() {} + + @Nullable + String s1 = null; + + @Deprecated + @Nullable + String m1() {} + + @Nullable + @Deprecated + String m2() {} + + @Nullable + @Regex(2) + @Interned + String s2 = null; + + @Deprecated + @Nullable + @Regex(2) + @Interned + String m3() {} + + @Nullable + @Deprecated + @Regex(2) + @Interned + String m4() {} + + @Nullable + // a comment + @Regex(2) + @Interned + String s3 = null; + + @Nullable // a comment + @Regex(2) + @Interned + String s4 = null; + + @Nullable + @Regex(2) + @Interned + // a comment + String s5 = null; + + @Deprecated + // a comment + @Nullable + @Regex(2) + @Interned + String m5() {} + + @Deprecated + @Nullable + // a comment + @Regex(2) + @Interned + String m6() {} + + @Empty + String e; + + @NonEmpty + String ne; + + @Localized + String localized; +} diff --git a/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestOutput.test b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestOutput.test new file mode 100644 index 0000000000..ca37e66d86 --- /dev/null +++ b/testlib/src/main/resources/java/formatannotations/FormatAnnotationsTestOutput.test @@ -0,0 +1,51 @@ +class FormatAnnotationsTest { + + public @Nullable String s0 = null; + + @Deprecated + public @Nullable String m0() {} + + @Nullable String s1 = null; + + @Deprecated + @Nullable String m1() {} + + @Nullable @Deprecated + String m2() {} + + @Nullable @Regex(2) @Interned String s2 = null; + + @Deprecated + @Nullable @Regex(2) @Interned String m3() {} + + @Nullable @Deprecated + @Regex(2) @Interned String m4() {} + + @Nullable + // a comment + @Regex(2) @Interned String s3 = null; + + @Nullable // a comment + @Regex(2) @Interned String s4 = null; + + @Nullable @Regex(2) @Interned + // a comment + String s5 = null; + + @Deprecated + // a comment + @Nullable @Regex(2) @Interned String m5() {} + + @Deprecated + @Nullable + // a comment + @Regex(2) @Interned String m6() {} + + @Empty + String e; + + @NonEmpty + String ne; + + @Localized String localized; +} diff --git a/testlib/src/test/java/com/diffplug/spotless/java/FormatAnnotationsStepTest.java b/testlib/src/test/java/com/diffplug/spotless/java/FormatAnnotationsStepTest.java new file mode 100644 index 0000000000..84d347e490 --- /dev/null +++ b/testlib/src/test/java/com/diffplug/spotless/java/FormatAnnotationsStepTest.java @@ -0,0 +1,65 @@ +/* + * Copyright 2022 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.java; + +import java.util.Arrays; + +import org.junit.jupiter.api.Test; + +import com.diffplug.spotless.FormatterStep; +import com.diffplug.spotless.ResourceHarness; +import com.diffplug.spotless.SerializableEqualityTester; + +class FormatAnnotationsStepTest extends ResourceHarness { + @Test + void formatAnnotations() throws Throwable { + FormatterStep step = FormatAnnotationsStep.create(); + assertOnResources(step, "java/formatannotations/FormatAnnotationsTestInput.test", "java/formatannotations/FormatAnnotationsTestOutput.test"); + } + + @Test + void formatAnnotationsInComments() throws Throwable { + FormatterStep step = FormatAnnotationsStep.create(); + assertOnResources(step, "java/formatannotations/FormatAnnotationsInCommentsInput.test", "java/formatannotations/FormatAnnotationsInCommentsOutput.test"); + } + + @Test + void formatAnnotationsAddRemove() throws Throwable { + FormatterStep step = FormatAnnotationsStep.create(Arrays.asList("Empty", "NonEmpty"), Arrays.asList("Localized")); + assertOnResources(step, "java/formatannotations/FormatAnnotationsAddRemoveInput.test", "java/formatannotations/FormatAnnotationsAddRemoveOutput.test"); + } + + @Test + void doesntThrowIfFormatAnnotationsIsntSerializable() { + FormatAnnotationsStep.create(); + } + + @Test + void equality() throws Exception { + new SerializableEqualityTester() { + @Override + protected void setupTest(API api) { + api.areDifferentThan(); + } + + @Override + protected FormatterStep create() { + return FormatAnnotationsStep.create(); + } + }.testEquals(); + } + +}