Skip to content

[WIP] Rework of SideOnly inspections #769

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Draft
wants to merge 5 commits into
base: dev
Choose a base branch
from
Draft
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion build.gradle.kts
Original file line number Diff line number Diff line change
@@ -109,7 +109,7 @@ intellij {
version = ideaVersion
// Bundled plugin dependencies
setPlugins(
"java", "maven", "gradle", "Groovy",
"java", "maven", "gradle", "Groovy", "Kotlin",
// needed dependencies for unit tests
"properties", "junit",
// useful to have when running for mods.toml
7 changes: 7 additions & 0 deletions src/main/kotlin/MinecraftSettings.kt
Original file line number Diff line number Diff line change
@@ -24,6 +24,7 @@ class MinecraftSettings : PersistentStateComponent<MinecraftSettings.State> {
var isShowEventListenerGutterIcons: Boolean = true,
var isShowChatColorGutterIcons: Boolean = true,
var isShowChatColorUnderlines: Boolean = false,
var isShowSideOnlyGutterIcons: Boolean = true,
var underlineType: MinecraftSettings.UnderlineType = MinecraftSettings.UnderlineType.DOTTED
)

@@ -62,6 +63,12 @@ class MinecraftSettings : PersistentStateComponent<MinecraftSettings.State> {
state.isShowChatColorUnderlines = showChatColorUnderlines
}

var isShowSideOnlyGutterIcons: Boolean
get() = state.isShowSideOnlyGutterIcons
set(showSideOnlyGutterIcons) {
state.isShowSideOnlyGutterIcons = showSideOnlyGutterIcons
}

var underlineType: UnderlineType
get() = state.underlineType
set(underlineType) {
Original file line number Diff line number Diff line change
@@ -17,7 +17,7 @@ import com.demonwav.mcdev.creator.buildsystem.gradle.GradleBuildSystem
import com.demonwav.mcdev.creator.buildsystem.gradle.GradleCreator
import com.demonwav.mcdev.platform.PlatformType
import com.demonwav.mcdev.platform.fabric.EntryPoint
import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side
import com.demonwav.mcdev.sideonly.Side
import com.demonwav.mcdev.util.License
import com.demonwav.mcdev.util.SemanticVersion
import com.intellij.openapi.module.Module
@@ -35,7 +35,7 @@ class FabricProjectConfig : ProjectConfig(), GradleCreator {
var apiMavenLocation: String? = null
var loomVersion = SemanticVersion.release()
var gradleVersion = SemanticVersion.release()
var environment = Side.NONE
var environment = Side.BOTH
var entryPoints: List<EntryPoint> = arrayListOf()
var modRepo: String? = null
var mixins = false
Original file line number Diff line number Diff line change
@@ -19,7 +19,7 @@ import com.demonwav.mcdev.creator.ValidatedFieldType.NON_BLANK
import com.demonwav.mcdev.platform.PlatformType
import com.demonwav.mcdev.platform.fabric.EntryPoint
import com.demonwav.mcdev.platform.fabric.util.FabricConstants
import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side
import com.demonwav.mcdev.sideonly.Side
import com.demonwav.mcdev.util.License
import com.demonwav.mcdev.util.SemanticVersion
import com.demonwav.mcdev.util.modUpdateStep
@@ -255,7 +255,7 @@ class FabricProjectSettingsWizard(private val creator: MinecraftProjectCreator)
conf.environment = when ((environmentBox.selectedItem as? String)?.toLowerCase(Locale.ROOT)) {
"client" -> Side.CLIENT
"server" -> Side.SERVER
else -> Side.NONE
else -> Side.BOTH
}
conf.license = licenseBox.selectedItem as? License
conf.entryPoints = entryPoints.filter { it.valid }
2 changes: 1 addition & 1 deletion src/main/kotlin/platform/fabric/creator/FabricTemplate.kt
Original file line number Diff line number Diff line change
@@ -12,7 +12,7 @@ package com.demonwav.mcdev.platform.fabric.creator

import com.demonwav.mcdev.creator.buildsystem.BuildSystem
import com.demonwav.mcdev.platform.BaseTemplate
import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side
import com.demonwav.mcdev.sideonly.Side
import com.demonwav.mcdev.util.License
import com.demonwav.mcdev.util.MinecraftTemplates.Companion.FABRIC_BUILD_GRADLE_TEMPLATE
import com.demonwav.mcdev.util.MinecraftTemplates.Companion.FABRIC_GRADLE_PROPERTIES_TEMPLATE
24 changes: 0 additions & 24 deletions src/main/kotlin/platform/forge/ForgeModule.kt
Original file line number Diff line number Diff line change
@@ -16,7 +16,6 @@ import com.demonwav.mcdev.insight.generation.GenerationData
import com.demonwav.mcdev.inspection.IsCancelled
import com.demonwav.mcdev.platform.AbstractModule
import com.demonwav.mcdev.platform.PlatformType
import com.demonwav.mcdev.platform.forge.inspections.sideonly.SidedProxyAnnotator
import com.demonwav.mcdev.platform.forge.util.ForgeConstants
import com.demonwav.mcdev.util.SourceType
import com.demonwav.mcdev.util.extendsOrImplements
@@ -26,7 +25,6 @@ import com.demonwav.mcdev.util.waitForAllSmart
import com.intellij.json.JsonFileType
import com.intellij.openapi.application.ApplicationManager
import com.intellij.openapi.fileTypes.FileTypeManager
import com.intellij.openapi.project.DumbService
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassType
@@ -36,7 +34,6 @@ import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiType
import com.intellij.psi.search.GlobalSearchScope
import com.intellij.psi.search.searches.AnnotatedElementsSearch

class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(facet) {

@@ -54,27 +51,6 @@ class ForgeModule internal constructor(facet: MinecraftFacet) : AbstractModule(f
runWriteTaskLater {
FileTypeManager.getInstance().associatePattern(JsonFileType.INSTANCE, ForgeConstants.MCMOD_INFO)
}

// Index @SideOnly
val service = DumbService.getInstance(project)
service.runReadActionInSmartMode runSmart@{
if (service.isDumb || project.isDisposed) {
return@runSmart
}

val scope = GlobalSearchScope.projectScope(project)
val sidedProxy = JavaPsiFacade.getInstance(project)
.findClass(ForgeConstants.SIDED_PROXY_ANNOTATION, scope) ?: return@runSmart
val annotatedFields = AnnotatedElementsSearch.searchPsiFields(sidedProxy, scope).findAll()

for (field in annotatedFields) {
if (service.isDumb || project.isDisposed) {
return@runSmart
}

SidedProxyAnnotator.check(field)
}
}
}
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.platform.forge.inspections

import com.demonwav.mcdev.platform.forge.util.ForgeConstants
import com.intellij.psi.PsiErrorElement
import com.intellij.psi.PsiExpression
import com.intellij.psi.PsiLambdaExpression
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiMethodReferenceExpression
import com.siyeh.ig.BaseInspection
import com.siyeh.ig.BaseInspectionVisitor

class DistExecutorInspection : BaseInspection() {
override fun getDisplayName() = "DistExecutor problems"

override fun getStaticDescription() = "DistExecutor problems"

override fun buildErrorString(vararg infos: Any?): String {
return infos[0] as String
}

override fun buildVisitor(): BaseInspectionVisitor {
return Visitor()
}

private class Visitor : BaseInspectionVisitor() {
override fun visitMethodCallExpression(expression: PsiMethodCallExpression) {
val method = expression.resolveMethod() ?: return
if (method.containingClass?.qualifiedName != ForgeConstants.DIST_EXECUTOR) return
when (method.name) {
"safeCallWhenOn", "safeRunWhenOn" -> {
checkSafeArgument(method.name, expression.argumentList.expressions.getOrNull(1))
}
"safeRunForDist" -> {
for (arg in expression.argumentList.expressions) {
checkSafeArgument(method.name, arg)
}
}
}
}

private fun checkSafeArgument(methodName: String, expression: PsiExpression?) {
if (expression == null || expression is PsiErrorElement || expression.textLength == 0) return

if (expression !is PsiLambdaExpression) {
registerError(expression, "DistExecutor.$methodName must contain lambda argument")
return
}

val lambdaBody = expression.body
if (lambdaBody != null &&
lambdaBody !is PsiMethodReferenceExpression &&
lambdaBody !is PsiErrorElement &&
lambdaBody.textLength != 0
) {
registerError(lambdaBody, "DistExecutor.$methodName must contain a method reference inside a lambda")
}
}
}
}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

25 changes: 0 additions & 25 deletions src/main/kotlin/platform/forge/inspections/sideonly/Side.kt

This file was deleted.

173 changes: 0 additions & 173 deletions src/main/kotlin/platform/forge/inspections/sideonly/SideOnlyUtil.kt

This file was deleted.

This file was deleted.

This file was deleted.

3 changes: 3 additions & 0 deletions src/main/kotlin/platform/forge/util/ForgeConstants.kt
Original file line number Diff line number Diff line change
@@ -14,7 +14,10 @@ object ForgeConstants {

const val SIDE_ONLY_ANNOTATION = "net.minecraftforge.fml.relauncher.SideOnly"
const val SIDE_ANNOTATION = "net.minecraftforge.fml.relauncher.Side"
const val ONLY_IN_ANNOTATION = "net.minecraftforge.api.distmarker.OnlyIn"
const val DIST_ANNOTATION = "net.minecraftforge.api.distmarker.Dist"
const val SIDED_PROXY_ANNOTATION = "net.minecraftforge.fml.common.SidedProxy"
const val DIST_EXECUTOR = "net.minecraftforge.fml.DistExecutor"
const val MOD_ANNOTATION = "net.minecraftforge.fml.common.Mod"
const val CORE_MOD_INTERFACE = "net.minecraftforge.fml.relauncher.IFMLLoadingPlugin"
const val EVENT_HANDLER_ANNOTATION = "net.minecraftforge.fml.common.Mod.EventHandler"
Original file line number Diff line number Diff line change
@@ -10,11 +10,12 @@

package com.demonwav.mcdev.platform.mixin.inspection

import com.demonwav.mcdev.platform.forge.inspections.sideonly.Side
import com.demonwav.mcdev.platform.forge.inspections.sideonly.SideOnlyUtil
import com.demonwav.mcdev.platform.mixin.MixinModule
import com.demonwav.mcdev.platform.mixin.config.MixinConfig
import com.demonwav.mcdev.platform.mixin.util.isMixin
import com.demonwav.mcdev.sideonly.Side
import com.demonwav.mcdev.sideonly.SideHardness
import com.demonwav.mcdev.sideonly.SideOnlyUtil
import com.demonwav.mcdev.util.findModule
import com.demonwav.mcdev.util.fullQualifiedName
import com.intellij.codeInspection.LocalQuickFix
@@ -61,7 +62,11 @@ class UnusedMixinInspection : MixinInspection() {
val bestQuickFixFile = bestQuickFixConfig?.file
val qualifiedName = clazz.fullQualifiedName
if (bestQuickFixFile != null && qualifiedName != null) {
val quickFix = QuickFix(bestQuickFixFile, qualifiedName, SideOnlyUtil.getSideForClass(clazz))
val quickFix = QuickFix(
bestQuickFixFile,
qualifiedName,
SideOnlyUtil.getContextSide(clazz, SideHardness.EITHER)?.side ?: Side.BOTH
)
holder.registerProblem(problematicElement, "Mixin not found in any mixin config", quickFix)
} else {
holder.registerProblem(problematicElement, "Mixin not found in any mixin config")
89 changes: 89 additions & 0 deletions src/main/kotlin/sideonly/HardSideOnlyUsageInspection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

import com.demonwav.mcdev.util.findModule
import com.demonwav.mcdev.util.runInSmartModeFromReadAction
import com.intellij.codeInspection.ProblemDescriptor
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.progress.util.ProgressWindow
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.SmartPointerManager
import com.intellij.psi.SmartPsiElementPointer
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.siyeh.ig.BaseInspection
import com.siyeh.ig.BaseInspectionVisitor
import com.siyeh.ig.InspectionGadgetsFix

class HardSideOnlyUsageInspection : BaseInspection() {
override fun getDisplayName(): String {
return "Usage of hard-SideOnly annotations"
}

override fun getStaticDescription(): String {
return "Usage of hard-SideOnly annotations"
}

override fun buildErrorString(vararg infos: Any?): String {
return "Usage of @${infos[0] as String}"
}

override fun buildVisitor(): BaseInspectionVisitor {
return Visitor()
}

override fun buildFix(vararg infos: Any?): InspectionGadgetsFix {
return Fix(SmartPointerManager.createPointer(infos[1] as PsiAnnotation))
}

private class Visitor : BaseInspectionVisitor() {
override fun visitAnnotation(annotation: PsiAnnotation) {
if (SideOnlyUtil.getAnnotationSide(annotation, SideHardness.HARD) != Side.BOTH) {
registerError(
annotation.navigationElement,
annotation.nameReferenceElement?.text ?: annotation.qualifiedName,
annotation
)
}
}
}

private class Fix(private val annotation: SmartPsiElementPointer<PsiAnnotation>) : InspectionGadgetsFix() {
override fun doFix(project: Project, descriptor: ProblemDescriptor) {
val annotation = this.annotation.element ?: return
val module = annotation.findModule() ?: return
if (!SideOnlyUtil.ensureMcdevDependencyPresent(project, module, name, annotation.resolveScope)) {
return
}
project.runInSmartModeFromReadAction(ProgressWindow(true, project)) {
val oldSide = SideOnlyUtil.getAnnotationSide(annotation, SideHardness.HARD)
val newAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText(
"@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.$oldSide)",
annotation
)
WriteCommandAction.runWriteCommandAction(project) {
val createdAnnotation = annotation.replace(newAnnotation)
val codeStyleManager = JavaCodeStyleManager.getInstance(project)
codeStyleManager.shortenClassReferences(createdAnnotation)
createdAnnotation.containingFile?.let { codeStyleManager.optimizeImports(it) }
}
}
}

override fun getName() = "Replace with @CheckEnv"

override fun getFamilyName() = name

override fun startInWriteAction() = false
}
}
90 changes: 90 additions & 0 deletions src/main/kotlin/sideonly/MakeInferredMcdevAnnotationExplicit.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

import com.demonwav.mcdev.util.findModule
import com.intellij.codeInsight.FileModificationService
import com.intellij.codeInsight.intention.impl.BaseIntentionAction
import com.intellij.lang.java.JavaLanguage
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiCompiledElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.psi.util.PsiUtilCore

class MakeInferredMcdevAnnotationExplicit : BaseIntentionAction() {
override fun getFamilyName() = "Make Inferred MinecraftDev Annotations Explicit"

override fun getText() = "Make Inferred MinecraftDev Annotations Explicit"

override fun isAvailable(project: Project, editor: Editor, file: PsiFile): Boolean {
val leaf = file.findElementAt(editor.caretModel.offset) ?: return false
val owner = leaf.parent as? PsiModifierListOwner
return isAvailable(file, owner)
}

fun isAvailable(file: PsiFile, owner: PsiModifierListOwner?): Boolean {
if (owner != null &&
owner.language.isKindOf(JavaLanguage.INSTANCE) &&
isWritable(owner) &&
file.findModule() != null
) {
val annotation = SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.HARD)
?: SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.SOFT)
if (annotation != null) {
text = "Insert '@CheckEnv(Env.${annotation.side})'"
return true
}
}
return false
}

private fun isWritable(owner: PsiModifierListOwner): Boolean {
if (owner is PsiCompiledElement) return false
val vFile = PsiUtilCore.getVirtualFile(owner)
return vFile != null && vFile.isInLocalFileSystem
}

override fun invoke(project: Project, editor: Editor, file: PsiFile) {
val leaf = file.findElementAt(editor.caretModel.offset) ?: return
val owner = leaf.parent as? PsiModifierListOwner ?: return
makeAnnotationExplicit(project, file, owner)
}

fun makeAnnotationExplicit(project: Project, file: PsiFile, owner: PsiModifierListOwner) {
val modifierList = owner.modifierList ?: return
val module = file.findModule() ?: return
if (!SideOnlyUtil.ensureMcdevDependencyPresent(project, module, familyName, file.resolveScope)) {
return
}
if (!FileModificationService.getInstance().preparePsiElementForWrite(owner)) return
val facade = JavaPsiFacade.getInstance(project)
val inferredSide = SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.HARD)
?: SideOnlyUtil.getInferredAnnotationOnly(owner, SideHardness.SOFT) ?: return
val inferred = facade.elementFactory.createAnnotationFromText(
"@${SideOnlyUtil.MCDEV_SIDEONLY_ANNOTATION}(${SideOnlyUtil.MCDEV_SIDE}.${inferredSide.side})",
owner
)
WriteCommandAction.runWriteCommandAction(project) {
DumbService.getInstance(project).withAlternativeResolveEnabled {
JavaCodeStyleManager.getInstance(project)
.shortenClassReferences(modifierList.addAfter(inferred, null))
}
}
}

override fun startInWriteAction() = false
}
15 changes: 15 additions & 0 deletions src/main/kotlin/sideonly/Side.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

enum class Side(val forgeName: String) {
CLIENT("CLIENT"), SERVER("DEDICATED_SERVER"), BOTH("BOTH")
}
19 changes: 19 additions & 0 deletions src/main/kotlin/sideonly/SideHardness.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

enum class SideHardness {
/** Stripped at runtime or compile-time */
HARD,
/** Not stripped but should only be loaded on the correct side */
SOFT,
EITHER
}
37 changes: 37 additions & 0 deletions src/main/kotlin/sideonly/SideInstance.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

import com.intellij.psi.PsiElement

class SideInstance private constructor(val side: Side, val element: PsiElement, val reason: String) {
companion object {
fun createSideOnly(side: Side, element: PsiElement): SideInstance {
return SideInstance(side, element, "annotated with @SideOnly(Side.$side)")
}
fun createEnvironment(side: Side, element: PsiElement): SideInstance {
return SideInstance(side, element, "annotated with @Environment(EnvType.$side)")
}
fun createOnlyIn(side: Side, element: PsiElement): SideInstance {
return SideInstance(side, element, "annotated with @OnlyIn(Dist.${side.forgeName})")
}
fun createMcDev(side: Side, element: PsiElement): SideInstance {
return SideInstance(side, element, "annotated with @CheckEnv(Env.$side)")
}
fun createImplicitMcDev(side: Side, element: PsiElement): SideInstance {
return SideInstance(side, element, "implicitly annotated with @CheckEnv(Env.$side)")
}

fun createDistExecutor(side: Side, element: PsiElement): SideInstance {
return SideInstance(side, element, "inside DistExecutor ${side.forgeName}")
}
}
}
187 changes: 187 additions & 0 deletions src/main/kotlin/sideonly/SideOnlyInspection.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

import com.intellij.codeInspection.AbstractBaseJavaLocalInspectionTool
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.psi.JavaElementVisitor
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiClassObjectAccessExpression
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiField
import com.intellij.psi.PsiInstanceOfExpression
import com.intellij.psi.PsiLambdaExpression
import com.intellij.psi.PsiMember
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiMethodReferenceExpression
import com.intellij.psi.PsiNewExpression
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypeCastExpression
import com.intellij.psi.util.parentOfType
import com.intellij.psi.util.parents

class SideOnlyInspection : AbstractBaseJavaLocalInspectionTool() {
override fun getStaticDescription() = "SideOnly problems"

override fun buildVisitor(holder: ProblemsHolder, isOnTheFly: Boolean): PsiElementVisitor {
return Visitor(holder)
}

private class Visitor(private val problems: ProblemsHolder) : JavaElementVisitor() {

// CHECK REFERENCES TO HARD AND SOFT SIDEONLY MEMBERS

override fun visitClass(clazz: PsiClass) {
val classSide = SideOnlyUtil.getContextSide(clazz, SideHardness.EITHER)

val superClass = clazz.superClass
if (superClass != null) {
val targetSide = SideOnlyUtil.getContextSide(superClass, SideHardness.EITHER)
val problemElement = clazz.extendsList?.referenceElements?.firstOrNull()
if (problemElement != null && targetSide != null && targetSide.side != classSide?.side) {
problems.registerProblem(
problemElement,
SideOnlyUtil.createInspectionMessage(classSide, targetSide)
)
}
}

val interfaceList = if (clazz.isInterface) {
clazz.extendsList
} else {
clazz.implementsList
}?.let { it.referencedTypes zip it.referenceElements } ?: emptyList()
val sidedInterfaces = SideOnlyUtil.getSidedInterfaces(clazz)
for ((itf, problemElement) in interfaceList) {
val itfClass = itf.resolve() ?: continue
val targetSide = SideOnlyUtil.getContextSide(itfClass, SideHardness.EITHER)
val sidedInterface = sidedInterfaces[itfClass.qualifiedName]
if (targetSide != null && targetSide.side != classSide?.side && targetSide.side != sidedInterface) {
problems.registerProblem(
problemElement,
SideOnlyUtil.createInspectionMessage(classSide, targetSide)
)
}
}
}

override fun visitField(field: PsiField) {
checkEitherAccess(field.type, field.typeElement)
}

override fun visitMethod(method: PsiMethod) {
checkEitherAccess(method.returnType, method.returnTypeElement)
for (parameter in method.parameterList.parameters) {
checkEitherAccess(parameter.type, parameter.typeElement)
}
for ((type, element) in method.throwsList.referencedTypes zip method.throwsList.referenceElements) {
checkEitherAccess(type, element)
}

val body = method.body ?: return
SideOnlyUtil.analyzeBodyForSoftSideProblems(body, problems)
}

override fun visitLambdaExpression(expression: PsiLambdaExpression) {
// TODO: lambda parameter types?

val body = expression.body ?: return
SideOnlyUtil.analyzeBodyForSoftSideProblems(body, problems)
}

private fun checkEitherAccess(targetType: PsiType?, element: PsiElement?) {
targetType ?: return
element ?: return

val targetClass = SideOnlyUtil.getClassInType(targetType) ?: return
val contextSide = SideOnlyUtil.getContextSide(element, SideHardness.EITHER)
val targetSide = SideOnlyUtil.getContextSide(targetClass, SideHardness.EITHER)
if (targetSide != null && targetSide.side != contextSide?.side) {
problems.registerProblem(element, SideOnlyUtil.createInspectionMessage(contextSide, targetSide))
}
}

// CHECK REFERENCES TO HARD SIDEONLY MEMBERS

override fun visitClassObjectAccessExpression(expression: PsiClassObjectAccessExpression) {
// class references in annotations are always legal
if (expression.parentOfType(PsiAnnotation::class, PsiMember::class, PsiClass::class) is PsiAnnotation) {
return
}

checkHardAccess(expression.operand.type, expression.operand)
}

override fun visitInstanceOfExpression(expression: PsiInstanceOfExpression) {
checkHardAccess(expression.checkType?.type, expression.checkType)
}

override fun visitMethodCallExpression(expression: PsiMethodCallExpression) {
checkHardAccess(expression.resolveMethod(), expression.methodExpression.referenceNameElement)
}

override fun visitNewExpression(expression: PsiNewExpression) {
checkHardAccess(
expression.classOrAnonymousClassReference?.resolve(),
expression.classOrAnonymousClassReference
)
}

override fun visitReferenceExpression(expression: PsiReferenceExpression) {
val field = expression.resolve() as? PsiField ?: return
checkHardAccess(field, expression.referenceNameElement)
}

override fun visitMethodReferenceExpression(expression: PsiMethodReferenceExpression) {
checkHardAccess(expression.potentiallyApplicableMember, expression.referenceNameElement)
}

override fun visitTypeCastExpression(expression: PsiTypeCastExpression) {
checkHardAccess(expression.castType?.type, expression.castType)
}

private fun checkHardAccess(target: PsiElement?, reference: PsiElement?) {
target ?: return
reference ?: return

val targetSide = SideOnlyUtil.getContextSide(target, SideHardness.HARD) ?: return
val contextSide = getContextSideForHardAccess(reference)
if (targetSide.side != contextSide?.side) {
val contextSideForMsg = SideOnlyUtil.getContextSide(reference, SideHardness.EITHER)
problems.registerProblem(reference, SideOnlyUtil.createInspectionMessage(contextSideForMsg, targetSide))
}
}

private fun checkHardAccess(targetType: PsiType?, element: PsiElement?) {
targetType ?: return
checkHardAccess(SideOnlyUtil.getClassInType(targetType), element)
}

private fun getContextSideForHardAccess(element: PsiElement): SideInstance? {
// Same as SideOnlyUtil.getContextSide(element, SideHardness.EITHER), with the exception that soft-sidedness
// of methods are ignored, as the mere presence of these methods can trip the verifier.
val softSide = SideOnlyUtil.getContextSide(element, SideHardness.SOFT)
val hardSide = SideOnlyUtil.getContextSide(element, SideHardness.HARD)
return if (softSide != null &&
softSide.element !is PsiMethod &&
softSide.element.parents().contains(hardSide?.element)
) {
softSide
} else {
hardSide
}
}
}
}
92 changes: 92 additions & 0 deletions src/main/kotlin/sideonly/SideOnlyLineMarkerProvider.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,92 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

import com.demonwav.mcdev.MinecraftSettings
import com.intellij.codeInsight.daemon.LineMarkerInfo
import com.intellij.codeInsight.daemon.LineMarkerProvider
import com.intellij.icons.AllIcons
import com.intellij.ide.actions.ApplyIntentionAction
import com.intellij.openapi.actionSystem.DefaultActionGroup
import com.intellij.openapi.actionSystem.impl.SimpleDataContext
import com.intellij.openapi.editor.Editor
import com.intellij.openapi.editor.markup.GutterIconRenderer
import com.intellij.openapi.fileEditor.FileEditorManager
import com.intellij.openapi.project.Project
import com.intellij.openapi.ui.popup.JBPopup
import com.intellij.openapi.ui.popup.JBPopupFactory
import com.intellij.psi.PsiDocumentManager
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiIdentifier
import com.intellij.psi.PsiModifierListOwner
import com.intellij.psi.util.PsiUtilCore
import com.intellij.ui.awt.RelativePoint
import java.awt.event.MouseEvent

class SideOnlyLineMarkerProvider : LineMarkerProvider {
override fun getLineMarkerInfo(element: PsiElement): LineMarkerInfo<*>? {
if (!MinecraftSettings.instance.isShowSideOnlyGutterIcons) {
return null
}
if (element !is PsiIdentifier) {
return null
}
val listOwner = element.parent as? PsiModifierListOwner ?: return null
val implicitHard = SideOnlyUtil.getInferredAnnotationOnly(listOwner, SideHardness.HARD)
val implicitSoft = SideOnlyUtil.getInferredAnnotationOnly(listOwner, SideHardness.SOFT)
val implicitAnnotation = implicitHard ?: implicitSoft ?: return null

var message = "Implicit "
message += if (implicitHard == null) {
"soft"
} else {
"hard"
}
message += "-sided annotation available: " + implicitAnnotation.reason
return LineMarkerInfo(
element,
element.textRange,
AllIcons.Gutter.ExtAnnotation,
{ message },
this::navigate,
GutterIconRenderer.Alignment.RIGHT
)
}

private fun navigate(event: MouseEvent, element: PsiElement) {
val listOwner = element.parent
val containingFile = listOwner.containingFile
val virtualFile = PsiUtilCore.getVirtualFile(listOwner)

if (virtualFile != null && containingFile != null) {
val project = listOwner.project
val editor = FileEditorManager.getInstance(project).selectedTextEditor
if (editor != null) {
editor.caretModel.moveToOffset(element.textOffset)
val file = PsiDocumentManager.getInstance(project).getPsiFile(editor.document)
if (file != null && virtualFile == file.virtualFile) {
val popup = createActionGroupPopup(containingFile, project, editor)
popup?.show(RelativePoint(event))
}
}
}
}

private fun createActionGroupPopup(file: PsiFile, project: Project, editor: Editor): JBPopup? {
val intention = MakeInferredMcdevAnnotationExplicit()
val action = ApplyIntentionAction(intention, intention.text, editor, file)
val group = DefaultActionGroup(action)
val context = SimpleDataContext.getProjectContext(null)
return JBPopupFactory.getInstance()
.createActionGroupPopup(null, group, context, JBPopupFactory.ActionSelectionAid.SPEEDSEARCH, true)
}
}
438 changes: 438 additions & 0 deletions src/main/kotlin/sideonly/SideOnlyUtil.kt

Large diffs are not rendered by default.

185 changes: 185 additions & 0 deletions src/main/kotlin/sideonly/SoftSideOnlyInstructionVisitor.kt
Original file line number Diff line number Diff line change
@@ -0,0 +1,185 @@
/*
* Minecraft Dev for IntelliJ
*
* https://minecraftdev.org
*
* Copyright (c) 2020 minecraft-dev
*
* MIT License
*/

package com.demonwav.mcdev.sideonly

import com.intellij.codeInsight.daemon.impl.quickfix.SimplifyBooleanExpressionFix
import com.intellij.codeInspection.LocalQuickFixOnPsiElement
import com.intellij.codeInspection.ProblemsHolder
import com.intellij.codeInspection.dataFlow.DataFlowRunner
import com.intellij.codeInspection.dataFlow.DfaInstructionState
import com.intellij.codeInspection.dataFlow.DfaMemoryState
import com.intellij.codeInspection.dataFlow.StandardInstructionVisitor
import com.intellij.codeInspection.dataFlow.instructions.InstanceofInstruction
import com.intellij.codeInspection.dataFlow.instructions.MethodCallInstruction
import com.intellij.codeInspection.dataFlow.instructions.MethodReferenceInstruction
import com.intellij.codeInspection.dataFlow.instructions.PushInstruction
import com.intellij.codeInspection.dataFlow.instructions.TypeCastInstruction
import com.intellij.codeInspection.dataFlow.value.DfaValueFactory
import com.intellij.codeInspection.dataFlow.value.DfaVariableValue
import com.intellij.psi.PsiAssignmentExpression
import com.intellij.psi.PsiClassObjectAccessExpression
import com.intellij.psi.PsiElement
import com.intellij.psi.PsiExpression
import com.intellij.psi.PsiField
import com.intellij.psi.PsiInstanceOfExpression
import com.intellij.psi.PsiMethodCallExpression
import com.intellij.psi.PsiNewExpression
import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.util.PsiTreeUtil

class SoftSideOnlyInstructionVisitor(
private val body: PsiElement,
private val factory: DfaValueFactory,
private val problems: ProblemsHolder
) : StandardInstructionVisitor() {
private val contextSide = SideOnlyUtil.getContextSide(body, SideHardness.EITHER)
private val isLogicalClientVars = mutableListOf<DfaVariableValue>()

override fun visitPush(
instruction: PushInstruction,
runner: DataFlowRunner,
memState: DfaMemoryState
): Array<DfaInstructionState> {
val dfaVar = instruction.value as? DfaVariableValue
?: return super.visitPush(instruction, runner, memState)

val expression = instruction.expression

if (expression is PsiClassObjectAccessExpression) {
val targetClass = SideOnlyUtil.getClassInType(expression.operand.type)
?: return super.visitPush(instruction, runner, memState)
val problemElement = expression.operand
checkAccess(memState, problemElement, targetClass)
return super.visitPush(instruction, runner, memState)
}

val field = dfaVar.psiVariable as? PsiField ?: return super.visitPush(instruction, runner, memState)
val problemElement = (expression as? PsiReferenceExpression)?.referenceNameElement

if (field.containingClass?.qualifiedName == "net.minecraft.world.World" &&
(field.name == "isRemote" || field.name == "isClient")
) {
if (isPhysicalServer(memState)) {
memState.setVarValue(dfaVar, factory.constFactory.`false`)
if (problemElement != null) {
val fix = createSimplifyBooleanFix(expression, false)
problems.registerProblem(
problemElement,
"Expression is always false",
*listOfNotNull(fix).toTypedArray()
)
}
}
isLogicalClientVars.add(dfaVar)
} else {
if (problemElement != null) {
checkAccess(memState, problemElement, field)
}
}

return super.visitPush(instruction, runner, memState)
}

override fun visitMethodCall(
instruction: MethodCallInstruction,
runner: DataFlowRunner,
memState: DfaMemoryState
): Array<DfaInstructionState> {
val result = super.visitMethodCall(instruction, runner, memState)

val targetMethod = instruction.targetMethod ?: return result
val problemElement = when (val callExpression = instruction.callExpression) {
is PsiMethodCallExpression -> callExpression.methodExpression.referenceNameElement
is PsiNewExpression -> callExpression.classOrAnonymousClassReference
else -> return result
} ?: return result
checkAccess(memState, problemElement, targetMethod)

return result
}

override fun visitTypeCast(
instruction: TypeCastInstruction,
runner: DataFlowRunner,
memState: DfaMemoryState
): Array<DfaInstructionState> {
val result = super.visitTypeCast(instruction, runner, memState)

val targetClass = SideOnlyUtil.getClassInType(instruction.castTo) ?: return result
val problemElement = instruction.expression.castType ?: return result
checkAccess(memState, problemElement, targetClass)

return result
}

override fun visitInstanceof(
instruction: InstanceofInstruction,
runner: DataFlowRunner,
memState: DfaMemoryState
): Array<DfaInstructionState> {
val result = super.visitInstanceof(instruction, runner, memState)

val targetType = instruction.castType ?: return result
val targetClass = SideOnlyUtil.getClassInType(targetType) ?: return result
val problemElement = (instruction.expression as? PsiInstanceOfExpression)?.checkType ?: return result
checkAccess(memState, problemElement, targetClass)

return result
}

override fun visitMethodReference(
instruction: MethodReferenceInstruction,
runner: DataFlowRunner,
memState: DfaMemoryState
): Array<DfaInstructionState> {
val result = super.visitMethodReference(instruction, runner, memState)

val targetMember = instruction.expression.potentiallyApplicableMember ?: return result
val problemElement = instruction.expression.referenceNameElement ?: return result
checkAccess(memState, problemElement, targetMember)

return result
}

private fun checkAccess(memState: DfaMemoryState, problemElement: PsiElement, targetElement: PsiElement) {
val targetSide = SideOnlyUtil.getContextSide(targetElement, SideHardness.SOFT)
if (targetSide?.side == Side.CLIENT && !isPhysicalClient(memState)) {
problems.registerProblem(problemElement, SideOnlyUtil.createInspectionMessage(contextSide, targetSide))
} else if (targetSide?.side == Side.SERVER && !isPhysicalServer(memState)) {
problems.registerProblem(problemElement, SideOnlyUtil.createInspectionMessage(contextSide, targetSide))
}
}

private fun isPhysicalClient(memState: DfaMemoryState): Boolean {
return contextSide?.side == Side.CLIENT ||
isLogicalClientVars.any { memState.getConstantValue(it) == factory.constFactory.`true` }
}

private fun isPhysicalServer(memState: DfaMemoryState): Boolean {
return contextSide?.side == Side.SERVER
}

private fun createSimplifyBooleanFix(element: PsiElement, value: Boolean): LocalQuickFixOnPsiElement? {
val expression = element as? PsiExpression ?: return null
if (PsiTreeUtil.findChildOfType(expression, PsiAssignmentExpression::class.java) != null) return null
var wholeExpression = expression
var parent = expression.parent
while (parent is PsiExpression) {
wholeExpression = parent
parent = wholeExpression.parent
}
val fix = SimplifyBooleanExpressionFix(expression, value)
// simplify intention already active
if (!fix.isAvailable || SimplifyBooleanExpressionFix.canBeSimplified(wholeExpression)) return null

return fix
}
}
94 changes: 94 additions & 0 deletions src/main/kotlin/util/gradle-util.kt
Original file line number Diff line number Diff line change
@@ -11,16 +11,35 @@
package com.demonwav.mcdev.util

import com.intellij.execution.executors.DefaultRunExecutor
import com.intellij.openapi.command.WriteCommandAction
import com.intellij.openapi.externalSystem.importing.ImportSpecBuilder
import com.intellij.openapi.externalSystem.model.execution.ExternalSystemTaskExecutionSettings
import com.intellij.openapi.externalSystem.service.execution.ProgressExecutionMode
import com.intellij.openapi.externalSystem.task.TaskCallback
import com.intellij.openapi.externalSystem.util.ExternalSystemUtil
import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.rootManager
import com.intellij.openapi.roots.ProjectRootManager
import com.intellij.psi.PsiManager
import com.intellij.psi.codeStyle.CodeStyleManager
import java.nio.file.Path
import java.util.concurrent.locks.Condition
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import org.jetbrains.kotlin.psi.KtCallElement
import org.jetbrains.kotlin.psi.KtFile
import org.jetbrains.kotlin.psi.KtLambdaExpression
import org.jetbrains.kotlin.psi.KtPsiFactory
import org.jetbrains.kotlin.psi.KtScriptInitializer
import org.jetbrains.kotlin.psi.KtSimpleNameExpression
import org.jetbrains.plugins.gradle.util.GradleConstants
import org.jetbrains.plugins.groovy.lang.psi.GroovyFile
import org.jetbrains.plugins.groovy.lang.psi.GroovyPsiElementFactory
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrMethodCall
import org.jetbrains.plugins.groovy.lang.psi.api.statements.expressions.GrReferenceExpression
import org.jetbrains.plugins.groovy.lang.psi.util.childrenOfType

inline fun runGradleTask(
project: Project,
@@ -51,6 +70,81 @@ inline fun runGradleTask(
}
}

fun addGradleDependency(
project: Project,
module: Module,
group: String,
artifact: String,
version: SemanticVersion
): Boolean {
val buildGradleFile = module.rootManager.contentRoots.mapFirstNotNull { moduleFile ->
moduleFile.findChild("build.gradle") ?: moduleFile.findChild("build.gradle.kts")
} ?: ProjectRootManager.getInstance(project).contentRoots.mapFirstNotNull { projectFile ->
projectFile.findChild("build.gradle") ?: projectFile.findChild("build.gradle.kts")
} ?: return false
val psiFile = PsiManager.getInstance(project).findFile(buildGradleFile) ?: return false
val success = when (psiFile) {
is GroovyFile -> addGroovyGradleDependency(project, psiFile, group, artifact, version)
is KtFile -> addKotlinGradleDependency(project, psiFile, group, artifact, version)
else -> false
}
if (success) {
FileDocumentManager.getInstance().saveAllDocuments()
ExternalSystemUtil.refreshProjects(
ImportSpecBuilder(project, GradleConstants.SYSTEM_ID).use(ProgressExecutionMode.MODAL_SYNC)
)
}
return success
}

private fun addGroovyGradleDependency(
project: Project,
file: GroovyFile,
group: String,
artifact: String,
version: SemanticVersion
): Boolean {
val dependenciesCall = file.childrenOfType<GrMethodCall>().firstOrNull { methodCall ->
(methodCall.invokedExpression as? GrReferenceExpression)?.referenceName == "dependencies"
} ?: return false
val dependenciesClosure = dependenciesCall.closureArguments.firstOrNull() ?: return false
val toInsert = GroovyPsiElementFactory.getInstance(project).createStatementFromText(
"implementation '$group:$artifact:$version'",
dependenciesClosure
)
WriteCommandAction.runWriteCommandAction(project) {
dependenciesClosure.addStatementBefore(toInsert, null)
CodeStyleManager.getInstance(project).reformat(dependenciesClosure)
}
return true
}

private fun addKotlinGradleDependency(
project: Project,
file: KtFile,
group: String,
artifact: String,
version: SemanticVersion
): Boolean {
val scriptBlock = file.script?.blockExpression ?: return false
val dependenciesCall = scriptBlock.statements.asSequence()
.mapNotNull { it as? KtScriptInitializer }
.mapNotNull { it.body as? KtCallElement }
.firstOrNull { (it.calleeExpression as? KtSimpleNameExpression)?.getReferencedName() == "dependencies" }
?: return false
val dependenciesLambda = dependenciesCall.lambdaArguments.firstOrNull()?.getArgumentExpression()
as? KtLambdaExpression ?: return false
val dependenciesBlock = dependenciesLambda.functionLiteral.bodyBlockExpression ?: return false
val factory = KtPsiFactory(project)
val toInsert = factory.createExpression("implementation(\"$group:$artifact:$version\")")
WriteCommandAction.runWriteCommandAction(project) {
dependenciesBlock.addBefore(factory.createNewLine(), dependenciesBlock.rBrace)
dependenciesBlock.addBefore(toInsert, dependenciesBlock.rBrace)
CodeStyleManager.getInstance(project).reformat(dependenciesBlock)
}
return true
}

class GradleCallback(private val lock: ReentrantLock, private val condition: Condition) : TaskCallback {

override fun onSuccess() = resume()
25 changes: 25 additions & 0 deletions src/main/kotlin/util/utils.kt
Original file line number Diff line number Diff line change
@@ -20,6 +20,8 @@ import com.intellij.openapi.fileEditor.FileDocumentManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.progress.ProgressIndicator
import com.intellij.openapi.progress.ProgressManager
import com.intellij.openapi.project.DumbService
import com.intellij.openapi.project.Project
import com.intellij.openapi.project.ProjectManager
@@ -67,6 +69,29 @@ inline fun <T : Any?> Project.runWriteTaskInSmartMode(crossinline func: () -> T)
return ref.get()
}

fun Project.runInSmartModeFromReadAction(
progressIndicator: ProgressIndicator? = null,
func: () -> Unit
) {
val dumbService = DumbService.getInstance(this)
if (dumbService.isDumb) {
ApplicationManager.getApplication().executeOnPooledThread {
ProgressManager.getInstance().runProcess(
{
ProgressManager.progress("Indexing")
dumbService.runReadActionInSmartMode {
ProgressManager.checkCanceled()
invokeLater(func)
}
},
progressIndicator
)
}
} else {
func()
}
}

fun <T : Any?> invokeAndWait(func: () -> T): T {
val ref = Ref<T>()
ApplicationManager.getApplication().invokeAndWait({ ref.set(func()) }, ModalityState.defaultModalityState())
80 changes: 29 additions & 51 deletions src/main/resources/META-INF/plugin.xml
Original file line number Diff line number Diff line change
@@ -14,6 +14,7 @@
<depends>org.jetbrains.idea.maven</depends>
<depends>com.intellij.gradle</depends>
<depends>org.intellij.groovy</depends>
<depends>org.jetbrains.kotlin</depends>

<id>com.demonwav.minecraft-dev</id>
<name>Minecraft Development</name>
@@ -57,10 +58,18 @@
<!-- File templates are for project creations -->
<fileTemplateGroup implementation="com.demonwav.mcdev.util.MinecraftTemplates"/>

<!-- Project-independent intention actions -->
<intentionAction>
<className>com.demonwav.mcdev.sideonly.MakeInferredMcdevAnnotationExplicit</className>
<category>Minecraft</category>
<descriptionDirectoryName>MakeInferredMcdevAnnotationExplicit</descriptionDirectoryName>
</intentionAction>

<!-- Project-independent Line Marker Providers -->
<codeInsight.lineMarkerProvider language="" implementationClass="com.demonwav.mcdev.insight.ListenerLineMarkerProvider"/>
<codeInsight.lineMarkerProvider language="" implementationClass="com.demonwav.mcdev.insight.ColorLineMarkerProvider"/>
<codeInsight.lineMarkerProvider language="JAVA" implementationClass="com.demonwav.mcdev.insight.PluginLineMarkerProvider"/>
<codeInsight.lineMarkerProvider language="JAVA" implementationClass="com.demonwav.mcdev.sideonly.SideOnlyLineMarkerProvider"/>

<!-- Project-independent Annotators-->
<annotator language="JAVA" implementationClass="com.demonwav.mcdev.insight.ListenerEventAnnotator"/>
@@ -185,8 +194,6 @@
<implicitUsageProvider implementation="com.demonwav.mcdev.platform.forge.insight.ForgeImplicitUsageProvider" />

<!-- Forge Annotator -->
<annotator language="JAVA" implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.SidedProxyAnnotator"/>

<library.presentationProvider implementation="com.demonwav.mcdev.platform.forge.framework.ForgePresentationProvider" />

<externalProjectDataService implementation="com.demonwav.mcdev.platform.forge.gradle.ForgeRunConfigDataService" />
@@ -322,6 +329,20 @@
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.translations.inspections.SuperfluousFormatInspection"/>
<localInspection displayName="SideOnly problems"
groupName="Minecraft"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.sideonly.SideOnlyInspection"/>
<localInspection displayName="Usage of hard-SideOnly annotations"
groupName="Minecraft"
language="JAVA"
enabledByDefault="true"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.sideonly.HardSideOnlyUsageInspection"/>
<!--endregion-->

<!--region BUKKIT INSPECTIONS-->
@@ -375,63 +396,20 @@
<!--endregion-->

<!--region FORGE INSPECTIONS-->
<localInspection displayName="Invalid usage of @SideOnly in nested class declaration"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="false"
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.NestedClassSideOnlyInspection"/>
<localInspection displayName="Invalid usage of @SideOnly in field declaration"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="false"
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.FieldDeclarationSideOnlyInspection"/>
<localInspection displayName="Invalid usage of @SideOnly in method declaration"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="false"
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.MethodSideOnlyInspection"/>
<localInspection displayName="Invalid usage of variable annotated with @SideOnly"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="false"
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.VariableUseSideOnlyInspection"/>
<localInspection displayName="Invalid usage of method annotated with @SideOnly"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="false"
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.MethodCallSideOnlyInspection"/>
<localInspection displayName="Invalid usage of new object annotated with @SideOnly"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="false"
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.NewExpressionSideOnlyInspection"/>
<localInspection displayName="Invalid usage of local variable declaration annotated with @SideOnly"
<localInspection displayName="IMessage or IMessageHandler implementation without empty constructor"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="false"
enabledByDefault="true"
level="ERROR"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.sideonly.LocalVariableDeclarationSideOnlyInspection"/>

<localInspection displayName="IMessage or IMessageHandler implementation without empty constructor"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.simpleimpl.MissingMessageConstructorInspection"/>
<localInspection displayName="DistExecutor problems"
groupName="MinecraftForge"
language="JAVA"
enabledByDefault="true"
level="ERROR"
level="WARNING"
hasStaticDescription="true"
implementationClass="com.demonwav.mcdev.platform.forge.inspections.simpleimpl.MissingMessageConstructorInspection"/>
implementationClass="com.demonwav.mcdev.platform.forge.inspections.DistExecutorInspection"/>
<!--endregion-->

<!--region FABRIC INSPECTIONS-->
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
import com.demonwav.mcdev.annotations.CheckEnv;
import com.demonwav.mcdev.annotations.Env;

<spot>@CheckEnv(Env.CLIENT)</spot>
class A extends ClientOnlyClass {
// stuff
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
class A extends ClientOnlyClass {
// stuff
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
<!--
Minecraft Dev for IntelliJ
https://minecraftdev.org
Copyright (c) 2020 minecraft-dev
MIT License
-->

<html>
<body>
This intention adds annotations inferred by MinecraftDev (@CheckEnv) to the code explicitly.
</body>
</html>