User: joe Date: 14 Nov 26 06:35 Revision: 47c71a2b70e78f5bb104fb31869c125a422b7533 Summary: Smarter detection of when implemented methods in interfaces should be prefixed. Closes #2540 TeamCity URL: http://ci.mcdev.io:80/viewModification.html?tab=vcsModificationFiles&modId=10784&personal=false Index: src/main/grammars/CtParser.bnf =================================================================== --- src/main/grammars/CtParser.bnf (revision 9b2e99f7a15e78a9b0f89be0417ea2a124b2224b) +++ src/main/grammars/CtParser.bnf (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -3,7 +3,7 @@ * * https://mcdev.io/ * - * Copyright (C) 2025 minecraft-dev + * Copyright (C) 2026 minecraft-dev * * This program is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published @@ -82,6 +82,10 @@ itf_entry ::= INJECT_INTERFACE_ELEMENT class_name signature { mixin="com.demonwav.mcdev.platform.minecraft.ct.psi.mixins.impl.CtItfEntryImplMixin" implements="com.demonwav.mcdev.platform.minecraft.ct.psi.mixins.CtItfEntryMixin" + methods=[ + interfaceSignature="/signature" + signature="" + ] pin = 1 } @@ -134,7 +138,13 @@ ] } -signature ::= CLASS_NAME_ELEMENT (LESS_THAN signature_element+ GREATER_THAN)? +signature ::= CLASS_NAME_ELEMENT (LESS_THAN signature_element+ GREATER_THAN)? { + mixin="com.demonwav.mcdev.platform.minecraft.ct.psi.mixins.impl.CtSignatureImplMixin" + implements="com.demonwav.mcdev.platform.minecraft.ct.psi.mixins.CtSignatureMixin" + methods=[ + classNameElement="CLASS_NAME_ELEMENT" + ] +} signature_element ::= OPEN_BRACKET* (PRIMITIVE | TYPE_VARIABLE | signature_class_element) signature_class_element ::= ASTERISK | ( (PLUS | MINUS)? SIGNATURE_CLASS_VALUE_START (LESS_THAN signature_element GREATER_THAN)? SIGNATURE_CLASS_VALUE_END ) Index: src/main/kotlin/platform/minecraft/ct/CtInterfaceInjections.kt =================================================================== --- src/main/kotlin/platform/minecraft/ct/CtInterfaceInjections.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) +++ src/main/kotlin/platform/minecraft/ct/CtInterfaceInjections.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -0,0 +1,94 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.minecraft.ct + +import com.demonwav.mcdev.platform.minecraft.ct.gen.psi.CtItfEntry +import com.demonwav.mcdev.util.findQualifiedClass +import com.intellij.openapi.project.Project +import com.intellij.openapi.util.Key +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiManager +import com.intellij.psi.search.FileTypeIndex +import com.intellij.psi.search.GlobalSearchScope +import com.intellij.psi.util.CachedValue +import com.intellij.psi.util.CachedValueProvider +import com.intellij.psi.util.CachedValuesManager +import com.intellij.psi.util.ClassUtil +import com.intellij.psi.util.PsiModificationTracker +import com.intellij.psi.util.PsiTreeUtil + +object CtInterfaceInjections { + private val TRANSITIVE_INJECTED_INTERFACES_KEY = Key>>("mcdev.ctTransitiveInjectedInterfaces") + + /** + * Returns the binary names (e.g. `com.example.Outer$Inner`) of interfaces that are injected with + * `transitive-inject-interface` by class tweaker files in the project. + */ + fun getTransitivelyInjectedInterfaces(project: Project): Set { + return CachedValuesManager.getManager(project).getCachedValue( + project, + TRANSITIVE_INJECTED_INTERFACES_KEY, + { + CachedValueProvider.Result( + computeTransitivelyInjectedInterfaces(project), + PsiModificationTracker.MODIFICATION_COUNT + ) + }, + false + ) + } + + private fun computeTransitivelyInjectedInterfaces(project: Project): Set { + val psiManager = PsiManager.getInstance(project) + val result = mutableSetOf() + for (file in FileTypeIndex.getFiles(CtFileType, GlobalSearchScope.projectScope(project))) { + val ctFile = psiManager.findFile(file) as? CtFile ?: continue + for (entry in PsiTreeUtil.findChildrenOfType(ctFile, CtItfEntry::class.java)) { + if (!entry.isTransitive) { + continue + } + val interfaceName = entry.interfaceName ?: continue + result += interfaceName.replace('/', '.') + } + } + return result + } + + /** + * Returns whether the given interface is available on the classes it is injected into from dependent mods, either + * because it is directly injected with `transitive-inject-interface`, or because it is a superinterface of such an + * interface. + */ + fun isTransitivelyInjected(iface: PsiClass): Boolean { + val injected = getTransitivelyInjectedInterfaces(iface.project) + if (injected.isEmpty()) { + return false + } + + if (ClassUtil.getJVMClassName(iface) in injected) { + return true + } + + return injected.any { name -> + findQualifiedClass(iface.project, name)?.isInheritor(iface, true) == true + } + } +} Index: src/main/kotlin/platform/minecraft/ct/psi/mixins/CtEntryMixin.kt =================================================================== --- src/main/kotlin/platform/minecraft/ct/psi/mixins/CtEntryMixin.kt (revision 9b2e99f7a15e78a9b0f89be0417ea2a124b2224b) +++ src/main/kotlin/platform/minecraft/ct/psi/mixins/CtEntryMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -22,4 +22,7 @@ import com.demonwav.mcdev.platform.minecraft.ct.psi.CtElement -interface CtEntryMixin : CtElement +interface CtEntryMixin : CtElement { + + val isTransitive: Boolean +} Index: src/main/kotlin/platform/minecraft/ct/psi/mixins/CtItfEntryMixin.kt =================================================================== --- src/main/kotlin/platform/minecraft/ct/psi/mixins/CtItfEntryMixin.kt (revision 9b2e99f7a15e78a9b0f89be0417ea2a124b2224b) +++ src/main/kotlin/platform/minecraft/ct/psi/mixins/CtItfEntryMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -20,4 +20,10 @@ package com.demonwav.mcdev.platform.minecraft.ct.psi.mixins -interface CtItfEntryMixin : CtEntryMixin +import com.demonwav.mcdev.platform.minecraft.ct.gen.psi.CtSignature + +interface CtItfEntryMixin : CtEntryMixin { + + val interfaceSignature: CtSignature? + val interfaceName: String? +} Index: src/main/kotlin/platform/minecraft/ct/psi/mixins/CtSignatureMixin.kt =================================================================== --- src/main/kotlin/platform/minecraft/ct/psi/mixins/CtSignatureMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) +++ src/main/kotlin/platform/minecraft/ct/psi/mixins/CtSignatureMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -0,0 +1,30 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.minecraft.ct.psi.mixins + +import com.demonwav.mcdev.platform.minecraft.ct.psi.CtElement +import com.intellij.psi.PsiElement + +interface CtSignatureMixin : CtElement { + + val classNameElement: PsiElement + val className: String +} Index: src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtEntryImplMixin.kt =================================================================== --- src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtEntryImplMixin.kt (revision 9b2e99f7a15e78a9b0f89be0417ea2a124b2224b) +++ src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtEntryImplMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -24,4 +24,8 @@ import com.intellij.extapi.psi.ASTWrapperPsiElement import com.intellij.lang.ASTNode -abstract class CtEntryImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), CtEntryMixin +abstract class CtEntryImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), CtEntryMixin { + + override val isTransitive: Boolean + get() = firstChild?.text?.startsWith("transitive-") == true +} Index: src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtItfEntryImplMixin.kt =================================================================== --- src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtItfEntryImplMixin.kt (revision 9b2e99f7a15e78a9b0f89be0417ea2a124b2224b) +++ src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtItfEntryImplMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -23,4 +23,8 @@ import com.demonwav.mcdev.platform.minecraft.ct.psi.mixins.CtItfEntryMixin import com.intellij.lang.ASTNode -abstract class CtItfEntryImplMixin(node: ASTNode): CtEntryImplMixin(node), CtItfEntryMixin +abstract class CtItfEntryImplMixin(node: ASTNode): CtEntryImplMixin(node), CtItfEntryMixin { + + override val interfaceName: String? + get() = interfaceSignature?.className +} Index: src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtSignatureImplMixin.kt =================================================================== --- src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtSignatureImplMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) +++ src/main/kotlin/platform/minecraft/ct/psi/mixins/impl/CtSignatureImplMixin.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -0,0 +1,31 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.minecraft.ct.psi.mixins.impl + +import com.demonwav.mcdev.platform.minecraft.ct.psi.mixins.CtSignatureMixin +import com.intellij.extapi.psi.ASTWrapperPsiElement +import com.intellij.lang.ASTNode + +abstract class CtSignatureImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), CtSignatureMixin { + + override val className: String + get() = classNameElement.text +} Index: src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt =================================================================== --- src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt (revision 9b2e99f7a15e78a9b0f89be0417ea2a124b2224b) +++ src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -22,6 +22,7 @@ import com.demonwav.mcdev.facet.MinecraftFacet import com.demonwav.mcdev.platform.fabric.FabricModuleType +import com.demonwav.mcdev.platform.minecraft.ct.CtInterfaceInjections import com.demonwav.mcdev.util.RegexConverter import com.demonwav.mcdev.util.decapitalize import com.demonwav.mcdev.util.doBindItem @@ -41,7 +42,10 @@ import com.intellij.openapi.editor.Editor import com.intellij.openapi.module.Module import com.intellij.openapi.project.Project +import com.intellij.openapi.roots.ProjectFileIndex import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType import com.intellij.psi.PsiElement import com.intellij.psi.PsiEnumConstant import com.intellij.psi.PsiField @@ -50,11 +54,14 @@ import com.intellij.psi.PsiNameIdentifierOwner import com.intellij.psi.PsiNamedElement import com.intellij.psi.util.PsiTreeUtil +import com.intellij.psi.util.PsiUtil +import com.intellij.psi.util.TypeConversionUtil import com.intellij.refactoring.rename.RenameProcessor import com.intellij.ui.EnumComboBoxModel import com.intellij.ui.components.JBLabel import com.intellij.ui.dsl.builder.COLUMNS_SHORT import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.bindSelected import com.intellij.ui.dsl.builder.columns import com.intellij.ui.dsl.builder.panel import com.intellij.util.xmlb.annotations.Attribute @@ -79,6 +86,17 @@ @JvmField var reportInheritedMethods = ReportMode.ALWAYS + @JvmField + var ignoreInheritedWithProjectTypeInDescriptor = true + @JvmField + var ignoreInheritedFromTransitivelyInjectedInterfaces = true + @JvmField + var ignoreInheritedFromApiInterfaces = true + + @Attribute(converter = RegexConverter::class) + @JvmField + var apiPackageFormat = DEFAULT_API_PACKAGE_FORMAT.toRegex() + override fun visitAddedField(holder: ProblemsHolder, field: PsiField) { if (field !is PsiEnumConstant && reportFields.shouldReport(field.findModule())) { visitAdded(holder, field) @@ -99,17 +117,48 @@ return false } - val superMethods = method.findDeepestSuperMethods() - val isWritableInterfaceMethod = superMethods.any { - val clazz = it.findContainingClass() ?: return@any false - clazz.isInterface && clazz.containingFile?.isWritable == true + val writableInterfaces = method.findDeepestSuperMethods().mapNotNull { superMethod -> + superMethod.findContainingClass()?.takeIf { it.isInterface && it.containingFile?.isWritable == true } } - return isWritableInterfaceMethod + if (writableInterfaces.isEmpty()) { + return false + } + + if (ignoreInheritedWithProjectTypeInDescriptor && hasProjectTypeInDescriptor(method)) { + return false + } + + return writableInterfaces.any { !isIgnoredInterface(it) } } else { return reportMethods.shouldReport(module) } } + private fun hasProjectTypeInDescriptor(method: PsiMethod): Boolean { + val fileIndex = ProjectFileIndex.getInstance(method.project) + val types = method.parameterList.parameters.map { it.type } + listOfNotNull(method.returnType) + return types.any { type -> + val erased = TypeConversionUtil.erasure(type).deepComponentType as? PsiClassType ?: return@any false + val file = erased.resolve()?.containingFile?.virtualFile ?: return@any false + fileIndex.isInSourceContent(file) + } + } + + private fun isIgnoredInterface(iface: PsiClass): Boolean { + if (ignoreInheritedFromApiInterfaces) { + val packageName = PsiUtil.getPackageName(iface) + if (packageName != null && apiPackageFormat.matches(packageName)) { + return true + } + } + + if (ignoreInheritedFromTransitivelyInjectedInterfaces && CtInterfaceInjections.isTransitivelyInjected(iface)) { + return true + } + + return false + } + private fun visitAdded(holder: ProblemsHolder, added: PsiNameIdentifierOwner) { val name = added.name ?: return if (validNameFormat.matches(name)) { @@ -187,9 +236,34 @@ .applyToComponent { toolTipText = "Reports methods that are inherited from duck interfaces" } comboBox(EnumComboBoxModel(ReportMode::class.java)).doBindItem(::reportInheritedMethods) } + indent { + row { + checkBox("Ignore if a parameter or return type is from the current project") + .bindSelected(::ignoreInheritedWithProjectTypeInDescriptor) - } + } + row { + checkBox("Ignore if the interface is transitively injected by a class tweaker") + .bindSelected(::ignoreInheritedFromTransitivelyInjectedInterfaces) - } + } + row { + checkBox("Ignore if the interface is in an API package:") + .bindSelected(::ignoreInheritedFromApiInterfaces) + textField() + .doBindText( + { apiPackageFormat.pattern }, + { apiPackageFormat = it.toRegexOrDefault(DEFAULT_API_PACKAGE_FORMAT) } + ) + .columns(COLUMNS_SHORT) + .regexValidator() + } + } + } + } + companion object { + private const val DEFAULT_API_PACKAGE_FORMAT = "(.+\\.)?api(\\..+)?" + } + enum class ReportMode(private val displayName: String) { ALWAYS("Always"), ON_FABRIC("On Fabric"), NOT_ON_FABRIC("Not on Fabric"), NEVER("Never"); Index: src/main/resources/inspectionDescriptions/AddedMixinMembersNamePattern.html =================================================================== --- src/main/resources/inspectionDescriptions/AddedMixinMembersNamePattern.html (revision 9b2e99f7a15e78a9b0f89be0417ea2a124b2224b) +++ src/main/resources/inspectionDescriptions/AddedMixinMembersNamePattern.html (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -33,5 +33,14 @@ @Override public void doSomething() { ... } // bad @Override public void myMod$doSomething() { ... } // good +

+ By default, methods implemented from interfaces in the current project are not reported in the following cases, each of + which can be disabled: +

+
    +
  • A parameter or return type of the method is a class from the current project, making a conflicting descriptor very unlikely.
  • +
  • The interface is injected with transitive-inject-interface in a class tweaker file, meaning it is intended to be used by other mods.
  • +
  • The interface is in a package matching the configured API package pattern.
  • +
Index: src/test/kotlin/platform/mixin/AddedMembersNameFormatInspectionTest.kt =================================================================== --- src/test/kotlin/platform/mixin/AddedMembersNameFormatInspectionTest.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) +++ src/test/kotlin/platform/mixin/AddedMembersNameFormatInspectionTest.kt (revision 47c71a2b70e78f5bb104fb31869c125a422b7533) @@ -0,0 +1,307 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2026 minecraft-dev + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU Lesser General Public License as published + * by the Free Software Foundation, version 3.0 only. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU Lesser General Public License + * along with this program. If not, see . + */ + +package com.demonwav.mcdev.platform.mixin + +import com.demonwav.mcdev.framework.EdtInterceptor +import com.demonwav.mcdev.framework.ProjectBuilder +import com.demonwav.mcdev.platform.mixin.inspection.addedMembers.AddedMembersNameFormatInspection +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(EdtInterceptor::class) +@DisplayName("Added Members Name Format Inspection Tests") +class AddedMembersNameFormatInspectionTest : BaseMixinTest() { + + private fun doTest( + @Language("JAVA") mixin: String, + configureInspection: AddedMembersNameFormatInspection.() -> Unit = {}, + builder: ProjectBuilder.() -> Unit = {}, + ) { + buildProject { + dir("test") { + java( + "ProjectType.java", + """ + package test; + + public class ProjectType { + } + """, + configure = false, + ) + java( + "Duck.java", + """ + package test; + + public interface Duck { + void doSomething(); + void doSomethingWithProjectType(ProjectType value); + ProjectType[] getProjectTypes(); + java.util.List getProjectTypeList(); + } + """, + configure = false, + ) + dir("api") { + java( + "ApiDuck.java", + """ + package test.api; + + public interface ApiDuck { + void doApiThing(); + } + """, + configure = false, + ) + } + java( + "InjectedDuck.java", + """ + package test; + + public interface InjectedDuck extends InjectedDuckSuper { + void doInjectedThing(); + + interface Inner { + void doInnerThing(); + } + } + """, + configure = false, + ) + java( + "InjectedDuckSuper.java", + """ + package test; + + public interface InjectedDuckSuper { + void doInjectedSuperThing(); + } + """, + configure = false, + ) + java( + "NonTransitiveDuck.java", + """ + package test; + + public interface NonTransitiveDuck { + void doNonTransitiveThing(); + } + """, + configure = false, + ) + builder() + java("TestMixin.java", mixin) + } + } + + val inspection = AddedMembersNameFormatInspection() + inspection.configureInspection() + fixture.enableInspections(inspection) + fixture.checkHighlighting(true, false, false) + } + + private fun ProjectBuilder.classTweaker() { + file( + "test.classtweaker", + """ + classTweaker v1 named + transitive-inject-interface java/lang/String test/InjectedDuck + transitive-inject-interface java/lang/String test/InjectedDuck${'$'}Inner + inject-interface java/lang/String test/NonTransitiveDuck + """, + ".classtweaker", + configure = false, + allowAst = true, + ) + } + + @Test + @DisplayName("Unprefixed Duck Interface Method") + fun unprefixedDuckInterfaceMethod() { + doTest( + """ + package test; + + import org.spongepowered.asm.mixin.Mixin; + + @Mixin(String.class) + abstract class TestMixin implements Duck { + @Override + public void doSomething() { + } + } + """ + ) + } + + @Test + @DisplayName("Project Type In Descriptor") + fun projectTypeInDescriptor() { + doTest( + """ + package test; + + import org.spongepowered.asm.mixin.Mixin; + + @Mixin(String.class) + abstract class TestMixin implements Duck { + @Override + public void doSomethingWithProjectType(ProjectType value) { + } + + @Override + public ProjectType[] getProjectTypes() { + return null; + } + + @Override + public java.util.List getProjectTypeList() { + return null; + } + } + """ + ) + } + + @Test + @DisplayName("Project Type In Descriptor Disabled") + fun projectTypeInDescriptorDisabled() { + doTest( + """ + package test; + + import org.spongepowered.asm.mixin.Mixin; + + @Mixin(String.class) + abstract class TestMixin implements Duck { + @Override + public void doSomethingWithProjectType(ProjectType value) { + } + } + """, + configureInspection = { ignoreInheritedWithProjectTypeInDescriptor = false }, + ) + } + + @Test + @DisplayName("API Interface") + fun apiInterface() { + doTest( + """ + package test; + + import org.spongepowered.asm.mixin.Mixin; + import test.api.ApiDuck; + + @Mixin(String.class) + abstract class TestMixin implements ApiDuck { + @Override + public void doApiThing() { + } + } + """ + ) + } + + @Test + @DisplayName("API Interface Disabled") + fun apiInterfaceDisabled() { + doTest( + """ + package test; + + import org.spongepowered.asm.mixin.Mixin; + import test.api.ApiDuck; + + @Mixin(String.class) + abstract class TestMixin implements ApiDuck { + @Override + public void doApiThing() { + } + } + """, + configureInspection = { ignoreInheritedFromApiInterfaces = false }, + ) + } + + @Test + @DisplayName("Transitively Injected Interface") + fun transitivelyInjectedInterface() { + doTest( + """ + package test; + + import org.spongepowered.asm.mixin.Mixin; + + @Mixin(String.class) + abstract class TestMixin implements InjectedDuck, InjectedDuck.Inner, NonTransitiveDuck { + @Override + public void doInjectedThing() { + } + + @Override + public void doInjectedSuperThing() { + } + + @Override + public void doInnerThing() { + } + + @Override + public void doNonTransitiveThing() { + } + } + """, + builder = { classTweaker() }, + ) + } + + @Test + @DisplayName("Transitively Injected Interface Disabled") + fun transitivelyInjectedInterfaceDisabled() { + doTest( + """ + package test; + + import org.spongepowered.asm.mixin.Mixin; + + @Mixin(String.class) + abstract class TestMixin implements InjectedDuck { + @Override + public void doInjectedThing() { + } + + @Override + public void doInjectedSuperThing() { + } + } + """, + configureInspection = { ignoreInheritedFromTransitivelyInjectedInterfaces = false }, + builder = { classTweaker() }, + ) + } +}