Skip to content

Commit bf660ea

Browse files
committed
Add initial insight for Bukkit-like plugin.yml
1 parent d8fb941 commit bf660ea

File tree

24 files changed

+1186
-269
lines changed

24 files changed

+1186
-269
lines changed

build.gradle.kts

Lines changed: 24 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -109,7 +109,21 @@ repositories {
109109
}
110110
}
111111
mavenCentral()
112-
maven("https://repo.spongepowered.org/maven/")
112+
maven("https://repo.spongepowered.org/maven/") {
113+
content {
114+
includeGroup("org.spongepowered")
115+
}
116+
}
117+
maven("https://hub.spigotmc.org/nexus/content/repositories/snapshots/") {
118+
content {
119+
includeGroup("org.spigotmc")
120+
}
121+
}
122+
maven("https://oss.sonatype.org/content/repositories/snapshots/") {
123+
content {
124+
includeGroup("net.md-5")
125+
}
126+
}
113127
}
114128

115129
dependencies {
@@ -141,6 +155,8 @@ dependencies {
141155

142156
testLibs(libs.test.mockJdk)
143157
testLibs(libs.test.mixin)
158+
testLibs(libs.test.spigotapi)
159+
testLibs(libs.test.bungeecord)
144160
testLibs(libs.test.spongeapi) {
145161
artifact {
146162
classifier = "shaded"
@@ -212,6 +228,7 @@ intellij {
212228
"ByteCodeViewer",
213229
"org.intellij.intelliLang",
214230
"properties",
231+
"org.jetbrains.plugins.yaml",
215232
// needed dependencies for unit tests
216233
"junit"
217234
)
@@ -327,7 +344,12 @@ license {
327344
exclude("META-INF/plugin.xml") // https://youtrack.jetbrains.com/issue/IDEA-345026
328345
include(endings.map { "**/*.$it" })
329346

330-
exclude("com/demonwav/mcdev/platform/mixin/invalidInjectorMethodSignature/*.java")
347+
val projectDir = layout.projectDirectory.asFile
348+
exclude {
349+
it.file.toRelativeString(projectDir)
350+
.replace("\\", "/")
351+
.startsWith("src/test/resources")
352+
}
331353

332354
this.tasks {
333355
register("gradle") {

changelog.md

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
# Minecraft Development for IntelliJ
22

3+
## [Unreleased]
4+
5+
### Added
6+
7+
- `plugin.yml`, `paper-plugin.yml` and `bungee.yml` main class reference and validity inspection
8+
39
## [1.8.1] - 2024-08-10
410

511
### Added

gradle/libs.versions.toml

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,8 @@ fuel-coroutines = { module = "com.github.kittinunf.fuel:fuel-coroutines", versio
3434
# Testing
3535
test-mockJdk = "org.jetbrains.idea:mock-jdk:1.7-4d76c50"
3636
test-mixin = "org.spongepowered:mixin:0.8.5"
37+
test-spigotapi = "org.spigotmc:spigot-api:1.21-R0.1-SNAPSHOT"
38+
test-bungeecord = "net.md-5:bungeecord-api:1.21-R0.1-SNAPSHOT"
3739
test-spongeapi = "org.spongepowered:spongeapi:7.4.0"
3840
test-fabricloader = "net.fabricmc:fabric-loader:0.15.11"
3941
test-nbt = "com.demonwav.mcdev:all-types-nbt:1.0"

src/main/kotlin/platform/bukkit/BukkitModule.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -42,21 +42,25 @@ import com.intellij.psi.PsiElement
4242
import com.intellij.psi.PsiLiteralExpression
4343
import com.intellij.psi.PsiMethod
4444
import com.intellij.psi.PsiMethodCallExpression
45+
import com.intellij.util.application
4546
import org.jetbrains.uast.UClass
4647
import org.jetbrains.uast.UIdentifier
4748
import org.jetbrains.uast.toUElementOfType
4849

4950
class BukkitModule<out T : AbstractModuleType<*>>(facet: MinecraftFacet, type: T) : AbstractModule(facet) {
5051

52+
// Light test cases only support a single source content root, so we mix sources and resources under unit test mode
53+
private val pluginYmlSourceType = if (application.isUnitTestMode) SourceType.SOURCE else SourceType.RESOURCE
54+
5155
var pluginYml by nullable {
5256
if (moduleType is PaperModuleType) {
53-
val paperPlugin = facet.findFile("paper-plugin.yml", SourceType.RESOURCE)
57+
val paperPlugin = facet.findFile("paper-plugin.yml", pluginYmlSourceType)
5458
if (paperPlugin != null) {
5559
return@nullable paperPlugin
5660
}
5761
}
5862

59-
facet.findFile("plugin.yml", SourceType.RESOURCE)
63+
facet.findFile("plugin.yml", pluginYmlSourceType)
6064
}
6165
private set
6266

src/main/kotlin/platform/bungeecord/BungeeCordModule.kt

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -37,14 +37,18 @@ import com.intellij.psi.JavaPsiFacade
3737
import com.intellij.psi.PsiClass
3838
import com.intellij.psi.PsiElement
3939
import com.intellij.psi.PsiMethod
40+
import com.intellij.util.application
4041
import org.jetbrains.uast.UClass
4142
import org.jetbrains.uast.UIdentifier
4243
import org.jetbrains.uast.toUElementOfType
4344

4445
class BungeeCordModule<out T : AbstractModuleType<*>>(facet: MinecraftFacet, type: T) : AbstractModule(facet) {
4546

47+
// Light test cases only support a single source content root, so we mix sources and resources under unit test mode
48+
private val pluginYmlSourceType = if (application.isUnitTestMode) SourceType.SOURCE else SourceType.RESOURCE
49+
4650
var pluginYml by nullable {
47-
val file = facet.findFile("bungee.yml", SourceType.RESOURCE)
51+
val file = facet.findFile("bungee.yml", pluginYmlSourceType)
4852
if (file != null) {
4953
return@nullable file
5054
}
@@ -53,7 +57,7 @@ class BungeeCordModule<out T : AbstractModuleType<*>>(facet: MinecraftFacet, typ
5357
// So we don't check
5458
return@nullable null
5559
}
56-
return@nullable facet.findFile("plugin.yml", SourceType.RESOURCE)
60+
return@nullable facet.findFile("plugin.yml", pluginYmlSourceType)
5761
}
5862
private set
5963

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2024 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.yaml
22+
23+
import com.demonwav.mcdev.facet.MinecraftFacet
24+
import com.demonwav.mcdev.platform.bukkit.PaperModuleType
25+
import com.demonwav.mcdev.platform.bukkit.SpigotModuleType
26+
import com.demonwav.mcdev.platform.bukkit.util.BukkitConstants
27+
import com.demonwav.mcdev.platform.bungeecord.BungeeCordModuleType
28+
import com.demonwav.mcdev.platform.bungeecord.util.BungeeCordConstants
29+
import com.demonwav.mcdev.util.findModule
30+
import com.intellij.codeInspection.LocalInspectionTool
31+
import com.intellij.codeInspection.ProblemsHolder
32+
import com.intellij.psi.JavaPsiFacade
33+
import com.intellij.psi.PsiClass
34+
import com.intellij.psi.PsiElementVisitor
35+
import org.jetbrains.annotations.Nls
36+
import org.jetbrains.yaml.psi.YAMLKeyValue
37+
import org.jetbrains.yaml.psi.YAMLScalar
38+
import org.jetbrains.yaml.psi.YamlPsiElementVisitor
39+
40+
class PluginYmlInspection : LocalInspectionTool() {
41+
42+
override fun getStaticDescription(): @Nls String? = "Reports issues in Bukkit-like plugin.yml files"
43+
44+
override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
45+
val module = holder.file.findModule() ?: return PsiElementVisitor.EMPTY_VISITOR
46+
val virtualFile = holder.file.virtualFile ?: return PsiElementVisitor.EMPTY_VISITOR
47+
val pluginClassFqn = when {
48+
MinecraftFacet.getInstance(module, SpigotModuleType, PaperModuleType)?.pluginYml == virtualFile ->
49+
BukkitConstants.PLUGIN
50+
MinecraftFacet.getInstance(module, BungeeCordModuleType)?.pluginYml == virtualFile ->
51+
BungeeCordConstants.PLUGIN
52+
else -> return PsiElementVisitor.EMPTY_VISITOR
53+
}
54+
55+
return Visitor(holder, pluginClassFqn)
56+
}
57+
58+
private class Visitor(val holder: ProblemsHolder, val pluginClassFqn: String) : YamlPsiElementVisitor() {
59+
60+
override fun visitScalar(scalar: YAMLScalar) {
61+
super.visitScalar(scalar)
62+
63+
if ((scalar.parent as? YAMLKeyValue)?.keyText != "main") {
64+
return
65+
}
66+
67+
val resolved = scalar.references.firstNotNullOfOrNull { it.resolve() as? PsiClass }
68+
if (resolved == null) {
69+
holder.registerProblem(scalar, "Unresolved reference")
70+
return
71+
}
72+
73+
val pluginClass = JavaPsiFacade.getInstance(holder.project).findClass(pluginClassFqn, scalar.resolveScope)
74+
?: return
75+
if (!resolved.isInheritor(pluginClass, true)) {
76+
holder.registerProblem(scalar, "Class must implement $pluginClassFqn")
77+
}
78+
}
79+
}
80+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
/*
2+
* Minecraft Development for IntelliJ
3+
*
4+
* https://mcdev.io/
5+
*
6+
* Copyright (C) 2024 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.yaml
22+
23+
import com.demonwav.mcdev.facet.MinecraftFacet
24+
import com.demonwav.mcdev.platform.bukkit.PaperModuleType
25+
import com.demonwav.mcdev.platform.bukkit.SpigotModuleType
26+
import com.demonwav.mcdev.platform.bukkit.util.BukkitConstants
27+
import com.demonwav.mcdev.platform.bungeecord.BungeeCordModuleType
28+
import com.demonwav.mcdev.platform.bungeecord.util.BungeeCordConstants
29+
import com.demonwav.mcdev.util.findModule
30+
import com.intellij.codeInsight.completion.JavaLookupElementBuilder
31+
import com.intellij.lang.jvm.JvmModifier
32+
import com.intellij.openapi.util.TextRange
33+
import com.intellij.patterns.ObjectPattern
34+
import com.intellij.patterns.PatternCondition
35+
import com.intellij.patterns.PlatformPatterns
36+
import com.intellij.psi.JavaPsiFacade
37+
import com.intellij.psi.PsiElement
38+
import com.intellij.psi.PsiReference
39+
import com.intellij.psi.PsiReferenceContributor
40+
import com.intellij.psi.PsiReferenceRegistrar
41+
import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReference
42+
import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceProvider
43+
import com.intellij.psi.impl.source.resolve.reference.impl.providers.JavaClassReferenceSet
44+
import com.intellij.psi.search.searches.ClassInheritorsSearch
45+
import com.intellij.util.ArrayUtilRt
46+
import com.intellij.util.ProcessingContext
47+
import org.jetbrains.yaml.psi.YAMLKeyValue
48+
import org.jetbrains.yaml.psi.YAMLScalar
49+
50+
private fun <P : ObjectPattern<out PsiElement, P>> P.inSpigotOrPaperPluginYml(): P = with(
51+
object : PatternCondition<PsiElement>("") {
52+
override fun accepts(t: PsiElement, context: ProcessingContext): Boolean {
53+
val module = t.findModule() ?: return false
54+
val instance = MinecraftFacet.getInstance(module, SpigotModuleType, PaperModuleType) ?: return false
55+
return instance.pluginYml == t.containingFile.originalFile.virtualFile
56+
}
57+
}
58+
)
59+
60+
private fun <P : ObjectPattern<out PsiElement, P>> P.inBungeePluginYml(): P = with(
61+
object : PatternCondition<PsiElement>("") {
62+
override fun accepts(t: PsiElement, context: ProcessingContext): Boolean {
63+
val module = t.findModule() ?: return false
64+
val instance = MinecraftFacet.getInstance(module, BungeeCordModuleType) ?: return false
65+
return instance.pluginYml == t.containingFile.originalFile.virtualFile
66+
}
67+
}
68+
)
69+
70+
class PluginYmlReferenceContributor : PsiReferenceContributor() {
71+
72+
override fun registerReferenceProviders(registrar: PsiReferenceRegistrar) {
73+
registrar.registerReferenceProvider(
74+
PlatformPatterns.psiElement(YAMLScalar::class.java)
75+
.withParent(PlatformPatterns.psiElement(YAMLKeyValue::class.java).withName("main"))
76+
.inSpigotOrPaperPluginYml(),
77+
PluginYmlClassReferenceProvider(BukkitConstants.PLUGIN)
78+
)
79+
registrar.registerReferenceProvider(
80+
PlatformPatterns.psiElement(YAMLScalar::class.java)
81+
.withParent(PlatformPatterns.psiElement(YAMLKeyValue::class.java).withName("main"))
82+
.inBungeePluginYml(),
83+
PluginYmlClassReferenceProvider(BungeeCordConstants.PLUGIN)
84+
)
85+
}
86+
}
87+
88+
class PluginYmlClassReferenceProvider(val superClass: String) : JavaClassReferenceProvider() {
89+
90+
init {
91+
setOption(ALLOW_DOLLAR_NAMES, true)
92+
setOption(JVM_FORMAT, true)
93+
setOption(CONCRETE, true)
94+
setOption(INSTANTIATABLE, true)
95+
setOption(SUPER_CLASSES, listOf(superClass))
96+
setOption(ADVANCED_RESOLVE, true)
97+
}
98+
99+
override fun getReferencesByString(
100+
str: String,
101+
position: PsiElement,
102+
offsetInPosition: Int
103+
): Array<out PsiReference?> {
104+
return object : JavaClassReferenceSet(str, position, offsetInPosition, true, this) {
105+
override fun isAllowDollarInNames(): Boolean = true
106+
107+
override fun isAllowSpaces(): Boolean = false
108+
109+
override fun createReference(
110+
referenceIndex: Int,
111+
referenceText: String,
112+
textRange: TextRange,
113+
staticImport: Boolean
114+
): JavaClassReference {
115+
return PluginYmlClassReference(this, referenceIndex, referenceText, textRange, staticImport, superClass)
116+
}
117+
}.allReferences
118+
}
119+
}
120+
121+
class PluginYmlClassReference(
122+
referenceSet: JavaClassReferenceSet,
123+
referenceIndex: Int,
124+
referenceText: String,
125+
textRange: TextRange,
126+
staticImport: Boolean,
127+
val superClass: String
128+
) : JavaClassReference(referenceSet, textRange, referenceIndex, referenceText, staticImport) {
129+
130+
override fun getVariants(): Array<out Any?> {
131+
val pluginClass =
132+
JavaPsiFacade.getInstance(element.project).findClass(superClass, element.resolveScope)
133+
?: return ArrayUtilRt.EMPTY_OBJECT_ARRAY
134+
val candidates =
135+
ClassInheritorsSearch.search(pluginClass, element.resolveScope, true)
136+
.filter { !it.hasModifier(JvmModifier.ABSTRACT) }
137+
.map { JavaLookupElementBuilder.forClass(it, it.qualifiedName) }
138+
.toTypedArray()
139+
return candidates
140+
}
141+
}

0 commit comments

Comments
 (0)