Skip to content

Commit c41d412

Browse files
committed
Merge branch '2022.3' into 2023.1
2 parents dc5f24b + 5142284 commit c41d412

21 files changed

+630
-113
lines changed

build.gradle.kts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -33,7 +33,7 @@ plugins {
3333
mcdev
3434
groovy
3535
idea
36-
id("org.jetbrains.intellij") version "1.13.2"
36+
id("org.jetbrains.intellij") version "1.14.1"
3737
id("org.cadixdev.licenser")
3838
id("org.jlleitschuh.gradle.ktlint") version "10.3.0"
3939
}

gradle.properties

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,7 @@ kotlin.code.style=official
2424
ideaVersion = 2023.1
2525
ideaVersionName = 2023.1
2626

27-
coreVersion = 1.6.5
27+
coreVersion = 1.6.6
2828
downloadIdeaSources = true
2929

3030
pluginTomlVersion = 231.8109.1

readme.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -35,7 +35,7 @@ Minecraft Development for IntelliJ
3535
</tr>
3636
</table>
3737

38-
Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.6.5-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327)
38+
Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.6.6-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327)
3939
----------------------
4040

4141
<a href="https://discord.gg/j6UNcfr"><img src="https://i.imgur.com/JXu9C1G.png" height="48px"></img></a>
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/reference/EntryPointReference.kt

Lines changed: 103 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,28 @@
2020

2121
package com.demonwav.mcdev.platform.fabric.reference
2222

23+
import com.demonwav.mcdev.platform.fabric.util.FabricConstants
24+
import com.demonwav.mcdev.util.fullQualifiedName
2325
import com.demonwav.mcdev.util.manipulator
2426
import com.demonwav.mcdev.util.reference.InspectionReference
27+
import com.intellij.codeInsight.completion.JavaLookupElementBuilder
2528
import com.intellij.json.psi.JsonStringLiteral
2629
import com.intellij.openapi.util.TextRange
2730
import com.intellij.psi.JavaPsiFacade
2831
import com.intellij.psi.PsiClass
32+
import com.intellij.psi.PsiClassType
2933
import com.intellij.psi.PsiElement
3034
import com.intellij.psi.PsiElementResolveResult
35+
import com.intellij.psi.PsiField
3136
import com.intellij.psi.PsiMethod
3237
import com.intellij.psi.PsiModifier
3338
import com.intellij.psi.PsiPolyVariantReference
3439
import com.intellij.psi.PsiReference
3540
import com.intellij.psi.PsiReferenceBase
3641
import com.intellij.psi.PsiReferenceProvider
3742
import com.intellij.psi.ResolveResult
43+
import com.intellij.psi.search.searches.ClassInheritorsSearch
44+
import com.intellij.util.ArrayUtil
3845
import com.intellij.util.IncorrectOperationException
3946
import com.intellij.util.ProcessingContext
4047

@@ -46,8 +53,8 @@ object EntryPointReference : PsiReferenceProvider() {
4653
val manipulator = element.manipulator ?: return PsiReference.EMPTY_ARRAY
4754
val range = manipulator.getRangeInElement(element)
4855
val text = element.text.substring(range.startOffset, range.endOffset)
49-
val methodParts = text.split("::", limit = 2)
50-
val clazzParts = methodParts[0].split("$", limit = 0)
56+
val memberParts = text.split("::", limit = 2)
57+
val clazzParts = memberParts[0].split("$", limit = 0)
5158
val references = mutableListOf<Reference>()
5259
var cursor = -1
5360
var innerClassDepth = -1
@@ -64,12 +71,12 @@ object EntryPointReference : PsiReferenceProvider() {
6471
)
6572
cursor += clazzPart.length
6673
}
67-
if (methodParts.size == 2) {
74+
if (memberParts.size == 2) {
6875
cursor += 2
6976
references.add(
7077
Reference(
7178
element,
72-
range.cutOut(TextRange.from(cursor, methodParts[1].length)),
79+
range.cutOut(TextRange.from(cursor, memberParts[1].length)),
7380
innerClassDepth,
7481
true,
7582
),
@@ -81,16 +88,16 @@ object EntryPointReference : PsiReferenceProvider() {
8188
private fun resolveReference(
8289
element: JsonStringLiteral,
8390
innerClassDepth: Int,
84-
isMethodReference: Boolean,
91+
isMemberReference: Boolean,
8592
): Array<PsiElement> {
8693
val strReference = element.value
87-
val methodParts = strReference.split("::", limit = 2)
94+
val memberParts = strReference.split("::", limit = 2)
8895
// split at dollar sign for inner class evaluation
89-
val clazzParts = methodParts[0].split("$", limit = 0)
96+
val clazzParts = memberParts[0].split("$", limit = 0)
9097
// this case should only happen if someone misuses the method, better protect against it anyways
9198
if (innerClassDepth >= clazzParts.size ||
9299
innerClassDepth + 1 < clazzParts.size &&
93-
isMethodReference
100+
isMemberReference
94101
) {
95102
throw IncorrectOperationException("Invalid reference")
96103
}
@@ -104,27 +111,63 @@ object EntryPointReference : PsiReferenceProvider() {
104111
if (inner.contains('.')) return PsiElement.EMPTY_ARRAY
105112
clazz = clazz.findInnerClassByName(inner, false) ?: return PsiElement.EMPTY_ARRAY
106113
}
107-
return if (isMethodReference) {
108-
if (methodParts.size == 1) {
114+
return if (isMemberReference) {
115+
if (memberParts.size == 1) {
109116
throw IncorrectOperationException("Invalid reference")
110117
}
111-
clazz.methods.filter { method ->
112-
method.name == methodParts[1] &&
113-
method.hasModifierProperty(PsiModifier.PUBLIC) &&
114-
method.hasModifierProperty(PsiModifier.STATIC)
115-
}.toTypedArray()
118+
119+
val members = mutableListOf<PsiElement>()
120+
clazz.fields.filterTo(members) { field ->
121+
field.name == memberParts[1] &&
122+
field.hasModifierProperty(PsiModifier.PUBLIC) &&
123+
field.hasModifierProperty(PsiModifier.STATIC)
124+
}
125+
126+
clazz.methods.filterTo(members) { method ->
127+
method.name == memberParts[1] &&
128+
method.hasModifierProperty(PsiModifier.PUBLIC)
129+
}
130+
131+
members.toTypedArray()
116132
} else {
117133
arrayOf(clazz)
118134
}
119135
}
120136

121137
fun isEntryPointReference(reference: PsiReference) = reference is Reference
122138

123-
private class Reference(
139+
fun isValidEntrypointClass(element: PsiClass): Boolean {
140+
val psiFacade = JavaPsiFacade.getInstance(element.project)
141+
var inheritsEntrypointInterface = false
142+
for (entrypoint in FabricConstants.ENTRYPOINTS) {
143+
val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope)
144+
?: continue
145+
if (element.isInheritor(entrypointClass, true)) {
146+
inheritsEntrypointInterface = true
147+
break
148+
}
149+
}
150+
return inheritsEntrypointInterface
151+
}
152+
153+
fun isValidEntrypointField(field: PsiField): Boolean {
154+
if (!field.hasModifierProperty(PsiModifier.PUBLIC) || !field.hasModifierProperty(PsiModifier.STATIC)) {
155+
return false
156+
}
157+
158+
val fieldTypeClass = (field.type as? PsiClassType)?.resolve()
159+
return fieldTypeClass != null && isValidEntrypointClass(fieldTypeClass)
160+
}
161+
162+
fun isValidEntrypointMethod(method: PsiMethod): Boolean {
163+
return method.hasModifierProperty(PsiModifier.PUBLIC) && !method.hasParameters()
164+
}
165+
166+
class Reference(
124167
element: JsonStringLiteral,
125168
range: TextRange,
126169
private val innerClassDepth: Int,
127-
private val isMethodReference: Boolean,
170+
private val isMemberReference: Boolean,
128171
) :
129172
PsiReferenceBase<JsonStringLiteral>(element, range),
130173
PsiPolyVariantReference,
@@ -134,7 +177,7 @@ object EntryPointReference : PsiReferenceProvider() {
134177
override val unresolved = resolve() == null
135178

136179
override fun multiResolve(incompleteCode: Boolean): Array<ResolveResult> {
137-
return resolveReference(element, innerClassDepth, isMethodReference)
180+
return resolveReference(element, innerClassDepth, isMemberReference)
138181
.map { PsiElementResolveResult(it) }.toTypedArray()
139182
}
140183

@@ -154,14 +197,17 @@ object EntryPointReference : PsiReferenceProvider() {
154197
val text = element.text.substring(range.startOffset, range.endOffset)
155198
val parts = text.split("::", limit = 2)
156199

157-
if (isMethodReference) {
158-
val targetMethod = newTarget as? PsiMethod
159-
?: throw IncorrectOperationException("Cannot target $newTarget")
200+
if (isMemberReference) {
201+
val newTargetName = when (newTarget) {
202+
is PsiMethod -> newTarget.name
203+
is PsiField -> newTarget.name
204+
else -> throw IncorrectOperationException("Cannot target $newTarget")
205+
}
160206
if (parts.size == 1) {
161207
throw IncorrectOperationException("Invalid reference")
162208
}
163-
val methodRange = range.cutOut(TextRange.from(parts[0].length + 2, parts[1].length))
164-
return manipulator.handleContentChange(element, methodRange, targetMethod.name)
209+
val memberRange = range.cutOut(TextRange.from(parts[0].length + 2, parts[1].length))
210+
return manipulator.handleContentChange(element, memberRange, newTargetName)
165211
} else {
166212
val targetClass = newTarget as? PsiClass
167213
?: throw IncorrectOperationException("Cannot target $newTarget")
@@ -173,5 +219,39 @@ object EntryPointReference : PsiReferenceProvider() {
173219
return manipulator.handleContentChange(element, classRange, targetClass.qualifiedName)
174220
}
175221
}
222+
223+
override fun getVariants(): Array<Any> {
224+
val manipulator = element.manipulator
225+
?: return ArrayUtil.EMPTY_OBJECT_ARRAY
226+
227+
val range = manipulator.getRangeInElement(element)
228+
val text = element.text.substring(range.startOffset, range.endOffset)
229+
val parts = text.split("::", limit = 2)
230+
231+
val variants = mutableListOf<Any>()
232+
if (!isMemberReference) {
233+
val psiFacade = JavaPsiFacade.getInstance(element.project)
234+
for (entrypoint in FabricConstants.ENTRYPOINTS) {
235+
val entrypointClass = psiFacade.findClass(entrypoint, element.resolveScope)
236+
?: continue
237+
ClassInheritorsSearch.search(entrypointClass, true)
238+
.mapNotNullTo(variants) {
239+
val shortName = it.name ?: return@mapNotNullTo null
240+
val fqName = it.fullQualifiedName ?: return@mapNotNullTo null
241+
JavaLookupElementBuilder.forClass(it, fqName, true).withPresentableText(shortName)
242+
}
243+
}
244+
} else if (parts.size >= 2) {
245+
val psiFacade = JavaPsiFacade.getInstance(element.project)
246+
val className = parts[0].replace('$', '.')
247+
val clazz = psiFacade.findClass(className, element.resolveScope)
248+
if (clazz != null) {
249+
clazz.fields.filterTo(variants, ::isValidEntrypointField)
250+
clazz.methods.filterTo(variants, ::isValidEntrypointMethod)
251+
}
252+
}
253+
254+
return variants.toTypedArray()
255+
}
176256
}
177257
}

0 commit comments

Comments
 (0)