build: add ArchUnit, JaCoCo, Spotless, SpotBugs, Error Prone/NullAway, forbidden-apis, CPD, dependency-analysis

This commit is contained in:
loki5512344 2026-10-06 13:50:25 +02:00
parent 7e227e39a4
commit b771cc1c98
Signed by: boba
GPG key ID: 253067914055423B
10 changed files with 275 additions and 2 deletions

View 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..");
}

View file

@ -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());
}
}
}

View 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

View file

@ -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)

View file

@ -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)

View 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