/*
* Copyright 2022-2024 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;
import com.diffplug.spotless.SerializedFunction;
/**
* 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 implements java.io.Serializable {
private static final long serialVersionUID = 1L;
/**
* 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",
"AinferDefaultType",
"AinferParent",
"AinferSibling1",
"AinferSibling2",
"AinferTop",
"AinferImplicitAnno",
"AinferSiblingWithFields",
"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",
"DoesNotMatchRegex",
"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",
"ValueTypeAnno",
"VariableNameDefaultBottom",
"VariableNameDefaultMiddle",
"VariableNameDefaultTop",
"Volume",
"WholeProgramInferenceBottom"
// TODO: Add type annotations from other tools here.
);
private 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), SerializedFunction.identity(), 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 this::fixupTypeAnnotations;
}
/**
* Removes line break between type annotations and the following type.
*
* @param unixStr 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);
}
}
}