From 9da38412022ada1eb86303c955939d4f0dee67cb Mon Sep 17 00:00:00 2001 From: RedNesto Date: Tue, 2 Jul 2024 14:19:07 +0200 Subject: [PATCH] Improve fabric.mod.json entrypoints insight - Recognize entrypoints declared in object form - Add more conditions to the inspection - Add some tests to cover the inspection Fixes #2296 --- build.gradle.kts | 2 + gradle/libs.versions.toml | 1 + .../inspection/FabricEntrypointsInspection.kt | 48 +++- .../fabric/reference/EntryPointReference.kt | 54 ++-- .../reference/FabricReferenceContributor.kt | 26 +- .../fabric/reference/ResourceFileReference.kt | 40 ++- src/test/kotlin/framework/ProjectBuilder.kt | 6 + .../fabric/FabricEntrypointsInspectionTest.kt | 264 ++++++++++++++++++ 8 files changed, 396 insertions(+), 45 deletions(-) create mode 100644 src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt diff --git a/build.gradle.kts b/build.gradle.kts index f92d06dc6..fd9b13534 100644 --- a/build.gradle.kts +++ b/build.gradle.kts @@ -83,6 +83,7 @@ repositories { maven("https://maven.fabricmc.net/") { content { includeModule("net.fabricmc", "mapping-io") + includeModule("net.fabricmc", "fabric-loader") } } mavenCentral() @@ -119,6 +120,7 @@ dependencies { classifier = "shaded" } } + testLibs(libs.test.fabricloader) testLibs(libs.test.nbt) { artifact { extension = "nbt" diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index abc4aad82..f8dabc6d7 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -33,6 +33,7 @@ fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", versio test-mockJdk = "org.jetbrains.idea:mock-jdk:1.7-4d76c50" test-mixin = "org.spongepowered:mixin:0.8.5" test-spongeapi = "org.spongepowered:spongeapi:7.4.0" +test-fabricloader = "net.fabricmc:fabric-loader:0.15.11" test-nbt = "com.demonwav.mcdev:all-types-nbt:1.0" junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "junit" } diff --git a/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt index 3ecde42dd..7092fca87 100644 --- a/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt +++ b/src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt @@ -22,12 +22,15 @@ package com.demonwav.mcdev.platform.fabric.inspection import com.demonwav.mcdev.platform.fabric.reference.EntryPointReference import com.demonwav.mcdev.platform.fabric.util.FabricConstants +import com.demonwav.mcdev.util.equivalentTo import com.intellij.codeInspection.InspectionManager import com.intellij.codeInspection.LocalInspectionTool import com.intellij.codeInspection.ProblemDescriptor import com.intellij.codeInspection.ProblemHighlightType import com.intellij.codeInspection.ProblemsHolder +import com.intellij.json.psi.JsonArray import com.intellij.json.psi.JsonElementVisitor +import com.intellij.json.psi.JsonLiteral import com.intellij.json.psi.JsonProperty import com.intellij.json.psi.JsonStringLiteral import com.intellij.psi.JavaPsiFacade @@ -79,8 +82,7 @@ class FabricEntrypointsInspection : LocalInspectionTool() { val element = resolved.singleOrNull()?.element when { element is PsiClass && !literal.text.contains("::") -> { - val propertyKey = literal.parentOfType()?.name - val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } + val (propertyKey, expectedType) = findEntrypointKeyAndType(literal) if (propertyKey != null && expectedType != null && !isEntrypointOfCorrectType(element, propertyKey) ) { @@ -111,21 +113,43 @@ class FabricEntrypointsInspection : LocalInspectionTool() { reference.rangeInElement, ) } + + if (!element.hasModifierProperty(PsiModifier.PUBLIC)) { + holder.registerProblem( + literal, + "Entrypoint method must be public", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + + if (!element.hasModifierProperty(PsiModifier.STATIC)) { + val clazz = element.containingClass + if (clazz != null && clazz.constructors.isNotEmpty() && + clazz.constructors.find { !it.hasParameters() } == null + ) { + holder.registerProblem( + literal, + "Entrypoint instance method class must have an empty constructor", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + } } element is PsiField -> { - if (!element.hasModifierProperty(PsiModifier.STATIC)) { + if (!element.hasModifierProperty(PsiModifier.PUBLIC)) { holder.registerProblem( literal, - "Entrypoint field must be static", + "Entrypoint field must be public", ProblemHighlightType.GENERIC_ERROR_OR_WARNING, reference.rangeInElement, ) } - val propertyKey = literal.parentOfType()?.name + val (propertyKey, expectedType) = findEntrypointKeyAndType(literal) val fieldTypeClass = (element.type as? PsiClassType)?.resolve() - val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } if (propertyKey != null && fieldTypeClass != null && expectedType != null && !isEntrypointOfCorrectType(fieldTypeClass, propertyKey) ) { @@ -141,11 +165,21 @@ class FabricEntrypointsInspection : LocalInspectionTool() { } } + private fun findEntrypointKeyAndType(literal: JsonLiteral): Pair { + val propertyKey = when (val parent = literal.parent) { + is JsonArray -> (parent.parent as? JsonProperty)?.name + is JsonProperty -> parent.parentOfType()?.name + else -> null + } + val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] } + return propertyKey to expectedType + } + private fun isEntrypointOfCorrectType(element: PsiClass, type: String): Boolean { val entrypointClass = FabricConstants.ENTRYPOINT_BY_TYPE[type] ?: return false val clazz = JavaPsiFacade.getInstance(element.project).findClass(entrypointClass, element.resolveScope) - return clazz != null && element.isInheritor(clazz, true) + return clazz != null && (element.equivalentTo(clazz) || element.isInheritor(clazz, true)) } } } diff --git a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt index 796058d64..1b8041c8a 100644 --- a/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt +++ b/src/main/kotlin/platform/fabric/reference/EntryPointReference.kt @@ -25,6 +25,8 @@ import com.demonwav.mcdev.util.fullQualifiedName import com.demonwav.mcdev.util.manipulator import com.demonwav.mcdev.util.reference.InspectionReference import com.intellij.codeInsight.completion.JavaLookupElementBuilder +import com.intellij.json.psi.JsonArray +import com.intellij.json.psi.JsonProperty import com.intellij.json.psi.JsonStringLiteral import com.intellij.openapi.util.TextRange import com.intellij.psi.JavaPsiFacade @@ -40,7 +42,9 @@ import com.intellij.psi.PsiReference import com.intellij.psi.PsiReferenceBase import com.intellij.psi.PsiReferenceProvider import com.intellij.psi.ResolveResult +import com.intellij.psi.search.GlobalSearchScope import com.intellij.psi.search.searches.ClassInheritorsSearch +import com.intellij.psi.util.parentOfType import com.intellij.util.ArrayUtil import com.intellij.util.IncorrectOperationException import com.intellij.util.ProcessingContext @@ -136,27 +140,17 @@ object EntryPointReference : PsiReferenceProvider() { fun isEntryPointReference(reference: PsiReference) = reference is Reference - fun isValidEntrypointClass(element: PsiClass): Boolean { - val psiFacade = JavaPsiFacade.getInstance(element.project) - var inheritsEntrypointInterface = false - for (entrypoint in FabricConstants.ENTRYPOINTS) { - val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope) - ?: continue - if (element.isInheritor(entrypointClass, true)) { - inheritsEntrypointInterface = true - break - } - } - return inheritsEntrypointInterface + fun isValidEntrypointClass(element: PsiClass, entrypointClass: PsiClass): Boolean { + return element.isInheritor(entrypointClass, true) } - fun isValidEntrypointField(field: PsiField): Boolean { + fun isValidEntrypointField(field: PsiField, entrypointClass: PsiClass): Boolean { if (!field.hasModifierProperty(PsiModifier.PUBLIC) || !field.hasModifierProperty(PsiModifier.STATIC)) { return false } val fieldTypeClass = (field.type as? PsiClassType)?.resolve() - return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass) + return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass, entrypointClass) } fun isValidEntrypointMethod(method: PsiMethod): Boolean { @@ -228,30 +222,36 @@ object EntryPointReference : PsiReferenceProvider() { val text = element.text.substring(range.startOffset, range.endOffset) val parts = text.split("::", limit = 2) + val psiFacade = JavaPsiFacade.getInstance(element.project) + val entrypointType = getEntrypointType()?.let(FabricConstants.ENTRYPOINT_BY_TYPE::get) + ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val entrypointClass = psiFacade.findClass(entrypointType, element.resolveScope) + ?: return ArrayUtil.EMPTY_OBJECT_ARRAY + val variants = mutableListOf() if (!isMemberReference) { - val psiFacade = JavaPsiFacade.getInstance(element.project) - for (entrypoint in FabricConstants.ENTRYPOINTS) { - val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope) - ?: continue - ClassInheritorsSearch.search(entrypointClass, true) - .mapNotNullTo(variants) { - val shortName = it.name ?: return@mapNotNullTo null - val fqName = it.fullQualifiedName ?: return@mapNotNullTo null - JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName) - } - } + val scope = element.resolveScope.intersectWith(GlobalSearchScope.projectScope(element.project)) + ClassInheritorsSearch.search(entrypointClass, scope, true) + .mapNotNullTo(variants) { + val shortName = it.name ?: return@mapNotNullTo null + val fqName = it.fullQualifiedName ?: return@mapNotNullTo null + JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName) + } } else if (parts.size >= 2) { - val psiFacade = JavaPsiFacade.getInstance(element.project) val className = parts[0].replace('$', '.') val clazz = psiFacade.findClass(className, element.resolveScope) if (clazz != null) { - clazz.fields.filterTo(variants, ::isValidEntrypointField) + clazz.fields.filterTo(variants) { isValidEntrypointField(it, entrypointClass) } clazz.methods.filterTo(variants, ::isValidEntrypointMethod) } } return variants.toTypedArray() } + + private fun getEntrypointType(): String? { + val entrypointsProperty = element.parentOfType()?.parent as? JsonProperty + return entrypointsProperty?.name + } } } diff --git a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt index 896da285e..758e3e890 100644 --- a/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt +++ b/src/main/kotlin/platform/fabric/reference/FabricReferenceContributor.kt @@ -23,9 +23,11 @@ package com.demonwav.mcdev.platform.fabric.reference import com.demonwav.mcdev.platform.fabric.util.FabricConstants import com.demonwav.mcdev.util.isPropertyValue import com.intellij.json.psi.JsonArray +import com.intellij.json.psi.JsonElement import com.intellij.json.psi.JsonObject import com.intellij.json.psi.JsonStringLiteral import com.intellij.patterns.PlatformPatterns +import com.intellij.patterns.StandardPatterns import com.intellij.psi.PsiReferenceContributor import com.intellij.psi.PsiReferenceRegistrar @@ -34,19 +36,25 @@ class FabricReferenceContributor : PsiReferenceContributor() { val stringInModJson = PlatformPatterns.psiElement(JsonStringLiteral::class.java) .inVirtualFile(PlatformPatterns.virtualFile().withName(FabricConstants.FABRIC_MOD_JSON)) - val entryPointPattern = stringInModJson.withParent( - PlatformPatterns.psiElement(JsonArray::class.java) - .withSuperParent( - 2, - PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints"), - ), - ) + val entrypointsArray = PlatformPatterns.psiElement(JsonArray::class.java) + .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).isPropertyValue("entrypoints")) + val entryPointSimplePattern = stringInModJson.withParent(entrypointsArray) + val entryPointObjectPattern = stringInModJson.isPropertyValue("value") + .withSuperParent(2, PlatformPatterns.psiElement(JsonObject::class.java).withParent(entrypointsArray)) + val entryPointPattern = StandardPatterns.or(entryPointSimplePattern, entryPointObjectPattern) registrar.registerReferenceProvider(entryPointPattern, EntryPointReference) - val mixinConfigPattern = stringInModJson.withParent( + val mixinConfigSimplePattern = stringInModJson.withParent( PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins"), ) - registrar.registerReferenceProvider(mixinConfigPattern, ResourceFileReference("mixin config '%s'")) + val mixinsConfigArray = PlatformPatterns.psiElement(JsonArray::class.java).isPropertyValue("mixins") + val mixinConfigObjectPattern = stringInModJson.isPropertyValue("config") + .withSuperParent(2, PlatformPatterns.psiElement(JsonElement::class.java).withParent(mixinsConfigArray)) + val mixinConfigPattern = StandardPatterns.or(mixinConfigSimplePattern, mixinConfigObjectPattern) + registrar.registerReferenceProvider( + mixinConfigPattern, + ResourceFileReference("mixin config '%s'", Regex("(.+)\\.mixins\\.json")) + ) registrar.registerReferenceProvider( stringInModJson.isPropertyValue("accessWidener"), diff --git a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt index 834ae6e0c..1088b03e9 100644 --- a/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt +++ b/src/main/kotlin/platform/fabric/reference/ResourceFileReference.kt @@ -27,8 +27,12 @@ import com.demonwav.mcdev.util.manipulator import com.demonwav.mcdev.util.mapFirstNotNull import com.demonwav.mcdev.util.reference.InspectionReference import com.intellij.json.psi.JsonStringLiteral +import com.intellij.openapi.application.runReadAction import com.intellij.openapi.module.Module +import com.intellij.openapi.module.ModuleManager +import com.intellij.openapi.project.rootManager import com.intellij.openapi.roots.ModuleRootManager +import com.intellij.openapi.vfs.findPsiFile import com.intellij.psi.PsiElement import com.intellij.psi.PsiFile import com.intellij.psi.PsiManager @@ -37,13 +41,17 @@ import com.intellij.psi.PsiReferenceBase import com.intellij.psi.PsiReferenceProvider import com.intellij.util.IncorrectOperationException import com.intellij.util.ProcessingContext +import org.jetbrains.jps.model.java.JavaResourceRootType -class ResourceFileReference(private val description: String) : PsiReferenceProvider() { +class ResourceFileReference( + private val description: String, + private val filenamePattern: Regex? = null +) : PsiReferenceProvider() { override fun getReferencesByElement(element: PsiElement, context: ProcessingContext): Array { return arrayOf(Reference(description, element as JsonStringLiteral)) } - private class Reference(desc: String, element: JsonStringLiteral) : + private inner class Reference(desc: String, element: JsonStringLiteral) : PsiReferenceBase(element), InspectionReference { override val description = desc @@ -61,6 +69,9 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi ?: ModuleRootManager.getInstance(module) .getDependencies(false) .mapFirstNotNull(::findFileIn) + ?: ModuleManager.getInstance(element.project) + .getModuleDependentModules(module) + .mapFirstNotNull(::findFileIn) } override fun bindToElement(newTarget: PsiElement): PsiElement? { @@ -70,5 +81,30 @@ class ResourceFileReference(private val description: String) : PsiReferenceProvi val manipulator = element.manipulator ?: return null return manipulator.handleContentChange(element, manipulator.getRangeInElement(element), newTarget.name) } + + override fun getVariants(): Array { + if (filenamePattern == null) { + return emptyArray() + } + + val module = element.findModule() ?: return emptyArray() + val variants = mutableListOf() + val relevantModules = ModuleManager.getInstance(element.project).getModuleDependentModules(module) + module + runReadAction { + val relevantRoots = relevantModules.flatMap { + it.rootManager.getSourceRoots(JavaResourceRootType.RESOURCE) + } + for (roots in relevantRoots) { + for (child in roots.children) { + val relativePath = child.path.removePrefix(roots.path) + val testRelativePath = "/$relativePath" + if (testRelativePath.matches(filenamePattern)) { + variants.add(child.findPsiFile(element.project) ?: relativePath) + } + } + } + } + return variants.toTypedArray() + } } } diff --git a/src/test/kotlin/framework/ProjectBuilder.kt b/src/test/kotlin/framework/ProjectBuilder.kt index 45e4dd2ed..07fdc5edf 100644 --- a/src/test/kotlin/framework/ProjectBuilder.kt +++ b/src/test/kotlin/framework/ProjectBuilder.kt @@ -63,6 +63,12 @@ class ProjectBuilder(private val fixture: JavaCodeInsightTestFixture, private va configure: Boolean = true, allowAst: Boolean = false, ) = file(path, code, ".nbtt", configure, allowAst) + fun json( + path: String, + @Language("JSON") code: String, + configure: Boolean = true, + allowAst: Boolean = false, + ) = file(path, code, ".json", configure, allowAst) inline fun dir(path: String, block: ProjectBuilder.() -> Unit) { val oldIntermediatePath = intermediatePath diff --git a/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt new file mode 100644 index 000000000..619fc4ad8 --- /dev/null +++ b/src/test/kotlin/platform/fabric/FabricEntrypointsInspectionTest.kt @@ -0,0 +1,264 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2024 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.fabric + +import com.demonwav.mcdev.framework.BaseMinecraftTest +import com.demonwav.mcdev.framework.EdtInterceptor +import com.demonwav.mcdev.framework.ProjectBuilder +import com.demonwav.mcdev.framework.createLibrary +import com.demonwav.mcdev.platform.PlatformType +import com.demonwav.mcdev.platform.fabric.inspection.FabricEntrypointsInspection +import com.demonwav.mcdev.util.runWriteTask +import com.intellij.openapi.roots.ModuleRootModificationUtil +import com.intellij.openapi.roots.libraries.Library +import com.intellij.openapi.roots.libraries.LibraryTablesRegistrar +import org.intellij.lang.annotations.Language +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.BeforeEach +import org.junit.jupiter.api.DisplayName +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.extension.ExtendWith + +@ExtendWith(EdtInterceptor::class) +@DisplayName("Fabric Entrypoints Inspection Tests") +class FabricEntrypointsInspectionTest : BaseMinecraftTest(PlatformType.FABRIC) { + + private var library: Library? = null + + @BeforeEach + fun initFabric() { + runWriteTask { + library = createLibrary(project, "fabric-loader") + } + + ModuleRootModificationUtil.updateModel(module) { model -> + model.addLibraryEntry(library ?: throw IllegalStateException("Library not created")) + } + } + + @AfterEach + fun cleanupFabric() { + library?.let { l -> + ModuleRootModificationUtil.updateModel(module) { model -> + model.removeOrderEntry( + model.findLibraryOrderEntry(l) ?: throw IllegalStateException("Library not found"), + ) + } + + runWriteTask { + val table = LibraryTablesRegistrar.getInstance().getLibraryTable(project) + table.modifiableModel.let { model -> + model.removeLibrary(l) + model.commit() + } + } + } + } + + private fun doTest(@Language("JSON") json: String, builder: (ProjectBuilder.() -> Unit) = {}) { + buildProject { + java( + "GoodSimpleModInitializer.java", + """ + import net.fabricmc.api.ModInitializer; + + public class GoodSimpleModInitializer implements ModInitializer { + @Override + public void onInitialize() { + } + + public void handle() {} + } + """.trimIndent() + ) + java( + "GoodSimpleClientModInitializer.java", + """ + import net.fabricmc.api.ClientModInitializer; + + public class GoodSimpleClientModInitializer implements ClientModInitializer { + @Override + public void onInitializeClient() { + } + } + """.trimIndent() + ) + java( + "BadSimpleModInitializer.java", + """ + public class BadSimpleModInitializer { + public void handle(String param) {} + } + """.trimIndent() + ) + java( + "BadSimpleClientModInitializer.java", + """ + public class BadSimpleClientModInitializer {} + """.trimIndent() + ) + + builder() + + json("fabric.mod.json", json) + } + + fixture.enableInspections(FabricEntrypointsInspection::class) + fixture.checkHighlighting(false, false, false) + } + + @Test + fun validInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + { + "value": "GoodSimpleModInitializer" + }, + "GoodSimpleModInitializer::handle" + ], + "client": [ + "GoodSimpleClientModInitializer" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun invalidInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + "GoodSimpleClientModInitializer", + { + "value": "BadSimpleModInitializer" + } + ], + "client": [ + "BadSimpleClientModInitializer", + "GoodSimpleModInitializer" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun missingEmptyConstructor() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadCtorSimpleModInitializer" + ] + } + } + """.trimIndent() + ) { + java( + "BadCtorSimpleModInitializer.java", + """ + import net.fabricmc.api.ModInitializer; + + public class BadCtorSimpleModInitializer implements ModInitializer { + public BadCtorSimpleModInitializer(String param) {} + } + """.trimIndent() + ) + } + } + + @Test + fun entrypointMethodWithParameter() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadSimpleModInitializer::handle" + ] + } + } + """.trimIndent() + ) + } + + @Test + fun entrypointInstanceMethodInClassWithNoEmptyCtor() { + doTest( + """ + { + "entrypoints": { + "main": [ + "BadTestInitializer::goodInitialize", + "BadTestInitializer::badInitialize" + ] + } + } + """.trimIndent() + ) { + java( + "BadTestInitializer.java", + """ + public class BadTestInitializer { + public BadTestInitializer(String param) {} + public static void goodInitialize() {} + public void badInitialize() {} + } + """.trimIndent() + ) + } + } + + @Test + fun entrypointFieldInitializers() { + doTest( + """ + { + "entrypoints": { + "main": [ + "ModInitializerContainer::initializer", + "ModInitializerContainer::badTypeInitializer" + ] + } + } + """.trimIndent() + ) { + java( + "ModInitializerContainer.java", + """ + public class ModInitializerContainer { + public static GoodSimpleModInitializer initializer = new GoodSimpleModInitializer(); + public static String badTypeInitializer = "No..."; + } + """.trimIndent() + ) + } + } +}