diff --git a/api/src/main/java/net/kyori/adventure/text/Component.java b/api/src/main/java/net/kyori/adventure/text/Component.java index 14d263436..a49dc24e8 100644 --- a/api/src/main/java/net/kyori/adventure/text/Component.java +++ b/api/src/main/java/net/kyori/adventure/text/Component.java @@ -2559,6 +2559,7 @@ default boolean hasStyling() { * @return the optimized component * @since 4.9.0 */ + @ScopedComponentOverrideNotRequired default @NotNull Component compact() { return ComponentCompaction.compact(this, null); } diff --git a/api/src/main/java/net/kyori/adventure/text/ScopedComponent.java b/api/src/main/java/net/kyori/adventure/text/ScopedComponent.java index 654a96a72..131ec293d 100644 --- a/api/src/main/java/net/kyori/adventure/text/ScopedComponent.java +++ b/api/src/main/java/net/kyori/adventure/text/ScopedComponent.java @@ -24,11 +24,14 @@ package net.kyori.adventure.text; import java.util.List; +import java.util.Map; import java.util.Set; import java.util.function.Consumer; +import net.kyori.adventure.key.Key; import net.kyori.adventure.text.event.ClickEvent; import net.kyori.adventure.text.event.HoverEventSource; import net.kyori.adventure.text.format.Style; +import net.kyori.adventure.text.format.StyleBuilderApplicable; import net.kyori.adventure.text.format.TextColor; import net.kyori.adventure.text.format.TextDecoration; import net.kyori.adventure.util.ARGBLike; @@ -42,6 +45,12 @@ * @since 4.0.0 */ public interface ScopedComponent extends Component { + @Override + @SuppressWarnings("unchecked") + default @NotNull C asComponent() { + return (C) Component.super.asComponent(); + } + @Override @NotNull C children(final @NotNull List children); @@ -60,6 +69,12 @@ public interface ScopedComponent extends Component { return (C) Component.super.style(style); } + @Override + @SuppressWarnings("unchecked") + default @NotNull C style(final @NotNull Consumer consumer, final Style.Merge.@NotNull Strategy strategy) { + return (C) Component.super.style(consumer, strategy); + } + @Override @SuppressWarnings("unchecked") default @NotNull C mergeStyle(final @NotNull Component that) { @@ -90,6 +105,30 @@ public interface ScopedComponent extends Component { return (C) Component.super.append(builder); } + @Override + @SuppressWarnings("unchecked") + default @NotNull C appendNewline() { + return (C) Component.super.appendNewline(); + } + + @Override + @SuppressWarnings("unchecked") + default @NotNull C appendSpace() { + return (C) Component.super.appendSpace(); + } + + @Override + @SuppressWarnings("unchecked") + default @NotNull C applyFallbackStyle(final @NotNull StyleBuilderApplicable @NotNull ... style) { + return (C) Component.super.applyFallbackStyle(style); + } + + @Override + @SuppressWarnings("unchecked") + default @NotNull C applyFallbackStyle(final @NotNull Style style) { + return (C) Component.super.applyFallbackStyle(style); + } + @Override @SuppressWarnings("unchecked") default @NotNull C mergeStyle(final @NotNull Component that, final @NotNull Set merges) { @@ -138,6 +177,18 @@ public interface ScopedComponent extends Component { return (C) Component.super.decoration(decoration, state); } + @Override + @SuppressWarnings("unchecked") + default @NotNull C decorationIfAbsent(final @NotNull TextDecoration decoration, final TextDecoration.@NotNull State state) { + return (C) Component.super.decorationIfAbsent(decoration, state); + } + + @Override + @SuppressWarnings("unchecked") + default @NotNull C decorations(final @NotNull Map decorations) { + return (C) Component.super.decorations(decorations); + } + @Override @SuppressWarnings("unchecked") default @NotNull C clickEvent(final @Nullable ClickEvent event) { @@ -155,4 +206,22 @@ public interface ScopedComponent extends Component { default @NotNull C insertion(final @Nullable String insertion) { return (C) Component.super.insertion(insertion); } + + @Override + @SuppressWarnings("unchecked") + default @NotNull C replaceText(final @NotNull Consumer configurer) { + return (C) Component.super.replaceText(configurer); + } + + @Override + @SuppressWarnings("unchecked") + default @NotNull C replaceText(final @NotNull TextReplacementConfig config) { + return (C) Component.super.replaceText(config); + } + + @Override + @SuppressWarnings("unchecked") + default @NotNull C font(final @Nullable Key key) { + return (C) Component.super.font(key); + } } diff --git a/api/src/main/java/net/kyori/adventure/text/ScopedComponentOverrideNotRequired.java b/api/src/main/java/net/kyori/adventure/text/ScopedComponentOverrideNotRequired.java new file mode 100644 index 000000000..33f621094 --- /dev/null +++ b/api/src/main/java/net/kyori/adventure/text/ScopedComponentOverrideNotRequired.java @@ -0,0 +1,39 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text; + +import java.lang.annotation.ElementType; +import java.lang.annotation.Retention; +import java.lang.annotation.RetentionPolicy; +import java.lang.annotation.Target; + +/** + * A marker annotation for {@link Component}-returning methods in a supertype of {@link ScopedComponent} that do not need generified overrides. + * + * @since 4.19.0 + */ +@Target(ElementType.METHOD) +@Retention(RetentionPolicy.RUNTIME) +@interface ScopedComponentOverrideNotRequired { +} diff --git a/api/src/test/java/net/kyori/adventure/audience/AudienceOverridesTest.java b/api/src/test/java/net/kyori/adventure/audience/AudienceOverridesTest.java new file mode 100644 index 000000000..ae86f13ea --- /dev/null +++ b/api/src/test/java/net/kyori/adventure/audience/AudienceOverridesTest.java @@ -0,0 +1,44 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.audience; + +import net.kyori.test.Overrides; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.ValueSource; + +class AudienceOverridesTest { + @ParameterizedTest + @ValueSource(classes = { + ForwardingAudience.class, + ForwardingAudience.Single.class + }) + void ensureForwardingAudiencesOverrideRequiredMethods(final Class audience) { + Overrides.failIfMissing( + Audience.class, + Overrides.methods(Audience.class, ForwardingAudienceOverrideNotRequired.class), + audience, + Overrides.methods(audience, ForwardingAudienceOverrideNotRequired.class) + ); + } +} diff --git a/api/src/test/java/net/kyori/adventure/text/ScopedComponentOverridesTest.java b/api/src/test/java/net/kyori/adventure/text/ScopedComponentOverridesTest.java new file mode 100644 index 000000000..8cb26ceab --- /dev/null +++ b/api/src/test/java/net/kyori/adventure/text/ScopedComponentOverridesTest.java @@ -0,0 +1,47 @@ +/* + * This file is part of adventure, licensed under the MIT License. + * + * Copyright (c) 2017-2025 KyoriPowered + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in all + * copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ +package net.kyori.adventure.text; + +import java.util.Set; +import java.util.stream.Collectors; +import net.kyori.test.Overrides; +import org.junit.jupiter.api.Test; + +public class ScopedComponentOverridesTest { + // check that all methods are overridden properly + @Test + void ensureScopedComponentOverridesComponentMethods() { + final Set expected = Overrides.inheritedMethodStream(Component.class, ScopedComponentOverrideNotRequired.class) + .filter(it -> it.returnType.equals(Component.class)) + .filter(it -> !it.isDeprecated) + .collect(Collectors.toSet()); + + Overrides.failIfMissing( + Component.class, + expected, + ScopedComponent.class, + Overrides.methods(ScopedComponent.class, ScopedComponentOverrideNotRequired.class) + ); + } +} diff --git a/api/src/test/java/net/kyori/adventure/audience/AudienceOverrides.java b/api/src/test/java/net/kyori/test/Overrides.java similarity index 64% rename from api/src/test/java/net/kyori/adventure/audience/AudienceOverrides.java rename to api/src/test/java/net/kyori/test/Overrides.java index 75aaa4982..5b4163a14 100644 --- a/api/src/test/java/net/kyori/adventure/audience/AudienceOverrides.java +++ b/api/src/test/java/net/kyori/test/Overrides.java @@ -21,32 +21,47 @@ * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE * SOFTWARE. */ -package net.kyori.adventure.audience; +package net.kyori.test; import com.google.common.collect.Sets; +import java.lang.annotation.Annotation; import java.lang.reflect.Method; import java.lang.reflect.Modifier; import java.util.Arrays; import java.util.Objects; import java.util.Set; import java.util.stream.Collectors; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.ValueSource; +import java.util.stream.Stream; +import org.jetbrains.annotations.Nullable; import static org.junit.jupiter.api.Assertions.fail; -class AudienceOverrides { - @ParameterizedTest - @ValueSource(classes = { - ForwardingAudience.class, - ForwardingAudience.Single.class - }) - void ensureForwardingAudiencesOverrideRequiredMethods(final Class audience) { - final Set missing = Sets.difference(methods(Audience.class), methods(audience)); +public final class Overrides { + private Overrides() { + } + + private static Stream methodStream(final Method[] methods, final @Nullable Class exclusionAnno) { + return Arrays.stream(methods) + .filter(method -> exclusionAnno == null || !method.isAnnotationPresent(exclusionAnno)) // there are some that truly are default methods + .filter(method -> !Modifier.isStatic(method.getModifiers())) // unlikely to exist, but best we exclude them just in case + .map(MethodInfo::new); + } + + public static Set methods(final Class in, final Class exclusionAnno) { + return methodStream(in.getDeclaredMethods(), exclusionAnno) + .collect(Collectors.toSet()); + } + + public static Stream inheritedMethodStream(final Class in, final Class exclusionAnno) { + return methodStream(in.getMethods(), exclusionAnno); + } + + public static void failIfMissing(final Class parent, final Set parentMethods, final Class child, final Set childMethods) { + final Set missing = Sets.difference(parentMethods, childMethods); if (!missing.isEmpty()) { final StringBuilder error = new StringBuilder(); - error.append(audience.getSimpleName()).append(" is missing override for ").append(Audience.class.getSimpleName()).append(" methods:"); - for (final MethodInfo method : missing) { + error.append(child.getSimpleName()).append(" is missing override for ").append(parent.getSimpleName()).append(" methods:"); + for (final Overrides.MethodInfo method : missing) { error.append('\n').append("- ").append(method); } error.append('\n'); @@ -54,24 +69,18 @@ void ensureForwardingAudiencesOverrideRequiredMethods(final Class audience) { } } - private static Set methods(final Class in) { - return Arrays.stream(in.getDeclaredMethods()) - .filter(method -> !method.isAnnotationPresent(ForwardingAudienceOverrideNotRequired.class)) // there are some that truly are default methods - .filter(method -> !Modifier.isStatic(method.getModifiers())) // unlikely to exist, but best we exclude them just in case - .map(MethodInfo::new) - .collect(Collectors.toSet()); - } - // todo(kashike): this can be a record once we have a Java 16 source-set - static final class MethodInfo { - final String name; - final Class returnType; - final Class[] paramTypes; + public static final class MethodInfo { + public final String name; + public final Class returnType; + public final Class[] paramTypes; + public final boolean isDeprecated; MethodInfo(final Method method) { this.name = method.getName(); this.returnType = method.getReturnType(); this.paramTypes = method.getParameterTypes(); + this.isDeprecated = method.isAnnotationPresent(Deprecated.class); } @Override diff --git a/build-logic/build.gradle.kts b/build-logic/build.gradle.kts index 9a5d07195..33e0163ec 100644 --- a/build-logic/build.gradle.kts +++ b/build-logic/build.gradle.kts @@ -1,3 +1,5 @@ +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + plugins { `kotlin-dsl` } @@ -24,10 +26,8 @@ java { kotlin { target { - compilations.configureEach { - kotlinOptions { - jvmTarget = "11" - } + compilerOptions { + jvmTarget = JvmTarget.JVM_11 } } }