User: rednesto Date: 17 Jun 23 08:55 Revision: 638c35fc692d3ebd304aaf9fe4892104e05caf19 Summary: Add inspection to validate fabric entrypoints TeamCity URL: https://ci.mcdev.io/viewModification.html?tab=vcsModificationFiles&modId=8528&personal=false Index: src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt =================================================================== --- src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt (revision 638c35fc692d3ebd304aaf9fe4892104e05caf19) +++ src/main/kotlin/platform/fabric/inspection/FabricEntrypointsInspection.kt (revision 638c35fc692d3ebd304aaf9fe4892104e05caf19) @@ -0,0 +1,151 @@ +/* + * Minecraft Development for IntelliJ + * + * https://mcdev.io/ + * + * Copyright (C) 2023 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.inspection + +import com.demonwav.mcdev.platform.fabric.reference.EntryPointReference +import com.demonwav.mcdev.platform.fabric.util.FabricConstants +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.JsonElementVisitor +import com.intellij.json.psi.JsonProperty +import com.intellij.json.psi.JsonStringLiteral +import com.intellij.psi.JavaPsiFacade +import com.intellij.psi.PsiClass +import com.intellij.psi.PsiClassType +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiField +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiModifier +import com.intellij.psi.util.parentOfType + +class FabricEntrypointsInspection : LocalInspectionTool() { + + override fun getStaticDescription() = "Validates entrypoints declared in Fabric mod JSON files." + + override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor { + if (holder.file.name == FabricConstants.FABRIC_MOD_JSON) { + return Visitor(holder) + } + return PsiElementVisitor.EMPTY_VISITOR + } + + override fun processFile(file: PsiFile, manager: InspectionManager): List { + if (file.name == FabricConstants.FABRIC_MOD_JSON) { + return super.processFile(file, manager) + } + return emptyList() + } + + private class Visitor(private val holder: ProblemsHolder) : JsonElementVisitor() { + + override fun visitStringLiteral(literal: JsonStringLiteral) { + for (reference in literal.references) { + if (reference !is EntryPointReference.Reference) { + continue + } + + val resolved = reference.multiResolve(false) + if (resolved.size > 1) { + holder.registerProblem( + literal, + "Ambiguous member reference", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + + 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] } + if (propertyKey != null && expectedType != null && + !isEntrypointOfCorrectType(element, propertyKey) + ) { + holder.registerProblem( + literal, + "'$propertyKey' entrypoints must implement $expectedType", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } else if (element.constructors.isNotEmpty() && + element.constructors.find { !it.hasParameters() } == null + ) { + holder.registerProblem( + literal, + "Entrypoint class must have an empty constructor", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + } + + element is PsiMethod -> { + if (element.hasParameters()) { + holder.registerProblem( + literal, + "Entrypoint method must have no parameters", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + } + + element is PsiField -> { + if (!element.hasModifierProperty(PsiModifier.STATIC)) { + holder.registerProblem( + literal, + "Entrypoint field must be static", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + + val propertyKey = literal.parentOfType()?.name + 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) + ) { + holder.registerProblem( + literal, + "'$propertyKey' entrypoints must be of type $expectedType", + ProblemHighlightType.GENERIC_ERROR_OR_WARNING, + reference.rangeInElement, + ) + } + } + } + } + } + + 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) + } + } +} Index: src/main/kotlin/platform/fabric/util/FabricConstants.kt =================================================================== --- src/main/kotlin/platform/fabric/util/FabricConstants.kt (revision 1b919feac3c261175857ad034684e4121babc6cc) +++ src/main/kotlin/platform/fabric/util/FabricConstants.kt (revision 638c35fc692d3ebd304aaf9fe4892104e05caf19) @@ -26,7 +26,17 @@ const val MOD_INITIALIZER = "net.fabricmc.api.ModInitializer" const val CLIENT_MOD_INITIALIZER = "net.fabricmc.api.ClientModInitializer" + const val SERVER_MOD_INITIALIZER = "net.fabricmc.api.DedicatedServerModInitializer" + const val PRE_LAUNCH_ENTRYPOINT = "net.fabricmc.loader.api.entrypoint.PreLaunchEntryPoint" const val ENVIRONMENT_ANNOTATION = "net.fabricmc.api.Environment" const val ENV_TYPE = "net.fabricmc.api.EnvType" const val ENVIRONMENT_INTERFACE_ANNOTATION = "net.fabricmc.api.EnvironmentInterface" + + val ENTRYPOINTS = setOf(MOD_INITIALIZER, CLIENT_MOD_INITIALIZER, SERVER_MOD_INITIALIZER, PRE_LAUNCH_ENTRYPOINT) + val ENTRYPOINT_BY_TYPE = mapOf( + "main" to MOD_INITIALIZER, + "client" to CLIENT_MOD_INITIALIZER, + "server" to SERVER_MOD_INITIALIZER, + "preLaunch" to PRE_LAUNCH_ENTRYPOINT + ) } Index: src/main/resources/META-INF/plugin.xml =================================================================== --- src/main/resources/META-INF/plugin.xml (revision 1b919feac3c261175857ad034684e4121babc6cc) +++ src/main/resources/META-INF/plugin.xml (revision 638c35fc692d3ebd304aaf9fe4892104e05caf19) @@ -625,6 +625,14 @@ level="ERROR" hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.fabric.inspection.UnresolvedReferenceInspection"/> +