diff --git a/.checkstyle/checkstyle_base.xml b/.checkstyle/checkstyle_base.xml new file mode 100644 index 0000000000..de2a8f13b3 --- /dev/null +++ b/.checkstyle/checkstyle_base.xml @@ -0,0 +1,203 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/.checkstyle/suppressions.xml b/.checkstyle/suppressions.xml new file mode 100644 index 0000000000..2b0d20aa39 --- /dev/null +++ b/.checkstyle/suppressions.xml @@ -0,0 +1,7 @@ + + + + + diff --git a/.checkstyle/type_use_annotations.txt b/.checkstyle/type_use_annotations.txt new file mode 100644 index 0000000000..4ac51acc7c --- /dev/null +++ b/.checkstyle/type_use_annotations.txt @@ -0,0 +1,10 @@ +NonNull +NotNull +Nullable +UnknownNullability +Unmodifiable +UnmodifiableView +Range +Positive +NonNegative +IntRange diff --git a/.checkstyle/xpath-suppressions.xml b/.checkstyle/xpath-suppressions.xml new file mode 100644 index 0000000000..b0ba3f631e --- /dev/null +++ b/.checkstyle/xpath-suppressions.xml @@ -0,0 +1,17 @@ + + + + + + + + + + + + + + + diff --git a/.editorconfig b/.editorconfig index 600eac0fb9..4911a3847a 100644 --- a/.editorconfig +++ b/.editorconfig @@ -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 diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index 181664ec91..4004e163e0 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -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 diff --git a/build.gradle.kts b/build.gradle.kts index 513b96803a..64f8f0aad6 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -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 { 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 { + typeUseAnnotationsFile.set(rootProject.layout.projectDirectory.file(".checkstyle/type_use_annotations.txt")) + } + + /*tasks.withType().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/" diff --git a/paper-api/.checkstyle/checkstyle.xml b/paper-api/.checkstyle/checkstyle.xml new file mode 100644 index 0000000000..f755c2af00 --- /dev/null +++ b/paper-api/.checkstyle/checkstyle.xml @@ -0,0 +1,26 @@ + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/paper-api/.checkstyle/ignored_directories.txt b/paper-api/.checkstyle/ignored_directories.txt new file mode 100644 index 0000000000..2d76afe894 --- /dev/null +++ b/paper-api/.checkstyle/ignored_directories.txt @@ -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/ diff --git a/paper-api/build.gradle.kts b/paper-api/build.gradle.kts index 987d61382b..c034f861f5 100644 --- a/paper-api/build.gradle.kts +++ b/paper-api/build.gradle.kts @@ -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().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 diff --git a/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java b/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java index 1590367ec2..64306b5745 100644 --- a/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java +++ b/paper-api/src/main/java/io/papermc/paper/InternalAPIBridge.java @@ -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(); } diff --git a/paper-api/src/main/java/io/papermc/paper/ServerBuildInfo.java b/paper-api/src/main/java/io/papermc/paper/ServerBuildInfo.java index 56a5877181..4e099b584c 100644 --- a/paper-api/src/main/java/io/papermc/paper/ServerBuildInfo.java +++ b/paper-api/src/main/java/io/papermc/paper/ServerBuildInfo.java @@ -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 INSTANCE = Services.service(ServerBuildInfo.class); } // + 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. diff --git a/paper-api/src/main/java/io/papermc/paper/math/Angle.java b/paper-api/src/main/java/io/papermc/paper/math/Angle.java index 5181f84935..1083ca4a8f 100644 --- a/paper-api/src/main/java/io/papermc/paper/math/Angle.java +++ b/paper-api/src/main/java/io/papermc/paper/math/Angle.java @@ -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); } diff --git a/paper-api/src/main/java/io/papermc/paper/math/BlockPosition.java b/paper-api/src/main/java/io/papermc/paper/math/BlockPosition.java index c358bfdefc..c247572422 100644 --- a/paper-api/src/main/java/io/papermc/paper/math/BlockPosition.java +++ b/paper-api/src/main/java/io/papermc/paper/math/BlockPosition.java @@ -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 */ diff --git a/paper-api/src/main/java/io/papermc/paper/math/FinePosition.java b/paper-api/src/main/java/io/papermc/paper/math/FinePosition.java index b9c0065d8a..91cbed4117 100644 --- a/paper-api/src/main/java/io/papermc/paper/math/FinePosition.java +++ b/paper-api/src/main/java/io/papermc/paper/math/FinePosition.java @@ -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 diff --git a/paper-api/src/main/java/io/papermc/paper/math/Position.java b/paper-api/src/main/java/io/papermc/paper/math/Position.java index 2fad6604a0..0e9d8ab38e 100644 --- a/paper-api/src/main/java/io/papermc/paper/math/Position.java +++ b/paper-api/src/main/java/io/papermc/paper/math/Position.java @@ -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; * May see breaking changes until Experimental annotation is removed. */ @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 diff --git a/paper-api/src/main/java/io/papermc/paper/math/Rotation.java b/paper-api/src/main/java/io/papermc/paper/math/Rotation.java index 73add399e4..e150572213 100644 --- a/paper-api/src/main/java/io/papermc/paper/math/Rotation.java +++ b/paper-api/src/main/java/io/papermc/paper/math/Rotation.java @@ -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. diff --git a/paper-api/src/main/java/io/papermc/paper/math/Rotations.java b/paper-api/src/main/java/io/papermc/paper/math/Rotations.java index 2a8e1aa29a..565628de3c 100644 --- a/paper-api/src/main/java/io/papermc/paper/math/Rotations.java +++ b/paper-api/src/main/java/io/papermc/paper/math/Rotations.java @@ -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 diff --git a/paper-api/src/main/java/io/papermc/paper/math/RotationsImpl.java b/paper-api/src/main/java/io/papermc/paper/math/RotationsImpl.java index 35b493b870..fd80eaa764 100644 --- a/paper-api/src/main/java/io/papermc/paper/math/RotationsImpl.java +++ b/paper-api/src/main/java/io/papermc/paper/math/RotationsImpl.java @@ -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 diff --git a/paper-api/src/main/java/io/papermc/paper/math/package-info.java b/paper-api/src/main/java/io/papermc/paper/math/package-info.java new file mode 100644 index 0000000000..b0116269d9 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/math/package-info.java @@ -0,0 +1,7 @@ +/** + * Package containing maths-related API, including position API. + */ +@NullMarked +package io.papermc.paper.math; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-api/src/main/java/io/papermc/paper/package-info.java b/paper-api/src/main/java/io/papermc/paper/package-info.java new file mode 100644 index 0000000000..c374169c56 --- /dev/null +++ b/paper-api/src/main/java/io/papermc/paper/package-info.java @@ -0,0 +1,7 @@ +/** + * Root package for the Paper API. + */ +@NullMarked +package io.papermc.paper; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-api/src/test/java/io/papermc/paper/TestServerBuildInfo.java b/paper-api/src/test/java/io/papermc/paper/TestServerBuildInfo.java index 17be27a869..43b8382917 100644 --- a/paper-api/src/test/java/io/papermc/paper/TestServerBuildInfo.java +++ b/paper-api/src/test/java/io/papermc/paper/TestServerBuildInfo.java @@ -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 gitBranch() { + public Optional gitBranch() { throw new UnsupportedOperationException(); } @Override - public @NotNull Optional gitCommit() { + public Optional gitCommit() { throw new UnsupportedOperationException(); } @Override - public @NotNull String asString(final @NotNull StringRepresentation representation) { + public String asString(final StringRepresentation representation) { return ""; } } diff --git a/paper-checkstyle/.checkstyle/checkstyle.xml b/paper-checkstyle/.checkstyle/checkstyle.xml new file mode 100644 index 0000000000..9e4ff22f88 --- /dev/null +++ b/paper-checkstyle/.checkstyle/checkstyle.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/paper-checkstyle/build.gradle.kts b/paper-checkstyle/build.gradle.kts new file mode 100644 index 0000000000..bac77afee2 --- /dev/null +++ b/paper-checkstyle/build.gradle.kts @@ -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() + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/Util.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/Util.java new file mode 100644 index 0000000000..0fd6bb463e --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/Util.java @@ -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 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 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 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 extractFullIdent(final DetailAST lastDot) { + final List 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); + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/JavadocAlignParameterDescriptionCheck.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/JavadocAlignParameterDescriptionCheck.java new file mode 100644 index 0000000000..ef47c063da --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/JavadocAlignParameterDescriptionCheck.java @@ -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 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) + ); + } + } + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/NullabilityAnnotationsCheck.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/NullabilityAnnotationsCheck.java new file mode 100644 index 0000000000..b32a288b72 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/NullabilityAnnotationsCheck.java @@ -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 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); + } + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/RedundantNullabilityCheck.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/RedundantNullabilityCheck.java new file mode 100644 index 0000000000..1ed6c69551 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/RedundantNullabilityCheck.java @@ -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"); + } + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/UnnecessaryFullyQualifiedImportCheck.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/UnnecessaryFullyQualifiedImportCheck.java new file mode 100644 index 0000000000..a3037f3ba3 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/UnnecessaryFullyQualifiedImportCheck.java @@ -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. + *
    + *
  • packages are always all lowercase
  • + *
  • class names start with a capital letter
  • + *
+ */ +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 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); + } + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/package-info.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/package-info.java new file mode 100644 index 0000000000..f33ebfddfa --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/package-info.java @@ -0,0 +1,8 @@ +/** + * Custom checkstyle checks for PaperMC projects. + */ +@NullMarked +@SuppressWarnings("unused") +package io.papermc.checkstyle.checks; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/util/ImportAwareAbstractCheck.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/util/ImportAwareAbstractCheck.java new file mode 100644 index 0000000000..f0b1494229 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/util/ImportAwareAbstractCheck.java @@ -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> imports = ThreadLocal.withInitial(LinkedHashSet::new); + private final ThreadLocal> simpleImports = ThreadLocal.withInitial(LinkedHashMap::new); + + @Override + public void beginTree(final DetailAST rootAST) { + final Set imports = new LinkedHashSet<>(); + final Map 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 fullImports, final Map 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); + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/util/package-info.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/util/package-info.java new file mode 100644 index 0000000000..c08c7bbe88 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/checks/util/package-info.java @@ -0,0 +1,7 @@ +/** + * Utility classes for checks. + */ +@NullMarked +package io.papermc.checkstyle.checks.util; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/BetterSuppressionSingleFilter.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/BetterSuppressionSingleFilter.java new file mode 100644 index 0000000000..93b220bb15 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/BetterSuppressionSingleFilter.java @@ -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 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); + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/package-info.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/package-info.java new file mode 100644 index 0000000000..5b684b88b0 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/package-info.java @@ -0,0 +1,7 @@ +/** + * Custom filters for Checkstyle. + */ +@NullMarked +package io.papermc.checkstyle.filters; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/util/BetterSuppressFilterElement.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/util/BetterSuppressFilterElement.java new file mode 100644 index 0000000000..971414b885 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/util/BetterSuppressFilterElement.java @@ -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 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 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(); + }); + } +} diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/util/package-info.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/util/package-info.java new file mode 100644 index 0000000000..a540a232a2 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/filters/util/package-info.java @@ -0,0 +1,7 @@ +/** + * Utility classes for filters. + */ +@NullMarked +package io.papermc.checkstyle.filters.util; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-checkstyle/src/main/java/io/papermc/checkstyle/package-info.java b/paper-checkstyle/src/main/java/io/papermc/checkstyle/package-info.java new file mode 100644 index 0000000000..cea2de0c77 --- /dev/null +++ b/paper-checkstyle/src/main/java/io/papermc/checkstyle/package-info.java @@ -0,0 +1,7 @@ +/** + * This package contains custom checkstyle rules for PaperMC projects. + */ +@NullMarked +package io.papermc.checkstyle; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-checkstyle/src/main/resources/checkstyle_packages.xml b/paper-checkstyle/src/main/resources/checkstyle_packages.xml new file mode 100644 index 0000000000..702587110a --- /dev/null +++ b/paper-checkstyle/src/main/resources/checkstyle_packages.xml @@ -0,0 +1,9 @@ + + + + + + + diff --git a/paper-checkstyle/src/test/java/io/papermc/checkstyle/CheckTest.java b/paper-checkstyle/src/test/java/io/papermc/checkstyle/CheckTest.java new file mode 100644 index 0000000000..09e96912ee --- /dev/null +++ b/paper-checkstyle/src/test/java/io/papermc/checkstyle/CheckTest.java @@ -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 violations = walker.process(file, new FileText(file, StandardCharsets.UTF_8.name())); + final Set violationSet = new LinkedHashSet<>(violations.stream().map(v -> "%d:%d: %s".formatted(v.getLineNo(), v.getColumnNo(), v.getViolation())).toList()); + final Set expectedSet = new LinkedHashSet<>(builder.expectedViolations); + final Set extraViolations = new LinkedHashSet<>(violationSet); + extraViolations.removeAll(expectedSet); + final Set 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()); + } + } + } +} diff --git a/paper-checkstyle/src/test/java/io/papermc/checkstyle/CheckTestBuilder.java b/paper-checkstyle/src/test/java/io/papermc/checkstyle/CheckTestBuilder.java new file mode 100644 index 0000000000..911ca22edb --- /dev/null +++ b/paper-checkstyle/src/test/java/io/papermc/checkstyle/CheckTestBuilder.java @@ -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 checks = new ArrayList<>(); + final List 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; + } + +} diff --git a/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestJavadocAlignParameterDescriptionCheck.java b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestJavadocAlignParameterDescriptionCheck.java new file mode 100644 index 0000000000..5820f51ed5 --- /dev/null +++ b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestJavadocAlignParameterDescriptionCheck.java @@ -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"); + } +} diff --git a/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestNullabilityAnnotationsCheck.java b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestNullabilityAnnotationsCheck.java new file mode 100644 index 0000000000..f139d23924 --- /dev/null +++ b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestNullabilityAnnotationsCheck.java @@ -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()); + } +} diff --git a/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestUnnecessaryFullyQualifiedImportCheck.java b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestUnnecessaryFullyQualifiedImportCheck.java new file mode 100644 index 0000000000..dd9f4ffe6c --- /dev/null +++ b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/TestUnnecessaryFullyQualifiedImportCheck.java @@ -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()); + } +} diff --git a/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/package-info.java b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/package-info.java new file mode 100644 index 0000000000..d30fb14449 --- /dev/null +++ b/paper-checkstyle/src/test/java/io/papermc/checkstyle/checks/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.papermc.checkstyle.checks; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-checkstyle/src/test/java/io/papermc/checkstyle/package-info.java b/paper-checkstyle/src/test/java/io/papermc/checkstyle/package-info.java new file mode 100644 index 0000000000..5cf7ad70ff --- /dev/null +++ b/paper-checkstyle/src/test/java/io/papermc/checkstyle/package-info.java @@ -0,0 +1,4 @@ +@NullMarked +package io.papermc.checkstyle; + +import org.jspecify.annotations.NullMarked; diff --git a/paper-checkstyle/src/testData/java/testdata/JavadocParamAlignment.java b/paper-checkstyle/src/testData/java/testdata/JavadocParamAlignment.java new file mode 100644 index 0000000000..7f2531c4d5 --- /dev/null +++ b/paper-checkstyle/src/testData/java/testdata/JavadocParamAlignment.java @@ -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) { + } + +} diff --git a/paper-checkstyle/src/testData/java/testdata/NullabilityAnnotations.java b/paper-checkstyle/src/testData/java/testdata/NullabilityAnnotations.java new file mode 100644 index 0000000000..e033035640 --- /dev/null +++ b/paper-checkstyle/src/testData/java/testdata/NullabilityAnnotations.java @@ -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) { + } +} diff --git a/paper-checkstyle/src/testData/java/testdata/imports/UnnecessaryFullyQualifiedImport.java b/paper-checkstyle/src/testData/java/testdata/imports/UnnecessaryFullyQualifiedImport.java new file mode 100644 index 0000000000..c5db898ae8 --- /dev/null +++ b/paper-checkstyle/src/testData/java/testdata/imports/UnnecessaryFullyQualifiedImport.java @@ -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 map = new HashMap<>(); + final Set> 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 map1 = new HashMap<>(); + final Map map2 = new HashMap<>(); + final Map map3 = new HashMap<>(); + final Map map4 = new HashMap<>(); + } + + public A m(final Map m) { + return null; + } + + public A m_correct(final Map m) { + return null; + } + + public A m2(final Map m) { + return null; + } + + public A m2_correct(final Map 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.test(); + testdata.imports.testclasses.subpkg.OtherInterface.test(); + } + + public void methods_correct() { + 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); + } +} diff --git a/paper-checkstyle/src/testData/java/testdata/imports/testclasses/OtherClass.java b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/OtherClass.java new file mode 100644 index 0000000000..7acde86c20 --- /dev/null +++ b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/OtherClass.java @@ -0,0 +1,12 @@ +// @formatter:off +package testdata.imports.testclasses; + +@SuppressWarnings("ALL") +public class OtherClass { + + public static class InnerClass { + } + + public interface InnerInterface { + } +} diff --git a/paper-checkstyle/src/testData/java/testdata/imports/testclasses/OtherInterface.java b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/OtherInterface.java new file mode 100644 index 0000000000..83ee651811 --- /dev/null +++ b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/OtherInterface.java @@ -0,0 +1,4 @@ +package testdata.imports.testclasses; + +public interface OtherInterface { +} diff --git a/paper-checkstyle/src/testData/java/testdata/imports/testclasses/subpkg/OtherClass.java b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/subpkg/OtherClass.java new file mode 100644 index 0000000000..7c19c5a5d2 --- /dev/null +++ b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/subpkg/OtherClass.java @@ -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() {} +} diff --git a/paper-checkstyle/src/testData/java/testdata/imports/testclasses/subpkg/OtherInterface.java b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/subpkg/OtherInterface.java new file mode 100644 index 0000000000..68aaf9d9b7 --- /dev/null +++ b/paper-checkstyle/src/testData/java/testdata/imports/testclasses/subpkg/OtherInterface.java @@ -0,0 +1,7 @@ +package testdata.imports.testclasses.subpkg; + +public interface OtherInterface { + + static void test() { + } +} diff --git a/settings.gradle.kts b/settings.gradle.kts index d7d8681bc8..d6bd517bb1 100644 --- a/settings.gradle.kts +++ b/settings.gradle.kts @@ -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")