diff --git a/src/main/java/org/avarion/yaml/TypeConverter.java b/src/main/java/org/avarion/yaml/TypeConverter.java index c6f950c..f0c8933 100644 --- a/src/main/java/org/avarion/yaml/TypeConverter.java +++ b/src/main/java/org/avarion/yaml/TypeConverter.java @@ -23,21 +23,33 @@ final class TypeConverter { static final Logger LOG = Logger.getLogger(TypeConverter.class.getName()); /** - * Per-thread sink for lenient warnings. Always non-null — defaults to {@link #LOG}'s - * {@code warning(String)} so callers don't need to null-check. - * {@link YamlFileInterface#load(Object)} replaces it with the plugin's logger for the - * duration of the load. + * Per-thread sink for lenient warnings, unset unless a caller installs one. + * {@link YamlFileInterface#load(Object)} installs the plugin's logger for the duration + * of the load, then restores what was there before. */ - private static final ThreadLocal> ACTIVE = ThreadLocal.withInitial(() -> LOG::warning); + private static final ThreadLocal> ACTIVE = new ThreadLocal<>(); static void warn(String message) { - ACTIVE.get().accept(message); + Consumer sink = ACTIVE.get(); + if (sink == null) { + LOG.warning(message); + } else { + sink.accept(message); + } } - /** Install {@code sink} for the current thread; returns the previous one for restore-in-finally. */ - static Consumer pushSink(@NotNull Consumer sink) { + /** + * Install {@code sink} for the current thread; returns the previous one for restore-in-finally. + * A {@code null} sink clears the entry rather than parking a stale reference on a thread that + * may outlive the load — server thread pools are long-lived. + */ + static @Nullable Consumer pushSink(final @Nullable Consumer sink) { Consumer prev = ACTIVE.get(); - ACTIVE.set(sink); + if (sink == null) { + ACTIVE.remove(); + } else { + ACTIVE.set(sink); + } return prev; } diff --git a/src/test/java/org/avarion/yaml/LeniencyTest.java b/src/test/java/org/avarion/yaml/LeniencyTest.java index 77d9934..0bf78ee 100644 --- a/src/test/java/org/avarion/yaml/LeniencyTest.java +++ b/src/test/java/org/avarion/yaml/LeniencyTest.java @@ -4,7 +4,6 @@ import org.junit.jupiter.api.Test; import java.io.IOException; -import java.nio.file.Files; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; diff --git a/src/test/java/org/avarion/yaml/testClasses/Sounds.java b/src/test/java/org/avarion/yaml/testClasses/Sounds.java index 7149299..c83cc8e 100644 --- a/src/test/java/org/avarion/yaml/testClasses/Sounds.java +++ b/src/test/java/org/avarion/yaml/testClasses/Sounds.java @@ -2,6 +2,11 @@ import org.jetbrains.annotations.NotNull; +/** + * Mirrors a Minecraft-style registry constant: named instances reached through a static + * factory. Deliberately has no public String constructor and no toString() override, so + * YamlWriter falls through to matching the value against a static field. + */ public class Sounds { public static final Sounds MY_SOUND_ROCKS = getSound("my.sound.rocks"); public static final Sounds YOUR_SOUND_ROCKS_TOO = getSound("your.sound.rocks.2"); @@ -15,4 +20,8 @@ public class Sounds { private Sounds(String name) { this.name = name; } + + public String getName() { + return name; + } } diff --git a/src/test/java/org/avarion/yaml/testClasses/StaticFieldTestClass.java b/src/test/java/org/avarion/yaml/testClasses/StaticFieldTestClass.java index 45cf558..afce9de 100644 --- a/src/test/java/org/avarion/yaml/testClasses/StaticFieldTestClass.java +++ b/src/test/java/org/avarion/yaml/testClasses/StaticFieldTestClass.java @@ -7,7 +7,10 @@ public class StaticFieldTestClass { // Public static field - should be found public static final StaticFieldTestClass PUBLIC_INSTANCE = new StaticFieldTestClass("public"); - // Private static field - should NOT be found (not public) + // Private static field - should NOT be found (not public). Read only by reflection, in + // YamlWriter#getStaticFieldName: it is the sole case exercising the canAccess(null) == false + // branch, so deleting it drops that method from 8/8 to 7/8 branches covered. + @SuppressWarnings("unused") private static final StaticFieldTestClass PRIVATE_INSTANCE = new StaticFieldTestClass("private"); // Public static but different value - should NOT match diff --git a/src/test/java/org/bukkit/NamespacedKey.java b/src/test/java/org/bukkit/NamespacedKey.java index af6532b..c3a030d 100644 --- a/src/test/java/org/bukkit/NamespacedKey.java +++ b/src/test/java/org/bukkit/NamespacedKey.java @@ -3,6 +3,10 @@ import net.kyori.adventure.key.Key; import org.jetbrains.annotations.NotNull; +/** + * Mirrors the shape of the real Bukkit NamespacedKey, including its namespace/key pair, + * so the reflective lookups in YamlWriter run against a realistic class. + */ public final class NamespacedKey implements Key { private final String ns; private final String key; @@ -12,6 +16,10 @@ public NamespacedKey(String ns, String key) { this.key = key; } + public @NotNull String getNamespace() { + return ns; + } + @Override public @NotNull String value() { return key;