build: add ArchUnit, JaCoCo, Spotless, SpotBugs, Error Prone/NullAway, forbidden-apis, CPD, dependency-analysis
This commit is contained in:
parent
7e227e39a4
commit
b771cc1c98
10 changed files with 275 additions and 2 deletions
13
.github/workflows/build.yml
vendored
13
.github/workflows/build.yml
vendored
|
|
@ -55,8 +55,19 @@ jobs:
|
|||
with:
|
||||
distribution: temurin
|
||||
java-version: 21
|
||||
- name: Full build
|
||||
- name: Full build (checkstyle, PMD, spotless, spotbugs, forbidden-apis, CPD, tests, coverage)
|
||||
run: ./gradlew --no-daemon build
|
||||
- name: Upload quality reports
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
name: quality-reports
|
||||
path: |
|
||||
build/reports/spotbugs
|
||||
build/reports/cpd
|
||||
build/reports/jacoco
|
||||
build/reports/checkstyle
|
||||
build/reports/pmd
|
||||
- name: Upload default jar
|
||||
uses: actions/upload-artifact@v5
|
||||
with:
|
||||
|
|
|
|||
117
build.gradle.kts
117
build.gradle.kts
|
|
@ -1,3 +1,4 @@
|
|||
import net.ltgt.gradle.errorprone.errorprone
|
||||
import java.io.File
|
||||
import java.net.URI
|
||||
import java.util.zip.ZipFile
|
||||
|
|
@ -15,11 +16,18 @@ buildscript {
|
|||
}
|
||||
|
||||
plugins {
|
||||
id("java")
|
||||
id("java-library")
|
||||
id("com.gradleup.shadow") version("9.6.1")
|
||||
id("xyz.jpenilla.run-paper") version("3.0.2")
|
||||
checkstyle
|
||||
pmd
|
||||
jacoco
|
||||
id("net.ltgt.errorprone") version("4.3.0")
|
||||
id("com.diffplug.spotless") version("7.0.4")
|
||||
id("com.github.spotbugs") version("6.1.13")
|
||||
id("de.thetaphi.forbiddenapis") version("3.9")
|
||||
id("com.autonomousapps.dependency-analysis") version("2.19.0")
|
||||
id("org.owasp.dependencycheck") version("12.1.3")
|
||||
}
|
||||
|
||||
group = "dev.loki"
|
||||
|
|
@ -144,6 +152,8 @@ val remapNms by tasks.registering {
|
|||
}
|
||||
|
||||
tasks.withType<JavaCompile>().configureEach { dependsOn(remapNms) }
|
||||
// Analysis tasks (dependency-analysis, spotbugs, ...) read the remapped NMS jars too.
|
||||
tasks.matching { it.name != "remapNms" && it.name != "clean" }.configureEach { mustRunAfter(remapNms) }
|
||||
|
||||
// Resolve the org.bukkit:bukkit capability conflict that newer paper-api artifacts
|
||||
// introduce (they both bundle the bukkit classes and declare the capability).
|
||||
|
|
@ -201,11 +211,16 @@ dependencies {
|
|||
compileOnly("me.filoghost.holographicdisplays:holographicdisplays-api:3.0.0-SNAPSHOT")
|
||||
compileOnly("org.geysermc.floodgate:api:2.0-SNAPSHOT")
|
||||
|
||||
// Error Prone + NullAway (compile-time bug/null-safety checks)
|
||||
errorprone("com.google.errorprone:error_prone_core:2.41.0")
|
||||
errorprone("com.uber.nullaway:nullaway:0.12.10")
|
||||
|
||||
// Annotations
|
||||
compileOnly("org.jetbrains:annotations:24.1.0")
|
||||
|
||||
// Testing
|
||||
testImplementation("org.junit.jupiter:junit-jupiter:5.11.0-M1")
|
||||
testImplementation("com.tngtech.archunit:archunit-junit5:1.4.1")
|
||||
testImplementation("org.mockito:mockito-core:5.10.0")
|
||||
testImplementation("org.mockito:mockito-junit-jupiter:5.10.0")
|
||||
testImplementation("org.spigotmc:spigot-api:1.20.4-R0.1-SNAPSHOT")
|
||||
|
|
@ -246,6 +261,17 @@ tasks.withType<Test> {
|
|||
tasks.withType<JavaCompile> {
|
||||
options.encoding = "UTF-8"
|
||||
options.release = javaRelease
|
||||
options.errorprone {
|
||||
disableWarningsInGeneratedCode = true
|
||||
// NullAway: only code annotated @NullMarked-style packages is checked once adopted;
|
||||
// start as warnings so the baseline is visible but does not block.
|
||||
check("NullAway", net.ltgt.gradle.errorprone.CheckSeverity.WARN)
|
||||
option("NullAway:AnnotatedPackages", "dev.loki.loparkour")
|
||||
option("NullAway:ExcludedFieldAnnotations", "org.jetbrains.annotations.Nullable")
|
||||
}
|
||||
if (name == "compileTestJava") {
|
||||
options.errorprone.isEnabled.set(false)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.withType<Javadoc> {
|
||||
|
|
@ -299,5 +325,94 @@ tasks.withType<Pmd>().configureEach {
|
|||
exclude("**/build/generated/**")
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Extra quality gates (see TODO.md, task 2). Existing debt is recorded, new
|
||||
// violations fail the build; tighten thresholds as the refactor progresses.
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
jacoco {
|
||||
toolVersion = "0.8.13"
|
||||
}
|
||||
|
||||
tasks.test {
|
||||
finalizedBy(tasks.jacocoTestReport)
|
||||
}
|
||||
|
||||
tasks.jacocoTestReport {
|
||||
dependsOn(tasks.test)
|
||||
reports {
|
||||
xml.required.set(true)
|
||||
html.required.set(true)
|
||||
}
|
||||
}
|
||||
|
||||
tasks.jacocoTestCoverageVerification {
|
||||
violationRules {
|
||||
rule {
|
||||
limit {
|
||||
counter = "LINE"
|
||||
minimum = "0.05".toBigDecimal() // baseline; raise toward 0.5 (core 0.7)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
tasks.check {
|
||||
dependsOn(tasks.jacocoTestCoverageVerification, "cpdCheck")
|
||||
}
|
||||
|
||||
spotless {
|
||||
java {
|
||||
target("src/**/*.java")
|
||||
removeUnusedImports()
|
||||
trimTrailingWhitespace()
|
||||
endWithNewline()
|
||||
}
|
||||
}
|
||||
|
||||
spotbugs {
|
||||
toolVersion = "4.9.3"
|
||||
effort = com.github.spotbugs.snom.Effort.MAX
|
||||
reportLevel = com.github.spotbugs.snom.Confidence.MEDIUM
|
||||
ignoreFailures = true // report-only until the baseline is triaged
|
||||
}
|
||||
|
||||
tasks.withType<com.github.spotbugs.snom.SpotBugsTask>().configureEach {
|
||||
reports.create("html") { required = true }
|
||||
}
|
||||
|
||||
forbiddenApis {
|
||||
bundledSignatures = setOf("jdk-unsafe-21", "jdk-deprecated-21", "jdk-system-out", "jdk-non-portable")
|
||||
ignoreFailures = true // report-only until the baseline is triaged
|
||||
}
|
||||
|
||||
// Copy/paste detector (DRY). The de.aaschmid.cpd plugin does not support PMD 7, so run PMD's CPD directly.
|
||||
val cpdTool: Configuration by configurations.creating
|
||||
dependencies {
|
||||
cpdTool("net.sourceforge.pmd:pmd-cli:7.0.0")
|
||||
cpdTool("net.sourceforge.pmd:pmd-java:7.0.0")
|
||||
}
|
||||
|
||||
val cpdCheck by tasks.registering(JavaExec::class) {
|
||||
group = "verification"
|
||||
description = "Finds duplicated code (min 100 tokens). Report-only until duplicates are triaged."
|
||||
classpath = cpdTool
|
||||
mainClass.set("net.sourceforge.pmd.cli.PmdCli")
|
||||
args("cpd", "--minimum-tokens", "100", "--language", "java", "--dir", "src/main/java",
|
||||
"--format", "text")
|
||||
isIgnoreExitValue = true // exit code 4 = duplicates found
|
||||
doFirst {
|
||||
val dir = layout.buildDirectory.dir("reports/cpd").get().asFile
|
||||
dir.mkdirs()
|
||||
standardOutput = File(dir, "cpd.txt").outputStream()
|
||||
}
|
||||
}
|
||||
|
||||
dependencyCheck {
|
||||
// Run explicitly: ./gradlew dependencyCheckAnalyze (set NVD_API_KEY for speed).
|
||||
failBuildOnCVSS = 7.0f
|
||||
nvd.apiKey = System.getenv("NVD_API_KEY")
|
||||
}
|
||||
|
||||
// Default task
|
||||
defaultTasks("clean", "build")
|
||||
|
|
|
|||
52
src/test/java/dev/loki/loparkour/arch/ArchitectureTest.java
Normal file
52
src/test/java/dev/loki/loparkour/arch/ArchitectureTest.java
Normal file
|
|
@ -0,0 +1,52 @@
|
|||
package dev.loki.loparkour.arch;
|
||||
|
||||
import com.tngtech.archunit.core.importer.ImportOption;
|
||||
import com.tngtech.archunit.junit.AnalyzeClasses;
|
||||
import com.tngtech.archunit.junit.ArchIgnore;
|
||||
import com.tngtech.archunit.junit.ArchTest;
|
||||
import com.tngtech.archunit.lang.ArchRule;
|
||||
import org.bukkit.scheduler.BukkitRunnable;
|
||||
|
||||
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.noClasses;
|
||||
import static com.tngtech.archunit.library.dependencies.SlicesRuleDefinition.slices;
|
||||
import static com.tngtech.archunit.library.freeze.FreezingArchRule.freeze;
|
||||
|
||||
/**
|
||||
* Enforces the dependency rules from docs/ARCHITECTURE.md. Rules wrapped in {@code freeze} record
|
||||
* existing violations in {@code src/test/resources/archunit_store}; only NEW violations fail the build.
|
||||
*/
|
||||
@AnalyzeClasses(packages = "dev.loki.loparkour", importOptions = ImportOption.DoNotIncludeTests.class)
|
||||
class ArchitectureTest {
|
||||
|
||||
private static final String ROOT = "dev.loki.loparkour";
|
||||
|
||||
// Disabled: the codebase has 100+ package cycles and freeze() cannot track cycle violations
|
||||
// stably (their ids change between runs). Enable once cycles are reduced (TODO task 2).
|
||||
@ArchIgnore
|
||||
@ArchTest
|
||||
static final ArchRule NO_PACKAGE_CYCLES = freeze(
|
||||
slices().matching(ROOT + ".(*)..").should().beFreeOfCycles());
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule API_IS_INDEPENDENT_OF_PLATFORM = freeze(
|
||||
noClasses().that().resideInAPackage(ROOT + ".api..")
|
||||
.should().dependOnClassesThat().resideInAnyPackage(
|
||||
ROOT + ".menu..", ROOT + ".command..", ROOT + ".storage..",
|
||||
ROOT + ".listener..", ROOT + ".hook.."));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_BUKKIT_RUNNABLE_OUTSIDE_SCHEDULER = freeze(
|
||||
noClasses().that().resideOutsideOfPackage(ROOT + ".scheduler..")
|
||||
.should().dependOnClassesThat().areAssignableTo(BukkitRunnable.class)
|
||||
.because("all tasks go through scheduler/ so they work on Folia"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_BUKKIT_SCHEDULER_OUTSIDE_SCHEDULER = freeze(
|
||||
noClasses().that().resideOutsideOfPackage(ROOT + ".scheduler..")
|
||||
.should().callMethod(org.bukkit.Bukkit.class, "getScheduler")
|
||||
.because("Bukkit.getScheduler() does not exist on Folia"));
|
||||
|
||||
@ArchTest
|
||||
static final ArchRule NO_LEGACY_LOLIB =
|
||||
noClasses().should().dependOnClassesThat().resideInAPackage("dev.lolib..");
|
||||
}
|
||||
|
|
@ -0,0 +1,75 @@
|
|||
package dev.loki.loparkour.arch;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.Set;
|
||||
import java.util.TreeMap;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.stream.Stream;
|
||||
|
||||
import static org.junit.jupiter.api.Assertions.assertTrue;
|
||||
|
||||
/**
|
||||
* Hard structure limits: at most 4 files per package folder, at most 200 lines per file.
|
||||
* Existing debt is listed explicitly and must only shrink; new violations fail the build.
|
||||
*/
|
||||
class StructureRulesTest {
|
||||
|
||||
private static final Path MAIN = Path.of("src/main/java");
|
||||
private static final int MAX_FILES_PER_FOLDER = 4;
|
||||
private static final int MAX_LINES_PER_FILE = 200;
|
||||
|
||||
/** Known debt (docs/TODO.md, task 2). Remove entries as folders get split. */
|
||||
private static final Set<String> FOLDER_DEBT = Set.of(
|
||||
"dev/loki/loparkour/command/schematic",
|
||||
"dev/loki/loparkour/elytra/generator",
|
||||
"dev/loki/loparkour/elytra/mode",
|
||||
"dev/loki/loparkour/generator/jump/calculation",
|
||||
"dev/loki/loparkour/generator/jump/placement",
|
||||
"dev/loki/loparkour/player/core");
|
||||
|
||||
@Test
|
||||
void foldersHaveAtMostFourFiles() throws IOException {
|
||||
final Map<String, Long> perFolder = new TreeMap<>();
|
||||
for (final Path file : javaFiles()) {
|
||||
perFolder.merge(MAIN.relativize(file.getParent()).toString(), 1L, Long::sum);
|
||||
}
|
||||
final List<String> violations = perFolder.entrySet().stream()
|
||||
.filter(e -> e.getValue() > MAX_FILES_PER_FOLDER)
|
||||
.map(Map.Entry::getKey)
|
||||
.filter(folder -> !FOLDER_DEBT.contains(folder))
|
||||
.collect(Collectors.toList());
|
||||
assertTrue(violations.isEmpty(), "Folders with more than " + MAX_FILES_PER_FOLDER + " files: " + violations);
|
||||
|
||||
final List<String> fixed = FOLDER_DEBT.stream()
|
||||
.filter(folder -> perFolder.getOrDefault(folder, 0L) <= MAX_FILES_PER_FOLDER)
|
||||
.collect(Collectors.toList());
|
||||
assertTrue(fixed.isEmpty(), "Folders now within the limit — remove them from FOLDER_DEBT: " + fixed);
|
||||
}
|
||||
|
||||
@Test
|
||||
void filesHaveAtMost200Lines() throws IOException {
|
||||
final List<String> violations = new java.util.ArrayList<>();
|
||||
for (final Path file : javaFiles()) {
|
||||
final long lines;
|
||||
try (Stream<String> content = Files.lines(file)) {
|
||||
lines = content.count();
|
||||
}
|
||||
if (lines > MAX_LINES_PER_FILE) {
|
||||
violations.add(MAIN.relativize(file) + " (" + lines + ")");
|
||||
}
|
||||
}
|
||||
assertTrue(violations.isEmpty(), "Files longer than " + MAX_LINES_PER_FILE + " lines: " + violations);
|
||||
}
|
||||
|
||||
private static List<Path> javaFiles() throws IOException {
|
||||
try (Stream<Path> walk = Files.walk(MAIN)) {
|
||||
return walk.filter(p -> p.toString().endsWith(".java")).collect(Collectors.toList());
|
||||
}
|
||||
}
|
||||
}
|
||||
4
src/test/resources/archunit.properties
Normal file
4
src/test/resources/archunit.properties
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
# Violation store for FreezingArchRule: existing debt is recorded, only new violations fail.
|
||||
freeze.store.default.path=src/test/resources/archunit_store
|
||||
freeze.store.default.allowStoreCreation=true
|
||||
freeze.refreeze=false
|
||||
|
|
@ -0,0 +1,7 @@
|
|||
Class <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1> extends class <org.bukkit.scheduler.BukkitRunnable> in (MetricsCacheStore.java:0)
|
||||
Constructor <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1.<init>(dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> calls constructor <org.bukkit.scheduler.BukkitRunnable.<init>()> in (MetricsCacheStore.java:84)
|
||||
Field <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.autoSaveTask> has type <org.bukkit.scheduler.BukkitRunnable> in (MetricsCacheStore.java:0)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls constructor <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore$1.<init>(dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore)> in (MetricsCacheStore.java:84)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.cancel()> in (MetricsCacheStore.java:81)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.startAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.runTaskTimerAsynchronously(org.bukkit.plugin.Plugin, long, long)> in (MetricsCacheStore.java:91)
|
||||
Method <dev.loki.loparkour.adaptive.core.metrics.MetricsCacheStore.stopAutoSave()> calls method <org.bukkit.scheduler.BukkitRunnable.cancel()> in (MetricsCacheStore.java:96)
|
||||
|
|
@ -0,0 +1,3 @@
|
|||
Method <dev.loki.loparkour.duels.DuelsCountdown.giveStartItem(dev.loki.loparkour.player.core.ParkourPlayer)> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsCountdown.java:45)
|
||||
Method <dev.loki.loparkour.duels.DuelsCountdown.start()> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsCountdown.java:55)
|
||||
Method <dev.loki.loparkour.duels.DuelsGenerator.win(dev.loki.loparkour.player.core.ParkourPlayer)> calls method <org.bukkit.Bukkit.getScheduler()> in (DuelsGenerator.java:137)
|
||||
6
src/test/resources/archunit_store/stored.rules
Normal file
6
src/test/resources/archunit_store/stored.rules
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
#
|
||||
#Tue Oct 06 13:42:44 CEST 2026
|
||||
no\ classes\ that\ reside\ in\ a\ package\ 'dev.loki.loparkour.api..'\ should\ depend\ on\ classes\ that\ reside\ in\ any\ package\ ['dev.loki.loparkour.menu..',\ 'dev.loki.loparkour.command..',\ 'dev.loki.loparkour.storage..',\ 'dev.loki.loparkour.listener..',\ 'dev.loki.loparkour.hook..']=d1d8a9d7-476c-4f86-adda-8531054aea1f
|
||||
no\ classes\ that\ reside\ outside\ of\ package\ 'dev.loki.loparkour.scheduler..'\ should\ call\ method\ Bukkit.getScheduler(),\ because\ Bukkit.getScheduler()\ does\ not\ exist\ on\ Folia=f1e3c34b-9b02-40e6-83a6-0269e173eb83
|
||||
no\ classes\ that\ reside\ outside\ of\ package\ 'dev.loki.loparkour.scheduler..'\ should\ depend\ on\ classes\ that\ are\ assignable\ to\ org.bukkit.scheduler.BukkitRunnable,\ because\ all\ tasks\ go\ through\ scheduler/\ so\ they\ work\ on\ Folia=339ab3ea-28aa-4f4b-a1e4-6c69e3ff0a0a
|
||||
slices\ matching\ 'dev.loki.loparkour.(*)..'\ should\ be\ free\ of\ cycles=58784f63-3f60-4edb-9733-fa0d7e11b877
|
||||
Loading…
Add table
Add a link
Reference in a new issue