Skip to content

Commit 638c35f

Browse files
committed
Add inspection to validate fabric entrypoints
1 parent 1b919fe commit 638c35f

File tree

3 files changed

+169
-0
lines changed

3 files changed

+169
-0
lines changed
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2023 minecraft-dev
7+
*
8+
* This program is free software: you can redistribute it and/or modify
9+
* it under the terms of the GNU Lesser General Public License as published
10+
* by the Free Software Foundation, version 3.0 only.
11+
*
12+
* This program is distributed in the hope that it will be useful,
13+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
14+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
15+
* GNU General Public License for more details.
16+
*
17+
* You should have received a copy of the GNU Lesser General Public License
18+
* along with this program. If not, see <https://www.gnu.org/licenses/>.
19+
*/
20+
21+
package com.demonwav.mcdev.platform.fabric.inspection
22+
23+
import com.demonwav.mcdev.platform.fabric.reference.EntryPointReference
24+
import com.demonwav.mcdev.platform.fabric.util.FabricConstants
25+
import com.intellij.codeInspection.InspectionManager
26+
import com.intellij.codeInspection.LocalInspectionTool
27+
import com.intellij.codeInspection.ProblemDescriptor
28+
import com.intellij.codeInspection.ProblemHighlightType
29+
import com.intellij.codeInspection.ProblemsHolder
30+
import com.intellij.json.psi.JsonElementVisitor
31+
import com.intellij.json.psi.JsonProperty
32+
import com.intellij.json.psi.JsonStringLiteral
33+
import com.intellij.psi.JavaPsiFacade
34+
import com.intellij.psi.PsiClass
35+
import com.intellij.psi.PsiClassType
36+
import com.intellij.psi.PsiElementVisitor
37+
import com.intellij.psi.PsiField
38+
import com.intellij.psi.PsiFile
39+
import com.intellij.psi.PsiMethod
40+
import com.intellij.psi.PsiModifier
41+
import com.intellij.psi.util.parentOfType
42+
43+
class FabricEntrypointsInspection : LocalInspectionTool() {
44+
45+
override fun getStaticDescription() = "Validates entrypoints declared in Fabric mod JSON files."
46+
47+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
48+
if (holder.file.name == FabricConstants.FABRIC_MOD_JSON) {
49+
return Visitor(holder)
50+
}
51+
return PsiElementVisitor.EMPTY_VISITOR
52+
}
53+
54+
override fun processFile(file: PsiFile, manager: InspectionManager): List<ProblemDescriptor> {
55+
if (file.name == FabricConstants.FABRIC_MOD_JSON) {
56+
return super.processFile(file, manager)
57+
}
58+
return emptyList()
59+
}
60+
61+
private class Visitor(private val holder: ProblemsHolder) : JsonElementVisitor() {
62+
63+
override fun visitStringLiteral(literal: JsonStringLiteral) {
64+
for (reference in literal.references) {
65+
if (reference !is EntryPointReference.Reference) {
66+
continue
67+
}
68+
69+
val resolved = reference.multiResolve(false)
70+
if (resolved.size > 1) {
71+
holder.registerProblem(
72+
literal,
73+
"Ambiguous member reference",
74+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
75+
reference.rangeInElement,
76+
)
77+
}
78+
79+
val element = resolved.singleOrNull()?.element
80+
when {
81+
element is PsiClass && !literal.text.contains("::") -> {
82+
val propertyKey = literal.parentOfType<JsonProperty>()?.name
83+
val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] }
84+
if (propertyKey != null && expectedType != null &&
85+
!isEntrypointOfCorrectType(element, propertyKey)
86+
) {
87+
holder.registerProblem(
88+
literal,
89+
"'$propertyKey' entrypoints must implement $expectedType",
90+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
91+
reference.rangeInElement,
92+
)
93+
} else if (element.constructors.isNotEmpty() &&
94+
element.constructors.find { !it.hasParameters() } == null
95+
) {
96+
holder.registerProblem(
97+
literal,
98+
"Entrypoint class must have an empty constructor",
99+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
100+
reference.rangeInElement,
101+
)
102+
}
103+
}
104+
105+
element is PsiMethod -> {
106+
if (element.hasParameters()) {
107+
holder.registerProblem(
108+
literal,
109+
"Entrypoint method must have no parameters",
110+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
111+
reference.rangeInElement,
112+
)
113+
}
114+
}
115+
116+
element is PsiField -> {
117+
if (!element.hasModifierProperty(PsiModifier.STATIC)) {
118+
holder.registerProblem(
119+
literal,
120+
"Entrypoint field must be static",
121+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
122+
reference.rangeInElement,
123+
)
124+
}
125+
126+
val propertyKey = literal.parentOfType<JsonProperty>()?.name
127+
val fieldTypeClass = (element.type as? PsiClassType)?.resolve()
128+
val expectedType = propertyKey?.let { FabricConstants.ENTRYPOINT_BY_TYPE[it] }
129+
if (propertyKey != null && fieldTypeClass != null && expectedType != null &&
130+
!isEntrypointOfCorrectType(fieldTypeClass, propertyKey)
131+
) {
132+
holder.registerProblem(
133+
literal,
134+
"'$propertyKey' entrypoints must be of type $expectedType",
135+
ProblemHighlightType.GENERIC_ERROR_OR_WARNING,
136+
reference.rangeInElement,
137+
)
138+
}
139+
}
140+
}
141+
}
142+
}
143+
144+
private fun isEntrypointOfCorrectType(element: PsiClass, type: String): Boolean {
145+
val entrypointClass = FabricConstants.ENTRYPOINT_BY_TYPE[type]
146+
?: return false
147+
val clazz = JavaPsiFacade.getInstance(element.project).findClass(entrypointClass, element.resolveScope)
148+
return clazz != null && element.isInheritor(clazz, true)
149+
}
150+
}
151+
}

src/main/kotlin/platform/fabric/util/FabricConstants.kt

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,17 @@ object FabricConstants {
2626

2727
const val MOD_INITIALIZER = "net.fabricmc.api.ModInitializer"
2828
const val CLIENT_MOD_INITIALIZER = "net.fabricmc.api.ClientModInitializer"
29+
const val SERVER_MOD_INITIALIZER = "net.fabricmc.api.DedicatedServerModInitializer"
30+
const val PRE_LAUNCH_ENTRYPOINT = "net.fabricmc.loader.api.entrypoint.PreLaunchEntryPoint"
2931
const val ENVIRONMENT_ANNOTATION = "net.fabricmc.api.Environment"
3032
const val ENV_TYPE = "net.fabricmc.api.EnvType"
3133
const val ENVIRONMENT_INTERFACE_ANNOTATION = "net.fabricmc.api.EnvironmentInterface"
34+
35+
val ENTRYPOINTS = setOf(MOD_INITIALIZER, CLIENT_MOD_INITIALIZER, SERVER_MOD_INITIALIZER, PRE_LAUNCH_ENTRYPOINT)
36+
val ENTRYPOINT_BY_TYPE = mapOf(
37+
"main" to MOD_INITIALIZER,
38+
"client" to CLIENT_MOD_INITIALIZER,
39+
"server" to SERVER_MOD_INITIALIZER,
40+
"preLaunch" to PRE_LAUNCH_ENTRYPOINT
41+
)
3242
}

src/main/resources/META-INF/plugin.xml

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -625,6 +625,14 @@
625625
level="ERROR"
626626
hasStaticDescription="true"
627627
implementationClass="com.demonwav.mcdev.platform.fabric.inspection.UnresolvedReferenceInspection"/>
628+
<localInspection displayName="Fabric mod JSON entrypoints validation"
629+
shortName="FabricModJsonEntrypointsValidation"
630+
groupName="Fabric"
631+
language="JSON"
632+
enabledByDefault="true"
633+
level="ERROR"
634+
hasStaticDescription="true"
635+
implementationClass="com.demonwav.mcdev.platform.fabric.inspection.FabricEntrypointsInspection"/>
628636
<!--endregion-->
629637

630638
<!--region MCP INSPECTIONS-->

0 commit comments

Comments
 (0)