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();
+ }
+
+}