Checkstyle configuration for API (#11859)

This commit is contained in:
Jake Potrebic 2026-08-07 07:36:45 -07:00 committed by GitHub
parent 9119d1d121
commit 1cd6d5799c
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
53 changed files with 1799 additions and 71 deletions

View file

@ -0,0 +1,203 @@
<!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="io.papermc.checkstyle.filters.BetterSuppressionSingleFilter">
<!--Suppresses warnings except for common TYPE_USE that should be on the same line-->
<property name="checks" value="AnnotationOnSameLine"/>
<property name="messageKey" value="annotation\.same\.line"/>
<property name="arguments" value="^(?!(${type_use_annotations}))"/>
</module>
<!--Javadoc Comments-->
<module name="JavadocPackage"/>
<!--Misc-->
<module name="NewlineAtEndOfFile"/>
<!--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>
<module name="SuppressionCommentFilter"/>
<!--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"/>
<!--Class Design-->
<module name="FinalClass"/>
<module name="HideUtilityClassConstructor"/>
<module name="InterfaceIsType"/>
<module name="OneTopLevelClass"/>
<module name="SealedShouldHavePermitsList"/>
<!--Coding-->
<module name="AvoidDoubleBraceInitialization"/>
<module name="AvoidNoArgumentSuperConstructorCall"/>
<module name="ConstructorsDeclarationGrouping"/>
<module name="CovariantEquals"/>
<module name="DeclarationOrder"/>
<module name="DefaultComesLast"/>
<module name="EmptyStatement"/>
<module name="EqualsHashCode"/>
<module name="FallThrough"/>
<!-- Use non-final variables as an identifier of old code for now -->
<!--<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="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|MonotonicNonNull)"/>
<!--jetbrains nullability-->
<property name="illegalClasses" value="org\.jetbrains\.annotations\.(NotNull|Nullable|NotNullByDefault)"/>
<!--javax nullability-->
<property name="illegalClasses" value="javax\.annotation\.(Nonnull|Nullable)"/>
<message key="import.illegal" value="Illegal nullability annotation - {0}, use JSpecify"/>
</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"/>
<!-- Overlaps with the configured FinalLocalVariable, but is more strict -->
<!--<module name="FinalParameters">
<property name="tokens" value="METHOD_DEF,CTOR_DEF,LITERAL_CATCH,FOR_EACH_CLAUSE,PATTERN_VARIABLE_DEF"/>
</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">
<property name="format" value="^[A-Z]+$"/>
</module>
<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 name="UnnecessaryFullyQualifiedImport"/>
</module>
</module>

View file

@ -0,0 +1,7 @@
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionFilter Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2.dtd">
<suppressions>
<suppress checks="MissingJavadoc.*" files="src[\\/]test[\\/]java[^\\/]*[\\/].*"/>
<suppress checks="JavadocPackage" files="src[\\/]test[\\/]java[^\\/]*[\\/].*"/>
</suppressions>

View file

@ -0,0 +1,10 @@
NonNull
NotNull
Nullable
UnknownNullability
Unmodifiable
UnmodifiableView
Range
Positive
NonNegative
IntRange

View file

@ -0,0 +1,17 @@
<!DOCTYPE suppressions PUBLIC
"-//Checkstyle//DTD SuppressionXpathFilter Experimental Configuration 1.2//EN"
"https://checkstyle.org/dtds/suppressions_1_2_xpath_experimental.dtd">
<suppressions>
<!--ignores all checks for @Deprecated(forRemoval=*) children-->
<suppress-xpath checks=".*" query="//*[MODIFIERS/ANNOTATION[(@text = 'Deprecated' or *[@text = 'Deprecated'] and ANNOTATION_MEMBER_VALUE_PAIR/IDENT[@text='forRemoval'])]]/descendant-or-self::node()"/>
<!--special case to ignore extra checks outside the root CLASS_DEF if the root CLASS_DEF is marked as deprecated for removal-->
<suppress-xpath checks=".*" query="/COMPILATION_UNIT[CLASS_DEF/MODIFIERS/ANNOTATION[IDENT[@text = 'Deprecated'] and ANNOTATION_MEMBER_VALUE_PAIR/IDENT[@text='forRemoval']]]/descendant-or-self::node()"/>
<!--ignores MissingJavadoc for ApiStatus.Internal children-->
<suppress-xpath checks="MissingJavadoc(Method|Type)" query="//*[MODIFIERS/ANNOTATION/DOT[@text = 'Internal' or *[@text = 'Internal']]]/descendant-or-self::node()"/>
<!--skip for private classes-->
<suppress-xpath checks="FinalClass" query="//CLASS_DEF[MODIFIERS/LITERAL_PRIVATE]"/>
<!--skip for classes inside of methods-->
<suppress-xpath checks="HideUtilityClassConstructor|MissingJavadoc(Method|Type)" query="//CLASS_DEF[ancestor::METHOD_DEF]"/>
</suppressions>

View file

@ -10,6 +10,7 @@ ij_any_block_comment_add_space = false
ij_any_block_comment_at_first_column = false
ij_any_line_comment_at_first_column = false
ij_any_line_comment_add_space = true
trim_trailing_whitespace = true
[*.tiny]
indent_style = tab
@ -20,7 +21,7 @@ end_of_line = crlf
[*.yml]
indent_size = 2
[*.patch]
[{*.patch,*.md}]
trim_trailing_whitespace = false
[*.java]
@ -29,12 +30,26 @@ ij_java_class_count_to_use_import_on_demand = 999999
ij_java_insert_inner_class_imports = false
ij_java_names_count_to_use_import_on_demand = 999999
ij_java_imports_layout = *, |, $*
ij_java_layout_static_imports_separately = true
ij_java_generate_final_locals = true
ij_java_generate_final_parameters = true
ij_java_method_parameters_new_line_after_left_paren = true
ij_java_method_parameters_right_paren_on_new_line = true
ij_java_use_fq_class_names = false
ij_java_class_names_in_javadoc = 1
# javadoc
ij_java_doc_add_blank_line_after_description = true
ij_java_doc_add_blank_line_after_param_comments = false
ij_java_doc_add_blank_line_after_return = false
ij_java_doc_add_p_tag_on_empty_lines = true
ij_java_doc_align_exception_comments = true
ij_java_doc_align_param_comments = true
ij_java_doc_do_not_wrap_if_one_line = false
ij_java_doc_enable_formatting = true
ij_java_doc_enable_leading_asterisks = true
ij_java_doc_indent_on_continuation = true
ij_java_doc_param_description_on_new_line = false
ij_java_doc_use_throws_not_exception_tag = true
[paper-server/src/minecraft/java/**/*.java]
ij_java_use_fq_class_names = true

View file

@ -111,6 +111,13 @@ jobs:
path: |
**/build/test-results/test/TEST-*.xml
- name: Publish Checkstyle Report
uses: Juuxel/publish-checkstyle-report@v3
if: failure() || success()
with:
reports: |
*/build/reports/checkstyle/*.xml
- name: Create Paperclip Jar
if: fromJSON(steps.determine.outputs.result).action == 'paperclip'
run: ./gradlew createPaperclipJar --stacktrace

View file

@ -1,3 +1,5 @@
import io.papermc.paperweight.checkstyle.PaperCheckstyleExt
import io.papermc.paperweight.checkstyle.tasks.PaperCheckstyleTask
import org.gradle.api.tasks.testing.logging.TestExceptionFormat
import org.gradle.api.tasks.testing.logging.TestLogEvent
@ -6,14 +8,39 @@ plugins {
}
subprojects {
apply(plugin = "java-library")
apply(plugin = "maven-publish")
apply {
plugin("java-library")
plugin("maven-publish")
}
extensions.configure<JavaPluginExtension> {
toolchain {
languageVersion = JavaLanguageVersion.of(25)
}
}
val tempDisabled = setOf("paper-server", "paper-generator", "test-plugin")
if (name !in tempDisabled) {
apply { plugin("io.papermc.paperweight.paper-checkstyle") }
extensions.configure<PaperCheckstyleExt> {
typeUseAnnotationsFile.set(rootProject.layout.projectDirectory.file(".checkstyle/type_use_annotations.txt"))
}
/*tasks.withType<PaperCheckstyleTask>().configureEach {
configDirectory = rootProject.layout.projectDirectory.dir(".checkstyle")
// configFile = layout.projectDirectory.file(".checkstyle/checkstyle.xml").asFile // use the base file if not overwritten
maxHeapSize = "2g"
reports {
xml.required = true
html.required = true
}
}*/
dependencies {
"checkstyle"(project(":paper-checkstyle"))
}
}
}
val paperMavenPublicUrl = "https://repo.papermc.io/repository/maven-public/"

View file

@ -0,0 +1,26 @@
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<!--Single Suppression Filters-->
<module name="io.papermc.checkstyle.filters.BetterSuppressionSingleFilter">
<property name="checks" value="Javadoc(Method|Type)"/>
<property name="messageKey" value="javadoc\.unknownTag"/>
<!--[A-Z].* check is for java annotations inside code blocks inside Javadocs-->
<property name="arguments" value="(${custom_javadoc_tags}|[A-Z].*)"/>
</module>
<module name="TreeWalker">
<module name="JavadocVariable">
<!--requires on public fields-->
<property name="accessModifiers" value="public"/>
</module>
<module name="MissingJavadocMethod">
<property name="allowedAnnotations" value="Override, ApiStatus.Internal"/>
<property name="ignoreMethodNamesRegex" value="^getHandlerList$"/>
</module>
<module name="MissingJavadocPackage"/>
<module name="MissingJavadocType">
<property name="skipAnnotations" value="Generated, ApiStatus.Internal" />
</module>
</module>
</module>

View file

@ -0,0 +1,201 @@
paper-api/src/generated/java/com/destroystokyo/paper/entity/ai/
paper-api/src/generated/java/io/papermc/paper/registry/keys/
paper-api/src/generated/java/io/papermc/paper/registry/keys/tags/
paper-api/src/main/java/co/aikar/timings/
paper-api/src/main/java/co/aikar/util/
paper-api/src/main/java/com/destroystokyo/paper/
paper-api/src/main/java/com/destroystokyo/paper/block/
paper-api/src/main/java/com/destroystokyo/paper/brigadier/
paper-api/src/main/java/com/destroystokyo/paper/entity/
paper-api/src/main/java/com/destroystokyo/paper/entity/ai/
paper-api/src/main/java/com/destroystokyo/paper/entity/villager/
paper-api/src/main/java/com/destroystokyo/paper/event/block/
paper-api/src/main/java/com/destroystokyo/paper/event/brigadier/
paper-api/src/main/java/com/destroystokyo/paper/event/entity/
paper-api/src/main/java/com/destroystokyo/paper/event/inventory/
paper-api/src/main/java/com/destroystokyo/paper/event/player/
paper-api/src/main/java/com/destroystokyo/paper/event/profile/
paper-api/src/main/java/com/destroystokyo/paper/event/server/
paper-api/src/main/java/com/destroystokyo/paper/exception/
paper-api/src/main/java/com/destroystokyo/paper/inventory/meta/
paper-api/src/main/java/com/destroystokyo/paper/loottable/
paper-api/src/main/java/com/destroystokyo/paper/network/
paper-api/src/main/java/com/destroystokyo/paper/profile/
paper-api/src/main/java/com/destroystokyo/paper/util/
paper-api/src/main/java/com/destroystokyo/paper/utils/
paper-api/src/main/java/io/papermc/paper/advancement/
paper-api/src/main/java/io/papermc/paper/annotation/
paper-api/src/main/java/io/papermc/paper/ban/
paper-api/src/main/java/io/papermc/paper/block/
paper-api/src/main/java/io/papermc/paper/block/bed/
paper-api/src/main/java/io/papermc/paper/block/fluid/
paper-api/src/main/java/io/papermc/paper/block/fluid/type/
paper-api/src/main/java/io/papermc/paper/brigadier/
paper-api/src/main/java/io/papermc/paper/chat/
paper-api/src/main/java/io/papermc/paper/command/
paper-api/src/main/java/io/papermc/paper/command/brigadier/
paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/
paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/position/
paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/predicate/
paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/range/
paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/resolvers/
paper-api/src/main/java/io/papermc/paper/command/brigadier/argument/resolvers/selector/
paper-api/src/main/java/io/papermc/paper/configuration/
paper-api/src/main/java/io/papermc/paper/connection/
paper-api/src/main/java/io/papermc/paper/datacomponent/
paper-api/src/main/java/io/papermc/paper/datacomponent/item/
paper-api/src/main/java/io/papermc/paper/datacomponent/item/attribute/
paper-api/src/main/java/io/papermc/paper/datacomponent/item/blocksattacks/
paper-api/src/main/java/io/papermc/paper/datacomponent/item/consumable/
paper-api/src/main/java/io/papermc/paper/datapack/
paper-api/src/main/java/io/papermc/paper/dialog/
paper-api/src/main/java/io/papermc/paper/enchantments/
paper-api/src/main/java/io/papermc/paper/entity/
paper-api/src/main/java/io/papermc/paper/entity/poi/
paper-api/src/main/java/io/papermc/paper/event/block/
paper-api/src/main/java/io/papermc/paper/event/connection/
paper-api/src/main/java/io/papermc/paper/event/connection/configuration/
paper-api/src/main/java/io/papermc/paper/event/entity/
paper-api/src/main/java/io/papermc/paper/event/executor/
paper-api/src/main/java/io/papermc/paper/event/inventory/
paper-api/src/main/java/io/papermc/paper/event/packet/
paper-api/src/main/java/io/papermc/paper/event/player/
paper-api/src/main/java/io/papermc/paper/event/server/
paper-api/src/main/java/io/papermc/paper/event/world/
paper-api/src/main/java/io/papermc/paper/event/world/border/
paper-api/src/main/java/io/papermc/paper/generated/
paper-api/src/main/java/io/papermc/paper/inventory/
paper-api/src/main/java/io/papermc/paper/inventory/tooltip/
paper-api/src/main/java/io/papermc/paper/item/
paper-api/src/main/java/io/papermc/paper/persistence/
paper-api/src/main/java/io/papermc/paper/plugin/
paper-api/src/main/java/io/papermc/paper/plugin/bootstrap/
paper-api/src/main/java/io/papermc/paper/plugin/configuration/
paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/
paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/
paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/handler/configuration/
paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/registrar/
paper-api/src/main/java/io/papermc/paper/plugin/lifecycle/event/types/
paper-api/src/main/java/io/papermc/paper/plugin/loader/
paper-api/src/main/java/io/papermc/paper/plugin/loader/library/
paper-api/src/main/java/io/papermc/paper/plugin/loader/library/impl/
paper-api/src/main/java/io/papermc/paper/plugin/provider/classloader/
paper-api/src/main/java/io/papermc/paper/plugin/provider/entrypoint/
paper-api/src/main/java/io/papermc/paper/plugin/provider/util/
paper-api/src/main/java/io/papermc/paper/potion/
paper-api/src/main/java/io/papermc/paper/raytracing/
paper-api/src/main/java/io/papermc/paper/registry/
paper-api/src/main/java/io/papermc/paper/registry/data/
paper-api/src/main/java/io/papermc/paper/registry/data/client/
paper-api/src/main/java/io/papermc/paper/registry/data/dialog/
paper-api/src/main/java/io/papermc/paper/registry/data/dialog/action/
paper-api/src/main/java/io/papermc/paper/registry/data/dialog/body/
paper-api/src/main/java/io/papermc/paper/registry/data/dialog/input/
paper-api/src/main/java/io/papermc/paper/registry/data/dialog/type/
paper-api/src/main/java/io/papermc/paper/registry/event/
paper-api/src/main/java/io/papermc/paper/registry/event/type/
paper-api/src/main/java/io/papermc/paper/registry/holder/
paper-api/src/main/java/io/papermc/paper/registry/set/
paper-api/src/main/java/io/papermc/paper/registry/tag/
paper-api/src/main/java/io/papermc/paper/scoreboard/numbers/
paper-api/src/main/java/io/papermc/paper/tag/
paper-api/src/main/java/io/papermc/paper/text/
paper-api/src/main/java/io/papermc/paper/threadedregions/
paper-api/src/main/java/io/papermc/paper/threadedregions/scheduler/
paper-api/src/main/java/io/papermc/paper/util/
paper-api/src/main/java/io/papermc/paper/world/
paper-api/src/main/java/io/papermc/paper/world/damagesource/
paper-api/src/main/java/io/papermc/paper/world/flag/
paper-api/src/main/java/org/bukkit/
paper-api/src/main/java/org/bukkit/advancement/
paper-api/src/main/java/org/bukkit/attribute/
paper-api/src/main/java/org/bukkit/ban/
paper-api/src/main/java/org/bukkit/block/
paper-api/src/main/java/org/bukkit/block/banner/
paper-api/src/main/java/org/bukkit/block/data/
paper-api/src/main/java/org/bukkit/block/data/type/
paper-api/src/main/java/org/bukkit/block/sign/
paper-api/src/main/java/org/bukkit/block/spawner/
paper-api/src/main/java/org/bukkit/block/structure/
paper-api/src/main/java/org/bukkit/boss/
paper-api/src/main/java/org/bukkit/command/
paper-api/src/main/java/org/bukkit/command/defaults/
paper-api/src/main/java/org/bukkit/configuration/
paper-api/src/main/java/org/bukkit/configuration/file/
paper-api/src/main/java/org/bukkit/configuration/serialization/
paper-api/src/main/java/org/bukkit/conversations/
paper-api/src/main/java/org/bukkit/damage/
paper-api/src/main/java/org/bukkit/enchantments/
paper-api/src/main/java/org/bukkit/entity/
paper-api/src/main/java/org/bukkit/entity/boat/
paper-api/src/main/java/org/bukkit/entity/memory/
paper-api/src/main/java/org/bukkit/entity/minecart/
paper-api/src/main/java/org/bukkit/event/
paper-api/src/main/java/org/bukkit/event/block/
paper-api/src/main/java/org/bukkit/event/command/
paper-api/src/main/java/org/bukkit/event/enchantment/
paper-api/src/main/java/org/bukkit/event/entity/
paper-api/src/main/java/org/bukkit/event/hanging/
paper-api/src/main/java/org/bukkit/event/inventory/
paper-api/src/main/java/org/bukkit/event/player/
paper-api/src/main/java/org/bukkit/event/raid/
paper-api/src/main/java/org/bukkit/event/server/
paper-api/src/main/java/org/bukkit/event/vehicle/
paper-api/src/main/java/org/bukkit/event/weather/
paper-api/src/main/java/org/bukkit/event/world/
paper-api/src/main/java/org/bukkit/generator/
paper-api/src/main/java/org/bukkit/generator/structure/
paper-api/src/main/java/org/bukkit/help/
paper-api/src/main/java/org/bukkit/inventory/
paper-api/src/main/java/org/bukkit/inventory/meta/
paper-api/src/main/java/org/bukkit/inventory/meta/components/
paper-api/src/main/java/org/bukkit/inventory/meta/tags/
paper-api/src/main/java/org/bukkit/inventory/meta/trim/
paper-api/src/main/java/org/bukkit/inventory/recipe/
paper-api/src/main/java/org/bukkit/inventory/view/
paper-api/src/main/java/org/bukkit/inventory/view/builder/
paper-api/src/main/java/org/bukkit/loot/
paper-api/src/main/java/org/bukkit/map/
paper-api/src/main/java/org/bukkit/material/
paper-api/src/main/java/org/bukkit/material/types/
paper-api/src/main/java/org/bukkit/metadata/
paper-api/src/main/java/org/bukkit/packs/
paper-api/src/main/java/org/bukkit/permissions/
paper-api/src/main/java/org/bukkit/persistence/
paper-api/src/main/java/org/bukkit/plugin/
paper-api/src/main/java/org/bukkit/plugin/java/
paper-api/src/main/java/org/bukkit/plugin/messaging/
paper-api/src/main/java/org/bukkit/potion/
paper-api/src/main/java/org/bukkit/profile/
paper-api/src/main/java/org/bukkit/projectiles/
paper-api/src/main/java/org/bukkit/scheduler/
paper-api/src/main/java/org/bukkit/scoreboard/
paper-api/src/main/java/org/bukkit/spawner/
paper-api/src/main/java/org/bukkit/structure/
paper-api/src/main/java/org/bukkit/tag/
paper-api/src/main/java/org/bukkit/util/
paper-api/src/main/java/org/bukkit/util/io/
paper-api/src/main/java/org/bukkit/util/noise/
paper-api/src/main/java/org/bukkit/util/permissions/
paper-api/src/main/java/org/spigotmc/
paper-api/src/main/java/org/spigotmc/event/player/
paper-api/src/test/java/com/destroystokyo/paper/
paper-api/src/test/java/io/papermc/paper/adventure/
paper-api/src/test/java/io/papermc/paper/registry/
paper-api/src/test/java/io/papermc/paper/testing/
paper-api/src/test/java/io/papermc/paper/util/
paper-api/src/test/java/org/bukkit/
paper-api/src/test/java/org/bukkit/configuration/
paper-api/src/test/java/org/bukkit/configuration/file/
paper-api/src/test/java/org/bukkit/conversations/
paper-api/src/test/java/org/bukkit/event/
paper-api/src/test/java/org/bukkit/materials/
paper-api/src/test/java/org/bukkit/metadata/
paper-api/src/test/java/org/bukkit/plugin/
paper-api/src/test/java/org/bukkit/plugin/messaging/
paper-api/src/test/java/org/bukkit/scoreboard/
paper-api/src/test/java/org/bukkit/support/
paper-api/src/test/java/org/bukkit/support/provider/
paper-api/src/test/java/org/bukkit/support/test/
paper-api/src/test/java/org/bukkit/util/
paper-api/src/test/java/org/bukkit/util/io/

View file

@ -1,9 +1,11 @@
import io.papermc.paperweight.checkstyle.JavadocTag
import paper.libs.com.google.gson.Gson
plugins {
`java-library`
`maven-publish`
idea
id("io.papermc.paperweight.paper-checkstyle")
}
java {
@ -11,6 +13,15 @@ java {
withJavadocJar()
}
val projectCustomJavadocTags = setOf(
JavadocTag("apiNote", "a", "API Note:"),
)
paperCheckstyle {
customJavadocTags = projectCustomJavadocTags
directoriesToSkipFile = layout.projectDirectory.file(".checkstyle/ignored_directories.txt")
}
val annotationsVersion = "26.0.2"
val adventureVersion = "5.2.0"
val bungeeCordChatVersion = "1.21-R0.2-deprecated+build.21"
@ -92,6 +103,9 @@ dependencies {
testImplementation("org.ow2.asm:asm-tree:9.9.1")
mockitoAgent("org.mockito:mockito-core:5.22.0") { isTransitive = false } // configure mockito agent that is needed in newer java versions
testRuntimeOnly("org.junit.platform:junit-platform-launcher:6.0.3")
// checkstyle
checkstyle(project(":paper-checkstyle"))
}
val generatedDir: java.nio.file.Path = layout.projectDirectory.dir("src/generated/java").asFile.toPath()
@ -201,7 +215,7 @@ tasks.withType<Javadoc>().configureEach {
"https://logging.apache.org/log4j/2.x/javadoc/log4j-api/",
"https://www.javadocs.dev/org.apache.maven.resolver/maven-resolver-api/1.7.3",
)
options.tags("apiNote:a:API Note:")
options.tags(projectCustomJavadocTags.map { it.toOptionString() })
inputs.files(javadocSourcepath).ignoreEmptyDirectories().withPropertyName(javadocSourcepath.name + "-configuration")
val javadocSourcepathElements = javadocSourcepath.elements

View file

@ -35,7 +35,6 @@ import org.bukkit.plugin.Plugin;
import org.bukkit.plugin.java.JavaPlugin;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
/**
@ -45,7 +44,6 @@ import org.jspecify.annotations.Nullable;
* cause issues when called under unexpected circumstances.
*/
@ApiStatus.Internal
@NullMarked
public interface InternalAPIBridge {
/**
@ -55,6 +53,7 @@ public interface InternalAPIBridge {
*/
static InternalAPIBridge get() {
class Holder {
public static final InternalAPIBridge INSTANCE = Services.service(InternalAPIBridge.class).orElseThrow();
}

View file

@ -6,12 +6,10 @@ import java.util.OptionalInt;
import net.kyori.adventure.key.Key;
import net.kyori.adventure.util.Services;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* Information about the current server build.
*/
@NullMarked
@ApiStatus.NonExtendable
public interface ServerBuildInfo {
/**
@ -30,6 +28,7 @@ public interface ServerBuildInfo {
static final Optional<ServerBuildInfo> INSTANCE = Services.service(ServerBuildInfo.class);
}
//</editor-fold>
return Holder.INSTANCE.orElseThrow();
}
@ -46,7 +45,7 @@ public interface ServerBuildInfo {
* @param brandId the brand to check (e.g. "papermc:folia")
* @return {@code true} if the server supports the specified brand
*/
boolean isBrandCompatible(final Key brandId);
boolean isBrandCompatible(Key brandId);
/**
* Gets the brand name of the server.
@ -103,7 +102,7 @@ public interface ServerBuildInfo {
* @param representation the type of representation
* @return a string
*/
String asString(final StringRepresentation representation);
String asString(StringRepresentation representation);
/**
* String representation types.

View file

@ -1,11 +1,8 @@
package io.papermc.paper.math;
import org.jspecify.annotations.NullMarked;
/**
* Represents an angle that can be relative or absolute.
*/
@NullMarked
public sealed interface Angle permits AngleImpl {
/**
@ -15,7 +12,7 @@ public sealed interface Angle permits AngleImpl {
* @param degrees the angle value, measured in degrees
* @return a new {@code Angle} instance with the specified value
*/
static Angle absolute(float degrees) {
static Angle absolute(final float degrees) {
return new AngleImpl(degrees, false);
}
@ -26,7 +23,7 @@ public sealed interface Angle permits AngleImpl {
* @param degrees the angle value, measured in degrees
* @return a new {@code Angle} instance with the specified value
*/
static Angle relative(float degrees) {
static Angle relative(final float degrees) {
return new AngleImpl(degrees, true);
}

View file

@ -4,7 +4,6 @@ import org.bukkit.Axis;
import org.bukkit.block.BlockFace;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
/**
* A position represented with integers.
@ -14,7 +13,6 @@ import org.jspecify.annotations.NullMarked;
* @see FinePosition
*/
@ApiStatus.Experimental
@NullMarked
public interface BlockPosition extends Position {
@Override
@ -73,7 +71,7 @@ public interface BlockPosition extends Position {
* multiplied by the amount.
*
* @param blockFace the block face to offset towards
* @param amount the number of times to move in that direction
* @param amount the number of times to move in that direction
* @return the offset block position
*/
@Contract(pure = true)
@ -85,7 +83,7 @@ public interface BlockPosition extends Position {
* Returns a block position offset by the amount along
* the specified axis.
*
* @param axis the axis to offset along
* @param axis the axis to offset along
* @param amount the amount to offset along that axis
* @return the offset block position
*/

View file

@ -2,7 +2,6 @@ package io.papermc.paper.math;
import org.bukkit.util.NumberConversions;
import org.jetbrains.annotations.ApiStatus;
import org.jspecify.annotations.NullMarked;
/**
* A position represented with doubles.
@ -12,7 +11,6 @@ import org.jspecify.annotations.NullMarked;
* @see BlockPosition
*/
@ApiStatus.Experimental
@NullMarked
public interface FinePosition extends Position {
@Override

View file

@ -5,7 +5,6 @@ import org.bukkit.World;
import org.bukkit.util.Vector;
import org.jetbrains.annotations.ApiStatus;
import org.jetbrains.annotations.Contract;
import org.jspecify.annotations.NullMarked;
/**
* Common interface for {@link FinePosition} and {@link BlockPosition}.
@ -13,63 +12,68 @@ import org.jspecify.annotations.NullMarked;
* <b>May see breaking changes until Experimental annotation is removed.</b>
*/
@ApiStatus.Experimental
@NullMarked
public interface Position {
/**
* A fine position at the origin (0, 0, 0).
*/
FinePosition FINE_ZERO = new FinePositionImpl(0, 0, 0);
/**
* A block position at the origin (0, 0, 0).
*/
BlockPosition BLOCK_ZERO = new BlockPositionImpl(0, 0, 0);
/**
* Gets the block x value for this position
* Gets the block x value for this position.
*
* @return the block x value
*/
int blockX();
/**
* Gets the block y value for this position
* Gets the block y value for this position.
*
* @return the block y value
*/
int blockY();
/**
* Gets the block z value for this position
* Gets the block z value for this position.
*
* @return the block z value
*/
int blockZ();
/**
* Gets the x value for this position
* Gets the x value for this position.
*
* @return the x value
*/
double x();
/**
* Gets the y value for this position
* Gets the y value for this position.
*
* @return the y value
*/
double y();
/**
* Gets the z value for this position
* Gets the z value for this position.
*
* @return the z value
*/
double z();
/**
* Checks of this position represents a {@link BlockPosition}
* Checks of this position represents a {@link BlockPosition}.
*
* @return true if block
*/
boolean isBlock();
/**
* Checks if this position represents a {@link FinePosition}
* Checks if this position represents a {@link FinePosition}.
*
* @return true if fine
*/
@ -77,6 +81,8 @@ public interface Position {
/**
* Checks if each component of this position is finite.
*
* @return true if each component of this position is finite
*/
default boolean isFinite() {
return Double.isFinite(this.x()) && Double.isFinite(this.y()) && Double.isFinite(this.z());
@ -103,7 +109,7 @@ public interface Position {
FinePosition offset(double x, double y, double z);
/**
* Returns a new position at the center of the block position this represents
* Returns a new position at the center of the block position this represents.
*
* @return a new center position
*/
@ -114,7 +120,7 @@ public interface Position {
/**
* Returns the block position of this position
* or itself if it already is a block position
* or itself if it already is a block position.
*
* @return the block position
*/
@ -122,7 +128,7 @@ public interface Position {
BlockPosition toBlock();
/**
* Converts this position to a vector
* Converts this position to a vector.
*
* @return a new vector
*/
@ -132,7 +138,7 @@ public interface Position {
}
/**
* Creates a new location object at this position with the specified world
* Creates a new location object at this position with the specified world.
*
* @param world the world for the location object
* @return a new location
@ -143,7 +149,7 @@ public interface Position {
}
/**
* Creates a position at the coordinates
* Creates a position at the coordinates.
*
* @param x x coord
* @param y y coord
@ -167,7 +173,7 @@ public interface Position {
}
/**
* Creates a position at the coordinates
* Creates a position at the coordinates.
*
* @param x x coord
* @param y y coord

View file

@ -1,11 +1,8 @@
package io.papermc.paper.math;
import org.jspecify.annotations.NullMarked;
/**
* Represents a rotation with specified pitch and yaw values.
*/
@NullMarked
public interface Rotation {
/**
* Creates a new rotation with the specified yaw and pitch values.

View file

@ -1,21 +1,18 @@
package io.papermc.paper.math;
import org.jspecify.annotations.NullMarked;
/**
* Rotations is an immutable object that stores rotations
* in degrees on each axis (X, Y, Z).
*/
@NullMarked
public interface Rotations {
/**
* Rotations instance with every axis set to 0
* Rotations instance with every axis set to 0.
*/
Rotations ZERO = ofDegrees(0, 0, 0);
/**
* Creates a new Rotations instance holding the provided rotations
* Creates a new Rotations instance holding the provided rotations.
*
* @param x the angle for the X axis in degrees
* @param y the angle for the Y axis in degrees
@ -27,21 +24,21 @@ public interface Rotations {
}
/**
* Returns the angle on the X axis in degrees
* Returns the angle on the X axis in degrees.
*
* @return the angle in degrees
*/
double x();
/**
* Returns the angle on the Y axis in degrees
* Returns the angle on the Y axis in degrees.
*
* @return the angle in degrees
*/
double y();
/**
* Returns the angle on the Z axis in degrees
* Returns the angle on the Z axis in degrees.
*
* @return the angle in degrees
*/
@ -49,7 +46,7 @@ public interface Rotations {
/**
* Returns a new Rotations instance which is the result
* of changing the X axis to the passed angle
* of changing the X axis to the passed angle.
*
* @param x the angle in degrees
* @return the resultant Rotations
@ -58,7 +55,7 @@ public interface Rotations {
/**
* Returns a new Rotations instance which is the result
* of changing the Y axis to the passed angle
* of changing the Y axis to the passed angle.
*
* @param y the angle in degrees
* @return the resultant Rotations
@ -67,7 +64,7 @@ public interface Rotations {
/**
* Returns a new Rotations instance which is the result
* of changing the Z axis to the passed angle
* of changing the Z axis to the passed angle.
*
* @param z the angle in degrees
* @return the resultant Rotations
@ -76,7 +73,7 @@ public interface Rotations {
/**
* Returns a new Rotations instance which is the result of adding
* the x, y, z components to this Rotations
* the x, y, z components to this Rotations.
*
* @param x the angle to add to the X axis in degrees
* @param y the angle to add to the Y axis in degrees
@ -87,7 +84,7 @@ public interface Rotations {
/**
* Returns a new Rotations instance which is the result of subtracting
* the x, y, z components from this Rotations
* the x, y, z components from this Rotations.
*
* @param x the angle to subtract from the X axis in degrees
* @param y the angle to subtract from the Y axis in degrees

View file

@ -1,8 +1,5 @@
package io.papermc.paper.math;
import org.jspecify.annotations.NullMarked;
@NullMarked
record RotationsImpl(double x, double y, double z) implements Rotations {
@Override

View file

@ -0,0 +1,7 @@
/**
* Package containing maths-related API, including position API.
*/
@NullMarked
package io.papermc.paper.math;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,7 @@
/**
* Root package for the Paper API.
*/
@NullMarked
package io.papermc.paper;
import org.jspecify.annotations.NullMarked;

View file

@ -4,56 +4,57 @@ import java.time.Instant;
import java.util.Optional;
import java.util.OptionalInt;
import net.kyori.adventure.key.Key;
import org.jetbrains.annotations.NotNull;
import org.jspecify.annotations.NullMarked;
@NullMarked
public class TestServerBuildInfo implements ServerBuildInfo {
@Override
public @NotNull Key brandId() {
public Key brandId() {
throw new UnsupportedOperationException();
}
@Override
public boolean isBrandCompatible(final @NotNull Key brandId) {
public boolean isBrandCompatible(final Key brandId) {
throw new UnsupportedOperationException();
}
@Override
public @NotNull String brandName() {
public String brandName() {
throw new UnsupportedOperationException();
}
@Override
public @NotNull String minecraftVersionId() {
public String minecraftVersionId() {
throw new UnsupportedOperationException();
}
@Override
public @NotNull String minecraftVersionName() {
public String minecraftVersionName() {
throw new UnsupportedOperationException();
}
@Override
public @NotNull OptionalInt buildNumber() {
public OptionalInt buildNumber() {
throw new UnsupportedOperationException();
}
@Override
public @NotNull Instant buildTime() {
public Instant buildTime() {
throw new UnsupportedOperationException();
}
@Override
public @NotNull Optional<String> gitBranch() {
public Optional<String> gitBranch() {
throw new UnsupportedOperationException();
}
@Override
public @NotNull Optional<String> gitCommit() {
public Optional<String> gitCommit() {
throw new UnsupportedOperationException();
}
@Override
public @NotNull String asString(final @NotNull StringRepresentation representation) {
public String asString(final StringRepresentation representation) {
return "";
}
}

View file

@ -0,0 +1,9 @@
<!DOCTYPE module PUBLIC
"-//Checkstyle//DTD Checkstyle Configuration 1.3//EN"
"https://checkstyle.org/dtds/configuration_1_3.dtd">
<module name="Checker">
<module name="SuppressionSingleFilter">
<property name="checks" value=".*"/>
<property name="files" value="src[\\/]testData[\\/]java[^\\/]*[\\/].*"/>
</module>
</module>

View file

@ -0,0 +1,26 @@
plugins {
java
id("io.papermc.paperweight.paper-checkstyle")
}
val testData = sourceSets.create("testData")
dependencies {
implementation("com.puppycrawl.tools:checkstyle:13.8.0")
implementation("org.jspecify:jspecify:1.0.0")
testCompileOnly("org.jetbrains:annotations:26.0.2")
testImplementation(testData.output)
testImplementation("org.junit.jupiter:junit-jupiter:5.13.3")
testRuntimeOnly("org.junit.platform:junit-platform-launcher")
testData.implementationConfigurationName("org.jspecify:jspecify:1.0.0")
checkstyle(project(":paper-checkstyle"))
}
tasks {
test {
useJUnitPlatform()
}
}

View file

@ -0,0 +1,185 @@
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.TokenUtil;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.Set;
import java.util.function.Predicate;
import org.apache.commons.lang3.tuple.Pair;
import org.jspecify.annotations.Nullable;
import static java.util.Objects.requireNonNull;
/**
* Utility class containing utility methods for custom checkstyle checks.
*/
public final class Util {
public static final Set<Integer> PRIMITIVE_TYPES = Set.of(
TokenTypes.LITERAL_VOID,
TokenTypes.LITERAL_BOOLEAN,
TokenTypes.LITERAL_BYTE,
TokenTypes.LITERAL_CHAR,
TokenTypes.LITERAL_SHORT,
TokenTypes.LITERAL_INT,
TokenTypes.LITERAL_LONG,
TokenTypes.LITERAL_FLOAT,
TokenTypes.LITERAL_DOUBLE
);
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 = node.getPreviousSibling();
while (sibling != null && sibling.getType() != type) {
sibling = sibling.getPreviousSibling();
}
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 last child of the given node with the given type.
*
* @param node the node
* @param type the type
* @return the last child with the given type or {@code null} if not found
*/
public static @Nullable DetailAST getLastChild(final DetailAST node, final int type) {
DetailAST child = node.getLastChild();
while (child != null && child.getType() != type) {
child = child.getPreviousSibling();
}
return child;
}
/**
* 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<? super 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);
if (annotations == null) {
return false;
}
for (final DetailAST annotation : Util.childrenIterator(annotations, TokenTypes.ANNOTATION)) {
if (annotationPredicate.test(annotation)) {
return true;
}
}
return false;
}
public static Pair<String, DetailAST> extractFullIdent(final DetailAST lastDot) {
final List<String> parts = new ArrayList<>();
DetailAST dot = lastDot;
while (dot.getChildCount(TokenTypes.DOT) > 0) {
parts.addFirst(requireNonNull(dot.findFirstToken(TokenTypes.IDENT)).getText());
dot = requireNonNull(dot.findFirstToken(TokenTypes.DOT));
}
if (dot.getChildCount(TokenTypes.IDENT) > 2) {
throw new IllegalArgumentException("Invalid AST structure, expected <= 2 IDENTs " + dot.getChildCount(TokenTypes.IDENT) + " " + dot.getLineNo() + " " + dot.getColumnNo());
}
final DetailAST ident = requireNonNull(dot.findFirstToken(TokenTypes.IDENT));
final DetailAST secondIdent = getNextSibling(ident, TokenTypes.IDENT);
if (secondIdent != null) {
parts.addFirst(secondIdent.getText());
}
parts.addFirst(ident.getText());
return Pair.of(String.join(".", parts), ident);
}
}

View file

@ -0,0 +1,69 @@
package io.papermc.checkstyle.checks;
import com.puppycrawl.tools.checkstyle.api.DetailNode;
import com.puppycrawl.tools.checkstyle.api.JavadocCommentsTokenTypes;
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;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* Checks that parameter descriptions in Javadoc are aligned.
*/
public final class JavadocAlignParameterDescriptionCheck extends AbstractJavadocCheck {
static final Pattern SPACE_PREFIX = Pattern.compile("^\\s*");
@Override
public int[] getDefaultJavadocTokens() {
return new int[]{JavadocCommentsTokenTypes.JAVADOC_CONTENT};
}
@Override
public void visitJavadocToken(final DetailNode detailNode) {
record ParamDesc(DetailNode node, int startCol) {
}
final List<ParamDesc> params = new ArrayList<>();
int maxColumn = -1;
DetailNode child = detailNode.getFirstChild();
for (final DetailNode javadocBlock : JavadocUtil.getAllNodesOfType(detailNode, JavadocCommentsTokenTypes.JAVADOC_BLOCK_TAG)) {
final DetailNode paramBlock = JavadocUtil.findFirstToken(javadocBlock, JavadocCommentsTokenTypes.PARAM_BLOCK_TAG);
if (paramBlock == null) {
continue;
}
final DetailNode paramDescription = JavadocUtil.findFirstToken(paramBlock, JavadocCommentsTokenTypes.DESCRIPTION);
if (paramDescription == null) {
continue;
}
// iterate over all text nodes (multiline)
for (final DetailNode textNode : JavadocUtil.getAllNodesOfType(paramDescription, JavadocCommentsTokenTypes.TEXT)) {
final Matcher matcher = SPACE_PREFIX.matcher(textNode.getText());
int paramDescColNum = textNode.getColumnNumber();
if (matcher.find()) {
paramDescColNum += matcher.group().length();
}
maxColumn = Math.max(maxColumn, paramDescColNum);
params.add(new ParamDesc(textNode, paramDescColNum));
}
child = child.getNextSibling();
}
for (final ParamDesc param : params) {
if (param.startCol() != maxColumn) {
final DetailNode paramNameNode = Util.getPreviousSibling(param.node().getParent(), JavadocCommentsTokenTypes.PARAMETER_NAME);
if (paramNameNode == null) {
continue;
}
this.log(
param.node().getLineNumber(),
param.startCol(),
"Param description for %s should start at column %d".formatted(paramNameNode.getText(), maxColumn + 1)
);
}
}
}
}

View file

@ -0,0 +1,120 @@
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 com.puppycrawl.tools.checkstyle.utils.TokenUtil;
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");
}
}
if (TokenUtil.findFirstTokenByPredicate(type, t -> Util.PRIMITIVE_TYPES.contains(t.getType())).isPresent()) {
// skip primitive types
return;
}
final DetailAST dot = type.findFirstToken(TokenTypes.DOT);
final DetailAST annotationHolder;
final DetailAST identLoc;
if (dot != null) {
annotationHolder = dot.findFirstToken(TokenTypes.ANNOTATIONS);
identLoc = Util.getLastChild(dot, TokenTypes.IDENT);
} else {
annotationHolder = holderDef.findFirstToken(baseAnnotationHolderType);
identLoc = type;
}
if (hasNoNullabilityAnnotationChildren(annotationHolder)) {
this.log(identLoc.getLineNo(), identLoc.getColumnNo(), "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,112 @@
package io.papermc.checkstyle.checks;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import io.papermc.checkstyle.Util;
import io.papermc.checkstyle.checks.util.ImportAwareAbstractCheck;
import org.apache.commons.lang3.tuple.Pair;
import org.jspecify.annotations.Nullable;
/**
* Requires that the codebase be set up to follow standard naming conventions.
* <ul>
* <li>packages are always all lowercase</li>
* <li>class names start with a capital letter</li>
* </ul>
*/
public class UnnecessaryFullyQualifiedImportCheck extends ImportAwareAbstractCheck {
@Override
public int[] getDefaultTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getAcceptableTokens() {
return this.getRequiredTokens();
}
@Override
public int[] getRequiredTokens() {
return new int[]{
TokenTypes.TYPE,
TokenTypes.TYPE_ARGUMENT,
TokenTypes.TYPE_PARAMETER,
TokenTypes.TYPE_UPPER_BOUNDS,
TokenTypes.TYPE_LOWER_BOUNDS,
TokenTypes.EXTENDS_CLAUSE,
TokenTypes.PERMITS_CLAUSE,
TokenTypes.IMPLEMENTS_CLAUSE,
TokenTypes.LITERAL_NEW,
TokenTypes.ANNOTATION,
TokenTypes.METHOD_CALL,
TokenTypes.LITERAL_CLASS,
};
}
private static @Nullable DetailAST fromMethodCall(final DetailAST methodCall) {
final DetailAST dot = methodCall.findFirstToken(TokenTypes.DOT);
if (dot == null) {
return null;
}
final DetailAST nextDot = dot.findFirstToken(TokenTypes.DOT);
if (nextDot == null) {
return null;
}
final DetailAST ident = nextDot.findFirstToken(TokenTypes.IDENT);
if (ident == null || Character.isLowerCase(ident.getText().charAt(0))) {
// method call not on a type
return null;
}
return dot;
}
private static @Nullable DetailAST fromLiteralClass(final DetailAST literalClass) {
final DetailAST dot = literalClass.getParent();
if (dot.getType() != TokenTypes.DOT) {
return null;
}
return dot;
}
@Override
public void visitToken(final DetailAST ast) {
final DetailAST actualAst = switch (ast.getType()) {
case TokenTypes.METHOD_CALL -> fromMethodCall(ast);
case TokenTypes.LITERAL_CLASS -> fromLiteralClass(ast);
default -> ast;
};
if (actualAst == null) {
return;
}
final DetailAST dot = actualAst.findFirstToken(TokenTypes.DOT);
if (dot == null) {
return;
}
final Pair<String, DetailAST> fullIdent = Util.extractFullIdent(dot);
final DetailAST startNode = fullIdent.getRight();
final String fullIdentName = fullIdent.getLeft();
if (Character.isUpperCase(fullIdentName.charAt(0))) {
// not a package
return;
}
String previousTypeName;
String typeName = fullIdentName;
int lastDotIdx = typeName.lastIndexOf('.');
do {
// Check if we have an import for this exact type
if (this.hasImportFor(typeName)) {
this.log(startNode.getLineNo(), startNode.getColumnNo(), "Fully qualified import used: " + fullIdentName);
return;
}
previousTypeName = typeName;
typeName = typeName.substring(0, lastDotIdx);
} while ((lastDotIdx = typeName.lastIndexOf('.')) != -1 && Character.isUpperCase(typeName.substring(0, lastDotIdx + 2).charAt(lastDotIdx + 1)));
final String outerSimpleName = previousTypeName.substring(previousTypeName.lastIndexOf('.') + 1);
final String fullImportBySimple = this.getFullImportBySimple(outerSimpleName);
if (fullImportBySimple == null) {
this.log(startNode.getLineNo(), startNode.getColumnNo(), "Unnecessary fully qualified import (no conflict): " + fullIdentName);
}
}
}

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,55 @@
package io.papermc.checkstyle.checks.util;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.DetailAST;
import com.puppycrawl.tools.checkstyle.api.FullIdent;
import com.puppycrawl.tools.checkstyle.api.TokenTypes;
import io.papermc.checkstyle.Util;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.Map;
import java.util.Objects;
import java.util.Set;
import org.jspecify.annotations.Nullable;
public abstract class ImportAwareAbstractCheck extends AbstractCheck {
private final ThreadLocal<Set<String>> imports = ThreadLocal.withInitial(LinkedHashSet::new);
private final ThreadLocal<Map<String, String>> simpleImports = ThreadLocal.withInitial(LinkedHashMap::new);
@Override
public void beginTree(final DetailAST rootAST) {
final Set<String> imports = new LinkedHashSet<>();
final Map<String, String> simpleImports = new LinkedHashMap<>();
for (final DetailAST importAst : Util.childrenIterator(rootAST, TokenTypes.IMPORT)) {
extractName(importAst, imports, simpleImports);
}
this.imports.set(imports);
this.simpleImports.set(simpleImports);
}
private static void extractName(final DetailAST parent, final Set<String> fullImports, final Map<String, String> simpleImports) {
final DetailAST lastDot = Objects.requireNonNull(parent.findFirstToken(TokenTypes.DOT));
if (lastDot.getChildCount(TokenTypes.STAR) > 0) {
// skip wildcard imports
return;
}
final DetailAST ident = Objects.requireNonNull(lastDot.findFirstToken(TokenTypes.IDENT));
final String fullImport = FullIdent.createFullIdent(lastDot).getText();
fullImports.add(fullImport);
simpleImports.put(ident.getText(), fullImport);
}
@Override
public void finishTree(final DetailAST rootAST) {
this.imports.remove();
}
protected boolean hasImportFor(final String type) {
return this.imports.get().contains(type);
}
protected @Nullable String getFullImportBySimple(final String simpleName) {
return this.simpleImports.get().get(simpleName);
}
}

View file

@ -0,0 +1,7 @@
/**
* Utility classes for checks.
*/
@NullMarked
package io.papermc.checkstyle.checks.util;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,49 @@
package io.papermc.checkstyle.filters;
import com.puppycrawl.tools.checkstyle.AbstractAutomaticBean;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.Filter;
import io.papermc.checkstyle.filters.util.BetterSuppressFilterElement;
import java.util.Arrays;
import java.util.List;
import java.util.regex.Pattern;
import org.jspecify.annotations.Nullable;
public class BetterSuppressionSingleFilter extends AbstractAutomaticBean implements Filter {
private @Nullable BetterSuppressFilterElement filter;
private @Nullable Pattern checks;
private @Nullable Pattern messageKey;
private @Nullable List<Pattern> arguments;
public void setChecks(final Pattern checks) {
this.checks = checks;
}
public void setMessageKey(final Pattern messageKey) {
this.messageKey = messageKey;
}
public void setArguments(final String... arguments) {
this.arguments = Arrays.stream(arguments).map(Pattern::compile).toList();
}
@Override
protected void finishLocalSetup() {
this.filter = new BetterSuppressFilterElement(
null,
this.checks,
null,
null,
null,
null,
this.messageKey,
this.arguments
);
}
@Override
public boolean accept(final AuditEvent event) {
return this.filter.accept(event);
}
}

View file

@ -0,0 +1,7 @@
/**
* Custom filters for Checkstyle.
*/
@NullMarked
package io.papermc.checkstyle.filters;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,68 @@
package io.papermc.checkstyle.filters.util;
import com.puppycrawl.tools.checkstyle.api.AuditEvent;
import com.puppycrawl.tools.checkstyle.api.Violation;
import com.puppycrawl.tools.checkstyle.filters.SuppressFilterElement;
import java.lang.invoke.MethodHandle;
import java.lang.invoke.MethodHandles;
import java.util.List;
import java.util.regex.Pattern;
import java.util.stream.IntStream;
import org.jspecify.annotations.Nullable;
public class BetterSuppressFilterElement extends SuppressFilterElement {
private static final MethodHandle VIOLATION_ARGS;
static {
try {
VIOLATION_ARGS = MethodHandles.privateLookupIn(Violation.class, MethodHandles.lookup()).findGetter(Violation.class, "args", Object[].class);
} catch (final IllegalAccessException | NoSuchFieldException e) {
throw new RuntimeException(e);
}
}
private final @Nullable Pattern messageKey;
private final @Nullable List<Pattern> arguments;
public BetterSuppressFilterElement(
final @Nullable Pattern files,
final @Nullable Pattern checks,
final @Nullable Pattern message,
final @Nullable String moduleId,
final @Nullable String lines,
final @Nullable String columns,
final @Nullable Pattern messageKey,
final @Nullable List<Pattern> arguments
) {
super(files, checks, message, moduleId, lines, columns);
this.messageKey = messageKey;
this.arguments = arguments;
}
@Override
public boolean accept(final AuditEvent event) {
return super.accept(event) || !this.isMessageKeyMatching(event) || !this.isMessageArgumentsMatching(event);
}
private boolean isMessageKeyMatching(final AuditEvent event) {
return this.messageKey == null || this.messageKey.matcher(event.getViolation().getKey()).find();
}
private boolean isMessageArgumentsMatching(final AuditEvent event) {
if (this.arguments == null || this.arguments.isEmpty()) {
return true;
}
final Object[] args;
try {
args = ((Object[]) VIOLATION_ARGS.invoke(event.getViolation()));
} catch (final Throwable e) {
throw new RuntimeException(e);
}
return IntStream.range(0, Math.min(this.arguments.size(), args.length)).allMatch(i -> {
final Object arg = args[i];
final Pattern argPattern = this.arguments.get(i);
return argPattern.matcher(arg.toString()).find();
});
}
}

View file

@ -0,0 +1,7 @@
/**
* Utility classes for filters.
*/
@NullMarked
package io.papermc.checkstyle.filters.util;
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>

View file

@ -0,0 +1,102 @@
package io.papermc.checkstyle;
import com.puppycrawl.tools.checkstyle.DefaultConfiguration;
import com.puppycrawl.tools.checkstyle.PackageObjectFactory;
import com.puppycrawl.tools.checkstyle.TreeWalker;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import com.puppycrawl.tools.checkstyle.api.FileText;
import com.puppycrawl.tools.checkstyle.api.Violation;
import java.io.File;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.LinkedHashSet;
import java.util.Set;
import org.apache.commons.lang3.reflect.MethodUtils;
import org.intellij.lang.annotations.Language;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.AfterTestExecutionCallback;
import org.junit.jupiter.api.extension.ExtendWith;
import org.junit.jupiter.api.extension.ExtensionContext;
import org.junit.jupiter.api.extension.ParameterContext;
import org.junit.jupiter.api.extension.ParameterResolutionException;
import org.junit.jupiter.api.extension.ParameterResolver;
import org.junit.platform.commons.support.AnnotationSupport;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
@Documented
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@ExtendWith({CheckTest.Parameters.class, CheckTest.RunCheckTest.class})
@Test
public @interface CheckTest {
Object BUILDER_KEY = new Object();
@Language("jvm-class-name")
String value();
final class Parameters implements ParameterResolver {
@Override
public boolean supportsParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException {
return parameterContext.getParameter().getType() == CheckTestBuilder.class;
}
@Override
public Object resolveParameter(final ParameterContext parameterContext, final ExtensionContext extensionContext) throws ParameterResolutionException {
final CheckTestBuilder builder = new CheckTestBuilder();
extensionContext.getStore(ExtensionContext.Namespace.GLOBAL).put(CheckTest.BUILDER_KEY, builder);
return builder;
}
}
final class RunCheckTest implements AfterTestExecutionCallback {
@Override
public void afterTestExecution(final ExtensionContext context) throws Exception {
final CheckTest checkTest = AnnotationSupport.findAnnotation(context.getTestMethod(), CheckTest.class).orElseThrow();
final CheckTestBuilder builder = context.getStore(ExtensionContext.Namespace.GLOBAL).get(CheckTest.BUILDER_KEY, CheckTestBuilder.class);
assertNotNull(builder, "Method must have a CheckTestBuilder parameter");
assertFalse(builder.checks.isEmpty(), "CheckTestBuilder must have at least one check");
final TreeWalker walker = new TreeWalker();
walker.setModuleFactory(new PackageObjectFactory("io.papermc.checkstyle.checks", Util.class.getClassLoader()));
for (final AbstractCheck check : builder.checks) {
check.init();
check.configure(new DefaultConfiguration("Test"));
MethodUtils.invokeMethod(walker, true, "registerCheck", check);
}
final Path filePath = Path.of("src", "testData", "java", checkTest.value().replace('.', File.separatorChar) + ".java");
assertTrue(Files.exists(filePath), "File not found: " + filePath);
assertFalse(Files.isDirectory(filePath), "File is a directory: " + filePath);
final File file = filePath.toFile();
final Set<Violation> violations = walker.process(file, new FileText(file, StandardCharsets.UTF_8.name()));
final Set<String> violationSet = new LinkedHashSet<>(violations.stream().map(v -> "%d:%d: %s".formatted(v.getLineNo(), v.getColumnNo(), v.getViolation())).toList());
final Set<String> expectedSet = new LinkedHashSet<>(builder.expectedViolations);
final Set<String> extraViolations = new LinkedHashSet<>(violationSet);
extraViolations.removeAll(expectedSet);
final Set<String> missingViolations = new LinkedHashSet<>(expectedSet);
missingViolations.removeAll(violationSet);
if (!extraViolations.isEmpty() || !missingViolations.isEmpty()) {
final StringBuilder sb = new StringBuilder();
sb.append("Violations do not match for ").append(filePath).append("\n");
if (!extraViolations.isEmpty()) {
sb.append("Violations not accounted for in test: \n");
extraViolations.forEach(v -> sb.append(" ").append(v).append("\n"));
}
if (!missingViolations.isEmpty()) {
sb.append("Violations in tests that don't exist: \n");
missingViolations.forEach(v -> sb.append(" ").append(v).append("\n"));
}
throw new AssertionError(sb.toString());
}
}
}
}

View file

@ -0,0 +1,22 @@
package io.papermc.checkstyle;
import com.puppycrawl.tools.checkstyle.api.AbstractCheck;
import java.util.ArrayList;
import java.util.List;
public class CheckTestBuilder {
final List<AbstractCheck> checks = new ArrayList<>();
final List<String> expectedViolations = new ArrayList<>();
public CheckTestBuilder addCheck(final AbstractCheck check) {
this.checks.add(check);
return this;
}
public CheckTestBuilder addViolation(final String expectedViolation) {
this.expectedViolations.add(expectedViolation);
return this;
}
}

View file

@ -0,0 +1,16 @@
package io.papermc.checkstyle.checks;
import io.papermc.checkstyle.CheckTest;
import io.papermc.checkstyle.CheckTestBuilder;
class TestJavadocAlignParameterDescriptionCheck {
@CheckTest("testdata.JavadocParamAlignment")
void test(final CheckTestBuilder builder) {
builder
.addCheck(new JavadocAlignParameterDescriptionCheck())
.addViolation("10:17: Param description for a should start at column 34")
.addViolation("30:23: Param description for a should start at column 34")
.addViolation("32:30: Param description for superLongParamName should start at column 34");
}
}

View file

@ -0,0 +1,21 @@
package io.papermc.checkstyle.checks;
import io.papermc.checkstyle.CheckTest;
import io.papermc.checkstyle.CheckTestBuilder;
class TestNullabilityAnnotationsCheck {
@CheckTest("testdata.NullabilityAnnotations")
void test(final CheckTestBuilder builder) {
builder
.addViolation("13:12: Missing nullability annotation for method2")
.addViolation("13:34: Missing nullability annotation for param")
.addViolation("17:27: Array is missing nullability annotation")
.addViolation("21:12: Missing nullability annotation for missingTypeAnnotation")
.addViolation("25:14: Array is missing nullability annotation")
.addViolation("25:34: Array is missing nullability annotation")
.addViolation("43:57: Missing nullability annotation for param")
.addViolation("49:73: Missing nullability annotation for param")
.addCheck(new NullabilityAnnotationsCheck());
}
}

View file

@ -0,0 +1,37 @@
package io.papermc.checkstyle.checks;
import io.papermc.checkstyle.CheckTest;
import io.papermc.checkstyle.CheckTestBuilder;
class TestUnnecessaryFullyQualifiedImportCheck {
@CheckTest("testdata.imports.UnnecessaryFullyQualifiedImport")
void test(final CheckTestBuilder builder) {
builder
// variable def
.addViolation("20:15: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass")
// new literal
.addViolation("20:84: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass")
// variable def
.addViolation("21:15: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass.InnerClass")
// new literal
.addViolation("21:95: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass.InnerClass")
// type parm def (extends)
.addViolation("29:23: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass.InnerClass")
// type argument, type param super
.addViolation("29:115: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass.InnerClass")
.addViolation("37:23: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass.InnerClass")
.addViolation("37:71: Unnecessary fully qualified import (no conflict): org.jspecify.annotations.Nullable")
.addViolation("37:143: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass.InnerClass")
.addViolation("46:41: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass.InnerInterface")
.addViolation("50:30: Unnecessary fully qualified import (no conflict): testdata.imports.testclasses.subpkg.OtherInterface")
.addViolation("54:9: Unnecessary fully qualified import (no conflict): testdata.imports.testclasses.subpkg.OtherInterface")
.addViolation("54:61: Unnecessary fully qualified import (no conflict): testdata.imports.testclasses.subpkg.OtherInterface")
.addViolation("55:9: Unnecessary fully qualified import (no conflict): testdata.imports.testclasses.subpkg.OtherInterface")
// field def
.addViolation("66:5: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass")
// class literal ".class"
.addViolation("70:28: Fully qualified import used: testdata.imports.testclasses.subpkg.OtherClass")
.addCheck(new UnnecessaryFullyQualifiedImportCheck());
}
}

View file

@ -0,0 +1,4 @@
@NullMarked
package io.papermc.checkstyle.checks;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,4 @@
@NullMarked
package io.papermc.checkstyle;
import org.jspecify.annotations.NullMarked;

View file

@ -0,0 +1,48 @@
// @formatter:off
package testdata;
@SuppressWarnings("ALL")
public class JavadocParamAlignment {
/**
* A boring method.
*
* @param a some description
* @param superLongParamName some description
*/
public void method(String a, String superLongParamName) {
}
/**
* A boring method.
*
* @param a some description
* @param superLongParamName some description
* @param another param
*/
public void correct_method(String a, String superLongParamName) {
}
/**
* A boring method.
*
* @param a some description
* with extra text
* @param superLongParamName some description
* blah
*/
public void method_multiline(String a, String superLongParamName) {
}
/**
* A boring method.
*
* @param a some description
* with extra text
* @param superLongParamName some description
* blah
*/
public void method_multiline_correct(String a, String superLongParamName) {
}
}

View file

@ -0,0 +1,54 @@
package testdata;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.NullMarked;
import org.jspecify.annotations.Nullable;
public class NullabilityAnnotations {
public int method(final int param) { // primitive, no annotation
return 0;
}
public Integer method2(final Integer param) {
return null;
}
public @Nullable String[] missingArrayAnnotation() {
return null;
}
public String @Nullable[] missingTypeAnnotation() {
return null;
}
public int[] method3(final int[] param) {
return null;
}
@NullMarked
public static final class InnerClass {
// no annotations cause null marked
public static String method(final String param) {
return "";
}
public static final class InnerInnerClass {
}
}
public void method1(final @NonNull Integer param) {
}
public void innerClass(final NullabilityAnnotations.InnerClass param) {
}
public void innerClassCorrect(final NullabilityAnnotations.@Nullable InnerClass param) {
}
public void innerInnerClass(final NullabilityAnnotations.InnerClass.InnerInnerClass param) {
}
public void innerInnerClassCorrect(final NullabilityAnnotations.InnerClass.@Nullable InnerInnerClass param) {
}
}

View file

@ -0,0 +1,73 @@
// @formatter:off
package testdata.imports;
import java.util.HashMap;
import java.util.Map;
import java.util.Set;
import testdata.imports.testclasses.subpkg.OtherClass;
import java.lang.*;
@SuppressWarnings("ALL")
public class UnnecessaryFullyQualifiedImport {
static {
final Map<String, String> map = new HashMap<>();
final Set<Map.Entry<String, String>> entries = map.entrySet();
final OtherClass otherClass = new OtherClass();
final OtherClass.InnerClass innerClass = new OtherClass.InnerClass();
final testdata.imports.testclasses.OtherClass otherClass1 = new testdata.imports.testclasses.OtherClass();
final testdata.imports.testclasses.OtherClass.InnerClass innerClass1 = new testdata.imports.testclasses.OtherClass.InnerClass();
final testdata.imports.testclasses.subpkg.OtherClass otherClass1_bad = new testdata.imports.testclasses.subpkg.OtherClass();
final testdata.imports.testclasses.subpkg.OtherClass.InnerClass innerClass1_bad = new testdata.imports.testclasses.subpkg.OtherClass.InnerClass();
final Map<testdata.imports.testclasses.OtherClass.InnerClass, OtherClass.InnerClass> map1 = new HashMap<>();
final Map<testdata.imports.testclasses.OtherClass.InnerClass, OtherClass> map2 = new HashMap<>();
final Map<? extends testdata.imports.testclasses.OtherClass.InnerClass, ? extends OtherClass.InnerClass> map3 = new HashMap<>();
final Map<? super testdata.imports.testclasses.OtherClass.InnerClass, ? extends OtherClass> map4 = new HashMap<>();
}
public <A extends testdata.imports.testclasses.subpkg.OtherClass.InnerClass> A m(final Map<? super A, ? super testdata.imports.testclasses.subpkg.OtherClass.InnerClass> m) {
return null;
}
public <A extends OtherClass.InnerClass> A m_correct(final Map<? super A, ? super OtherClass.InnerClass> m) {
return null;
}
public <A extends testdata.imports.testclasses.subpkg.OtherClass.@org.jspecify.annotations.Nullable InnerClass> A m2(final Map<? super A, testdata.imports.testclasses.subpkg.OtherClass.InnerClass> m) {
return null;
}
public <A extends OtherClass.InnerClass> A m2_correct(final Map<? super A, OtherClass.InnerClass> m) {
return null;
}
public Object o() {
return (OtherClass.InnerClass & testdata.imports.testclasses.subpkg.OtherClass.InnerInterface) null;
}
public Object o2() {
return (OtherClass & testdata.imports.testclasses.subpkg.OtherInterface) null;
}
public void methods() {
testdata.imports.testclasses.subpkg.OtherInterface.<testdata.imports.testclasses.subpkg.OtherInterface>test();
testdata.imports.testclasses.subpkg.OtherInterface.test();
}
public void methods_correct() {
OtherClass.<OtherClass>test();
new OtherClass().instance_test();
OtherClass oc = new OtherClass();
oc.instance_test();
}
testdata.imports.testclasses.subpkg.OtherClass v = null;
testdata.imports.testclasses.OtherClass v2 = null;
public void clazz() {
System.out.println(testdata.imports.testclasses.subpkg.OtherClass.class);
System.out.println(OtherClass.class);
}
}

View file

@ -0,0 +1,12 @@
// @formatter:off
package testdata.imports.testclasses;
@SuppressWarnings("ALL")
public class OtherClass {
public static class InnerClass {
}
public interface InnerInterface {
}
}

View file

@ -0,0 +1,4 @@
package testdata.imports.testclasses;
public interface OtherInterface {
}

View file

@ -0,0 +1,14 @@
package testdata.imports.testclasses.subpkg;
public class OtherClass {
public static class InnerClass {
}
public interface InnerInterface {
}
public static void test() {}
public void instance_test() {}
}

View file

@ -0,0 +1,7 @@
package testdata.imports.testclasses.subpkg;
public interface OtherInterface {
static <T> void test() {
}
}

View file

@ -11,17 +11,17 @@ plugins {
if (!file(".git").exists()) {
val errorText = """
=====================[ ERROR ]=====================
The Paper project directory is not a properly cloned Git repository.
In order to build Paper from source you must clone
the Paper repository using Git, not download a code
zip from GitHub.
Built Paper jars are available for download at
https://papermc.io/downloads/paper
See https://github.com/PaperMC/Paper/blob/main/CONTRIBUTING.md
for further information on building and modifying Paper.
===================================================
@ -36,6 +36,8 @@ for (name in listOf("paper-api", "paper-server")) {
file(name).mkdirs()
}
include("paper-checkstyle")
optionalInclude("test-plugin")
optionalInclude("paper-generator")