Checkstyle for API

This commit is contained in:
Jake Potrebic
2024-12-27 23:45:56 -08:00
parent 782ce950b8
commit bfe7c906ca
22 changed files with 1315 additions and 23 deletions

View File

@@ -0,0 +1,206 @@
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="SuppressionFilter">
<property name="file" value="${config_loc}/suppressions.xml"/>
<property name="optional" value="false"/>
</module>
<!--Single Suppression Filters-->
<module name="SuppressionSingleFilter">
<!--Suppresses warnings except for common TYPE_USE that should be on same line-->
<property name="checks" value="AnnotationOnSameLine"/>
<property name="message" value="^Annotation '(?!(${type_use_annotations}))"/>
</module>
<!--Javadoc Comments-->
<module name="JavadocPackage"/>
<!--Misc-->
<module name="NewlineAtEndOfFile"/>
<module name="OrderedProperties"/>
<!--Whitespace-->
<module name="FileTabCharacter"/>
<module name="TreeWalker">
<module name="SuppressionXpathFilter">
<property name="file" value="${config_loc}/xpath-suppressions.xml"/>
<property name="optional" value="false"/>
</module>
<!--Annotations-->
<module name="AnnotationOnSameLine">
<!--matches all annotations, but most are suppressed in above-->
<property name="tokens" value="METHOD_DEF"/>
</module>
<module name="AnnotationUseStyle"/>
<module name="MissingDeprecated"/>
<!--Block Checks-->
<module name="AvoidNestedBlocks"/>
<module name="EmptyBlock"/>
<module name="LeftCurly"/>
<module name="RightCurly"/>
<module name="OneTopLevelClass"/>
<module name="SealedShouldHavePermitsList"/>
<!--Class Design-->
<module name="FinalClass"/>
<module name="HideUtilityClassConstructor"/>
<module name="InterfaceIsType"/>
<!--Coding-->
<module name="ArrayTrailingComma"/>
<module name="AvoidDoubleBraceInitialization"/>
<module name="AvoidNoArgumentSuperConstructorCall"/>
<module name="ConstructorsDeclarationGrouping"/>
<module name="CovariantEquals"/>
<module name="DeclarationOrder"/>
<module name="DefaultComesLast"/>
<module name="EmptyStatement"/>
<module name="EqualsAvoidNull"/>
<module name="EqualsHashCode"/>
<module name="FallThrough"/>
<module name="FinalLocalVariable">
<property name="validateEnhancedForLoopVariable" value="true"/>
<property name="validateUnnamedVariables" value="true"/>
<property name="tokens" value="PARAMETER_DEF,VARIABLE_DEF"/>
</module>
<module name="IllegalToken"/> <!--just labels by default-->
<module name="IllegalType"/>
<module name="PatternVariableAssignment"/>
<module name="RequireThis">
<property name="validateOnlyOverlapping" value="false"/>
</module>
<module name="SimplifyBooleanExpression"/>
<module name="SimplifyBooleanReturn"/>
<module name="StringLiteralEquality"/>
<module name="UnnecessaryNullCheckWithInstanceOf"/>
<module name="UnnecessarySemicolonAfterOuterTypeDeclaration"/>
<module name="UnnecessarySemicolonAfterTypeMemberDeclaration"/>
<module name="UnnecessarySemicolonInEnumeration"/>
<module name="UnnecessarySemicolonInTryWithResources"/>
<module name="UnusedCatchParameterShouldBeUnnamed"/>
<module name="UnusedLambdaParameterShouldBeUnnamed"/>
<module name="UnusedLocalVariable"/>
<module name="WhenShouldBeUsed"/>
<!--Headers--> <!--N/A-->
<!--Imports-->
<module name="AvoidStarImport"/>
<module name="CustomImportOrder">
<property name="customImportOrderRules" value="THIRD_PARTY_PACKAGE,STATIC"/>
<property name="standardPackageRegExp" value="^$"/>
<property name="sortImportsInGroupAlphabetically" value="true"/>
</module>
<module name="IllegalImport">
<property name="regexp" value="true"/>
<!--checker-qual nullability-->
<property name="illegalClasses" value="org\.checkerframework\.checker\.nullness\.qual\.(Nullable|NonNull|DefaultQualifier)"/>
<!--jetbrains nullability-->
<property name="illegalClasses" value="org\.jetbrains\.annotations\.(NotNull|Nullable|NotNullByDefault)"/>
<!--javax nullability-->
</module>
<module name="IllegalImport">
<property name="regexp" value="true"/>
<!--attempts to guard against nested imports (by looking for capital letters in imports)-->
<property name="illegalClasses" value="^[^\.A-Z]+(\.[^\.A-Z]+)+?(\.[A-Z][^\.]*)(\.[A-Z][^\.]*)+$"/>
<message key="import.illegal" value="Illegal nested import - {0}"/>
</module>
<module name="RedundantImport"/>
<module name="UnusedImports"/>
<!--Javadoc Comments-->
<module name="AtclauseOrder">
<property name="tagOrder" value="@param, @return, @throws, @see, @deprecated, @hidden"/>
</module>
<module name="InvalidJavadocPosition"/>
<module name="JavadocBlockTagLocation"/>
<module name="JavadocContentLocation"/>
<module name="JavadocLeadingAsteriskAlign"/>
<module name="JavadocMethod">
<!--checks all, but doesn't require. If we have a doc, it should be valid-->
<property name="validateThrows" value="true"/>
</module>
<module name="JavadocMissingLeadingAsterisk"/>
<module name="JavadocMissingWhitespaceAfterAsterisk"/>
<module name="JavadocStyle"/> <!--checks all, but doesn't require. If we have a doc, it should be valid-->
<module name="JavadocTagContinuationIndentation"/>
<module name="JavadocType"/> <!--checks all, but doesn't require. If we have a doc, it should be valid-->
<module name="NonEmptyAtclauseDescription"/>
<module name="RequireEmptyLineBeforeBlockTagGroup"/>
<!--Metrics--> <!--N/A-->
<!--Miscellaneous-->
<module name="ArrayTypeStyle"/>
<module name="AvoidEscapedUnicodeCharacters"/>
<module name="CommentsIndentation"/>
<module name="FinalParameters">
<!-- TODO pattern variable assignment isn't checked yet: PATTERN_VARIABLE_DEF-->
<!-- https://github.com/checkstyle/checkstyle/issues/17366 -->
<property name="tokens" value="METHOD_DEF,CTOR_DEF,LITERAL_CATCH,FOR_EACH_CLAUSE"/>
</module>
<module name="NoCodeInFile"/>
<module name="OuterTypeFilename"/>
<module name="UpperEll"/>
<!--Modifiers-->
<module name="ModifierOrder"/>
<module name="RedundantModifier"/>
<!--Naming Conventions-->
<module name="AbbreviationAsWordInName">
<property name="allowedAbbreviations" value="JSON,UUID"/>
<property name="ignoreFinal" value="false"/>
</module>
<module name="ClassTypeParameterName"/>
<module name="ConstantName"/>
<module name="IllegalIdentifierName"/>
<module name="InterfaceTypeParameterName"/>
<module name="LambdaParameterName"/>
<module name="LocalFinalVariableName"/>
<module name="LocalVariableName"/>
<module name="MemberName"/>
<module name="MethodName"/>
<module name="MethodTypeParameterName"/>
<module name="PackageName">
<property name="format" value="^[a-z]+(\.[a-z][a-z0-9]*)*$"/>
</module>
<module name="ParameterName"/>
<module name="PatternVariableName"/>
<module name="RecordComponentName"/>
<module name="RecordTypeParameterName"/>
<module name="StaticVariableName"/>
<module name="TypeName"/>
<!--Regexp--> <!--N/A-->
<!--Size Violations--> <!--N/A-->
<!--Whitespace-->
<module name="EmptyForInitializerPad"/>
<module name="EmptyForIteratorPad"/>
<module name="EmptyLineSeparator">
<property name="allowNoEmptyLineBetweenFields" value="true"/>
<property name="tokens" value="IMPORT,STATIC_IMPORT,CLASS_DEF,INTERFACE_DEF,ENUM_DEF,STATIC_INIT,INSTANCE_INIT,METHOD_DEF,CTOR_DEF,VARIABLE_DEF,RECORD_DEF,COMPACT_CTOR_DEF"/>
</module>
<module name="GenericWhitespace"/>
<module name="MethodParamPad"/>
<module name="NoLineWrap"/> <!--just imports and packages-->
<module name="NoWhitespaceAfter"/>
<module name="NoWhitespaceBefore"/>
<module name="NoWhitespaceBeforeCaseDefaultColon"/>
<module name="ParenPad"/>
<module name="SingleSpaceSeparator"/>
<module name="TypecastParenPad"/>
<module name="WhitespaceAfter"/>
<module name="WhitespaceAround"/>
<!--Custom-->
<module name="JavadocAlignParameterDescription"/>
<module name="NullabilityAnnotations"/>
<module name="RedundantNullability"/>
</module>
</module>

View File

@@ -0,0 +1,8 @@
plugins {
java
}
dependencies {
implementation("com.puppycrawl.tools:checkstyle:10.26.1")
implementation("org.jspecify:jspecify:1.0.0")
}

View File

@@ -0,0 +1,129 @@
package io.papermc.checkstyle;
import com.puppycrawl.tools.checkstyle.JavaParser;
import com.puppycrawl.tools.checkstyle.api.CheckstyleException;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
import com.puppycrawl.tools.checkstyle.utils.TokenUtil;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Iterator;
import java.util.NoSuchElementException;
import java.util.function.Predicate;
import org.jspecify.annotations.Nullable;
/**
* Utility class containing utility methods for custom checkstyle checks.
*/
public final class Util {
private Util() {
}
/**
* Gets the previous sibling of the given node with the given type.
*
* @param node the node
* @param type the type
* @return the previous sibling with the given type, or {@code null} if not found
*/
public static @Nullable DetailNode getPreviousSibling(final DetailNode node, final int type) {
DetailNode sibling = JavadocUtil.getPreviousSibling(node);
while (sibling != null && sibling.getType() != type) {
sibling = JavadocUtil.getPreviousSibling(sibling);
}
return sibling;
}
/**
* Gets the next sibling of the given node with the given type.
*
* @param node the node
* @param type the type
* @return the next sibling with the given type, or {@code null} if not found
*/
public static @Nullable DetailAST getNextSibling(final DetailAST node, final int type) {
DetailAST sibling = node.getNextSibling();
while (sibling != null && sibling.getType() != type) {
sibling = sibling.getNextSibling();
}
return sibling;
}
/**
* Gets the enclosing type declaration of the given node.
*
* @param node the node
* @return the enclosing type declaration, or {@code null} if not found
*/
public static @Nullable DetailAST getEnclosingTypeDeclaration(final DetailAST node) {
DetailAST parent = node.getParent();
while (parent != null && !TokenUtil.isTypeDeclaration(parent.getType())) {
parent = parent.getParent();
}
return parent;
}
/**
* Gets an iterator over the children of the given node with the given type.
*
* @param ast the node
* @param type the type
* @return the iterator
*/
public static Iterable<DetailAST> childrenIterator(final DetailAST ast, final int type) {
return () -> new Iterator<>() {
private @Nullable DetailAST current = TokenUtil.findFirstTokenByPredicate(ast, child -> child.getType() == type).orElse(null);
@Override
public boolean hasNext() {
return this.current != null;
}
@Override
public DetailAST next() {
if (this.current == null) {
throw new NoSuchElementException();
}
final DetailAST result = this.current;
this.current = getNextSibling(this.current, type);
return result;
}
};
}
public static @Nullable DetailAST findPackageInfoFor(final Path filePath) {
final Path packageInfo = filePath.getParent().resolve("package-info.java");
if (Files.notExists(packageInfo)) {
return null;
}
final DetailAST packageInfoAst;
try {
packageInfoAst = JavaParser.parseFile(packageInfo.toFile(), JavaParser.Options.WITHOUT_COMMENTS);
} catch (final IOException | CheckstyleException e) {
throw new RuntimeException(e);
}
return packageInfoAst;
}
public static boolean isPackageInfoAnnotated(final Path filePath, final Predicate<DetailAST> annotationPredicate) {
final DetailAST packageInfoAst = Util.findPackageInfoFor(filePath);
if (packageInfoAst == null) {
return false;
}
final DetailAST firstToken = packageInfoAst.findFirstToken(TokenTypes.PACKAGE_DEF);
if (firstToken == null) {
return false;
}
final DetailAST annotations = firstToken.findFirstToken(TokenTypes.ANNOTATIONS);
for (final DetailAST annotation : Util.childrenIterator(annotations, TokenTypes.ANNOTATION)) {
if (annotationPredicate.test(annotation)) {
return true;
}
}
return false;
}
}

View File

@@ -0,0 +1,49 @@
package io.papermc.checkstyle.checks;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.JavadocTokenTypes;
import com.puppycrawl.tools.checkstyle.checks.javadoc.AbstractJavadocCheck;
import com.puppycrawl.tools.checkstyle.utils.JavadocUtil;
import io.papermc.checkstyle.Util;
import java.util.ArrayList;
import java.util.List;
/**
* Checks that parameter descriptions in Javadoc are aligned.
*/
public final class JavadocAlignParameterDescriptionCheck extends AbstractJavadocCheck {
@Override
public int[] getDefaultJavadocTokens() {
return new int[]{JavadocTokenTypes.JAVADOC};
}
@Override
public void visitJavadocToken(final DetailNode detailNode) {
final List<DetailNode> params = new ArrayList<>();
int maxColumn = -1;
for (final DetailNode child : detailNode.getChildren()) {
final DetailNode paramLiteralNode = JavadocUtil.findFirstToken(child, JavadocTokenTypes.PARAM_LITERAL);
if (child.getType() != JavadocTokenTypes.JAVADOC_TAG || paramLiteralNode == null) {
continue;
}
final DetailNode paramDescription = JavadocUtil.getNextSibling(paramLiteralNode, JavadocTokenTypes.DESCRIPTION);
maxColumn = Math.max(maxColumn, paramDescription.getColumnNumber());
params.add(paramDescription);
}
for (final DetailNode param : params) {
if (param.getColumnNumber() != maxColumn) {
final DetailNode paramNameNode = Util.getPreviousSibling(param, JavadocTokenTypes.PARAMETER_NAME);
if (paramNameNode == null) {
continue;
}
this.log(
param.getLineNumber(),
param.getColumnNumber() - 1,
"Param description for %s should start at column %d".formatted(paramNameNode.getText(), maxColumn)
);
}
}
}
}

View File

@@ -0,0 +1,116 @@
package io.papermc.checkstyle.checks;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import io.papermc.checkstyle.Util;
import java.nio.file.Path;
import java.util.Set;
import org.jspecify.annotations.Nullable;
/**
* Checks that nullability annotations are present where required.
*/
public final class NullabilityAnnotationsCheck extends AbstractCheck {
private static final Set<String> NULLABILITY_ANNOTATIONS = Set.of("Nullable", "NonNull");
@Override
public int[] getDefaultTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getAcceptableTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return new int[]{
TokenTypes.METHOD_DEF,
TokenTypes.PARAMETER_DEF,
TokenTypes.ANNOTATION_FIELD_DEF,
TokenTypes.RECORD_COMPONENT_DEF, // annotations are in ANNOTATIONS token block
};
}
private static boolean hasNoNullabilityAnnotationChildren(final @Nullable DetailAST ast) {
if (ast == null) {
return true;
}
for (final DetailAST annotation : Util.childrenIterator(ast, TokenTypes.ANNOTATION)) {
if (annotation.getChildCount(TokenTypes.IDENT) != 1) {
// skip `.` annotations like ApiStatus.Internal as these aren't nullability annotations
continue;
}
final String ident = annotation.findFirstToken(TokenTypes.IDENT).getText();
if (NULLABILITY_ANNOTATIONS.contains(ident)) {
return false;
}
}
return true;
}
private void visitMethodDefOrParamDef(final DetailAST holderDef, final int baseAnnotationHolderType) {
final DetailAST type = holderDef.findFirstToken(TokenTypes.TYPE);
final DetailAST arrayTypeStart = type.findFirstToken(TokenTypes.ARRAY_DECLARATOR);
if (arrayTypeStart != null) {
final DetailAST arrayAnnotations = type.findFirstToken(TokenTypes.ANNOTATIONS);
if (hasNoNullabilityAnnotationChildren(arrayAnnotations)) {
this.log(arrayTypeStart.getLineNo(), arrayTypeStart.getColumnNo() - 1, "Array is missing nullability annotation");
}
}
final DetailAST dot = type.findFirstToken(TokenTypes.DOT);
final DetailAST annotationHolder;
final DetailAST identLoc;
if (dot != null) {
annotationHolder = dot.findFirstToken(TokenTypes.ANNOTATIONS);
identLoc = dot.findFirstToken(TokenTypes.IDENT);
} else {
annotationHolder = holderDef.findFirstToken(baseAnnotationHolderType);
identLoc = type;
}
if (hasNoNullabilityAnnotationChildren(annotationHolder)) {
this.log(identLoc.getLineNo(), identLoc.getColumnNo() - 1, "Missing nullability annotation for '" + holderDef.findFirstToken(TokenTypes.IDENT).getText() + "'");
}
}
public static boolean isNullMarkedAnnotation(final DetailAST annotation) {
if (annotation.getChildCount(TokenTypes.IDENT) != 1) {
return false;
}
final String ident = annotation.findFirstToken(TokenTypes.IDENT).getText();
return "NullMarked".equals(ident);
}
public static @Nullable DetailAST getNullMarkedAnnotation(final DetailAST typeDeclaration) {
final DetailAST modifiers = typeDeclaration.findFirstToken(TokenTypes.MODIFIERS);
if (modifiers == null) {
return null;
}
for (final DetailAST annotation : Util.childrenIterator(modifiers, TokenTypes.ANNOTATION)) {
if (isNullMarkedAnnotation(annotation)) {
return annotation;
}
}
return null;
}
@Override
public void visitToken(final DetailAST ast) {
if (Util.isPackageInfoAnnotated(Path.of(this.getFilePath()), NullabilityAnnotationsCheck::isNullMarkedAnnotation)) {
return;
}
for (DetailAST parentDef = Util.getEnclosingTypeDeclaration(ast); parentDef != null; parentDef = Util.getEnclosingTypeDeclaration(parentDef)) {
if (getNullMarkedAnnotation(parentDef) != null) {
return;
}
}
switch (ast.getType()) {
case TokenTypes.METHOD_DEF, TokenTypes.PARAMETER_DEF, TokenTypes.ANNOTATION_FIELD_DEF -> this.visitMethodDefOrParamDef(ast, TokenTypes.MODIFIERS);
case TokenTypes.RECORD_COMPONENT_DEF -> this.visitMethodDefOrParamDef(ast, TokenTypes.ANNOTATIONS);
}
}
}

View File

@@ -0,0 +1,40 @@
package io.papermc.checkstyle.checks;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import io.papermc.checkstyle.Util;
import java.nio.file.Path;
public class RedundantNullabilityCheck extends AbstractCheck {
@Override
public int[] getDefaultTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getAcceptableTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return new int[]{
TokenTypes.CLASS_DEF,
TokenTypes.INTERFACE_DEF,
TokenTypes.ANNOTATION_DEF,
TokenTypes.RECORD_DEF,
TokenTypes.ENUM_DEF,
};
}
@Override
public void visitToken(final DetailAST ast) {
final boolean pkgIsNullMarked = Util.isPackageInfoAnnotated(Path.of(this.getFilePath()), NullabilityAnnotationsCheck::isNullMarkedAnnotation);
final DetailAST nullMarkedAnnotation = NullabilityAnnotationsCheck.getNullMarkedAnnotation(ast);
if (pkgIsNullMarked && nullMarkedAnnotation != null) {
this.log(nullMarkedAnnotation.getLineNo(), ast.getColumnNo() - 1, "Redundant NullMarked annotation");
}
}
}

View File

@@ -0,0 +1,8 @@
/**
* Custom checkstyle checks for PaperMC projects.
*/
@NullMarked
@SuppressWarnings("unused")
package io.papermc.checkstyle.checks;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,7 @@
/**
* This package contains custom checkstyle rules for PaperMC projects.
*/
@NullMarked
package io.papermc.checkstyle;
import org.jspecify.annotations.NullMarked;

View File

@@ -0,0 +1,9 @@
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE checkstyle-packages PUBLIC
"-//Checkstyle//DTD Package Names Configuration 1.0//EN"
"https://checkstyle.org/dtds/packages_1_0.dtd">
<checkstyle-packages>
<package name="io.papermc.checkstyle.checks"/>
</checkstyle-packages>