diff --git a/gradle.properties b/gradle.properties index 02fc19279..9ab5d690a 100644 --- a/gradle.properties +++ b/gradle.properties @@ -24,7 +24,7 @@ kotlin.code.style=official ideaVersion = 232.8453.116-EAP-SNAPSHOT ideaVersionName = 2023.2 -coreVersion = 1.6.6 +coreVersion = 1.6.7 downloadIdeaSources = true pluginTomlVersion = 232.8453.111 diff --git a/readme.md b/readme.md index 2e14d3c61..2206971aa 100644 --- a/readme.md +++ b/readme.md @@ -35,7 +35,7 @@ Minecraft Development for IntelliJ -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) +Info and Documentation [![Current Release](https://img.shields.io/badge/release-1.6.7-orange.svg?style=flat-square)](https://plugins.jetbrains.com/plugin/8327) ---------------------- diff --git a/src/main/kotlin/creator/PlatformVersion.kt b/src/main/kotlin/creator/PlatformVersion.kt index 924ac3d6b..4b17ee1ca 100644 --- a/src/main/kotlin/creator/PlatformVersion.kt +++ b/src/main/kotlin/creator/PlatformVersion.kt @@ -23,6 +23,9 @@ package com.demonwav.mcdev.creator import com.demonwav.mcdev.platform.PlatformType import com.demonwav.mcdev.update.PluginUtil import com.demonwav.mcdev.util.fromJson +import com.demonwav.mcdev.util.mapFirstNotNull +import com.demonwav.mcdev.util.withSuppressed +import com.github.kittinunf.fuel.core.FuelError import com.github.kittinunf.fuel.core.FuelManager import com.github.kittinunf.fuel.core.requests.suspendable import com.github.kittinunf.fuel.coroutines.awaitString @@ -35,8 +38,20 @@ import java.net.Proxy import java.net.URI import kotlin.reflect.KClass -private const val CLOUDFLARE_BASE_URL = "https://minecraftdev.org/versions/" +// Cloudflare and GitHub are both global CDNs +// Cloudflare is used first / preferred simply due to domain preference +private const val CLOUDFLARE_BASE_URL = "https://mcdev.io/versions/" +// Directly retrieving the file via GitHub is the second option. In some regions / networks Cloudflare is blocked, +// but we may still be able to reach GitHub private const val GITHUB_BASE_URL = "https://raw.githubusercontent.com/minecraft-dev/minecraftdev.org/master/versions/" +// Finally, there are apparently also regions / networks where both Cloudflare and GitHub is blocked. +// Or maybe the domain `mcdev.io` (and prior to that, `minecraftdev.org`) is blocked due to weird domain +// rules (perhaps blocking on the word "minecraft"). In one last ditch effort to retrieve the version json +// we can also pull from this host, a separate host using a separate domain. This is an OVH server, not +// proxied through Cloudflare. +private const val OVH_BASE_URL = "https://versions.denwav.com/versions/" + +private val URLS = listOf(CLOUDFLARE_BASE_URL, GITHUB_BASE_URL, OVH_BASE_URL) val PLATFORM_VERSION_LOGGER = logger() @@ -62,19 +77,16 @@ suspend fun getVersionJson(path: String, type: KClass): T { } suspend fun getText(path: String): String { - return try { - // attempt cloudflare - doCall(CLOUDFLARE_BASE_URL + path) - } catch (e: IOException) { - PLATFORM_VERSION_LOGGER.warn("Failed to reach cloudflare URL ${CLOUDFLARE_BASE_URL + path}", e) - // if that fails, attempt github + var thrown: FuelError? = null + return URLS.mapFirstNotNull { url -> try { - doCall(GITHUB_BASE_URL + path) - } catch (e: IOException) { - PLATFORM_VERSION_LOGGER.warn("Failed to reach fallback GitHub URL ${GITHUB_BASE_URL + path}", e) - throw e + doCall(url + path) + } catch (e: FuelError) { + PLATFORM_VERSION_LOGGER.warn("Failed to reach URL $url$path") + thrown = withSuppressed(thrown, e) + null } - } + } ?: throw thrown!! } private suspend fun doCall(urlText: String): String { diff --git a/src/main/kotlin/insight/ColorLineMarkerProvider.kt b/src/main/kotlin/insight/ColorLineMarkerProvider.kt index c8cd43052..317e1086b 100644 --- a/src/main/kotlin/insight/ColorLineMarkerProvider.kt +++ b/src/main/kotlin/insight/ColorLineMarkerProvider.kt @@ -21,6 +21,7 @@ package com.demonwav.mcdev.insight import com.demonwav.mcdev.MinecraftSettings +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.codeInsight.daemon.GutterIconNavigationHandler import com.intellij.codeInsight.daemon.LineMarkerInfo import com.intellij.codeInsight.daemon.LineMarkerProvider @@ -52,7 +53,9 @@ class ColorLineMarkerProvider : LineMarkerProvider { } val identifier = element.toUElementOfType() ?: return null - val info = identifier.findColor { map, chosen -> ColorInfo(element, chosen.value, map, chosen.key, identifier) } + val info = runCatchingKtIdeaExceptions { + identifier.findColor { map, chosen -> ColorInfo(element, chosen.value, map, chosen.key, identifier) } + } if (info != null) { NavigateAction.setNavigateAction(info, "Change Color", null) } @@ -164,6 +167,7 @@ class ColorLineMarkerProvider : LineMarkerProvider { } } } + is UCallExpression -> { if (workElement.methodName == "hsvLike") { val (h, s, v) = Color.RGBtoHSB(c.red, c.green, c.blue, null) diff --git a/src/main/kotlin/insight/ColorUtil.kt b/src/main/kotlin/insight/ColorUtil.kt index f0ed6d574..2c9fe8ded 100644 --- a/src/main/kotlin/insight/ColorUtil.kt +++ b/src/main/kotlin/insight/ColorUtil.kt @@ -49,9 +49,11 @@ import org.jetbrains.uast.generate.replace import org.jetbrains.uast.resolveToUElement fun UIdentifier.findColor(function: (Map, Map.Entry) -> T): T? { - val parent = this.uastParent - val expression = parent as? UReferenceExpression ?: return null - return findColorFromExpression(expression, function) + return runCatchingKtIdeaExceptions { + val parent = this.uastParent + val expression = parent as? UReferenceExpression ?: return null + findColorFromExpression(expression, function) + } } private fun findColorFromExpression( diff --git a/src/main/kotlin/insight/ListenerLineMarkerProvider.kt b/src/main/kotlin/insight/ListenerLineMarkerProvider.kt index 4e309b600..cc9351d68 100644 --- a/src/main/kotlin/insight/ListenerLineMarkerProvider.kt +++ b/src/main/kotlin/insight/ListenerLineMarkerProvider.kt @@ -22,6 +22,7 @@ package com.demonwav.mcdev.insight import com.demonwav.mcdev.MinecraftSettings import com.demonwav.mcdev.asset.GeneralAssets +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.codeInsight.daemon.GutterIconNavigationHandler import com.intellij.codeInsight.daemon.LineMarkerInfo import com.intellij.codeInsight.daemon.LineMarkerProviderDescriptor @@ -51,19 +52,11 @@ class ListenerLineMarkerProvider : LineMarkerProviderDescriptor() { return null } - try { + runCatchingKtIdeaExceptions { val identifier = element.toUElementOfType() ?: return null if (identifier.uastParent !is UMethod || identifier.uastEventListener == null) { return null } - } catch (e: Exception) { - // Kotlin plugin is buggy and can throw exceptions here - // We do the check like this because we don't actually have this class on the classpath - if (e.javaClass.name == "org.jetbrains.kotlin.idea.caches.resolve.KotlinIdeaResolutionException") { - return null - } - // Don't swallow unexpected errors - throw e } // By this point, we can guarantee that the action of "go to declaration" will work diff --git a/src/main/kotlin/platform/bukkit/BukkitModule.kt b/src/main/kotlin/platform/bukkit/BukkitModule.kt index 568a76ca0..1a73b9488 100644 --- a/src/main/kotlin/platform/bukkit/BukkitModule.kt +++ b/src/main/kotlin/platform/bukkit/BukkitModule.kt @@ -35,6 +35,7 @@ import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.findContainingMethod import com.demonwav.mcdev.util.nullable +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier import com.intellij.openapi.project.Project import com.intellij.psi.JavaPsiFacade @@ -182,7 +183,7 @@ class BukkitModule>(facet: MinecraftFacet, type: T val identifier = element?.toUElementOfType() ?: return false - val psiClass = (identifier.uastParent as? UClass)?.javaPsi + val psiClass = runCatchingKtIdeaExceptions { (identifier.uastParent as? UClass)?.javaPsi } ?: return false if (psiClass.hasModifier(JvmModifier.ABSTRACT)) { diff --git a/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt b/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt index b8e5ddc18..e736be8bb 100644 --- a/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt +++ b/src/main/kotlin/platform/bungeecord/BungeeCordModule.kt @@ -35,6 +35,7 @@ import com.demonwav.mcdev.util.SourceType import com.demonwav.mcdev.util.addImplements import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.nullable +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass @@ -117,7 +118,7 @@ class BungeeCordModule>(facet: MinecraftFacet, typ val identifier = element?.toUElementOfType() ?: return false - val psiClass = (identifier.uastParent as? UClass)?.javaPsi + val psiClass = runCatchingKtIdeaExceptions { (identifier.uastParent as? UClass)?.javaPsi } ?: return false val pluginInterface = JavaPsiFacade.getInstance(element.project) diff --git a/src/main/kotlin/platform/fabric/FabricModule.kt b/src/main/kotlin/platform/fabric/FabricModule.kt index e42bb2140..6d02073a8 100644 --- a/src/main/kotlin/platform/fabric/FabricModule.kt +++ b/src/main/kotlin/platform/fabric/FabricModule.kt @@ -28,6 +28,7 @@ import com.demonwav.mcdev.platform.fabric.reference.EntryPointReference import com.demonwav.mcdev.platform.fabric.util.FabricConstants import com.demonwav.mcdev.util.SourceType import com.demonwav.mcdev.util.nullable +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.psi.PsiClass import com.intellij.psi.PsiElement import com.intellij.psi.PsiMethod @@ -54,7 +55,7 @@ class FabricModule internal constructor(facet: MinecraftFacet) : AbstractModule( val identifier = element?.toUElementOfType() ?: return false - val parent = identifier.uastParent + val parent = runCatchingKtIdeaExceptions { identifier.uastParent } if (parent !is UClass && parent !is UMethod) { return false } diff --git a/src/main/kotlin/platform/forge/ForgeModule.kt b/src/main/kotlin/platform/forge/ForgeModule.kt index 2390b50e8..02ebdce1b 100644 --- a/src/main/kotlin/platform/forge/ForgeModule.kt +++ b/src/main/kotlin/platform/forge/ForgeModule.kt @@ -34,6 +34,7 @@ import com.demonwav.mcdev.util.SourceType import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.nullable +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.demonwav.mcdev.util.runWriteTaskLater import com.demonwav.mcdev.util.waitForAllSmart import com.intellij.json.JsonFileType @@ -187,7 +188,7 @@ class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(f val identifier = element?.toUElementOfType() ?: return false - val psiClass = identifier.uastParent as? UClass + val psiClass = runCatchingKtIdeaExceptions { identifier.uastParent as? UClass } ?: return false return !psiClass.hasModifier(JvmModifier.ABSTRACT) && diff --git a/src/main/kotlin/platform/forge/creator/asset-steps.kt b/src/main/kotlin/platform/forge/creator/asset-steps.kt index a79352729..345a15399 100644 --- a/src/main/kotlin/platform/forge/creator/asset-steps.kt +++ b/src/main/kotlin/platform/forge/creator/asset-steps.kt @@ -118,6 +118,18 @@ class ForgeProjectFilesStep(parent: NewProjectWizardStep) : AbstractLongRunningA "src/main/resources/META-INF/mods.toml" to MinecraftTemplates.MODS_TOML_TEMPLATE, ) + val configTemplate = when { + mcVersion >= MinecraftVersions.MC1_20 -> MinecraftTemplates.FG3_1_20_CONFIG_TEMPLATE + else -> null + } + + if (configTemplate != null) { + assets.addTemplates( + project, + "src/main/java/${mainPackageName.replace('.', '/')}/Config.java" to configTemplate, + ) + } + assets.addLicense(project) } } diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt index d97fa678b..e316523a5 100644 --- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt +++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/NewInsnInjectionPoint.kt @@ -219,7 +219,7 @@ private class NewInsnSelector( } private fun classToMemberReference(value: String): MemberReference? { - val fqn = value.replace('/', '.').replace('$', '.') + val fqn = value.replace('/', '.') if (fqn.isNotEmpty() && !fqn.startsWith('.') && !fqn.endsWith('.') && !fqn.contains("..")) { if (StringUtil.isJavaIdentifier(fqn.replace('.', '_'))) { return MemberReference("", owner = fqn) diff --git a/src/main/kotlin/platform/mixin/inspection/addedMembers/AbstractAddedMembersInspection.kt b/src/main/kotlin/platform/mixin/inspection/addedMembers/AbstractAddedMembersInspection.kt new file mode 100644 index 000000000..fa2f184e2 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/addedMembers/AbstractAddedMembersInspection.kt @@ -0,0 +1,88 @@ +/* + * 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.mixin.inspection.addedMembers + +import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler +import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.JavaElementVisitor +import com.intellij.psi.PsiElementVisitor +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod +import com.intellij.psi.search.searches.SuperMethodsSearch + +abstract class AbstractAddedMembersInspection : MixinInspection() { + abstract fun visitAddedField(holder: ProblemsHolder, field: PsiField) + abstract fun visitAddedMethod(holder: ProblemsHolder, method: PsiMethod, isInherited: Boolean) + + final override fun buildVisitor(holder: ProblemsHolder): PsiElementVisitor = Visitor(holder) + + private inner class Visitor(private val holder: ProblemsHolder) : JavaElementVisitor() { + override fun visitField(field: PsiField) { + if (field.containingClass?.isMixin != true) { + return + } + + if (field.hasAnnotation(MixinConstants.Annotations.SHADOW)) { + return + } + + visitAddedField(holder, field) + } + + override fun visitMethod(method: PsiMethod) { + if (method.containingClass?.isMixin != true) { + return + } + + if (method.isConstructor) { + return + } + + val hasMixinAnnotation = method.annotations.any { + val fqn = it.qualifiedName ?: return@any false + fqn in ignoredMethodAnnotations || MixinAnnotationHandler.forMixinAnnotation( + fqn, + holder.project + ) != null + } + if (hasMixinAnnotation) { + return + } + + val superMethod = SuperMethodsSearch.search(method, null, true, false).findFirst() + visitAddedMethod(holder, method, superMethod != null) + } + } + + companion object { + private val ignoredMethodAnnotations = setOf( + MixinConstants.Annotations.SHADOW, + MixinConstants.Annotations.ACCESSOR, + MixinConstants.Annotations.INVOKER, + MixinConstants.Annotations.OVERWRITE, + MixinConstants.Annotations.INTRINSIC, + MixinConstants.Annotations.SOFT_OVERRIDE, + ) + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt b/src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt new file mode 100644 index 000000000..0f9a8e899 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/addedMembers/AddedMembersNameFormatInspection.kt @@ -0,0 +1,321 @@ +/* + * 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.mixin.inspection.addedMembers + +import com.demonwav.mcdev.facet.MinecraftFacet +import com.demonwav.mcdev.platform.fabric.FabricModuleType +import com.demonwav.mcdev.util.decapitalize +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findModule +import com.demonwav.mcdev.util.onShown +import com.demonwav.mcdev.util.toJavaIdentifier +import com.intellij.codeInsight.CodeInsightBundle +import com.intellij.codeInsight.FileModificationService +import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.icons.AllIcons +import com.intellij.ide.util.SuperMethodWarningUtil +import com.intellij.openapi.editor.Editor +import com.intellij.openapi.module.Module +import com.intellij.openapi.project.Project +import com.intellij.openapi.ui.ComboBox +import com.intellij.openapi.ui.ComponentValidator +import com.intellij.openapi.ui.DialogWrapper +import com.intellij.openapi.util.text.StringUtil +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiField +import com.intellij.psi.PsiFile +import com.intellij.psi.PsiMethod +import com.intellij.psi.PsiNameIdentifierOwner +import com.intellij.psi.PsiNamedElement +import com.intellij.refactoring.rename.RenameProcessor +import com.intellij.ui.DocumentAdapter +import com.intellij.ui.EnumComboBoxModel +import com.intellij.ui.components.JBLabel +import com.intellij.ui.components.JBTextField +import com.intellij.ui.dsl.builder.COLUMNS_SHORT +import com.intellij.ui.dsl.builder.Cell +import com.intellij.ui.dsl.builder.RowLayout +import com.intellij.ui.dsl.builder.columns +import com.intellij.ui.dsl.builder.panel +import com.intellij.ui.layout.ValidationInfoBuilder +import com.intellij.util.xmlb.Converter +import com.intellij.util.xmlb.annotations.Attribute +import java.util.function.Supplier +import java.util.regex.Pattern +import java.util.regex.PatternSyntaxException +import javax.swing.JComponent +import javax.swing.event.DocumentEvent +import kotlin.reflect.KMutableProperty0 +import org.intellij.lang.annotations.Language + +class AddedMembersNameFormatInspection : AbstractAddedMembersInspection() { + @Attribute(converter = RegexConverter::class) + @JvmField + var validNameFormat = ".+[_$].+".toRegex() + + @Attribute(converter = RegexConverter::class) + @JvmField + var validNameFixSearch = "^.+$".toRegex() + + @JvmField + var validNameFixReplace = "MOD_ID\\\$\$0" + + @JvmField + var reportFields = ReportMode.NOT_ON_FABRIC + @JvmField + var reportMethods = ReportMode.NOT_ON_FABRIC + @JvmField + var reportInheritedMethods = ReportMode.ALWAYS + + override fun getStaticDescription() = "Reports added members not matching the correct name format" + + override fun visitAddedField(holder: ProblemsHolder, field: PsiField) { + if (reportFields.shouldReport(field.findModule())) { + visitAdded(holder, field) + } + } + + override fun visitAddedMethod(holder: ProblemsHolder, method: PsiMethod, isInherited: Boolean) { + if (shouldReportMethod(method, isInherited)) { + visitAdded(holder, method) + } + } + + private fun shouldReportMethod(method: PsiMethod, isInherited: Boolean): Boolean { + val module = method.findModule() + + if (isInherited) { + if (!reportInheritedMethods.shouldReport(module)) { + return false + } + + val superMethods = method.findDeepestSuperMethods() + val isWritableInterfaceMethod = superMethods.any { + val clazz = it.findContainingClass() ?: return@any false + clazz.isInterface && clazz.containingFile?.isWritable == true + } + return isWritableInterfaceMethod + } else { + return reportMethods.shouldReport(module) + } + } + + private fun visitAdded(holder: ProblemsHolder, added: PsiNameIdentifierOwner) { + val name = added.name ?: return + if (validNameFormat.matches(name)) { + return + } + + // try to get a quick fix + val fixed = try { + validNameFixSearch.replace(name, validNameFixReplace) + .replace("MOD_ID", getAppropriatePrefix(holder.project)) + } catch (e: RuntimeException) { + null + } + + if (fixed != null && StringUtil.isJavaIdentifier(fixed) && validNameFormat.matches(fixed)) { + holder.registerProblem( + added.nameIdentifier ?: return, + "Name does not match the pattern for added mixin members: \"${validNameFormat.pattern}\"", + RenameWithInheritanceFix(added, fixed) + ) + } else { + holder.registerProblem( + added.nameIdentifier ?: return, + "Name does not match the pattern for added mixin members: \"${validNameFormat.pattern}\"", + ) + } + } + + private fun getAppropriatePrefix(project: Project): String { + return StringUtil.capitalizeWords(project.name, true) + .decapitalize() + .replace(" ", "") + .toJavaIdentifier(allowDollars = false) + } + + override fun createOptionsPanel(): JComponent { + return panel { + row("Valid name format:") { + textField() + .doBindText({ validNameFormat.pattern }, { validNameFormat = it.toRegexOrDefault(".+[_$].+") }) + .columns(COLUMNS_SHORT) + .regexValidator() + } + row("Valid name fix search:") { + textField() + .doBindText({ validNameFixSearch.pattern }, { validNameFixSearch = it.toRegexOrDefault(".+") }) + .columns(COLUMNS_SHORT) + .regexValidator() + } + row { + layout(RowLayout.LABEL_ALIGNED) + val toolTip = + "Uses regex replacement syntax after matching from the regex in the option above.
" + + "\"MOD_ID\" is replaced with the project name, converted to a valid Java identifier." + label("Valid name fix replace:") + .applyToComponent { horizontalTextPosition = JBLabel.LEFT } + .applyToComponent { icon = AllIcons.General.ContextHelp } + .applyToComponent { toolTipText = toolTip } + textField().doBindText(::validNameFixReplace).columns(COLUMNS_SHORT) + } + + separator() + + row("Report fields:") { + comboBox(EnumComboBoxModel(ReportMode::class.java)).doBindItem(::reportFields) + } + row("Report methods:") { + comboBox(EnumComboBoxModel(ReportMode::class.java)).doBindItem(::reportMethods) + } + row { + layout(RowLayout.LABEL_ALIGNED) + label("Report inherited methods:") + .applyToComponent { horizontalTextPosition = JBLabel.LEFT } + .applyToComponent { icon = AllIcons.General.ContextHelp } + .applyToComponent { toolTipText = "Reports methods that are inherited from duck interfaces" } + comboBox(EnumComboBoxModel(ReportMode::class.java)).doBindItem(::reportInheritedMethods) + } + } + } + + enum class ReportMode(private val displayName: String) { + ALWAYS("Always"), ON_FABRIC("On Fabric"), NOT_ON_FABRIC("Not on Fabric"), NEVER("Never"); + + override fun toString() = displayName + + fun shouldReport(module: Module?): Boolean { + if (this == NEVER) { + return false + } + if (this == ALWAYS) { + return true + } + val isFabric = module?.let { MinecraftFacet.getInstance(module, FabricModuleType) != null } ?: false + return (this == ON_FABRIC) == isFabric + } + } +} + +private fun String.toRegexOrDefault(@Language("RegExp") default: String): Regex { + return try { + this.toRegex() + } catch (e: PatternSyntaxException) { + default.toRegex() + } +} + +private fun Cell.doBindText(property: KMutableProperty0): Cell { + return doBindText(property.getter, property.setter) +} + +private fun Cell.doBindText(getter: () -> String, setter: (String) -> Unit): Cell { + component.text = getter() + component.document.addDocumentListener(object : DocumentAdapter() { + override fun textChanged(e: DocumentEvent) { + setter(component.text) + } + }) + return this +} + +private fun Cell>.doBindItem(property: KMutableProperty0): Cell> { + component.selectedItem = property.get() + component.addActionListener { + @Suppress("UNCHECKED_CAST") + val selectedItem = component.selectedItem as T? + if (selectedItem != null) { + property.set(selectedItem) + } + } + return this +} + +private fun Cell.regexValidator(): Cell { + var hasRegisteredValidator = false + component.onShown { + if (!hasRegisteredValidator) { + hasRegisteredValidator = true + val disposable = DialogWrapper.findInstance(component)?.disposable ?: return@onShown + ComponentValidator(disposable).withValidator( + Supplier { + try { + Pattern.compile(component.text) + null + } catch (e: PatternSyntaxException) { + ValidationInfoBuilder(component).error("Invalid regex") + } + } + ).andRegisterOnDocumentListener(component).installOn(component) + } + } + return this +} + +private class RegexConverter : Converter() { + override fun toString(value: Regex) = value.pattern + + override fun fromString(value: String) = runCatching { value.toRegex() }.getOrNull() +} + +private class RenameWithInheritanceFix( + element: PsiNamedElement, + private val newName: String +) : LocalQuickFixAndIntentionActionOnPsiElement(element) { + private val isMethod = element is PsiMethod + private val text = CodeInsightBundle.message("rename.named.element.text", element.name, newName) + + override fun getFamilyName() = CodeInsightBundle.message("rename.element.family") + + override fun getText() = text + + override fun invoke( + project: Project, + file: PsiFile, + editor: Editor?, + startElement: PsiElement, + endElement: PsiElement + ) { + if (isMethod) { + val method = startElement as? PsiMethod ?: return + if (editor != null) { + SuperMethodWarningUtil.checkSuperMethod(method, { md -> + RenameProcessor(project, md, newName, false, false).run() + true + }, editor) + } else { + val superMethod = method.findDeepestSuperMethods().firstOrNull() + for (md in listOfNotNull(superMethod, method)) { + RenameProcessor(project, md, newName, false, false).run() + } + } + } else { + if (!FileModificationService.getInstance().prepareFileForWrite(file)) { + return + } + RenameProcessor(project, startElement, newName, false, false).run() + } + } + + override fun startInWriteAction() = isMethod +} diff --git a/src/main/kotlin/platform/mixin/inspection/addedMembers/MissingUniqueAnnotationInspection.kt b/src/main/kotlin/platform/mixin/inspection/addedMembers/MissingUniqueAnnotationInspection.kt new file mode 100644 index 000000000..9c6e1ed08 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/addedMembers/MissingUniqueAnnotationInspection.kt @@ -0,0 +1,51 @@ +/* + * 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.mixin.inspection.addedMembers + +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.intellij.codeInsight.intention.AddAnnotationFix +import com.intellij.codeInspection.ProblemsHolder +import com.intellij.psi.PsiField +import com.intellij.psi.PsiMethod + +class MissingUniqueAnnotationInspection : AbstractAddedMembersInspection() { + override fun getStaticDescription() = "Reports missing @Unique annotations" + + override fun visitAddedField(holder: ProblemsHolder, field: PsiField) { + if (!field.hasAnnotation(MixinConstants.Annotations.UNIQUE)) { + holder.registerProblem( + field.nameIdentifier, + "Missing @Unique annotation", + AddAnnotationFix(MixinConstants.Annotations.UNIQUE, field) + ) + } + } + + override fun visitAddedMethod(holder: ProblemsHolder, method: PsiMethod, isInherited: Boolean) { + if (!isInherited && !method.hasAnnotation(MixinConstants.Annotations.UNIQUE)) { + holder.registerProblem( + method.nameIdentifier ?: return, + "Missing @Unique annotation", + AddAnnotationFix(MixinConstants.Annotations.UNIQUE, method) + ) + } + } +} diff --git a/src/main/kotlin/platform/mixin/inspection/suppress/StaticInvokerUnusedParamInspectionSuppressor.kt b/src/main/kotlin/platform/mixin/inspection/suppress/StaticInvokerUnusedParamInspectionSuppressor.kt new file mode 100644 index 000000000..710ca1372 --- /dev/null +++ b/src/main/kotlin/platform/mixin/inspection/suppress/StaticInvokerUnusedParamInspectionSuppressor.kt @@ -0,0 +1,70 @@ +/* + * 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.mixin.inspection.suppress + +import com.demonwav.mcdev.platform.mixin.util.MixinConstants +import com.demonwav.mcdev.platform.mixin.util.isMixin +import com.demonwav.mcdev.util.equivalentTo +import com.demonwav.mcdev.util.findContainingClass +import com.demonwav.mcdev.util.findContainingMethod +import com.intellij.codeInspection.InspectionSuppressor +import com.intellij.codeInspection.SuppressQuickFix +import com.intellij.psi.PsiElement +import com.intellij.psi.PsiModifier +import com.intellij.psi.PsiParameter + +class StaticInvokerUnusedParamInspectionSuppressor : InspectionSuppressor { + companion object { + private const val INSPECTION = "unused" + } + + override fun isSuppressedFor(element: PsiElement, toolId: String): Boolean { + if (toolId != INSPECTION) { + return false + } + + val parent = element.parent as? PsiParameter ?: return false + if (!(element equivalentTo parent.nameIdentifier)) { + return false + } + + val method = parent.findContainingMethod() ?: return false + if (!method.hasModifierProperty(PsiModifier.STATIC)) { + return false + } + + if (!method.hasAnnotation(MixinConstants.Annotations.ACCESSOR) && + !method.hasAnnotation(MixinConstants.Annotations.INVOKER) + ) { + return false + } + + val clazz = method.findContainingClass() ?: return false + if (!clazz.isMixin) { + return false + } + + return true + } + + override fun getSuppressActions(element: PsiElement?, toolId: String): Array = + SuppressQuickFix.EMPTY_ARRAY +} diff --git a/src/main/kotlin/platform/sponge/SpongeModule.kt b/src/main/kotlin/platform/sponge/SpongeModule.kt index da4d3718a..8bd09dc7e 100644 --- a/src/main/kotlin/platform/sponge/SpongeModule.kt +++ b/src/main/kotlin/platform/sponge/SpongeModule.kt @@ -31,6 +31,7 @@ import com.demonwav.mcdev.platform.sponge.util.SpongeConstants import com.demonwav.mcdev.util.createVoidMethodWithParameterType import com.demonwav.mcdev.util.extendsOrImplements import com.demonwav.mcdev.util.findContainingMethod +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiAnnotationMemberValue @@ -93,9 +94,9 @@ class SpongeModule(facet: MinecraftFacet) : AbstractModule(facet) { val identifier = element?.toUElementOfType() ?: return false - val psiClass = identifier.uastParent as? UClass ?: return false + val psiClass = runCatchingKtIdeaExceptions { identifier.uastParent as? UClass ?: return false } - if (psiClass.javaPsi.hasModifier(JvmModifier.ABSTRACT)) { + if (psiClass == null || psiClass.javaPsi.hasModifier(JvmModifier.ABSTRACT)) { return false } diff --git a/src/main/kotlin/platform/velocity/VelocityModule.kt b/src/main/kotlin/platform/velocity/VelocityModule.kt index 69599c137..33c51bf40 100644 --- a/src/main/kotlin/platform/velocity/VelocityModule.kt +++ b/src/main/kotlin/platform/velocity/VelocityModule.kt @@ -29,6 +29,7 @@ import com.demonwav.mcdev.platform.velocity.generation.VelocityGenerationData import com.demonwav.mcdev.platform.velocity.util.VelocityConstants import com.demonwav.mcdev.platform.velocity.util.VelocityConstants.SUBSCRIBE_ANNOTATION import com.demonwav.mcdev.util.createVoidMethodWithParameterType +import com.demonwav.mcdev.util.runCatchingKtIdeaExceptions import com.intellij.lang.jvm.JvmModifier import com.intellij.psi.JavaPsiFacade import com.intellij.psi.PsiClass @@ -75,7 +76,7 @@ class VelocityModule(facet: MinecraftFacet) : AbstractModule(facet) { val identifier = element?.toUElementOfType() ?: return false - val psiClass = identifier.uastParent as? UClass + val psiClass = runCatchingKtIdeaExceptions { identifier.uastParent as? UClass } ?: return false return !psiClass.hasModifier(JvmModifier.ABSTRACT) && diff --git a/src/main/kotlin/util/MemberReference.kt b/src/main/kotlin/util/MemberReference.kt index 4700a6b9f..bbc5ab857 100644 --- a/src/main/kotlin/util/MemberReference.kt +++ b/src/main/kotlin/util/MemberReference.kt @@ -64,7 +64,7 @@ data class MemberReference( private fun matchOwner(clazz: String): Boolean { assert(!clazz.contains('.')) - return this.owner == null || this.owner == clazz.replace('/', '.').replace('$', '.') + return this.owner == null || this.owner == clazz.replace('/', '.') } override fun matchField(owner: String, name: String, desc: String): Boolean { diff --git a/src/main/kotlin/util/MinecraftTemplates.kt b/src/main/kotlin/util/MinecraftTemplates.kt index 634878f46..3fcc02b79 100644 --- a/src/main/kotlin/util/MinecraftTemplates.kt +++ b/src/main/kotlin/util/MinecraftTemplates.kt @@ -87,6 +87,7 @@ class MinecraftTemplates : FileTemplateGroupDescriptorFactory { forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_19_MAIN_CLASS_TEMPLATE)) forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_19_3_MAIN_CLASS_TEMPLATE)) forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_20_MAIN_CLASS_TEMPLATE)) + forgeGroup.addTemplate(FileTemplateDescriptor(FG3_1_20_CONFIG_TEMPLATE)) forgeGroup.addTemplate(FileTemplateDescriptor(FG3_BUILD_GRADLE_TEMPLATE)) forgeGroup.addTemplate(FileTemplateDescriptor(FG3_GRADLE_PROPERTIES_TEMPLATE)) forgeGroup.addTemplate(FileTemplateDescriptor(FG3_SETTINGS_GRADLE_TEMPLATE)) @@ -204,6 +205,7 @@ class MinecraftTemplates : FileTemplateGroupDescriptorFactory { const val FG3_1_19_MAIN_CLASS_TEMPLATE = "Forge (1.19+) Main Class.java" const val FG3_1_19_3_MAIN_CLASS_TEMPLATE = "Forge (1.19.3+) Main Class.java" const val FG3_1_20_MAIN_CLASS_TEMPLATE = "Forge (1.20+) Main Class.java" + const val FG3_1_20_CONFIG_TEMPLATE = "Forge (1.20+) Config.java" const val FG3_BUILD_GRADLE_TEMPLATE = "Forge (1.13+) build.gradle" const val FG3_GRADLE_PROPERTIES_TEMPLATE = "Forge (1.13+) gradle.properties" const val FG3_SETTINGS_GRADLE_TEMPLATE = "Forge (1.13+) settings.gradle" diff --git a/src/main/kotlin/util/psi-utils.kt b/src/main/kotlin/util/psi-utils.kt index 2dc31cc9c..1396bcaad 100644 --- a/src/main/kotlin/util/psi-utils.kt +++ b/src/main/kotlin/util/psi-utils.kt @@ -195,7 +195,7 @@ fun isAccessModifier(@ModifierConstant modifier: String): Boolean { return modifier in ACCESS_MODIFIERS } -infix fun PsiElement.equivalentTo(other: PsiElement): Boolean { +infix fun PsiElement.equivalentTo(other: PsiElement?): Boolean { return manager.areElementsEquivalent(this, other) } diff --git a/src/main/kotlin/util/utils.kt b/src/main/kotlin/util/utils.kt index 818a50036..f08bea1e1 100644 --- a/src/main/kotlin/util/utils.kt +++ b/src/main/kotlin/util/utils.kt @@ -369,8 +369,12 @@ inline fun runCatchingKtIdeaExceptions(action: () -> T): T? = try { action() } catch (e: Exception) { if (e.javaClass.name == "org.jetbrains.kotlin.idea.caches.resolve.KotlinIdeaResolutionException") { + loggerForTopLevel().info("Caught Kotlin plugin exception", e) null } else { throw e } } + +fun withSuppressed(original: T?, other: T): T = + original?.apply { addSuppressed(other) } ?: other diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml index a3ce9becc..3223819a7 100644 --- a/src/main/resources/META-INF/plugin.xml +++ b/src/main/resources/META-INF/plugin.xml @@ -18,7 +18,7 @@ along with this program. If not, see . --> - + com.intellij.modules.java org.jetbrains.idea.maven @@ -408,6 +408,7 @@ + @@ -750,6 +751,24 @@ hasStaticDescription="true" implementationClass="com.demonwav.mcdev.platform.mixin.inspection.MixinAnnotationTargetInspection"/> + + + + MAGIC_NUMBER_INTRODUCTION = BUILDER + .comment("What you want the introduction message to be for the magic number") + .define("magicNumberIntroduction", "The magic number is... "); + + // a list of strings that are treated as resource locations for items + private static final ForgeConfigSpec.ConfigValue> ITEM_STRINGS = BUILDER + .comment("A list of items to log on common setup.") + .defineListAllowEmpty(Collections.singletonList("items"), () -> List.of("minecraft:iron_ingot"), Config::validateItemName); + + static final ForgeConfigSpec SPEC = BUILDER.build(); + + public static boolean logDirtBlock; + public static int magicNumber; + public static String magicNumberIntroduction; + public static Set items; + + private static boolean validateItemName(final Object obj) + { + return obj instanceof final String itemName && ForgeRegistries.ITEMS.containsKey(new ResourceLocation(itemName)); + } + + @SubscribeEvent + static void onLoad(final ModConfigEvent event) + { + logDirtBlock = LOG_DIRT_BLOCK.get(); + magicNumber = MAGIC_NUMBER.get(); + magicNumberIntroduction = MAGIC_NUMBER_INTRODUCTION.get(); + + // convert the list of strings into a set of items + items = ITEM_STRINGS.get().stream() + .map(itemName -> ForgeRegistries.ITEMS.getValue(new ResourceLocation(itemName))) + .collect(Collectors.toSet()); + } +} diff --git a/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20+) Config.java.html b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20+) Config.java.html new file mode 100644 index 000000000..69445a0df --- /dev/null +++ b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20+) Config.java.html @@ -0,0 +1,25 @@ + + + + +

This is a built-in file template used to create a new config class for Forge projects 1.20 and above

+ + diff --git a/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20+) Main Class.java.ft b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20+) Main Class.java.ft index 360d7d11a..af36d796e 100644 --- a/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20+) Main Class.java.ft +++ b/src/main/resources/fileTemplates/j2ee/forge/Forge (1.20+) Main Class.java.ft @@ -18,7 +18,9 @@ import net.minecraftforge.event.BuildCreativeModeTabContentsEvent; import net.minecraftforge.event.server.ServerStartingEvent; import net.minecraftforge.eventbus.api.IEventBus; import net.minecraftforge.eventbus.api.SubscribeEvent; +import net.minecraftforge.fml.ModLoadingContext; import net.minecraftforge.fml.common.Mod; +import net.minecraftforge.fml.config.ModConfig; import net.minecraftforge.fml.event.lifecycle.FMLClientSetupEvent; import net.minecraftforge.fml.event.lifecycle.FMLCommonSetupEvent; import net.minecraftforge.fml.javafmlmod.FMLJavaModLoadingContext; @@ -77,12 +79,22 @@ public class ${CLASS_NAME} { // Register the item to a creative tab modEventBus.addListener(this::addCreative); + + // Register our mod's ForgeConfigSpec so that Forge can create and load the config file for us + ModLoadingContext.get().registerConfig(ModConfig.Type.COMMON, Config.SPEC); } private void commonSetup(final FMLCommonSetupEvent event) { // Some common setup code LOGGER.info("HELLO FROM COMMON SETUP"); LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT)); + + if (Config.logDirtBlock) + LOGGER.info("DIRT BLOCK >> {}", ForgeRegistries.BLOCKS.getKey(Blocks.DIRT)); + + LOGGER.info(Config.magicNumberIntroduction + Config.magicNumber); + + Config.items.forEach((item) -> LOGGER.info("ITEM >> {}", item.toString())); } // Add the example block item to the building blocks tab