diff --git a/build.gradle.kts b/build.gradle.kts
index 48e4ab67b..450c44ad1 100644
--- a/build.gradle.kts
+++ b/build.gradle.kts
@@ -107,12 +107,17 @@ repositories {
}
}
mavenCentral()
+ maven("https://repo.spongepowered.org/maven/")
}
dependencies {
// Add tools.jar for the JDI API
implementation(files(Jvm.current().toolsJar))
+ implementation(libs.mixinExtras.expressions)
+ testLibs(libs.mixinExtras.common)
+ implementation("org.ow2.asm:asm-util:9.3")
+
// Kotlin
implementation(kotlin("stdlib-jdk8"))
implementation(kotlin("reflect"))
@@ -198,6 +203,7 @@ intellij {
"Kotlin",
"org.toml.lang:$pluginTomlVersion",
"ByteCodeViewer",
+ "org.intellij.intelliLang",
"properties",
// needed dependencies for unit tests
"junit"
@@ -363,6 +369,9 @@ val generateNbttParser by parser("NbttParser", "com/demonwav/mcdev/nbt/lang/gen"
val generateLangLexer by lexer("LangLexer", "com/demonwav/mcdev/translations/lang/gen")
val generateLangParser by parser("LangParser", "com/demonwav/mcdev/translations/lang/gen")
+val generateMEExpressionLexer by lexer("MEExpressionLexer", "com/demonwav/mcdev/platform/mixin/expression/gen")
+val generateMEExpressionParser by parser("MEExpressionParser", "com/demonwav/mcdev/platform/mixin/expression/gen")
+
val generateTranslationTemplateLexer by lexer(
"TranslationTemplateLexer",
"com/demonwav/mcdev/translations/template/gen"
@@ -381,6 +390,8 @@ val generate by tasks.registering {
generateNbttParser,
generateLangLexer,
generateLangParser,
+ generateMEExpressionLexer,
+ generateMEExpressionParser,
generateTranslationTemplateLexer,
)
}
diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml
index f8dabc6d7..941db8f7c 100644
--- a/gradle/libs.versions.toml
+++ b/gradle/libs.versions.toml
@@ -11,6 +11,7 @@ coroutines-jdk8 = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-jdk8", ve
coroutines-swing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "coroutines" }
mappingIo = "net.fabricmc:mapping-io:0.2.1"
+mixinExtras-expressions = "io.github.llamalad7:mixinextras-expressions:0.0.1"
# GrammarKit
jflex-lib = "org.jetbrains.idea:jflex:1.7.0-b7f882a"
@@ -40,6 +41,8 @@ junit-api = { module = "org.junit.jupiter:junit-jupiter-api", version.ref = "jun
junit-entine = { module = "org.junit.jupiter:junit-jupiter-engine", version.ref = "junit" }
junit-platform-launcher = { module = "org.junit.platform:junit-platform-launcher", version.ref = "junit-platform" }
+mixinExtras-common = "io.github.llamalad7:mixinextras-common:0.5.0-beta.1"
+
[bundles]
coroutines = ["coroutines-core", "coroutines-jdk8", "coroutines-swing"]
asm = ["asm", "asm-tree", "asm-analysis"]
diff --git a/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java
new file mode 100644
index 000000000..c5efbe9a6
--- /dev/null
+++ b/mixin-test-data/src/main/java/com/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData.java
@@ -0,0 +1,84 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.mixintestdata.meExpression;
+
+import java.util.ArrayList;
+import java.util.stream.Stream;
+
+public class MEExpressionTestData {
+ private static final SynchedData STINGER_COUNT = null;
+ private SynchedDataManager synchedData;
+
+ public void complexFunction() {
+ int one = 1;
+ String local1 = "Hello";
+ String local2 = "World";
+
+ System.out.println(new StringBuilder(local1).append(", ").append(local2));
+ System.out.println(one);
+
+ new ArrayList<>(10);
+
+ InaccessibleType varOfInaccessibleType = new InaccessibleType();
+ acceptInaccessibleType(varOfInaccessibleType);
+ noArgMethod();
+
+ String[] strings1 = new String[] { local1, local2 };
+ String[] strings2 = new String[one];
+
+ Stream.empty().map(this::nonStaticMapper).map(MEExpressionTestData::staticMapper).map(ConstructedByMethodReference::new);
+ }
+
+ private static void acceptInaccessibleType(InaccessibleType type) {
+ }
+
+ private static void noArgMethod() {
+ }
+
+ public int getStingerCount() {
+ return (Integer) this.synchedData.get(STINGER_COUNT);
+ }
+
+ private Object nonStaticMapper(Object arg) {
+ return arg;
+ }
+
+ private static Object staticMapper(Object arg) {
+ return arg;
+ }
+
+ private static class InaccessibleType {
+
+ }
+
+ public static class SynchedDataManager {
+ public V get(SynchedData data) {
+ return null;
+ }
+ }
+
+ public static class SynchedData {
+ }
+
+ public static class ConstructedByMethodReference {
+ public ConstructedByMethodReference(Object bar) {}
+ }
+}
diff --git a/src/main/grammars/MEExpressionLexer.flex b/src/main/grammars/MEExpressionLexer.flex
new file mode 100644
index 000000000..c7d9fad8a
--- /dev/null
+++ b/src/main/grammars/MEExpressionLexer.flex
@@ -0,0 +1,146 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression;
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes;
+import com.intellij.lexer.FlexLexer;
+import com.intellij.psi.tree.IElementType;
+import com.intellij.psi.TokenType;
+
+%%
+
+%public
+%class MEExpressionLexer
+%implements FlexLexer
+%function advance
+%type IElementType
+
+%state STRING
+
+%unicode
+
+WHITE_SPACE = [\ \n\t\r]
+RESERVED = assert|break|case|catch|const|continue|default|else|finally|for|goto|if|switch|synchronized|try|while|yield|_
+WILDCARD = "?"
+NEW = new
+INSTANCEOF = instanceof
+BOOL_LIT = true|false
+NULL_LIT = null
+DO = do
+RETURN = return
+THROW = throw
+THIS = this
+SUPER = super
+CLASS = class
+IDENTIFIER = [A-Za-z_][A-Za-z0-9_]*
+INT_LIT = ( [0-9]+ | 0x[0-9a-fA-F]+ )
+DEC_LIT = [0-9]*\.[0-9]+
+PLUS = "+"
+MINUS = -
+MULT = "*"
+DIV = "/"
+MOD = %
+BITWISE_NOT = "~"
+DOT = "."
+COMMA = ,
+LEFT_PAREN = "("
+RIGHT_PAREN = ")"
+LEFT_BRACKET = "["
+RIGHT_BRACKET = "]"
+LEFT_BRACE = "{"
+RIGHT_BRACE = "}"
+AT = @
+SHL = <<
+SHR = >>
+USHR = >>>
+LT = <
+LE = <=
+GT = >
+GE = >=
+EQ = ==
+NE = "!="
+BITWISE_AND = &
+BITWISE_XOR = "^"
+BITWISE_OR = "|"
+ASSIGN = =
+METHOD_REF = ::
+
+STRING_TERMINATOR = '
+STRING_ESCAPE = \\'|\\\\
+
+%%
+
+ {
+ {WHITE_SPACE}+ { return TokenType.WHITE_SPACE; }
+ {RESERVED} { return MEExpressionTypes.TOKEN_RESERVED; }
+ {WILDCARD} { return MEExpressionTypes.TOKEN_WILDCARD; }
+ {NEW} { return MEExpressionTypes.TOKEN_NEW; }
+ {INSTANCEOF} { return MEExpressionTypes.TOKEN_INSTANCEOF; }
+ {BOOL_LIT} { return MEExpressionTypes.TOKEN_BOOL_LIT; }
+ {NULL_LIT} { return MEExpressionTypes.TOKEN_NULL_LIT; }
+ {DO} { return MEExpressionTypes.TOKEN_DO; }
+ {RETURN} { return MEExpressionTypes.TOKEN_RETURN; }
+ {THROW} { return MEExpressionTypes.TOKEN_THROW; }
+ {THIS} { return MEExpressionTypes.TOKEN_THIS; }
+ {SUPER} { return MEExpressionTypes.TOKEN_SUPER; }
+ {CLASS} { return MEExpressionTypes.TOKEN_CLASS; }
+ {IDENTIFIER} { return MEExpressionTypes.TOKEN_IDENTIFIER; }
+ {INT_LIT} { return MEExpressionTypes.TOKEN_INT_LIT; }
+ {DEC_LIT} { return MEExpressionTypes.TOKEN_DEC_LIT; }
+ {PLUS} { return MEExpressionTypes.TOKEN_PLUS; }
+ {MINUS} { return MEExpressionTypes.TOKEN_MINUS; }
+ {MULT} { return MEExpressionTypes.TOKEN_MULT; }
+ {DIV} { return MEExpressionTypes.TOKEN_DIV; }
+ {MOD} { return MEExpressionTypes.TOKEN_MOD; }
+ {BITWISE_NOT} { return MEExpressionTypes.TOKEN_BITWISE_NOT; }
+ {DOT} { return MEExpressionTypes.TOKEN_DOT; }
+ {COMMA} { return MEExpressionTypes.TOKEN_COMMA; }
+ {LEFT_PAREN} { return MEExpressionTypes.TOKEN_LEFT_PAREN; }
+ {RIGHT_PAREN} { return MEExpressionTypes.TOKEN_RIGHT_PAREN; }
+ {LEFT_BRACKET} { return MEExpressionTypes.TOKEN_LEFT_BRACKET; }
+ {RIGHT_BRACKET} { return MEExpressionTypes.TOKEN_RIGHT_BRACKET; }
+ {LEFT_BRACE} { return MEExpressionTypes.TOKEN_LEFT_BRACE; }
+ {RIGHT_BRACE} { return MEExpressionTypes.TOKEN_RIGHT_BRACE; }
+ {AT} { return MEExpressionTypes.TOKEN_AT; }
+ {SHL} { return MEExpressionTypes.TOKEN_SHL; }
+ {SHR} { return MEExpressionTypes.TOKEN_SHR; }
+ {USHR} { return MEExpressionTypes.TOKEN_USHR; }
+ {LT} { return MEExpressionTypes.TOKEN_LT; }
+ {LE} { return MEExpressionTypes.TOKEN_LE; }
+ {GT} { return MEExpressionTypes.TOKEN_GT; }
+ {GE} { return MEExpressionTypes.TOKEN_GE; }
+ {EQ} { return MEExpressionTypes.TOKEN_EQ; }
+ {NE} { return MEExpressionTypes.TOKEN_NE; }
+ {BITWISE_AND} { return MEExpressionTypes.TOKEN_BITWISE_AND; }
+ {BITWISE_XOR} { return MEExpressionTypes.TOKEN_BITWISE_XOR; }
+ {BITWISE_OR} { return MEExpressionTypes.TOKEN_BITWISE_OR; }
+ {ASSIGN} { return MEExpressionTypes.TOKEN_ASSIGN; }
+ {METHOD_REF} { return MEExpressionTypes.TOKEN_METHOD_REF; }
+ {STRING_TERMINATOR} { yybegin(STRING); return MEExpressionTypes.TOKEN_STRING_TERMINATOR; }
+}
+
+ {
+ {STRING_ESCAPE} { return MEExpressionTypes.TOKEN_STRING_ESCAPE; }
+ {STRING_TERMINATOR} { yybegin(YYINITIAL); return MEExpressionTypes.TOKEN_STRING_TERMINATOR; }
+ [^'\\]+ { return MEExpressionTypes.TOKEN_STRING; }
+}
+
+[^] { return TokenType.BAD_CHARACTER; }
diff --git a/src/main/grammars/MEExpressionParser.bnf b/src/main/grammars/MEExpressionParser.bnf
new file mode 100644
index 000000000..47d509e01
--- /dev/null
+++ b/src/main/grammars/MEExpressionParser.bnf
@@ -0,0 +1,335 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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 .
+ */
+
+{
+ parserClass="com.demonwav.mcdev.platform.mixin.expression.gen.MEExpressionParser"
+ extends="com.intellij.extapi.psi.ASTWrapperPsiElement"
+ parserImports = ["static com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionParserUtil.*"]
+
+ psiClassPrefix="ME"
+ psiImplClassSuffix="Impl"
+ psiPackage="com.demonwav.mcdev.platform.mixin.expression.gen.psi"
+ psiImplPackage="com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl"
+
+ elementTypeHolderClass="com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes"
+ elementTypeClass="com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionElementType"
+ tokenTypeClass="com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenType"
+
+ tokens = [
+ TOKEN_RESERVED = "TOKEN_RESERVED"
+ ]
+
+ extends(".+Expression") = expression
+ extends(".+Statement") = statement
+}
+
+meExpressionFile ::= item* <>
+
+item ::= declarationItem | statementItem
+
+declarationItem ::= TOKEN_CLASS TOKEN_BOOL_LIT declaration {
+ pin = 1
+ extends = item
+ implements = [
+ "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEDeclarationItemMixin"
+ ]
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEDeclarationItemImplMixin"
+}
+
+declaration ::= TOKEN_IDENTIFIER {
+ implements = [
+ "com.intellij.psi.PsiNamedElement"
+ "com.intellij.psi.PsiNameIdentifierOwner"
+ "com.intellij.psi.NavigatablePsiElement"
+ ]
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEDeclarationImplMixin"
+}
+
+statementItem ::= TOKEN_DO TOKEN_LEFT_BRACE statement TOKEN_RIGHT_BRACE {
+ pin = 1
+ extends = item
+}
+
+private statementRecover ::= !TOKEN_RIGHT_BRACE
+
+statement ::= assignStatement |
+ returnStatement |
+ throwStatement |
+ expressionStatement {
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEStatementImplMixin"
+ recoverWhile = statementRecover
+}
+
+assignStatement ::= assignableExpression TOKEN_ASSIGN expression {
+ pin = 2
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEAssignStatementImplMixin"
+ methods = [
+ targetExpr = "expression[0]"
+ rightExpr = "expression[1]"
+ ]
+}
+
+private assignableExpression ::= arrayAccessExpression | memberAccessExpression | nameExpression
+
+returnStatement ::= TOKEN_RETURN expression {
+ pin = 1
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEReturnStatementImplMixin"
+ methods = [
+ valueExpr = "expression"
+ ]
+}
+
+throwStatement ::= TOKEN_THROW expression {
+ pin = 1
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METhrowStatementImplMixin"
+ methods = [
+ valueExpr = "expression"
+ ]
+}
+
+expressionStatement ::= expression {
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEExpressionStatementImplMixin"
+}
+
+private exprRecover ::= !( TOKEN_COMMA | TOKEN_RIGHT_PAREN | TOKEN_RIGHT_BRACKET | TOKEN_RIGHT_BRACE )
+
+expression ::= capturingExpression |
+ superCallExpression |
+ staticMethodCallExpression |
+ classConstantExpression |
+ unaryExpression |
+ binaryExpression |
+ castExpression |
+ parenthesizedExpression |
+ methodCallExpression |
+ boundMethodReferenceExpression |
+ freeMethodReferenceExpression |
+ constructorReferenceExpression |
+ arrayAccessExpression |
+ memberAccessExpression |
+ newExpression |
+ litExpression |
+ thisExpression |
+ nameExpression {
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEExpressionImplMixin"
+ recoverWhile = exprRecover
+}
+
+external rightParen ::= parseToRightBracket exprRecover TOKEN_RIGHT_PAREN
+external rightBracket ::= parseToRightBracket exprRecover TOKEN_RIGHT_BRACKET
+external rightBrace ::= parseToRightBracket exprRecover TOKEN_RIGHT_BRACE
+
+capturingExpression ::= TOKEN_AT TOKEN_LEFT_PAREN expression rightParen {
+ pin = 1
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MECapturingExpressionImplMixin"
+}
+
+parenthesizedExpression ::= TOKEN_LEFT_PAREN expression rightParen {
+ pin = 1
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEParenthesizedExpressionImplMixin"
+}
+
+superCallExpression ::= TOKEN_SUPER TOKEN_DOT name TOKEN_LEFT_PAREN arguments rightParen {
+ pin = 1
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MESuperCallExpressionImplMixin"
+ methods = [
+ memberName = "name"
+ ]
+}
+
+methodCallExpression ::= expression TOKEN_DOT name TOKEN_LEFT_PAREN arguments rightParen {
+ pin = 4
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEMethodCallExpressionImplMixin"
+ methods = [
+ receiverExpr = "expression"
+ memberName = "name"
+ ]
+}
+
+staticMethodCallExpression ::= name TOKEN_LEFT_PAREN arguments rightParen {
+ pin = 2
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEStaticMethodCallExpressionImplMixin"
+ methods = [
+ memberName = "name"
+ ]
+}
+
+boundMethodReferenceExpression ::= expression !(TOKEN_METHOD_REF TOKEN_NEW) TOKEN_METHOD_REF name {
+ pin = 3
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEBoundReferenceExpressionImplMixin"
+ methods = [
+ receiverExpr = "expression"
+ memberName = "name"
+ ]
+}
+
+freeMethodReferenceExpression ::= TOKEN_METHOD_REF name {
+ pin = 1
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEFreeMethodReferenceExpressionImplMixin"
+ methods = [
+ memberName = "name"
+ ]
+}
+
+constructorReferenceExpression ::= type TOKEN_METHOD_REF TOKEN_NEW {
+ pin = 3
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEConstructorReferenceExpressionImplMixin"
+ methods = [
+ className = "type"
+ ]
+}
+
+arrayAccessExpression ::= expression TOKEN_LEFT_BRACKET expression? rightBracket {
+ pin = 2
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArrayAccessExpressionMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEArrayAccessExpressionImplMixin"
+ methods = [
+ arrayExpr = "expression[0]"
+ indexExpr = "expression[1]"
+ ]
+}
+
+classConstantExpression ::= type TOKEN_DOT TOKEN_CLASS {
+ pin = 3
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEClassConstantExpressionImplMixin"
+ methods = [
+ className = "name"
+ ]
+}
+
+memberAccessExpression ::= expression TOKEN_DOT name {
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEMemberAccessExpressionImplMixin"
+ methods = [
+ receiverExpr = "expression"
+ memberName = "name"
+ ]
+}
+
+unaryExpression ::= ((TOKEN_MINUS !(TOKEN_DEC_LIT | TOKEN_INT_LIT)) | TOKEN_BITWISE_NOT) expression {
+ pin = 2
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEUnaryExpressionMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEUnaryExpressionImplMixin"
+}
+
+castExpression ::= parenthesizedExpression expression {
+ rightAssociative = true
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MECastExpressionMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MECastExpressionImplMixin"
+}
+
+binaryExpression ::= expression binaryOp expression {
+ pin = 2
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEBinaryExpressionMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEBinaryExpressionImplMixin"
+ methods = [
+ leftExpr = "expression[0]"
+ rightExpr = "expression[1]"
+ ]
+}
+
+private binaryOp ::= multiplicativeOp |
+ additiveOp |
+ shiftOp |
+ comparisonOp |
+ TOKEN_INSTANCEOF |
+ equalityOp |
+ TOKEN_BITWISE_AND |
+ TOKEN_BITWISE_XOR |
+ TOKEN_BITWISE_OR
+
+private multiplicativeOp ::= TOKEN_MULT | TOKEN_DIV | TOKEN_MOD
+private additiveOp ::= TOKEN_PLUS | TOKEN_MINUS
+private shiftOp ::= TOKEN_SHL | TOKEN_SHR | TOKEN_USHR
+private comparisonOp ::= TOKEN_LT | TOKEN_LE | TOKEN_GT | TOKEN_GE
+private equalityOp ::= TOKEN_EQ | TOKEN_NE
+
+newExpression ::= TOKEN_NEW name (
+ (TOKEN_LEFT_PAREN arguments rightParen) |
+ (
+ TOKEN_LEFT_BRACKET expression? rightBracket
+ ( TOKEN_LEFT_BRACKET expression? rightBracket )*
+ ( TOKEN_LEFT_BRACE arguments rightBrace )?
+ )
+) {
+ pin = 1
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENewExpressionMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENewExpressionImplMixin"
+ methods = [
+ type = "name"
+ dimExprs = "expression"
+ ]
+}
+
+litExpression ::= decimalLitExpression | intLitExpression | stringLitExpression | boolLitExpression | nulLLitExpression {
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MELitExpressionMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MELitExpressionImplMixin"
+}
+
+private decimalLitExpression ::= TOKEN_MINUS? TOKEN_DEC_LIT {
+ extends = litExpression
+}
+
+private intLitExpression ::= TOKEN_MINUS? TOKEN_INT_LIT {
+ extends = litExpression
+}
+
+private stringLitExpression ::= TOKEN_STRING_TERMINATOR ( TOKEN_STRING | TOKEN_STRING_ESCAPE )* TOKEN_STRING_TERMINATOR {
+ pin = 1
+ extends = litExpression
+}
+
+private boolLitExpression ::= TOKEN_BOOL_LIT {
+ extends = litExpression
+}
+
+private nulLLitExpression ::= TOKEN_NULL_LIT {
+ extends = litExpression
+}
+
+thisExpression ::= TOKEN_THIS {
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METhisExpressionImplMixin"
+}
+
+nameExpression ::= name {
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENameExpressionImplMixin"
+ methods = [
+ MEName = "name"
+ ]
+}
+
+type ::= name ( TOKEN_LEFT_BRACKET TOKEN_RIGHT_BRACKET )* {
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.METypeMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.METypeImplMixin"
+ methods = [
+ MEName = "name"
+ ]
+}
+
+name ::= TOKEN_IDENTIFIER | TOKEN_WILDCARD {
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENameMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MENameImplMixin"
+}
+
+arguments ::= (expression (TOKEN_COMMA expression)*)? {
+ implements = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArgumentsMixin"
+ mixin = "com.demonwav.mcdev.platform.mixin.expression.psi.mixins.impl.MEArgumentsImplMixin"
+}
diff --git a/src/main/kotlin/MinecraftConfigurable.kt b/src/main/kotlin/MinecraftConfigurable.kt
index 60f5a32ab..12fc11567 100644
--- a/src/main/kotlin/MinecraftConfigurable.kt
+++ b/src/main/kotlin/MinecraftConfigurable.kt
@@ -86,13 +86,6 @@ class MinecraftConfigurable : Configurable {
}
}
- group(MCDevBundle("minecraft.settings.mixin")) {
- row {
- checkBox(MCDevBundle("minecraft.settings.mixin.shadow_annotation_same_line"))
- .bindSelected(settings::isShadowAnnotationsSameLine)
- }
- }
-
group(MCDevBundle("minecraft.settings.creator")) {
row(MCDevBundle("minecraft.settings.creator.repos")) {}
diff --git a/src/main/kotlin/MinecraftProjectConfigurable.kt b/src/main/kotlin/MinecraftProjectConfigurable.kt
new file mode 100644
index 000000000..0e676f0b7
--- /dev/null
+++ b/src/main/kotlin/MinecraftProjectConfigurable.kt
@@ -0,0 +1,64 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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
+
+import com.demonwav.mcdev.asset.MCDevBundle
+import com.demonwav.mcdev.util.BeforeOrAfter
+import com.intellij.openapi.options.Configurable
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.ui.DialogPanel
+import com.intellij.ui.EnumComboBoxModel
+import com.intellij.ui.dsl.builder.bindItem
+import com.intellij.ui.dsl.builder.bindSelected
+import com.intellij.ui.dsl.builder.panel
+import javax.swing.JComponent
+import org.jetbrains.annotations.Nls
+
+class MinecraftProjectConfigurable(private val project: Project) : Configurable {
+ private lateinit var panel: DialogPanel
+
+ @Nls
+ override fun getDisplayName() = MCDevBundle("minecraft.settings.project.display_name")
+
+ override fun createComponent(): JComponent = panel {
+ val settings = MinecraftProjectSettings.getInstance(project)
+
+ group(MCDevBundle("minecraft.settings.mixin")) {
+ row {
+ checkBox(MCDevBundle("minecraft.settings.mixin.shadow_annotation_same_line"))
+ .bindSelected(settings::isShadowAnnotationsSameLine)
+ }
+ row {
+ label(MCDevBundle("minecraft.settings.mixin.definition_pos_relative_to_expression"))
+ comboBox(EnumComboBoxModel(BeforeOrAfter::class.java))
+ .bindItem(settings::definitionPosRelativeToExpression) {
+ settings.definitionPosRelativeToExpression = it ?: BeforeOrAfter.BEFORE
+ }
+ }
+ }
+ }.also { panel = it }
+
+ override fun isModified(): Boolean = panel.isModified()
+
+ override fun apply() = panel.apply()
+
+ override fun reset() = panel.reset()
+}
diff --git a/src/main/kotlin/MinecraftProjectSettings.kt b/src/main/kotlin/MinecraftProjectSettings.kt
new file mode 100644
index 000000000..f22bf178b
--- /dev/null
+++ b/src/main/kotlin/MinecraftProjectSettings.kt
@@ -0,0 +1,46 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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
+
+import com.demonwav.mcdev.util.BeforeOrAfter
+import com.intellij.openapi.components.PersistentStateComponent
+import com.intellij.openapi.components.Service
+import com.intellij.openapi.components.State
+import com.intellij.openapi.components.Storage
+import com.intellij.openapi.components.service
+import com.intellij.openapi.project.Project
+import com.intellij.util.xmlb.XmlSerializerUtil
+
+@Service(Service.Level.PROJECT)
+@State(name = "MinecraftSettings", storages = [Storage("minecraft_dev.xml")])
+class MinecraftProjectSettings : PersistentStateComponent {
+ var isShadowAnnotationsSameLine = true
+ var definitionPosRelativeToExpression = BeforeOrAfter.BEFORE
+
+ override fun getState() = this
+ override fun loadState(state: MinecraftProjectSettings) {
+ XmlSerializerUtil.copyBean(state, this)
+ }
+
+ companion object {
+ fun getInstance(project: Project) = project.service()
+ }
+}
diff --git a/src/main/kotlin/MinecraftSettings.kt b/src/main/kotlin/MinecraftSettings.kt
index 0a924aa64..18ae02acf 100644
--- a/src/main/kotlin/MinecraftSettings.kt
+++ b/src/main/kotlin/MinecraftSettings.kt
@@ -40,8 +40,6 @@ class MinecraftSettings : PersistentStateComponent {
var isShowChatColorUnderlines: Boolean = false,
var underlineType: UnderlineType = UnderlineType.DOTTED,
- var isShadowAnnotationsSameLine: Boolean = true,
-
var creatorTemplateRepos: List = listOf(TemplateRepo.makeBuiltinRepo()),
)
@@ -108,12 +106,6 @@ class MinecraftSettings : PersistentStateComponent {
state.underlineType = underlineType
}
- var isShadowAnnotationsSameLine: Boolean
- get() = state.isShadowAnnotationsSameLine
- set(shadowAnnotationsSameLine) {
- state.isShadowAnnotationsSameLine = shadowAnnotationsSameLine
- }
-
var creatorTemplateRepos: List
get() = state.creatorTemplateRepos.map { it.copy() }
set(creatorTemplateRepos) {
diff --git a/src/main/kotlin/asset/MCDevBundle.kt b/src/main/kotlin/asset/MCDevBundle.kt
index 04b9b3d0b..6246c2bae 100644
--- a/src/main/kotlin/asset/MCDevBundle.kt
+++ b/src/main/kotlin/asset/MCDevBundle.kt
@@ -21,6 +21,7 @@
package com.demonwav.mcdev.asset
import com.intellij.DynamicBundle
+import java.util.function.Supplier
import org.jetbrains.annotations.NonNls
import org.jetbrains.annotations.PropertyKey
@@ -36,4 +37,9 @@ object MCDevBundle : DynamicBundle(BUNDLE) {
operator fun invoke(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?): String {
return getMessage(key, *params)
}
+
+ fun pointer(@PropertyKey(resourceBundle = BUNDLE) key: String) = Supplier { invoke(key) }
+
+ fun pointer(@PropertyKey(resourceBundle = BUNDLE) key: String, vararg params: Any?) =
+ Supplier { invoke(key, params) }
}
diff --git a/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt b/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt
index 712fce2de..dca1c1108 100644
--- a/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt
+++ b/src/main/kotlin/nbt/lang/colors/NbttColorSettingsPage.kt
@@ -88,21 +88,24 @@ class NbttColorSettingsPage : ColorSettingsPage {
companion object {
private val DESCRIPTORS = arrayOf(
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.keyword.display_name"), KEYWORD),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.string.display_name"), STRING),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.unquoted_string.display_name"), UNQUOTED_STRING),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.name.display_name"), STRING_NAME),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.keyword.display_name"), KEYWORD),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.string.display_name"), STRING),
AttributesDescriptor(
- MCDevBundle("nbt.lang.highlighting.unquoted_name.display_name"),
+ MCDevBundle.pointer("nbt.lang.highlighting.unquoted_string.display_name"),
+ UNQUOTED_STRING
+ ),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.name.display_name"), STRING_NAME),
+ AttributesDescriptor(
+ MCDevBundle.pointer("nbt.lang.highlighting.unquoted_name.display_name"),
UNQUOTED_STRING_NAME
),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.byte.display_name"), BYTE),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.short.display_name"), SHORT),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.int.display_name"), INT),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.long.display_name"), LONG),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.float.display_name"), FLOAT),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.double.display_name"), DOUBLE),
- AttributesDescriptor(MCDevBundle("nbt.lang.highlighting.material.display_name"), MATERIAL),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.byte.display_name"), BYTE),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.short.display_name"), SHORT),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.int.display_name"), INT),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.long.display_name"), LONG),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.float.display_name"), FLOAT),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.double.display_name"), DOUBLE),
+ AttributesDescriptor(MCDevBundle.pointer("nbt.lang.highlighting.material.display_name"), MATERIAL),
)
private val map = mapOf(
diff --git a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt
index f53938550..293543c47 100644
--- a/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt
+++ b/src/main/kotlin/platform/mixin/action/GenerateShadowAction.kt
@@ -20,7 +20,7 @@
package com.demonwav.mcdev.platform.mixin.action
-import com.demonwav.mcdev.MinecraftSettings
+import com.demonwav.mcdev.MinecraftProjectSettings
import com.demonwav.mcdev.platform.mixin.util.MixinConstants
import com.demonwav.mcdev.platform.mixin.util.findFields
import com.demonwav.mcdev.platform.mixin.util.findMethods
@@ -237,7 +237,7 @@ private fun copyAnnotation(modifiers: PsiModifierList, newModifiers: PsiModifier
}
inline fun disableAnnotationWrapping(project: Project, func: () -> Unit) {
- if (!MinecraftSettings.instance.isShadowAnnotationsSameLine) {
+ if (!MinecraftProjectSettings.getInstance(project).isShadowAnnotationsSameLine) {
func()
return
}
diff --git a/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt b/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt
index d8d5535a8..d3a5c35f0 100644
--- a/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt
+++ b/src/main/kotlin/platform/mixin/completion/MixinCompletionConfidence.kt
@@ -39,6 +39,7 @@ class MixinCompletionConfidence : CompletionConfidence() {
PsiJavaPatterns.psiAnnotation().qName(
StandardPatterns.or(
StandardPatterns.string().startsWith(MixinConstants.PACKAGE),
+ StandardPatterns.string().startsWith(MixinConstants.MixinExtras.PACKAGE),
StandardPatterns.string()
.oneOf(MixinAnnotationHandler.getBuiltinHandlers().map { it.first }.toList()),
)
diff --git a/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt
new file mode 100644
index 000000000..147b34023
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEDefinitionFoldingBuilder.kt
@@ -0,0 +1,117 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.MixinModuleType
+import com.demonwav.mcdev.platform.mixin.folding.MixinFoldingSettings
+import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReference
+import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.util.MemberReference
+import com.intellij.lang.ASTNode
+import com.intellij.lang.folding.CustomFoldingBuilder
+import com.intellij.lang.folding.FoldingDescriptor
+import com.intellij.openapi.editor.Document
+import com.intellij.openapi.util.TextRange
+import com.intellij.psi.JavaRecursiveElementWalkingVisitor
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiJavaFile
+import com.intellij.psi.PsiLiteralExpression
+import com.intellij.psi.PsiModifierList
+import com.intellij.psi.util.PsiTreeUtil
+
+class MEDefinitionFoldingBuilder : CustomFoldingBuilder() {
+ override fun isDumbAware() = false
+
+ override fun isRegionCollapsedByDefault(node: ASTNode): Boolean =
+ MixinFoldingSettings.instance.state.foldDefinitions
+
+ override fun getLanguagePlaceholderText(node: ASTNode, range: TextRange): String {
+ val psi = node.psi
+ if (psi is PsiLiteralExpression) {
+ val value = psi.value as? String ?: return "..."
+ val memberReference = MemberReference.parse(value) ?: return "..."
+ return memberReference.presentableText
+ }
+ return "..."
+ }
+
+ override fun buildLanguageFoldRegions(
+ descriptors: MutableList,
+ root: PsiElement,
+ document: Document,
+ quick: Boolean
+ ) {
+ if (root !is PsiJavaFile || !MixinModuleType.isInModule(root)) {
+ return
+ }
+
+ root.accept(Visitor(descriptors))
+ }
+
+ private class Visitor(private val descriptors: MutableList) :
+ JavaRecursiveElementWalkingVisitor() {
+ override fun visitModifierList(list: PsiModifierList) {
+ val currentDefinitionList = mutableListOf()
+ val definitionLists = mutableListOf>()
+
+ for (annotation in list.annotations) {
+ if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) {
+ currentDefinitionList += annotation
+ } else if (currentDefinitionList.isNotEmpty()) {
+ definitionLists += currentDefinitionList.toList()
+ currentDefinitionList.clear()
+ }
+ }
+
+ if (currentDefinitionList.isNotEmpty()) {
+ definitionLists += currentDefinitionList
+ }
+
+ if (definitionLists.isEmpty()) {
+ return
+ }
+
+ for (definitionList in definitionLists) {
+ val range = TextRange(
+ definitionList.first().parameterList.firstChild.nextSibling.textRange.startOffset,
+ PsiTreeUtil.getDeepestVisibleLast(definitionList.last())!!.textRange.startOffset,
+ )
+ if (!range.isEmpty) {
+ descriptors.add(FoldingDescriptor(list.node, range))
+ }
+ }
+
+ super.visitModifierList(list)
+ }
+
+ override fun visitLiteralExpression(expression: PsiLiteralExpression) {
+ if (FieldDefinitionReference.ELEMENT_PATTERN.accepts(expression) ||
+ MethodDefinitionReference.ELEMENT_PATTERN.accepts(expression)
+ ) {
+ if (MemberReference.parse(expression.value as String) != null) {
+ descriptors.add(FoldingDescriptor(expression.node, expression.textRange))
+ }
+ }
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt
new file mode 100644
index 000000000..638bf6f13
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionAnnotator.kt
@@ -0,0 +1,368 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.asset.MCDevBundle
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBinaryExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBoundMethodReferenceExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclaration
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEFreeMethodReferenceExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MELitExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMemberAccessExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMethodCallExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENewExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStaticMethodCallExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MESuperCallExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.util.findMultiInjectionHost
+import com.intellij.codeInsight.AutoPopupController
+import com.intellij.codeInspection.InspectionManager
+import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
+import com.intellij.codeInspection.ProblemHighlightType
+import com.intellij.codeInspection.RemoveAnnotationQuickFix
+import com.intellij.lang.annotation.AnnotationBuilder
+import com.intellij.lang.annotation.AnnotationHolder
+import com.intellij.lang.annotation.Annotator
+import com.intellij.lang.annotation.HighlightSeverity
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.colors.TextAttributesKey
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiDocumentManager
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFile
+import com.intellij.psi.PsiModifierListOwner
+import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil
+import com.intellij.psi.search.searches.ReferencesSearch
+import com.intellij.psi.util.TypeConversionUtil
+import com.intellij.psi.util.parentOfType
+
+class MEExpressionAnnotator : Annotator {
+ override fun annotate(element: PsiElement, holder: AnnotationHolder) {
+ when (element) {
+ is MEDeclaration -> {
+ val parent = element.parent as? MEDeclarationItem ?: return
+ if (parent.isType) {
+ highlightDeclaration(holder, element, MEExpressionSyntaxHighlighter.IDENTIFIER_TYPE_DECLARATION)
+ } else {
+ highlightDeclaration(holder, element, MEExpressionSyntaxHighlighter.IDENTIFIER_DECLARATION)
+ }
+ }
+ is MEName -> {
+ if (!element.isWildcard) {
+ when (val parent = element.parent) {
+ is METype,
+ is MENewExpression -> highlightType(holder, element)
+ is MEMemberAccessExpression -> highlightVariable(
+ holder,
+ element,
+ MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME,
+ true,
+ )
+ is MESuperCallExpression,
+ is MEMethodCallExpression,
+ is MEStaticMethodCallExpression,
+ is MEBoundMethodReferenceExpression,
+ is MEFreeMethodReferenceExpression -> highlightVariable(
+ holder,
+ element,
+ MEExpressionSyntaxHighlighter.IDENTIFIER_CALL,
+ false,
+ )
+ is MENameExpression -> {
+ if (METypeUtil.isExpressionDirectlyInTypePosition(parent)) {
+ highlightType(holder, element)
+ } else {
+ highlightVariable(
+ holder,
+ element,
+ MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE,
+ false,
+ )
+ }
+ }
+ else -> highlightType(holder, element)
+ }
+ }
+ }
+ is MELitExpression -> {
+ val minusToken = element.minusToken
+ if (minusToken != null) {
+ holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES)
+ .range(minusToken)
+ .textAttributes(MEExpressionSyntaxHighlighter.NUMBER)
+ .create()
+ }
+
+ if (!element.isNull && !element.isString && element.value == null) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.invalid_number")
+ )
+ .range(element)
+ .create()
+ }
+ }
+ is MEBinaryExpression -> {
+ val rightExpr = element.rightExpr
+ if (element.operator == MEExpressionTypes.TOKEN_INSTANCEOF &&
+ rightExpr !is MENameExpression &&
+ rightExpr !is MEArrayAccessExpression &&
+ rightExpr != null
+ ) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.instanceof_non_type")
+ )
+ .range(rightExpr)
+ .create()
+ }
+ }
+ is MEArrayAccessExpression -> {
+ if (METypeUtil.isExpressionDirectlyInTypePosition(element)) {
+ val indexExpr = element.indexExpr
+ if (indexExpr != null) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.index_not_expected_in_type"),
+ )
+ .range(indexExpr)
+ .create()
+ }
+ val arrayExpr = element.arrayExpr
+ if (arrayExpr !is MEArrayAccessExpression && arrayExpr !is MENameExpression) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.instanceof_non_type"),
+ )
+ .range(arrayExpr)
+ .create()
+ }
+ } else if (element.indexExpr == null) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.array_access_missing_index"),
+ )
+ .range(element.leftBracketToken)
+ .create()
+ }
+ }
+ is MENewExpression -> {
+ if (element.isArrayCreation) {
+ val initializer = element.arrayInitializer
+ if (initializer != null) {
+ if (element.dimExprs.isNotEmpty()) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.new_array_dim_expr_with_initializer"),
+ )
+ .range(initializer)
+ .create()
+ } else if (initializer.expressionList.isEmpty()) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.empty_array_initializer"),
+ )
+ .range(initializer)
+ .create()
+ }
+ } else {
+ if (element.dimExprs.isEmpty()) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.missing_array_length")
+ )
+ .range(element.dimExprTokens[0].leftBracket)
+ .create()
+ } else {
+ element.dimExprTokens.asSequence().dropWhile { it.expr != null }.forEach {
+ if (it.expr != null) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.array_length_after_empty")
+ )
+ .range(it.expr)
+ .create()
+ }
+ }
+ }
+ }
+ } else if (!element.hasConstructorArguments) {
+ val type = element.type
+ if (type != null) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.new_no_constructor_args_or_array"),
+ )
+ .range(type)
+ .create()
+ }
+ }
+ }
+ }
+ }
+
+ private fun highlightDeclaration(
+ holder: AnnotationHolder,
+ declaration: MEDeclaration,
+ defaultColor: TextAttributesKey,
+ ) {
+ val isUnused = ReferencesSearch.search(declaration).findFirst() == null
+
+ if (isUnused) {
+ val message = MCDevBundle("mixinextras.expression.lang.errors.unused_definition")
+ val annotation = holder.newAnnotation(HighlightSeverity.WARNING, message)
+ .range(declaration)
+ .highlightType(ProblemHighlightType.LIKE_UNUSED_SYMBOL)
+
+ val containingAnnotation = declaration.findMultiInjectionHost()?.parentOfType()?.takeIf {
+ it.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)
+ }
+ if (containingAnnotation != null) {
+ val inspectionManager = InspectionManager.getInstance(containingAnnotation.project)
+ @Suppress("StatefulEp") // IntelliJ is wrong here
+ val fix = object : RemoveAnnotationQuickFix(
+ containingAnnotation,
+ containingAnnotation.parentOfType()
+ ) {
+ override fun getFamilyName() = MCDevBundle("mixinextras.expression.lang.errors.unused_symbol.fix")
+ }
+ val problemDescriptor = inspectionManager.createProblemDescriptor(
+ declaration,
+ message,
+ fix,
+ ProblemHighlightType.LIKE_UNKNOWN_SYMBOL,
+ true
+ )
+ annotation.newLocalQuickFix(fix, problemDescriptor).registerFix()
+ }
+
+ annotation.create()
+ } else {
+ holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES)
+ .range(declaration)
+ .textAttributes(defaultColor)
+ .create()
+ }
+ }
+
+ private fun highlightType(holder: AnnotationHolder, type: MEName) {
+ val typeName = type.text
+ val isPrimitive = typeName != "void" && TypeConversionUtil.isPrimitive(typeName)
+ val isUnresolved = !isPrimitive && type.reference?.resolve() == null
+
+ if (isUnresolved) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.unresolved_symbol")
+ )
+ .range(type)
+ .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL)
+ .withDefinitionFix(type)
+ .create()
+ } else {
+ holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES)
+ .range(type)
+ .textAttributes(
+ if (isPrimitive) {
+ MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE
+ } else {
+ MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME
+ }
+ )
+ .create()
+ }
+ }
+
+ private fun highlightVariable(
+ holder: AnnotationHolder,
+ variable: MEName,
+ defaultColor: TextAttributesKey,
+ isMember: Boolean,
+ ) {
+ val variableName = variable.text
+ val isUnresolved = (variableName != "length" || !isMember) && variable.reference?.resolve() == null
+
+ if (isUnresolved) {
+ holder.newAnnotation(
+ HighlightSeverity.ERROR,
+ MCDevBundle("mixinextras.expression.lang.errors.unresolved_symbol")
+ )
+ .range(variable)
+ .highlightType(ProblemHighlightType.LIKE_UNKNOWN_SYMBOL)
+ .withDefinitionFix(variable)
+ .create()
+ } else {
+ holder.newSilentAnnotation(HighlightSeverity.TEXT_ATTRIBUTES)
+ .range(variable)
+ .textAttributes(defaultColor)
+ .create()
+ }
+ }
+
+ private fun AnnotationBuilder.withDefinitionFix(name: MEName) =
+ withFix(AddDefinitionInspection(name))
+
+ private class AddDefinitionInspection(name: MEName) : LocalQuickFixAndIntentionActionOnPsiElement(name) {
+ private val id = name.text
+
+ override fun getFamilyName(): String = "Add @Definition"
+
+ override fun getText(): String = "$familyName(id = \"$id\")"
+
+ override fun invoke(
+ project: Project,
+ file: PsiFile,
+ editor: Editor?,
+ startElement: PsiElement,
+ endElement: PsiElement
+ ) {
+ if (editor == null) {
+ MEExpressionCompletionUtil.addDefinition(
+ project,
+ startElement,
+ id,
+ ""
+ )
+ return
+ }
+ val annotation = MEExpressionCompletionUtil.addDefinition(
+ project,
+ startElement,
+ id,
+ "dummy"
+ ) ?: return
+ val dummy = annotation.findAttribute("dummy") as? PsiElement ?: return
+ val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(editor)
+ hostEditor.caretModel.moveToOffset(dummy.textOffset)
+ PsiDocumentManager.getInstance(project).doPostponedOperationsAndUnblockDocument(hostEditor.document)
+ hostEditor.document.replaceString(dummy.textRange.startOffset, dummy.textRange.endOffset, "")
+ AutoPopupController.getInstance(project).autoPopupMemberLookup(hostEditor, null)
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt
new file mode 100644
index 000000000..323856006
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionBraceMatcher.kt
@@ -0,0 +1,41 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.intellij.lang.BracePair
+import com.intellij.lang.PairedBraceMatcher
+import com.intellij.psi.PsiFile
+import com.intellij.psi.tree.IElementType
+
+class MEExpressionBraceMatcher : PairedBraceMatcher {
+ companion object {
+ private val PAIRS = arrayOf(
+ BracePair(MEExpressionTypes.TOKEN_LEFT_PAREN, MEExpressionTypes.TOKEN_RIGHT_PAREN, false),
+ BracePair(MEExpressionTypes.TOKEN_LEFT_BRACKET, MEExpressionTypes.TOKEN_RIGHT_BRACKET, false),
+ BracePair(MEExpressionTypes.TOKEN_LEFT_BRACE, MEExpressionTypes.TOKEN_RIGHT_BRACE, false),
+ )
+ }
+
+ override fun getPairs() = PAIRS
+ override fun isPairedBracesAllowedBeforeType(lbraceType: IElementType, contextType: IElementType?) = true
+ override fun getCodeConstructStart(file: PsiFile?, openingBraceOffset: Int) = openingBraceOffset
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt
new file mode 100644
index 000000000..964bd4a67
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionColorSettingsPage.kt
@@ -0,0 +1,153 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.asset.MCDevBundle
+import com.demonwav.mcdev.asset.PlatformAssets
+import com.intellij.openapi.options.colors.AttributesDescriptor
+import com.intellij.openapi.options.colors.ColorDescriptor
+import com.intellij.openapi.options.colors.ColorSettingsPage
+
+class MEExpressionColorSettingsPage : ColorSettingsPage {
+ companion object {
+ private val DESCRIPTORS = arrayOf(
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.string.display_name"),
+ MEExpressionSyntaxHighlighter.STRING
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.string_escape.display_name"),
+ MEExpressionSyntaxHighlighter.STRING_ESCAPE
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.number.display_name"),
+ MEExpressionSyntaxHighlighter.NUMBER
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.keyword.display_name"),
+ MEExpressionSyntaxHighlighter.KEYWORD
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.operator.display_name"),
+ MEExpressionSyntaxHighlighter.OPERATOR
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.parens.display_name"),
+ MEExpressionSyntaxHighlighter.PARENS
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.brackets.display_name"),
+ MEExpressionSyntaxHighlighter.BRACKETS
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.braces.display_name"),
+ MEExpressionSyntaxHighlighter.BRACES
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.dot.display_name"),
+ MEExpressionSyntaxHighlighter.DOT
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.method_reference.display_name"),
+ MEExpressionSyntaxHighlighter.METHOD_REFERENCE
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.comma.display_name"),
+ MEExpressionSyntaxHighlighter.COMMA
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.capture.display_name"),
+ MEExpressionSyntaxHighlighter.CAPTURE
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.wildcard.display_name"),
+ MEExpressionSyntaxHighlighter.WILDCARD
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.identifier.display_name"),
+ MEExpressionSyntaxHighlighter.IDENTIFIER
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.call_identifier.display_name"),
+ MEExpressionSyntaxHighlighter.IDENTIFIER_CALL
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.class_name_identifier.display_name"),
+ MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.primitive_type_identifier.display_name"),
+ MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.member_name_identifier.display_name"),
+ MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.variable_identifier.display_name"),
+ MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer(
+ "mixinextras.expression.lang.highlighting.type_declaration_identifier.display_name"
+ ),
+ MEExpressionSyntaxHighlighter.IDENTIFIER_TYPE_DECLARATION
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.declaration_identifier.display_name"),
+ MEExpressionSyntaxHighlighter.IDENTIFIER_DECLARATION
+ ),
+ AttributesDescriptor(
+ MCDevBundle.pointer("mixinextras.expression.lang.highlighting.bad_char.display_name"),
+ MEExpressionSyntaxHighlighter.BAD_CHAR
+ ),
+ )
+
+ private val TAGS = mapOf(
+ "call" to MEExpressionSyntaxHighlighter.IDENTIFIER_CALL,
+ "class_name" to MEExpressionSyntaxHighlighter.IDENTIFIER_CLASS_NAME,
+ "member_name" to MEExpressionSyntaxHighlighter.IDENTIFIER_MEMBER_NAME,
+ "primitive_type" to MEExpressionSyntaxHighlighter.IDENTIFIER_PRIMITIVE_TYPE,
+ "variable" to MEExpressionSyntaxHighlighter.IDENTIFIER_VARIABLE,
+ )
+ }
+
+ override fun getIcon() = PlatformAssets.MIXIN_ICON
+ override fun getHighlighter() = MEExpressionSyntaxHighlighter()
+
+ override fun getDemoText() = """
+ variable.function(
+ 'a string with \\ escapes',
+ 123 + @(45),
+ ?,
+ ClassName.class,
+ foo.bar,
+ new int[] { 1, 2, 3 },
+ method::reference,
+ 'a bad character: ' # other_identifier
+ )[0]
+ """.trimIndent()
+
+ override fun getAdditionalHighlightingTagToDescriptorMap() = TAGS
+ override fun getAttributeDescriptors() = DESCRIPTORS
+ override fun getColorDescriptors(): Array = ColorDescriptor.EMPTY_ARRAY
+ override fun getDisplayName() = MCDevBundle("mixinextras.expression.lang.display_name")
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt
new file mode 100644
index 000000000..339634a39
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionContributor.kt
@@ -0,0 +1,138 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.intellij.codeInsight.TailType
+import com.intellij.codeInsight.completion.BasicExpressionCompletionContributor
+import com.intellij.codeInsight.completion.CompletionContributor
+import com.intellij.codeInsight.completion.CompletionParameters
+import com.intellij.codeInsight.completion.CompletionProvider
+import com.intellij.codeInsight.completion.CompletionResultSet
+import com.intellij.codeInsight.completion.CompletionType
+import com.intellij.codeInsight.completion.InsertionContext
+import com.intellij.codeInsight.lookup.LookupElement
+import com.intellij.codeInsight.lookup.TailTypeDecorator
+import com.intellij.util.ProcessingContext
+
+class MEExpressionCompletionContributor : CompletionContributor() {
+ init {
+ extend(
+ CompletionType.BASIC,
+ MEExpressionCompletionUtil.STATEMENT_KEYWORD_PLACE,
+ KeywordCompletionProvider(
+ Keyword("return", TailType.INSERT_SPACE),
+ Keyword("throw", TailType.INSERT_SPACE),
+ )
+ )
+ extend(
+ CompletionType.BASIC,
+ MEExpressionCompletionUtil.VALUE_KEYWORD_PLACE,
+ KeywordCompletionProvider(
+ Keyword("this"),
+ Keyword("super"),
+ Keyword("true"),
+ Keyword("false"),
+ Keyword("null"),
+ Keyword("new", TailType.INSERT_SPACE),
+ )
+ )
+ extend(
+ CompletionType.BASIC,
+ MEExpressionCompletionUtil.CLASS_PLACE,
+ KeywordCompletionProvider(
+ Keyword("class")
+ )
+ )
+ extend(
+ CompletionType.BASIC,
+ MEExpressionCompletionUtil.INSTANCEOF_PLACE,
+ KeywordCompletionProvider(
+ Keyword("instanceof", TailType.INSERT_SPACE)
+ )
+ )
+ extend(
+ CompletionType.BASIC,
+ MEExpressionCompletionUtil.METHOD_REFERENCE_PLACE,
+ KeywordCompletionProvider(
+ Keyword("new")
+ )
+ )
+ extend(
+ CompletionType.BASIC,
+ MEExpressionCompletionUtil.STRING_LITERAL_PLACE,
+ object : CompletionProvider() {
+ override fun addCompletions(
+ parameters: CompletionParameters,
+ context: ProcessingContext,
+ result: CompletionResultSet
+ ) {
+ result.addAllElements(
+ MEExpressionCompletionUtil.getStringCompletions(
+ parameters.originalFile.project,
+ parameters.position
+ )
+ )
+ }
+ }
+ )
+ extend(
+ CompletionType.BASIC,
+ MEExpressionCompletionUtil.FROM_BYTECODE_PLACE,
+ object : CompletionProvider() {
+ override fun addCompletions(
+ parameters: CompletionParameters,
+ context: ProcessingContext,
+ result: CompletionResultSet
+ ) {
+ val project = parameters.originalFile.project
+ result.addAllElements(
+ MEExpressionCompletionUtil.getCompletionVariantsFromBytecode(project, parameters.position)
+ )
+ }
+ }
+ )
+ }
+
+ private class KeywordCompletionProvider(
+ private vararg val keywords: Keyword,
+ ) : CompletionProvider() {
+ override fun addCompletions(
+ parameters: CompletionParameters,
+ context: ProcessingContext,
+ result: CompletionResultSet
+ ) {
+ result.addAllElements(
+ keywords.map { keyword ->
+ var lookupItem =
+ BasicExpressionCompletionContributor.createKeywordLookupItem(parameters.position, keyword.name)
+ if (keyword.tailType != TailType.NONE) {
+ lookupItem = object : TailTypeDecorator(lookupItem) {
+ override fun computeTailType(context: InsertionContext?) = keyword.tailType
+ }
+ }
+ lookupItem
+ }
+ )
+ }
+ }
+
+ private class Keyword(val name: String, val tailType: TailType = TailType.NONE)
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt
new file mode 100644
index 000000000..c172b57d6
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionCompletionUtil.kt
@@ -0,0 +1,1339 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.MinecraftProjectSettings
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil.virtualInsn
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil.virtualInsnOrNull
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEAssignStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBoundMethodReferenceExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECapturingExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECastExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEClassConstantExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEFreeMethodReferenceExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MELitExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMemberAccessExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEMethodCallExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENewExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatementItem
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStaticMethodCallExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MESuperCallExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil
+import com.demonwav.mcdev.platform.mixin.expression.psi.MERecursiveWalkingVisitor
+import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil
+import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil.notInTypePosition
+import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil.validType
+import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.util.AsmDfaUtil
+import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.platform.mixin.util.SignatureToPsi
+import com.demonwav.mcdev.platform.mixin.util.canonicalName
+import com.demonwav.mcdev.platform.mixin.util.hasAccess
+import com.demonwav.mcdev.platform.mixin.util.isPrimitive
+import com.demonwav.mcdev.platform.mixin.util.mixinTargets
+import com.demonwav.mcdev.platform.mixin.util.textify
+import com.demonwav.mcdev.platform.mixin.util.toPsiType
+import com.demonwav.mcdev.util.BeforeOrAfter
+import com.demonwav.mcdev.util.constantStringValue
+import com.demonwav.mcdev.util.findContainingClass
+import com.demonwav.mcdev.util.findContainingModifierList
+import com.demonwav.mcdev.util.findContainingNameValuePair
+import com.demonwav.mcdev.util.findModule
+import com.demonwav.mcdev.util.findMultiInjectionHost
+import com.demonwav.mcdev.util.invokeLater
+import com.demonwav.mcdev.util.mapFirstNotNull
+import com.demonwav.mcdev.util.packageName
+import com.intellij.codeInsight.TailType
+import com.intellij.codeInsight.completion.InsertionContext
+import com.intellij.codeInsight.folding.CodeFoldingManager
+import com.intellij.codeInsight.lookup.LookupElement
+import com.intellij.codeInsight.lookup.LookupElementBuilder
+import com.intellij.codeInsight.lookup.TailTypeDecorator
+import com.intellij.codeInsight.template.Expression
+import com.intellij.codeInsight.template.Template
+import com.intellij.codeInsight.template.TemplateBuilderImpl
+import com.intellij.codeInsight.template.TemplateEditingAdapter
+import com.intellij.codeInsight.template.TemplateManager
+import com.intellij.codeInsight.template.TextResult
+import com.intellij.openapi.application.runWriteAction
+import com.intellij.openapi.command.CommandProcessor
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.editor.FoldRegion
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PlatformPatterns
+import com.intellij.patterns.StandardPatterns
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiAnonymousClass
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiDocumentManager
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFile
+import com.intellij.psi.PsiModifierList
+import com.intellij.psi.codeStyle.CodeStyleManager
+import com.intellij.psi.codeStyle.JavaCodeStyleManager
+import com.intellij.psi.impl.source.tree.injected.InjectedLanguageEditorUtil
+import com.intellij.psi.tree.TokenSet
+import com.intellij.psi.util.PsiTreeUtil
+import com.intellij.psi.util.PsiUtil
+import com.intellij.psi.util.createSmartPointer
+import com.intellij.psi.util.parentOfType
+import com.intellij.psi.util.parents
+import com.intellij.util.PlatformIcons
+import com.intellij.util.text.CharArrayUtil
+import com.llamalad7.mixinextras.expression.impl.flow.ComplexFlowValue
+import com.llamalad7.mixinextras.expression.impl.flow.DummyFlowValue
+import com.llamalad7.mixinextras.expression.impl.flow.FlowValue
+import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander
+import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.InstantiationInfo
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
+import com.llamalad7.mixinextras.expression.impl.pool.IdentifierPool
+import com.llamalad7.mixinextras.expression.impl.utils.FlowDecorations
+import org.apache.commons.lang3.mutable.MutableInt
+import org.objectweb.asm.Handle
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+import org.objectweb.asm.signature.SignatureReader
+import org.objectweb.asm.tree.AbstractInsnNode
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.FieldInsnNode
+import org.objectweb.asm.tree.IincInsnNode
+import org.objectweb.asm.tree.InsnNode
+import org.objectweb.asm.tree.IntInsnNode
+import org.objectweb.asm.tree.InvokeDynamicInsnNode
+import org.objectweb.asm.tree.LdcInsnNode
+import org.objectweb.asm.tree.MethodInsnNode
+import org.objectweb.asm.tree.MethodNode
+import org.objectweb.asm.tree.MultiANewArrayInsnNode
+import org.objectweb.asm.tree.TypeInsnNode
+import org.objectweb.asm.tree.VarInsnNode
+
+private typealias TemplateExpressionContext = com.intellij.codeInsight.template.ExpressionContext
+
+object MEExpressionCompletionUtil {
+ private const val DEBUG_COMPLETION = false
+
+ private val NORMAL_ELEMENT = PlatformPatterns.psiElement()
+ .inside(MEStatement::class.java)
+ .andNot(PlatformPatterns.psiElement().inside(MELitExpression::class.java))
+ .notInTypePosition()
+ private val TYPE_PATTERN = PlatformPatterns.psiElement()
+ .inside(MEStatement::class.java)
+ .validType()
+ private val AFTER_END_EXPRESSION_PATTERN = StandardPatterns.or(
+ PlatformPatterns.psiElement().afterLeaf(
+ PlatformPatterns.psiElement().withElementType(
+ TokenSet.create(
+ MEExpressionTypes.TOKEN_IDENTIFIER,
+ MEExpressionTypes.TOKEN_WILDCARD,
+ MEExpressionTypes.TOKEN_RIGHT_PAREN,
+ MEExpressionTypes.TOKEN_RIGHT_BRACKET,
+ MEExpressionTypes.TOKEN_RIGHT_BRACE,
+ MEExpressionTypes.TOKEN_BOOL_LIT,
+ MEExpressionTypes.TOKEN_CLASS,
+ MEExpressionTypes.TOKEN_INT_LIT,
+ MEExpressionTypes.TOKEN_DEC_LIT,
+ MEExpressionTypes.TOKEN_NULL_LIT,
+ MEExpressionTypes.TOKEN_STRING_TERMINATOR,
+ )
+ )
+ ),
+ PlatformPatterns.psiElement().afterLeaf(PlatformPatterns.psiElement().withText("new").afterLeaf("::")),
+ )
+
+ val STATEMENT_KEYWORD_PLACE = PlatformPatterns.psiElement().afterLeaf(
+ PlatformPatterns.psiElement().withText("{").withParent(MEStatementItem::class.java)
+ )
+ val VALUE_KEYWORD_PLACE = StandardPatterns.and(
+ NORMAL_ELEMENT,
+ StandardPatterns.not(AFTER_END_EXPRESSION_PATTERN),
+ StandardPatterns.not(PlatformPatterns.psiElement().afterLeaf(".")),
+ StandardPatterns.not(PlatformPatterns.psiElement().afterLeaf("::")),
+ )
+ val CLASS_PLACE = StandardPatterns.and(
+ NORMAL_ELEMENT,
+ PlatformPatterns.psiElement()
+ .afterLeaf(
+ PlatformPatterns.psiElement().withText(".")
+ .withParent(PlatformPatterns.psiElement().withFirstChild(TYPE_PATTERN))
+ ),
+ )
+ val INSTANCEOF_PLACE = StandardPatterns.and(
+ NORMAL_ELEMENT,
+ AFTER_END_EXPRESSION_PATTERN,
+ )
+ val METHOD_REFERENCE_PLACE = StandardPatterns.and(
+ NORMAL_ELEMENT,
+ PlatformPatterns.psiElement().afterLeaf("::"),
+ )
+ val STRING_LITERAL_PLACE = PlatformPatterns.psiElement().withElementType(
+ TokenSet.create(MEExpressionTypes.TOKEN_STRING, MEExpressionTypes.TOKEN_STRING_TERMINATOR)
+ )
+ val FROM_BYTECODE_PLACE = PlatformPatterns.psiElement()
+ .inside(MEStatement::class.java)
+ .andNot(PlatformPatterns.psiElement().inside(MELitExpression::class.java))
+
+ private val DOT_CLASS_TAIL = object : TailType() {
+ override fun processTail(editor: Editor, tailOffset: Int): Int {
+ editor.document.insertString(tailOffset, ".class")
+ return moveCaret(editor, tailOffset, 6)
+ }
+
+ override fun isApplicable(context: InsertionContext): Boolean {
+ val chars = context.document.charsSequence
+ val dotOffset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t")
+ if (!CharArrayUtil.regionMatches(chars, dotOffset, ".")) {
+ return true
+ }
+ val classOffset = CharArrayUtil.shiftForward(chars, dotOffset + 1, " \n\t")
+ return !CharArrayUtil.regionMatches(chars, classOffset, "class")
+ }
+ }
+
+ private val COLON_COLON_NEW_TAIL = object : TailType() {
+ override fun processTail(editor: Editor, tailOffset: Int): Int {
+ editor.document.insertString(tailOffset, "::new")
+ return moveCaret(editor, tailOffset, 5)
+ }
+
+ override fun isApplicable(context: InsertionContext): Boolean {
+ val chars = context.document.charsSequence
+ val colonColonOffset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t")
+ if (!CharArrayUtil.regionMatches(chars, colonColonOffset, "::")) {
+ return true
+ }
+ val newOffset = CharArrayUtil.shiftForward(chars, colonColonOffset + 2, " \n\t")
+ return !CharArrayUtil.regionMatches(chars, newOffset, "new")
+ }
+ }
+
+ fun getStringCompletions(project: Project, contextElement: PsiElement): List {
+ val expressionAnnotation = contextElement.findMultiInjectionHost()?.parentOfType()
+ ?: return emptyList()
+ if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) {
+ return emptyList()
+ }
+
+ val modifierList = expressionAnnotation.findContainingModifierList() ?: return emptyList()
+
+ val (handler, handlerAnnotation) = modifierList.annotations.mapFirstNotNull { annotation ->
+ val qName = annotation.qualifiedName ?: return@mapFirstNotNull null
+ val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null
+ handler to annotation
+ } ?: return emptyList()
+
+ return handler.resolveTarget(handlerAnnotation).flatMap {
+ (it as? MethodTargetMember)?.classAndMethod?.method?.instructions?.mapNotNull { insn ->
+ if (insn is LdcInsnNode && insn.cst is String) {
+ LookupElementBuilder.create(insn.cst)
+ } else {
+ null
+ }
+ } ?: emptyList()
+ }
+ }
+
+ fun getCompletionVariantsFromBytecode(project: Project, contextElement: PsiElement): List {
+ val statement = contextElement.parentOfType() ?: return emptyList()
+
+ val expressionAnnotation = contextElement.findMultiInjectionHost()?.parentOfType()
+ ?: return emptyList()
+ if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) {
+ return emptyList()
+ }
+
+ val modifierList = expressionAnnotation.findContainingModifierList() ?: return emptyList()
+ val module = modifierList.findModule() ?: return emptyList()
+
+ val mixinClass = modifierList.findContainingClass() ?: return emptyList()
+
+ val (handler, handlerAnnotation) = modifierList.annotations.mapFirstNotNull { annotation ->
+ val qName = annotation.qualifiedName ?: return@mapFirstNotNull null
+ val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null
+ handler to annotation
+ } ?: return emptyList()
+
+ val cursorOffset = contextElement.textRange.startOffset - statement.textRange.startOffset
+
+ return mixinClass.mixinTargets.flatMap { targetClass ->
+ val poolFactory = MEExpressionMatchUtil.createIdentifierPoolFactory(module, targetClass, modifierList)
+ handler.resolveTarget(handlerAnnotation, targetClass)
+ .filterIsInstance()
+ .flatMap { methodTarget ->
+ getCompletionVariantsFromBytecode(
+ project,
+ mixinClass,
+ cursorOffset,
+ statement.copy() as MEStatement,
+ targetClass,
+ methodTarget.classAndMethod.method,
+ poolFactory,
+ )
+ }
+ }
+ }
+
+ private fun getCompletionVariantsFromBytecode(
+ project: Project,
+ mixinClass: PsiClass,
+ cursorOffsetIn: Int,
+ statement: MEStatement,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ poolFactory: IdentifierPoolFactory,
+ ): List {
+ /*
+ * MixinExtras isn't designed to match against incomplete expressions, which is what we need to do to produce
+ * completion options. The only support there is, is to match incomplete parameter lists and so on
+ * ("list inputs" to expressions). What follows is a kind of DIY match where we figure out different options
+ * for what the user might be trying to complete and hand it to MixinExtras to do the actual matching. Note that
+ * IntelliJ already inserts an identifier at the caret position to make auto-completion easier.
+ *
+ * We have four classes of problems to solve here:
+ * 1. There may already be a capture in the expression causing MixinExtras to return the wrong instructions.
+ * 2. There may be unresolved identifiers in the expression, causing MixinExtras to match nothing, which isn't
+ * ideal.
+ * 3. "this." expands to a field access, but the user may be trying to complete a method call (and other
+ * similar situations).
+ * 4. What the user is typing may form only a subexpression of a larger expression. For example, with
+ * "foo()", the user may actually be trying to type the expression "foo(x + y) + z". That is, "x",
+ * which is where the caret is, may not be a direct subexpression to the "foo" call expression, which itself
+ * may not be a direct subexpression of its parent.
+ *
+ * Throughout this process, we have to keep careful track of where the caret is, because:
+ * 1. As we make changes to the expression to the left of the caret, the caret may shift.
+ * 2. As we make copies of the element, or entirely new elements, that new element's textOffset may be different
+ * from the original one.
+ */
+
+ if (DEBUG_COMPLETION) {
+ println("======")
+ println(targetMethod.textify())
+ println("======")
+ }
+
+ if (targetMethod.instructions == null) {
+ return emptyList()
+ }
+
+ val cursorOffset = MutableInt(cursorOffsetIn)
+ val pool = poolFactory(targetMethod)
+ val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, targetMethod) ?: return emptyList()
+
+ // Removing all explicit captures from the expression solves problem 1 (see comment above).
+ removeExplicitCaptures(statement, cursorOffset)
+ // Replacing unresolved names with wildcards solves problem 2 (see comment above).
+ replaceUnresolvedNamesWithWildcards(project, statement, cursorOffset, pool)
+
+ val elementAtCursor = statement.findElementAt(cursorOffset.toInt()) ?: return emptyList()
+
+ /*
+ * To solve problem 4 (see comment above), we first find matches for the top level statement, ignoring the
+ * subexpression that the caret is on. Then we iterate down into the subexpression that contains the caret and
+ * match that against all the statement's input flows in the same way as we matched the statement against all
+ * the instructions in the target method. Then we keep iterating until we reach the identifier the caret is on.
+ */
+
+ // Replace the subexpression the caret is on with a wildcard expression, so MixinExtras ignores it.
+ val wildcardReplacedStatement = statement.copy() as MEStatement
+ var cursorOffsetInCopyFile =
+ cursorOffset.toInt() - statement.textRange.startOffset + wildcardReplacedStatement.textRange.startOffset
+ replaceCursorInputWithWildcard(project, wildcardReplacedStatement, cursorOffsetInCopyFile)
+
+ // Iterate through possible "variants" of the statement that the user may be trying to complete; it doesn't
+ // matter if they don't parse, then we just skip them. This solves problem 3 (see comment above).
+ var matchingFlows = mutableListOf()
+ for (statementToMatch in getStatementVariants(project.meExpressionElementFactory, wildcardReplacedStatement)) {
+ if (DEBUG_COMPLETION) {
+ println("Matching against statement ${statementToMatch.text}")
+ }
+
+ val meStatement = MEExpressionMatchUtil.createExpression(statementToMatch.text) ?: continue
+ MEExpressionMatchUtil.findMatchingInstructions(
+ targetClass,
+ targetMethod,
+ pool,
+ flows,
+ meStatement,
+ flows.keys,
+ ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // use most permissive type for completion
+ true,
+ ) { match ->
+ matchingFlows += match.flow
+ if (DEBUG_COMPLETION) {
+ println("Matched ${match.flow.virtualInsnOrNull?.insn?.textify()}")
+ }
+ }
+ }
+ if (matchingFlows.isEmpty()) {
+ return emptyList()
+ }
+
+ // Iterate through subexpressions until we reach the identifier the caret is on
+ var roundNumber = 0
+ var subExpr: MEMatchableElement = statement
+ while (true) {
+ // Replace the subexpression the caret is on with a wildcard expression, so MixinExtras ignores it.
+ val inputExprOnCursor = subExpr.getInputExprs().firstOrNull { it.textRange.contains(cursorOffset.toInt()) }
+ ?: break
+ val wildcardReplacedExpr = inputExprOnCursor.copy() as MEExpression
+ cursorOffsetInCopyFile = cursorOffset.toInt() -
+ inputExprOnCursor.textRange.startOffset + wildcardReplacedExpr.textRange.startOffset
+
+ if (DEBUG_COMPLETION) {
+ val exprText = wildcardReplacedExpr.text
+ val cursorOffsetInExpr = cursorOffsetInCopyFile - wildcardReplacedExpr.textRange.startOffset
+ val exprWithCaretMarker = when {
+ cursorOffsetInExpr < 0 -> "$exprText"
+ cursorOffsetInExpr > exprText.length -> "$exprText"
+ else -> exprText.replaceRange(cursorOffsetInExpr, cursorOffsetInExpr, "")
+ }
+ println("=== Round ${++roundNumber}: handling $exprWithCaretMarker")
+ }
+
+ replaceCursorInputWithWildcard(project, wildcardReplacedExpr, cursorOffsetInCopyFile)
+
+ // Iterate through the possible "varaints" of the expression in the same way as we did for the statement
+ // above. This solves problem 3 (see comment above).
+ val newMatchingFlows = mutableSetOf()
+ for (exprToMatch in getExpressionVariants(project.meExpressionElementFactory, wildcardReplacedExpr)) {
+ if (DEBUG_COMPLETION) {
+ println("Matching against expression ${exprToMatch.text}")
+ }
+
+ val meExpression = MEExpressionMatchUtil.createExpression(exprToMatch.text) ?: continue
+
+ val flattenedInstructions = mutableSetOf()
+ for (flow in matchingFlows) {
+ getInstructionsInFlowTree(
+ flow,
+ flattenedInstructions,
+ subExpr !is MEExpressionStatement && subExpr !is MEParenthesizedExpression
+ )
+ }
+
+ MEExpressionMatchUtil.findMatchingInstructions(
+ targetClass,
+ targetMethod,
+ pool,
+ flows,
+ meExpression,
+ flattenedInstructions.map { it.insn },
+ ExpressionContext.Type.MODIFY_EXPRESSION_VALUE, // use most permissive type for completion
+ true,
+ ) { match ->
+ newMatchingFlows += match.flow
+ if (DEBUG_COMPLETION) {
+ println("Matched ${match.flow.virtualInsnOrNull?.insn?.textify()}")
+ }
+ }
+ }
+
+ if (newMatchingFlows.isEmpty()) {
+ return emptyList()
+ }
+ matchingFlows = newMatchingFlows.toMutableList()
+
+ subExpr = inputExprOnCursor
+ }
+
+ val cursorInstructions = mutableSetOf()
+ for (flow in matchingFlows) {
+ getInstructionsInFlowTree(flow, cursorInstructions, false)
+ }
+
+ if (DEBUG_COMPLETION) {
+ println("Found ${cursorInstructions.size} matching instructions:")
+ for (insn in cursorInstructions) {
+ println("- ${insn.insn.insn.textify()}")
+ }
+ }
+
+ // Try to decide if we should be completing types or normal expressions.
+ // Not as easy as it sounds (think incomplete casts looking like parenthesized expressions).
+ // Note that it's possible to complete types and expressions at the same time.
+ val isInsideMeType = PsiTreeUtil.getParentOfType(
+ elementAtCursor,
+ METype::class.java,
+ false,
+ MEExpression::class.java
+ ) != null
+ val isInsideNewExpr = PsiTreeUtil.getParentOfType(
+ elementAtCursor,
+ MENewExpression::class.java,
+ false,
+ MEExpression::class.java
+ ) != null
+ val cursorExprInTypePosition = !isInsideMeType &&
+ elementAtCursor.parentOfType()?.let(METypeUtil::isExpressionInTypePosition) == true
+ val inTypePosition = isInsideMeType || isInsideNewExpr || cursorExprInTypePosition
+ val isPossiblyIncompleteCast = !inTypePosition &&
+ elementAtCursor.parentOfType()
+ ?.parents(false)
+ ?.dropWhile { it is MEArrayAccessExpression && it.indexExpr == null }
+ ?.firstOrNull() is MEParenthesizedExpression
+ val canCompleteExprs = !inTypePosition
+ val canCompleteTypes = inTypePosition || isPossiblyIncompleteCast
+
+ if (DEBUG_COMPLETION) {
+ println("canCompleteExprs = $canCompleteExprs")
+ println("canCompleteTypes = $canCompleteTypes")
+ }
+
+ val eliminableResults = cursorInstructions.flatMap { insn ->
+ getCompletionsForInstruction(
+ project,
+ targetClass,
+ targetMethod,
+ insn.insn,
+ insn.originalInsn,
+ flows,
+ mixinClass,
+ canCompleteExprs,
+ canCompleteTypes
+ )
+ }
+
+ // In the case of multiple instructions producing the same lookup, attempt to show only the "best" lookup.
+ // For example, if a local variable is only sometimes able to be targeted using implicit ordinals in this
+ // expression, prefer specifying the ordinal.
+ return eliminableResults.groupBy { it.uniquenessKey }.values.map { it.max().lookupElement }
+ }
+
+ private fun replaceUnresolvedNamesWithWildcards(
+ project: Project,
+ statement: MEStatement,
+ cursorOffset: MutableInt,
+ pool: IdentifierPool,
+ ) {
+ val unresolvedNames = mutableListOf()
+ statement.accept(object : MERecursiveWalkingVisitor() {
+ override fun visitType(o: METype) {
+ val name = o.meName
+ if (!name.isWildcard && !pool.typeExists(name.text)) {
+ unresolvedNames += name
+ }
+ }
+
+ override fun visitNameExpression(o: MENameExpression) {
+ val name = o.meName
+ if (!name.isWildcard) {
+ if (METypeUtil.isExpressionDirectlyInTypePosition(o)) {
+ if (!pool.typeExists(name.text)) {
+ unresolvedNames += name
+ }
+ } else {
+ if (!pool.memberExists(name.text)) {
+ unresolvedNames += name
+ }
+ }
+ }
+ }
+
+ override fun visitSuperCallExpression(o: MESuperCallExpression) {
+ val name = o.memberName
+ if (name != null && !name.isWildcard && !pool.memberExists(name.text)) {
+ unresolvedNames += name
+ }
+ super.visitSuperCallExpression(o)
+ }
+
+ override fun visitMethodCallExpression(o: MEMethodCallExpression) {
+ val name = o.memberName
+ if (!name.isWildcard && !pool.memberExists(name.text)) {
+ unresolvedNames += name
+ }
+ super.visitMethodCallExpression(o)
+ }
+
+ override fun visitStaticMethodCallExpression(o: MEStaticMethodCallExpression) {
+ val name = o.memberName
+ if (!name.isWildcard && !pool.memberExists(name.text)) {
+ unresolvedNames += name
+ }
+ super.visitStaticMethodCallExpression(o)
+ }
+
+ override fun visitBoundMethodReferenceExpression(o: MEBoundMethodReferenceExpression) {
+ val name = o.memberName
+ if (name != null && !name.isWildcard && !pool.memberExists(name.text)) {
+ unresolvedNames += name
+ }
+ super.visitBoundMethodReferenceExpression(o)
+ }
+
+ override fun visitFreeMethodReferenceExpression(o: MEFreeMethodReferenceExpression) {
+ val name = o.memberName
+ if (name != null && !name.isWildcard && !pool.memberExists(name.text)) {
+ unresolvedNames += name
+ }
+ super.visitFreeMethodReferenceExpression(o)
+ }
+
+ override fun visitMemberAccessExpression(o: MEMemberAccessExpression) {
+ val name = o.memberName
+ if (!name.isWildcard && !pool.memberExists(name.text)) {
+ unresolvedNames += name
+ }
+ super.visitMemberAccessExpression(o)
+ }
+
+ override fun visitNewExpression(o: MENewExpression) {
+ val name = o.type
+ if (name != null && !name.isWildcard && !pool.typeExists(name.text)) {
+ unresolvedNames += name
+ }
+ super.visitNewExpression(o)
+ }
+ })
+
+ for (unresolvedName in unresolvedNames) {
+ val startOffset = unresolvedName.textRange.startOffset
+ if (cursorOffset.toInt() > startOffset) {
+ cursorOffset.setValue(cursorOffset.toInt() - unresolvedName.textLength + 1)
+ }
+
+ unresolvedName.replace(project.meExpressionElementFactory.createName("?"))
+ }
+ }
+
+ private fun removeExplicitCaptures(statement: MEStatement, cursorOffset: MutableInt) {
+ val captures = mutableListOf()
+
+ statement.accept(object : MERecursiveWalkingVisitor() {
+ override fun elementFinished(element: PsiElement) {
+ // do this on elementFinished to ensure that inner captures are replaced before outer captures
+ if (element is MECapturingExpression) {
+ captures += element
+ }
+ }
+ })
+
+ for (capture in captures) {
+ val innerExpr = capture.expression ?: continue
+ val textRange = capture.textRange
+
+ if (cursorOffset.toInt() > textRange.startOffset) {
+ cursorOffset.setValue(cursorOffset.toInt() - if (cursorOffset.toInt() >= textRange.endOffset) 3 else 2)
+ }
+
+ capture.replace(innerExpr)
+ }
+ }
+
+ private fun replaceCursorInputWithWildcard(project: Project, element: MEMatchableElement, cursorOffset: Int) {
+ for (input in element.getInputExprs()) {
+ if (input.textRange.contains(cursorOffset)) {
+ input.replace(project.meExpressionElementFactory.createExpression("?"))
+ return
+ }
+ }
+ }
+
+ private fun getInstructionsInFlowTree(
+ flow: FlowValue,
+ outInstructions: MutableSet,
+ strict: Boolean,
+ ) {
+ if (flow is DummyFlowValue || flow is ComplexFlowValue) {
+ return
+ }
+
+ if (!strict) {
+ val originalInsn = InsnExpander.getRepresentative(flow) ?: flow.insn
+ if (!outInstructions.add(ExpandedInstruction(flow.virtualInsn, originalInsn))) {
+ return
+ }
+ }
+ for (i in 0 until flow.inputCount()) {
+ getInstructionsInFlowTree(flow.getInput(i), outInstructions, false)
+ }
+ }
+
+ private fun getCompletionsForInstruction(
+ project: Project,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ insn: VirtualInsn,
+ originalInsn: AbstractInsnNode,
+ flows: FlowMap,
+ mixinClass: PsiClass,
+ canCompleteExprs: Boolean,
+ canCompleteTypes: Boolean
+ ): List {
+ val flow = flows[insn]
+ when (insn.insn) {
+ is LdcInsnNode -> {
+ when (val cst = insn.insn.cst) {
+ is Type -> {
+ if (canCompleteExprs && cst.isAccessibleFrom(mixinClass)) {
+ return listOf(
+ createTypeLookup(cst)
+ .withTailText(".class")
+ .withTail(DOT_CLASS_TAIL)
+ .createEliminable("class ${insn.insn.cst}")
+ )
+ }
+ }
+ }
+ }
+ is VarInsnNode -> return createLocalVariableLookups(
+ project,
+ targetClass,
+ targetMethod,
+ originalInsn,
+ insn.insn.`var`,
+ insn.insn.opcode in Opcodes.ISTORE..Opcodes.ASTORE,
+ mixinClass
+ )
+ is IincInsnNode -> return createLocalVariableLookups(
+ project,
+ targetClass,
+ targetMethod,
+ originalInsn,
+ insn.insn.`var`,
+ false,
+ mixinClass
+ )
+ is FieldInsnNode -> {
+ if (canCompleteExprs) {
+ val definitionValue = "field = \"L${insn.insn.owner};${insn.insn.name}:${insn.insn.desc}\""
+ var lookup = createUniqueLookup(insn.insn.name.toValidIdentifier())
+ .withIcon(PlatformIcons.FIELD_ICON)
+ .withPresentableText(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name)
+ .withTypeText(Type.getType(insn.insn.desc).presentableName())
+ .withDefinitionAndFold(insn.insn.name.toValidIdentifier(), "field", definitionValue)
+ if (insn.insn.opcode == Opcodes.GETSTATIC || insn.insn.opcode == Opcodes.PUTSTATIC) {
+ lookup = lookup.withLookupString(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name)
+ }
+ return listOf(
+ lookup.createEliminable("field ${insn.insn.owner}.${insn.insn.name}:${insn.insn.desc}")
+ )
+ }
+ }
+ is MethodInsnNode -> {
+ if (canCompleteExprs) {
+ val definitionValue = "method = \"L${insn.insn.owner};${insn.insn.name}${insn.insn.desc}\""
+ var lookup = createUniqueLookup(insn.insn.name.toValidIdentifier())
+ .withIcon(PlatformIcons.METHOD_ICON)
+ .withPresentableText(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name)
+ .withDescTailText(insn.insn.desc)
+ .withTypeText(Type.getReturnType(insn.insn.desc).presentableName())
+ .withDefinitionAndFold(insn.insn.name.toValidIdentifier(), "method", definitionValue)
+ if (insn.insn.opcode == Opcodes.INVOKESTATIC) {
+ lookup = lookup.withLookupString(insn.insn.owner.substringAfterLast('/') + "." + insn.insn.name)
+ }
+ return listOf(
+ lookup.withTail(ParenthesesTailType(!insn.insn.desc.startsWith("()")))
+ .createEliminable("invoke ${insn.insn.owner}.${insn.insn.name}${insn.insn.desc}")
+ )
+ }
+ }
+ is TypeInsnNode -> {
+ val type = Type.getObjectType(insn.insn.desc)
+ if (canCompleteTypes && type.isAccessibleFrom(mixinClass)) {
+ val lookup = createTypeLookup(type)
+ when (insn.insn.opcode) {
+ Opcodes.ANEWARRAY -> {
+ val arrayType = Type.getType('[' + Type.getObjectType(insn.insn.desc).descriptor)
+ return createNewArrayCompletion(flow, arrayType)
+ }
+ Opcodes.NEW -> {
+ val initCall = flow
+ ?.getDecoration(FlowDecorations.INSTANTIATION_INFO)
+ ?.initCall
+ ?.virtualInsnOrNull
+ ?.insn as? MethodInsnNode
+ ?: return emptyList()
+ return listOf(
+ lookup
+ .withDescTailText(initCall.desc)
+ .withTail(ParenthesesTailType(!initCall.desc.startsWith("()")))
+ .createEliminable("new ${insn.insn.desc}${initCall.desc}")
+ )
+ }
+ else -> return listOf(lookup.createEliminable("type ${insn.insn.desc}"))
+ }
+ }
+ }
+ is IntInsnNode -> {
+ if (insn.insn.opcode == Opcodes.NEWARRAY) {
+ if (canCompleteTypes) {
+ val arrayType = Type.getType(
+ when (insn.insn.operand) {
+ Opcodes.T_BOOLEAN -> "[B"
+ Opcodes.T_CHAR -> "[C"
+ Opcodes.T_FLOAT -> "[F"
+ Opcodes.T_DOUBLE -> "[D"
+ Opcodes.T_BYTE -> "[B"
+ Opcodes.T_SHORT -> "[S"
+ Opcodes.T_INT -> "[I"
+ Opcodes.T_LONG -> "[J"
+ else -> "[Lnull;" // wtf?
+ }
+ )
+ return createNewArrayCompletion(flow, arrayType)
+ }
+ }
+ }
+ is MultiANewArrayInsnNode -> {
+ if (canCompleteTypes) {
+ val arrayType = Type.getType(insn.insn.desc)
+ return createNewArrayCompletion(flow, arrayType)
+ }
+ }
+ is InsnNode -> {
+ when (insn.insn.opcode) {
+ Opcodes.ARRAYLENGTH -> {
+ if (canCompleteExprs) {
+ return listOf(
+ createUniqueLookup("length")
+ .withIcon(PlatformIcons.FIELD_ICON)
+ .withTypeText("int")
+ .createEliminable("arraylength")
+ )
+ }
+ }
+ }
+ }
+ is InvokeDynamicInsnNode -> {
+ if (insn.insn.bsm.owner == "java/lang/invoke/LambdaMetafactory") {
+ if (!canCompleteExprs) {
+ return emptyList()
+ }
+
+ val handle = insn.insn.bsmArgs.getOrNull(1) as? Handle ?: return emptyList()
+ val definitionValue = "method = \"L${handle.owner};${handle.name}${handle.desc}\""
+ if (handle.tag !in Opcodes.H_INVOKEVIRTUAL..Opcodes.H_INVOKEINTERFACE) {
+ return emptyList()
+ }
+ if (handle.tag == Opcodes.H_NEWINVOKESPECIAL) {
+ return listOf(
+ createTypeLookup(Type.getObjectType(handle.owner))
+ .withTailText("::new")
+ .withTail(COLON_COLON_NEW_TAIL)
+ .createEliminable("constructorRef ${handle.owner}")
+ )
+ } else {
+ return listOf(
+ createUniqueLookup(handle.name.toValidIdentifier())
+ .withIcon(PlatformIcons.METHOD_ICON)
+ .withPresentableText(handle.owner.substringAfterLast('/') + "." + handle.name)
+ .withTypeText(Type.getReturnType(handle.desc).presentableName())
+ .withDefinitionAndFold(handle.name.toValidIdentifier(), "method", definitionValue)
+ .createEliminable("methodRef ${handle.owner}.${handle.name}${handle.desc}")
+ )
+ }
+ }
+ }
+ }
+
+ return emptyList()
+ }
+
+ private fun Type.typeNameToInsert(): String {
+ if (sort == Type.ARRAY) {
+ return elementType.typeNameToInsert() + "[]".repeat(dimensions)
+ }
+ if (sort != Type.OBJECT) {
+ return className
+ }
+
+ val simpleName = internalName.substringAfterLast('/')
+ val lastValidCharIndex = (simpleName.length - 1 downTo 0).firstOrNull {
+ MEPsiUtil.isIdentifierStart(simpleName[it])
+ } ?: return "_" + simpleName.filterInvalidIdentifierChars()
+
+ return simpleName.substring(simpleName.lastIndexOf('$', lastValidCharIndex) + 1).toValidIdentifier()
+ }
+
+ private fun String.toValidIdentifier(): String {
+ return when {
+ isEmpty() -> "_"
+ !MEPsiUtil.isIdentifierStart(this[0]) -> "_" + filterInvalidIdentifierChars()
+ else -> this[0] + substring(1).filterInvalidIdentifierChars()
+ }
+ }
+
+ private fun String.filterInvalidIdentifierChars(): String {
+ return asSequence().joinToString("") {
+ if (MEPsiUtil.isIdentifierPart(it)) it.toString() else "_"
+ }
+ }
+
+ private fun Type.presentableName(): String = when (sort) {
+ Type.ARRAY -> elementType.presentableName() + "[]".repeat(dimensions)
+ Type.OBJECT -> internalName.substringAfterLast('/')
+ else -> className
+ }
+
+ private fun Type.isAccessibleFrom(fromClass: PsiClass): Boolean {
+ return when (sort) {
+ Type.ARRAY -> elementType.isAccessibleFrom(fromClass)
+ Type.OBJECT -> {
+ val facade = JavaPsiFacade.getInstance(fromClass.project)
+ val clazz = facade.findClass(canonicalName, fromClass.resolveScope) ?: return false
+ val pkg = fromClass.packageName?.let(facade::findPackage) ?: return false
+ clazz !is PsiAnonymousClass && PsiUtil.isAccessibleFromPackage(clazz, pkg)
+ }
+ else -> true
+ }
+ }
+
+ private fun createTypeLookup(type: Type): LookupElementBuilder {
+ val definitionId = type.typeNameToInsert()
+
+ val lookupElement = createUniqueLookup(definitionId)
+ .withIcon(PlatformIcons.CLASS_ICON)
+ .withPresentableText(type.presentableName())
+
+ return if (type.isPrimitive) {
+ lookupElement
+ } else {
+ lookupElement.withDefinition(definitionId, "type = ${type.canonicalName}.class")
+ }
+ }
+
+ private fun createNewArrayCompletion(flow: FlowValue?, arrayType: Type): List {
+ val hasInitializer = flow?.hasDecoration(FlowDecorations.ARRAY_CREATION_INFO) == true
+ val initializerText = if (hasInitializer) "{}" else ""
+ return listOf(
+ createTypeLookup(arrayType.elementType)
+ .withTailText("[]".repeat(arrayType.dimensions) + initializerText)
+ .withTail(
+ BracketsTailType(
+ arrayType.dimensions,
+ hasInitializer,
+ )
+ )
+ .createEliminable("new ${arrayType.descriptor}$initializerText")
+ )
+ }
+
+ private fun createLocalVariableLookups(
+ project: Project,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ originalInsn: AbstractInsnNode,
+ index: Int,
+ isStore: Boolean,
+ mixinClass: PsiClass,
+ ): List {
+ // ignore "this"
+ if (!targetMethod.hasAccess(Opcodes.ACC_STATIC) && index == 0) {
+ return emptyList()
+ }
+
+ var argumentsSize = Type.getArgumentsAndReturnSizes(targetMethod.desc) shr 2
+ if (targetMethod.hasAccess(Opcodes.ACC_STATIC)) {
+ argumentsSize--
+ }
+ val isArgsOnly = index < argumentsSize
+
+ if (targetMethod.localVariables != null) {
+ val localsHere = targetMethod.localVariables.filter { localVariable ->
+ val firstValidInstruction = if (isStore) {
+ generateSequence(localVariable.start) { it.previous }
+ .firstOrNull { it.opcode >= 0 }
+ } else {
+ localVariable.start.next
+ }
+ if (firstValidInstruction == null) {
+ return@filter false
+ }
+ val validRange = targetMethod.instructions.indexOf(firstValidInstruction) until
+ targetMethod.instructions.indexOf(localVariable.end)
+ targetMethod.instructions.indexOf(originalInsn) in validRange
+ }
+ val locals = localsHere.filter { it.index == index }
+
+ val elementFactory = JavaPsiFacade.getElementFactory(project)
+
+ return locals.map { localVariable ->
+ val localPsiType = if (localVariable.signature != null) {
+ val sigToPsi = SignatureToPsi(elementFactory, mixinClass)
+ SignatureReader(localVariable.signature).acceptType(sigToPsi)
+ sigToPsi.type
+ } else {
+ Type.getType(localVariable.desc).toPsiType(elementFactory, mixinClass)
+ }
+ val localsOfMyType = localsHere.filter { it.desc == localVariable.desc }
+ val ordinal = localsOfMyType.indexOf(localVariable)
+ val isImplicit = localsOfMyType.size == 1
+ val localName = localVariable.name.toValidIdentifier()
+ createUniqueLookup(localName)
+ .withIcon(PlatformIcons.VARIABLE_ICON)
+ .withTypeText(localPsiType.presentableText)
+ .withLocalDefinition(
+ localName,
+ Type.getType(localVariable.desc),
+ ordinal,
+ isArgsOnly,
+ isImplicit,
+ mixinClass,
+ )
+ .createEliminable("local $localName", if (isImplicit) -1 else 0)
+ }
+ }
+
+ // fallback to ASM dataflow
+ val localTypes = AsmDfaUtil.getLocalVariableTypes(project, targetClass, targetMethod, originalInsn)
+ ?: return emptyList()
+ val localType = localTypes.getOrNull(index) ?: return emptyList()
+ val ordinal = localTypes.asSequence().take(index).filter { it == localType }.count()
+ val localName = localType.typeNameToInsert().replace("[]", "Array") + (ordinal + 1)
+ val isImplicit = localTypes.count { it == localType } == 1
+ return listOf(
+ createUniqueLookup(localName)
+ .withIcon(PlatformIcons.VARIABLE_ICON)
+ .withTypeText(localType.presentableName())
+ .withLocalDefinition(localName, localType, ordinal, isArgsOnly, isImplicit, mixinClass)
+ .createEliminable("local $localName", if (isImplicit) -1 else 0)
+ )
+ }
+
+ private fun LookupElementBuilder.withDescTailText(desc: String) =
+ withTailText(
+ Type.getArgumentTypes(desc).joinToString(prefix = "(", postfix = ")") { it.presentableName() }
+ )
+
+ private fun LookupElement.withTail(tailType: TailType?) = object : TailTypeDecorator(this) {
+ override fun computeTailType(context: InsertionContext?) = tailType
+ }
+
+ private fun LookupElementBuilder.withDefinition(id: String, definitionValue: String) =
+ withDefinition(id, definitionValue) { _, _ -> }
+
+ private fun LookupElementBuilder.withDefinitionAndFold(id: String, foldAttribute: String, definitionValue: String) =
+ withDefinition(id, definitionValue) { context, annotation ->
+ val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(context.editor)
+ CodeFoldingManager.getInstance(context.project).updateFoldRegions(hostEditor)
+ val foldingModel = hostEditor.foldingModel
+ val regionsToFold = mutableListOf()
+ val annotationRange = annotation.textRange
+ for (foldRegion in foldingModel.allFoldRegions) {
+ if (!annotationRange.contains(foldRegion.textRange)) {
+ continue
+ }
+ val nameValuePair = annotation.findElementAt(foldRegion.startOffset - annotationRange.startOffset)
+ ?.findContainingNameValuePair() ?: continue
+ if (nameValuePair.name == foldAttribute &&
+ nameValuePair.parentOfType()?.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)
+ == true
+ ) {
+ regionsToFold += foldRegion
+ }
+ }
+
+ foldingModel.runBatchFoldingOperation {
+ for (foldRegion in regionsToFold) {
+ foldRegion.isExpanded = false
+ }
+ }
+ }
+
+ private fun LookupElementBuilder.withLocalDefinition(
+ name: String,
+ type: Type,
+ ordinal: Int,
+ isArgsOnly: Boolean,
+ canBeImplicit: Boolean,
+ mixinClass: PsiClass,
+ ): LookupElementBuilder {
+ val isTypeAccessible = type.isAccessibleFrom(mixinClass)
+ val isImplicit = canBeImplicit && isTypeAccessible
+
+ val definitionValue = buildString {
+ append("local = @${MixinConstants.MixinExtras.LOCAL}(")
+ if (isTypeAccessible) {
+ append("type = ${type.className}.class, ")
+ }
+ if (!isImplicit) {
+ append("ordinal = ")
+ append(ordinal)
+ append(", ")
+ }
+ if (isArgsOnly) {
+ append("argsOnly = true, ")
+ }
+
+ if (endsWith(", ")) {
+ setLength(length - 2)
+ }
+
+ append(")")
+ }
+ return withDefinition(name, definitionValue) { context, annotation ->
+ if (isImplicit) {
+ return@withDefinition
+ }
+
+ invokeLater {
+ WriteCommandAction.runWriteCommandAction(
+ context.project,
+ "Choose How to Target Local Variable",
+ null,
+ { runLocalTemplate(context.project, context.editor, context.file, annotation, ordinal, name) },
+ annotation.containingFile,
+ )
+ }
+ }
+ }
+
+ private fun runLocalTemplate(
+ project: Project,
+ editor: Editor,
+ file: PsiFile,
+ annotation: PsiAnnotation,
+ ordinal: Int,
+ name: String
+ ) {
+ val elementToReplace =
+ (annotation.findDeclaredAttributeValue("local") as? PsiAnnotation)
+ ?.findDeclaredAttributeValue("ordinal")
+ ?.findContainingNameValuePair() ?: return
+
+ val hostEditor = InjectedLanguageEditorUtil.getTopLevelEditor(editor)
+ val hostElement = file.findElementAt(editor.caretModel.offset)?.findMultiInjectionHost() ?: return
+
+ val template = TemplateBuilderImpl(annotation)
+ val lookupItems = arrayOf(
+ LookupElementBuilder.create("ordinal = $ordinal"),
+ LookupElementBuilder.create("name = \"$name\"")
+ )
+ template.replaceElement(
+ elementToReplace,
+ object : Expression() {
+ override fun calculateLookupItems(context: TemplateExpressionContext?) = lookupItems
+ override fun calculateQuickResult(context: TemplateExpressionContext?) = calculateResult(context)
+ override fun calculateResult(context: TemplateExpressionContext?) =
+ TextResult("ordinal = $ordinal")
+ },
+ true,
+ )
+
+ val prevCursorPosInLiteral = hostEditor.caretModel.offset - hostElement.textRange.startOffset
+ val hostElementPtr = hostElement.createSmartPointer(project)
+ hostEditor.caretModel.moveToOffset(annotation.textRange.startOffset)
+ TemplateManager.getInstance(project).startTemplate(
+ hostEditor,
+ template.buildInlineTemplate(),
+ object : TemplateEditingAdapter() {
+ override fun templateFinished(template: Template, brokenOff: Boolean) {
+ PsiDocumentManager.getInstance(project).commitDocument(hostEditor.document)
+ val newHostElement = hostElementPtr.element ?: return
+ hostEditor.caretModel.moveToOffset(newHostElement.textRange.startOffset + prevCursorPosInLiteral)
+ }
+ }
+ )
+ }
+
+ private inline fun LookupElementBuilder.withDefinition(
+ id: String,
+ definitionValue: String,
+ crossinline andThen: (InsertionContext, PsiAnnotation) -> Unit
+ ) = withInsertHandler { context, _ ->
+ context.laterRunnable = Runnable {
+ context.commitDocument()
+ CommandProcessor.getInstance().runUndoTransparentAction {
+ runWriteAction {
+ val annotation = addDefinition(context, id, definitionValue)
+ if (annotation != null) {
+ andThen(context, annotation)
+ }
+ }
+ }
+ }
+ }
+
+ private fun addDefinition(context: InsertionContext, id: String, definitionValue: String): PsiAnnotation? {
+ val contextElement = context.file.findElementAt(context.startOffset) ?: return null
+ return addDefinition(context.project, contextElement, id, definitionValue)
+ }
+
+ fun addDefinition(
+ project: Project,
+ contextElement: PsiElement,
+ id: String,
+ definitionValue: String
+ ): PsiAnnotation? {
+ val injectionHost = contextElement.findMultiInjectionHost() ?: return null
+ val expressionAnnotation = injectionHost.parentOfType() ?: return null
+ if (!expressionAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) {
+ return null
+ }
+ val modifierList = expressionAnnotation.findContainingModifierList() ?: return null
+
+ // look for an existing definition with this id, skip if it exists
+ for (annotation in modifierList.annotations) {
+ if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) &&
+ annotation.findDeclaredAttributeValue("id")?.constantStringValue == id
+ ) {
+ return null
+ }
+ }
+
+ // create and add the new @Definition annotation
+ var newAnnotation = JavaPsiFacade.getElementFactory(project).createAnnotationFromText(
+ "@${MixinConstants.MixinExtras.DEFINITION}(id = \"$id\", $definitionValue)",
+ modifierList,
+ )
+ var anchor = modifierList.annotations.lastOrNull { it.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) }
+ if (anchor == null) {
+ val definitionPosRelativeToExpression =
+ MinecraftProjectSettings.getInstance(project).definitionPosRelativeToExpression
+ if (definitionPosRelativeToExpression == BeforeOrAfter.AFTER) {
+ anchor = expressionAnnotation
+ }
+ }
+ newAnnotation = modifierList.addAfter(newAnnotation, anchor) as PsiAnnotation
+
+ // add imports and reformat
+ newAnnotation =
+ JavaCodeStyleManager.getInstance(project).shortenClassReferences(newAnnotation) as PsiAnnotation
+ JavaCodeStyleManager.getInstance(project).optimizeImports(modifierList.containingFile)
+ val annotationIndex = modifierList.annotations.indexOf(newAnnotation)
+ val formattedModifierList =
+ CodeStyleManager.getInstance(project).reformat(modifierList) as PsiModifierList
+ return formattedModifierList.annotations.getOrNull(annotationIndex)
+ }
+
+ private fun getStatementVariants(
+ factory: MEExpressionElementFactory,
+ statement: MEStatement
+ ): List {
+ return if (statement is MEExpressionStatement) {
+ getExpressionVariants(factory, statement.expression)
+ } else {
+ listOf(statement)
+ }
+ }
+
+ private fun getExpressionVariants(
+ factory: MEExpressionElementFactory,
+ expression: MEExpression
+ ): List {
+ val variants = mutableListOf(expression)
+
+ val assignmentStatement = factory.createStatement("? = ?") as MEAssignStatement
+ assignmentStatement.targetExpr.replace(expression.copy())
+ variants += assignmentStatement
+
+ when (expression) {
+ is MEParenthesizedExpression -> {
+ val castExpr = factory.createExpression("(?) ?") as MECastExpression
+ castExpr.castTypeExpr!!.replace(expression.copy())
+ variants += castExpr
+ }
+ is MENameExpression -> {
+ val callExpr = factory.createExpression("?()") as MEStaticMethodCallExpression
+ callExpr.memberName.replace(expression.meName)
+ variants += callExpr
+
+ val classExpr = factory.createExpression("${expression.text}.class") as MEClassConstantExpression
+ variants += classExpr
+ }
+ is MEMemberAccessExpression -> {
+ val callExpr = factory.createExpression("?.?()") as MEMethodCallExpression
+ callExpr.receiverExpr.replace(expression.receiverExpr)
+ callExpr.memberName.replace(expression.memberName)
+ variants += callExpr
+ }
+ is MENewExpression -> {
+ val type = expression.type
+ if (type != null && !expression.hasConstructorArguments && !expression.isArrayCreation) {
+ val fixedNewExpr = factory.createExpression("new ?()") as MENewExpression
+ fixedNewExpr.type!!.replace(type)
+ variants += fixedNewExpr
+
+ val fixedNewArrayExpr = factory.createExpression("new ?[?]") as MENewExpression
+ fixedNewArrayExpr.type!!.replace(type)
+ variants += fixedNewArrayExpr
+
+ val arrayLitExpr = factory.createExpression("new ?[]{?}") as MENewExpression
+ arrayLitExpr.type!!.replace(type)
+ variants += arrayLitExpr
+ }
+ }
+ is MESuperCallExpression -> {
+ // Might be missing its parentheses
+ val callExpr = factory.createExpression("super.?()") as MESuperCallExpression
+ expression.memberName?.let { callExpr.memberName!!.replace(it) }
+ variants += callExpr
+ }
+ }
+
+ return variants
+ }
+
+ private fun createUniqueLookup(text: String) = LookupElementBuilder.create(Any(), text)
+
+ private fun LookupElement.createEliminable(uniquenessKey: String, priority: Int = 0) =
+ EliminableLookup(uniquenessKey, this, priority)
+
+ private class EliminableLookup(
+ val uniquenessKey: String,
+ val lookupElement: LookupElement,
+ private val priority: Int
+ ) : Comparable {
+ override fun compareTo(other: EliminableLookup) = priority.compareTo(other.priority)
+ }
+
+ private data class ExpandedInstruction(val insn: VirtualInsn, val originalInsn: AbstractInsnNode)
+
+ private class ParenthesesTailType(private val hasParameters: Boolean) : TailType() {
+ override fun processTail(editor: Editor, tailOffset: Int): Int {
+ editor.document.insertString(tailOffset, "()")
+ return moveCaret(editor, tailOffset, if (hasParameters) 1 else 2)
+ }
+
+ override fun isApplicable(context: InsertionContext): Boolean {
+ val chars = context.document.charsSequence
+ val offset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t")
+ return !CharArrayUtil.regionMatches(chars, offset, "(")
+ }
+ }
+
+ private class BracketsTailType(private val dimensions: Int, private val hasInitializer: Boolean) : TailType() {
+ override fun processTail(editor: Editor, tailOffset: Int): Int {
+ editor.document.insertString(tailOffset, "[]".repeat(dimensions) + if (hasInitializer) "{}" else "")
+ return moveCaret(editor, tailOffset, if (hasInitializer) 2 * dimensions + 1 else 1)
+ }
+
+ override fun isApplicable(context: InsertionContext): Boolean {
+ val chars = context.document.charsSequence
+ val offset = CharArrayUtil.shiftForward(chars, context.tailOffset, " \n\t")
+ return !CharArrayUtil.regionMatches(chars, offset, "[")
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt
new file mode 100644
index 000000000..1f93df64b
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionElementFactory.kt
@@ -0,0 +1,73 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEClassConstantExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiFileFactory
+import com.intellij.util.IncorrectOperationException
+
+class MEExpressionElementFactory(private val project: Project) {
+ fun createFile(text: String): MEExpressionFile {
+ return PsiFileFactory.getInstance(project).createFileFromText(
+ "dummy.mixinextrasexpression",
+ MEExpressionFileType,
+ text
+ ) as MEExpressionFile
+ }
+
+ fun createStatement(text: String): MEStatement {
+ return createFile("do {$text}").statements.singleOrNull()
+ ?: throw IncorrectOperationException("'$text' is not a statement")
+ }
+
+ fun createExpression(text: String): MEExpression {
+ return (createStatement(text) as? MEExpressionStatement)?.expression
+ ?: throw IncorrectOperationException("'$text' is not an expression")
+ }
+
+ fun createName(text: String): MEName {
+ return (createExpression(text) as? MENameExpression)?.meName
+ ?: throw IncorrectOperationException("'$text' is not a name")
+ }
+
+ fun createIdentifier(text: String): PsiElement {
+ return createName(text).identifierElement
+ ?: throw IncorrectOperationException("'$text' is not an identifier")
+ }
+
+ fun createType(text: String): METype {
+ return (createExpression("$text.class") as? MEClassConstantExpression)?.type
+ ?: throw IncorrectOperationException("'$text' is not a type")
+ }
+
+ fun createType(name: MEName) = createType(name.text)
+}
+
+val Project.meExpressionElementFactory get() = MEExpressionElementFactory(this)
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt
new file mode 100644
index 000000000..87d4bd31c
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionFileType.kt
@@ -0,0 +1,31 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.asset.PlatformAssets
+import com.intellij.openapi.fileTypes.LanguageFileType
+
+object MEExpressionFileType : LanguageFileType(MEExpressionLanguage) {
+ override fun getName() = "MixinExtras Expression File"
+ override fun getDescription() = "MixinExtras expression file"
+ override fun getDefaultExtension() = "mixinextrasexpression"
+ override fun getIcon() = PlatformAssets.MIXIN_ICON
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt
new file mode 100644
index 000000000..22de47c71
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionInjector.kt
@@ -0,0 +1,207 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.util.findContainingModifierList
+import com.demonwav.mcdev.util.findContainingNameValuePair
+import com.intellij.lang.injection.InjectedLanguageManager
+import com.intellij.lang.injection.MultiHostInjector
+import com.intellij.lang.injection.MultiHostRegistrar
+import com.intellij.openapi.util.Key
+import com.intellij.openapi.util.TextRange
+import com.intellij.openapi.util.component1
+import com.intellij.openapi.util.component2
+import com.intellij.psi.ElementManipulators
+import com.intellij.psi.JavaTokenType
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiLanguageInjectionHost
+import com.intellij.psi.PsiLiteralExpression
+import com.intellij.psi.PsiParenthesizedExpression
+import com.intellij.psi.PsiPolyadicExpression
+import com.intellij.psi.impl.source.tree.injected.JavaConcatenationToInjectorAdapter
+import com.intellij.psi.util.PsiLiteralUtil
+import com.intellij.psi.util.PsiModificationTracker
+import com.intellij.psi.util.PsiUtil
+import com.intellij.psi.util.parentOfType
+import com.intellij.util.SmartList
+
+class MEExpressionInjector : MultiHostInjector {
+ companion object {
+ private val ELEMENTS = listOf(PsiLiteralExpression::class.java)
+ private val ME_EXPRESSION_INJECTION = Key.create("mcdev.meExpressionInjection")
+
+ private val CLASS_INJECTION_RESULT =
+ Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionResult")
+ private val CLASS_INJECTION_REGISTRAR_IMPL =
+ Class.forName("com.intellij.psi.impl.source.tree.injected.InjectionRegistrarImpl")
+ @JvmStatic
+ private val METHOD_ADD_TO_RESULTS =
+ CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("addToResults", CLASS_INJECTION_RESULT)
+ .also { it.isAccessible = true }
+ @JvmStatic
+ private val METHOD_GET_INJECTED_RESULT =
+ CLASS_INJECTION_REGISTRAR_IMPL.getDeclaredMethod("getInjectedResult")
+ .also { it.isAccessible = true }
+ }
+
+ private data class MEExpressionInjection(val modCount: Long, val injectionResult: Any)
+
+ private fun shouldInjectIn(anchor: PsiElement): Boolean {
+ val nameValuePair = anchor.findContainingNameValuePair() ?: return false
+ return when (nameValuePair.name) {
+ "value", null -> nameValuePair.parentOfType()
+ ?.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION) == true
+ "id" -> nameValuePair.parentOfType()
+ ?.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION) == true
+ else -> false
+ }
+ }
+
+ override fun getLanguagesToInject(registrar: MultiHostRegistrar, context: PsiElement) {
+ val project = context.project
+ val (anchor, _) = JavaConcatenationToInjectorAdapter(project).computeAnchorAndOperands(context)
+
+ if (!shouldInjectIn(anchor)) {
+ return
+ }
+
+ val modifierList = anchor.findContainingModifierList() ?: return
+
+ val modCount = PsiModificationTracker.getInstance(project).modificationCount
+ val primaryElement = modifierList.getUserData(ME_EXPRESSION_INJECTION)
+ if (primaryElement != null && primaryElement.modCount == modCount) {
+ METHOD_ADD_TO_RESULTS.invoke(registrar, primaryElement.injectionResult)
+ return
+ }
+
+ // A Frankenstein injection is an injection where we don't know the entire contents, and therefore errors should
+ // not be reported.
+ var isFrankenstein = false
+ registrar.startInjecting(MEExpressionLanguage)
+
+ for (annotation in modifierList.annotations) {
+ if (annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) {
+ val idExpr = annotation.findDeclaredAttributeValue("id") ?: continue
+ val isType = annotation.findDeclaredAttributeValue("type") != null
+ var needsPrefix = true
+ iterateConcatenation(idExpr) { op ->
+ if (op is PsiLanguageInjectionHost) {
+ for (textRange in getTextRanges(op)) {
+ val prefix = "\nclass $isType ".takeIf { needsPrefix }
+ needsPrefix = false
+ registrar.addPlace(prefix, null, op, textRange)
+ }
+ } else {
+ isFrankenstein = true
+ }
+ }
+ } else if (annotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION)) {
+ val valueExpr = annotation.findDeclaredAttributeValue("value") ?: continue
+ val places = mutableListOf>()
+ iterateConcatenation(valueExpr) { op ->
+ if (op is PsiLanguageInjectionHost) {
+ for (textRange in getTextRanges(op)) {
+ places += op to textRange
+ }
+ } else {
+ isFrankenstein = true
+ }
+ }
+ if (places.isNotEmpty()) {
+ for ((i, place) in places.withIndex()) {
+ val (host, range) = place
+ val prefix = "\ndo { ".takeIf { i == 0 }
+ val suffix = " }".takeIf { i == places.size - 1 }
+ registrar.addPlace(prefix, suffix, host, range)
+ }
+ }
+ }
+ }
+
+ registrar.doneInjecting()
+
+ modifierList.putUserData(
+ ME_EXPRESSION_INJECTION,
+ MEExpressionInjection(modCount, METHOD_GET_INJECTED_RESULT.invoke(registrar))
+ )
+
+ if (isFrankenstein) {
+ @Suppress("DEPRECATION") // no replacement for this method
+ com.intellij.psi.impl.source.tree.injected.InjectedLanguageUtil.putInjectedFileUserData(
+ context,
+ MEExpressionLanguage,
+ InjectedLanguageManager.FRANKENSTEIN_INJECTION,
+ true
+ )
+ }
+ }
+
+ private fun iterateConcatenation(element: PsiElement, consumer: (PsiElement) -> Unit) {
+ when (element) {
+ is PsiParenthesizedExpression -> {
+ val inner = PsiUtil.skipParenthesizedExprDown(element) ?: return
+ iterateConcatenation(inner, consumer)
+ }
+ is PsiPolyadicExpression -> {
+ if (element.operationTokenType == JavaTokenType.PLUS) {
+ for (operand in element.operands) {
+ iterateConcatenation(operand, consumer)
+ }
+ } else {
+ consumer(element)
+ }
+ }
+ else -> consumer(element)
+ }
+ }
+
+ private fun getTextRanges(host: PsiLanguageInjectionHost): List {
+ if (host is PsiLiteralExpression && host.isTextBlock) {
+ val textRange = ElementManipulators.getValueTextRange(host)
+ val indent = PsiLiteralUtil.getTextBlockIndent(host)
+ if (indent <= 0) {
+ return listOf(textRange)
+ }
+
+ val text = (host as PsiElement).text
+ var startOffset = textRange.startOffset + indent
+ var endOffset = text.indexOf('\n', startOffset)
+ val result = SmartList()
+ while (endOffset > 0) {
+ endOffset++
+ result.add(TextRange(startOffset, endOffset))
+ startOffset = endOffset + indent
+ endOffset = text.indexOf('\n', startOffset)
+ }
+ endOffset = textRange.endOffset
+ if (startOffset < endOffset) {
+ result.add(TextRange(startOffset, endOffset))
+ }
+ return result
+ } else {
+ return listOf(ElementManipulators.getValueTextRange(host))
+ }
+ }
+
+ override fun elementsToInjectIn() = ELEMENTS
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt
new file mode 100644
index 000000000..af5496462
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionLanguage.kt
@@ -0,0 +1,25 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.intellij.lang.Language
+
+object MEExpressionLanguage : Language("MEExpression")
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt
new file mode 100644
index 000000000..8eb499188
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionLexerAdapter.kt
@@ -0,0 +1,25 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.intellij.lexer.FlexAdapter
+
+class MEExpressionLexerAdapter : FlexAdapter(MEExpressionLexer(null))
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt
new file mode 100644
index 000000000..541cdd455
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionMatchUtil.kt
@@ -0,0 +1,317 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
+import com.demonwav.mcdev.platform.mixin.util.LocalInfo
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.platform.mixin.util.cached
+import com.demonwav.mcdev.util.MemberReference
+import com.demonwav.mcdev.util.computeStringArray
+import com.demonwav.mcdev.util.constantStringValue
+import com.demonwav.mcdev.util.descriptor
+import com.demonwav.mcdev.util.findAnnotations
+import com.demonwav.mcdev.util.resolveType
+import com.demonwav.mcdev.util.resolveTypeArray
+import com.intellij.openapi.diagnostic.logger
+import com.intellij.openapi.module.Module
+import com.intellij.openapi.progress.ProcessCanceledException
+import com.intellij.openapi.progress.ProgressManager
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiModifierList
+import com.llamalad7.mixinextras.expression.impl.ExpressionParserFacade
+import com.llamalad7.mixinextras.expression.impl.ExpressionService
+import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression
+import com.llamalad7.mixinextras.expression.impl.flow.ComplexDataException
+import com.llamalad7.mixinextras.expression.impl.flow.FlowInterpreter
+import com.llamalad7.mixinextras.expression.impl.flow.FlowValue
+import com.llamalad7.mixinextras.expression.impl.flow.expansion.InsnExpander
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
+import com.llamalad7.mixinextras.expression.impl.pool.IdentifierPool
+import com.llamalad7.mixinextras.expression.impl.pool.SimpleMemberDefinition
+import org.objectweb.asm.Handle
+import org.objectweb.asm.Opcodes
+import org.objectweb.asm.Type
+import org.objectweb.asm.tree.AbstractInsnNode
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.FieldInsnNode
+import org.objectweb.asm.tree.MethodInsnNode
+import org.objectweb.asm.tree.MethodNode
+import org.objectweb.asm.tree.VarInsnNode
+import org.objectweb.asm.tree.analysis.Analyzer
+
+typealias IdentifierPoolFactory = (MethodNode) -> IdentifierPool
+typealias FlowMap = Map
+
+/**
+ * An instruction that MixinExtras generates (via instruction expansion), as opposed to an instruction in the original
+ * method. One type of instruction cannot be directly assigned to another, to avoid a method instruction being used when
+ * a virtual instruction is expected and vice versa.
+ */
+@JvmInline
+value class VirtualInsn(val insn: AbstractInsnNode)
+
+object MEExpressionMatchUtil {
+ private val LOGGER = logger()
+
+ init {
+ ExpressionService.offerInstance(MEExpressionService)
+ }
+
+ fun getFlowMap(project: Project, classIn: ClassNode, methodIn: MethodNode): FlowMap? {
+ if (methodIn.instructions == null) {
+ return null
+ }
+
+ return methodIn.cached(classIn, project) { classNode, methodNode ->
+ val interpreter = object : FlowInterpreter(classNode, methodNode, MEFlowContext(project)) {
+ override fun newValue(type: Type?): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.newValue(type)
+ }
+
+ override fun newOperation(insn: AbstractInsnNode?): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.newOperation(insn)
+ }
+
+ override fun copyOperation(insn: AbstractInsnNode?, value: FlowValue?): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.copyOperation(insn, value)
+ }
+
+ override fun unaryOperation(insn: AbstractInsnNode?, value: FlowValue?): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.unaryOperation(insn, value)
+ }
+
+ override fun binaryOperation(
+ insn: AbstractInsnNode?,
+ value1: FlowValue?,
+ value2: FlowValue?
+ ): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.binaryOperation(insn, value1, value2)
+ }
+
+ override fun ternaryOperation(
+ insn: AbstractInsnNode?,
+ value1: FlowValue?,
+ value2: FlowValue?,
+ value3: FlowValue?
+ ): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.ternaryOperation(insn, value1, value2, value3)
+ }
+
+ override fun naryOperation(insn: AbstractInsnNode?, values: MutableList?): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.naryOperation(insn, values)
+ }
+
+ override fun returnOperation(insn: AbstractInsnNode?, value: FlowValue?, expected: FlowValue?) {
+ ProgressManager.checkCanceled()
+ super.returnOperation(insn, value, expected)
+ }
+
+ override fun merge(value1: FlowValue?, value2: FlowValue?): FlowValue? {
+ ProgressManager.checkCanceled()
+ return super.merge(value1, value2)
+ }
+ }
+
+ try {
+ Analyzer(interpreter).analyze(classNode.name, methodNode)
+ } catch (e: RuntimeException) {
+ if (e is ProcessCanceledException) {
+ throw e
+ }
+ LOGGER.warn("MEExpressionMatchUtil.getFlowMap failed", e)
+ return@cached null
+ }
+
+ interpreter.finish().asSequence().mapNotNull { flow -> flow.virtualInsnOrNull?.let { it to flow } }.toMap()
+ }
+ }
+
+ fun createIdentifierPoolFactory(
+ module: Module,
+ targetClass: ClassNode,
+ modifierList: PsiModifierList,
+ ): IdentifierPoolFactory = { targetMethod ->
+ val pool = IdentifierPool()
+
+ for (annotation in modifierList.annotations) {
+ if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) {
+ continue
+ }
+
+ val definitionId = annotation.findDeclaredAttributeValue("id")?.constantStringValue ?: ""
+
+ val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList()
+ for (field in fields) {
+ val fieldRef = MemberReference.parse(field) ?: continue
+ pool.addMember(
+ definitionId,
+ SimpleMemberDefinition {
+ it is FieldInsnNode && fieldRef.matchField(it.owner, it.name, it.desc)
+ }
+ )
+ }
+
+ val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList()
+ for (method in methods) {
+ val methodRef = MemberReference.parse(method) ?: continue
+ pool.addMember(
+ definitionId,
+ object : SimpleMemberDefinition {
+ override fun matches(insn: AbstractInsnNode) =
+ insn is MethodInsnNode && methodRef.matchMethod(insn.owner, insn.name, insn.desc)
+
+ override fun matches(handle: Handle) =
+ handle.tag in Opcodes.H_INVOKEVIRTUAL..Opcodes.H_INVOKEINTERFACE &&
+ methodRef.matchMethod(handle.owner, handle.name, handle.desc)
+ }
+ )
+ }
+
+ val types = annotation.findDeclaredAttributeValue("type")?.resolveTypeArray() ?: emptyList()
+ for (type in types) {
+ val asmType = Type.getType(type.descriptor)
+ pool.addType(definitionId) { it == asmType }
+ }
+
+ val locals = annotation.findDeclaredAttributeValue("local")?.findAnnotations() ?: emptyList()
+ for (localAnnotation in locals) {
+ val localType = localAnnotation.findDeclaredAttributeValue("type")?.resolveType()
+ val localInfo = LocalInfo.fromAnnotation(localType, localAnnotation)
+ pool.addMember(definitionId) { node ->
+ val virtualInsn = node.insn
+ if (virtualInsn !is VarInsnNode) {
+ return@addMember false
+ }
+ val physicalInsn = InsnExpander.getRepresentative(node)
+ val actualInsn = if (virtualInsn.opcode >= Opcodes.ISTORE && virtualInsn.opcode <= Opcodes.ASTORE) {
+ physicalInsn.next ?: return@addMember false
+ } else {
+ physicalInsn
+ }
+
+ val unfilteredLocals = localInfo.getLocals(module, targetClass, targetMethod, actualInsn)
+ ?: return@addMember false
+ val filteredLocals = localInfo.matchLocals(unfilteredLocals, CollectVisitor.Mode.MATCH_ALL)
+ filteredLocals.any { it.index == virtualInsn.`var` }
+ }
+ }
+ }
+
+ pool
+ }
+
+ fun createExpression(text: String): Expression? {
+ return try {
+ ExpressionParserFacade.parse(text)
+ } catch (e: Exception) {
+ null
+ } catch (e: StackOverflowError) {
+ null
+ }
+ }
+
+ fun getContextType(project: Project, annotationName: String?): ExpressionContext.Type {
+ if (annotationName == null) {
+ return ExpressionContext.Type.CUSTOM
+ }
+ if (annotationName == MixinConstants.Annotations.SLICE) {
+ return ExpressionContext.Type.SLICE
+ }
+
+ val handler = MixinAnnotationHandler.forMixinAnnotation(annotationName, project) as? InjectorAnnotationHandler
+ ?: return ExpressionContext.Type.CUSTOM
+ return handler.mixinExtrasExpressionContextType
+ }
+
+ inline fun findMatchingInstructions(
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ pool: IdentifierPool,
+ flows: FlowMap,
+ expr: Expression,
+ insns: Iterable,
+ contextType: ExpressionContext.Type,
+ forCompletion: Boolean,
+ callback: (ExpressionMatch) -> Unit
+ ) {
+ for (insn in insns) {
+ val decorations = mutableMapOf>()
+ val captured = mutableListOf>()
+
+ val sink = object : Expression.OutputSink {
+ override fun capture(node: FlowValue, expr: Expression?, ctx: ExpressionContext?) {
+ captured += node to (expr?.src?.startIndex ?: 0)
+ decorations.getOrPut(insn, ::mutableMapOf).putAll(node.decorations)
+ }
+
+ override fun decorate(insn: AbstractInsnNode, key: String, value: Any?) {
+ decorations.getOrPut(VirtualInsn(insn), ::mutableMapOf)[key] = value
+ }
+
+ override fun decorateInjectorSpecific(insn: AbstractInsnNode, key: String, value: Any?) {
+ // Our maps are per-injector anyway, so this is just a normal decoration.
+ decorations.getOrPut(VirtualInsn(insn), ::mutableMapOf)[key] = value
+ }
+ }
+
+ val flow = flows[insn] ?: continue
+ try {
+ val context = ExpressionContext(pool, sink, targetClass, targetMethod, contextType, forCompletion)
+ if (expr.matches(flow, context)) {
+ for ((capturedFlow, startOffset) in captured) {
+ val capturedInsn = capturedFlow.virtualInsnOrNull ?: continue
+ val originalInsn = InsnExpander.getRepresentative(capturedFlow) ?: capturedInsn.insn
+ callback(ExpressionMatch(flow, originalInsn, startOffset, decorations[capturedInsn].orEmpty()))
+ }
+ }
+ } catch (e: ProcessCanceledException) {
+ throw e
+ } catch (ignored: Exception) {
+ // MixinExtras throws lots of different exceptions
+ }
+ }
+ }
+
+ val FlowValue.virtualInsn: VirtualInsn get() = VirtualInsn(insn)
+
+ val FlowValue.virtualInsnOrNull: VirtualInsn? get() = try {
+ VirtualInsn(insn)
+ } catch (e: ComplexDataException) {
+ null
+ }
+
+ class ExpressionMatch @PublishedApi internal constructor(
+ val flow: FlowValue,
+ val originalInsn: AbstractInsnNode,
+ val startOffset: Int,
+ val decorations: Map,
+ )
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt
new file mode 100644
index 000000000..7d16e8816
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionParserDefinition.kt
@@ -0,0 +1,46 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.MEExpressionParser
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets
+import com.intellij.lang.ASTNode
+import com.intellij.lang.ParserDefinition
+import com.intellij.openapi.project.Project
+import com.intellij.psi.FileViewProvider
+import com.intellij.psi.PsiElement
+import com.intellij.psi.tree.IFileElementType
+import com.intellij.psi.tree.TokenSet
+
+class MEExpressionParserDefinition : ParserDefinition {
+
+ override fun createLexer(project: Project) = MEExpressionLexerAdapter()
+ override fun getCommentTokens(): TokenSet = TokenSet.EMPTY
+ override fun getStringLiteralElements() = MEExpressionTokenSets.STRINGS
+ override fun createParser(project: Project) = MEExpressionParser()
+ override fun getFileNodeType() = FILE
+ override fun createFile(viewProvider: FileViewProvider) = MEExpressionFile(viewProvider)
+ override fun createElement(node: ASTNode): PsiElement = MEExpressionTypes.Factory.createElement(node)
+}
+
+val FILE = IFileElementType(MEExpressionLanguage)
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt
new file mode 100644
index 000000000..dc1a1796d
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionQuoteHandler.kt
@@ -0,0 +1,26 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets
+import com.intellij.codeInsight.editorActions.SimpleTokenSetQuoteHandler
+
+class MEExpressionQuoteHandler : SimpleTokenSetQuoteHandler(MEExpressionTokenSets.STRINGS)
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt
new file mode 100644
index 000000000..2294b4bdd
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionRefactoringSupport.kt
@@ -0,0 +1,29 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.intellij.lang.refactoring.RefactoringSupportProvider
+import com.intellij.psi.PsiElement
+
+class MEExpressionRefactoringSupport : RefactoringSupportProvider() {
+ // Inplace renaming doesn't work due to IDEA-348784
+ override fun isInplaceRenameAvailable(element: PsiElement, context: PsiElement?) = false
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt
new file mode 100644
index 000000000..473717217
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionService.kt
@@ -0,0 +1,76 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.util.toPsiType
+import com.demonwav.mcdev.util.descriptor
+import com.intellij.openapi.project.Project
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiElementFactory
+import com.intellij.psi.PsiManager
+import com.intellij.psi.PsiType
+import com.llamalad7.mixinextras.expression.impl.ExpressionService
+import com.llamalad7.mixinextras.expression.impl.flow.FlowContext
+import org.objectweb.asm.Type
+
+object MEExpressionService : ExpressionService() {
+ override fun getCommonSuperClass(ctx: FlowContext, type1: Type, type2: Type): Type {
+ ctx as MEFlowContext
+ val elementFactory = JavaPsiFacade.getElementFactory(ctx.project)
+ return Type.getType(
+ getCommonSuperClass(
+ ctx.project,
+ type1.toPsiType(elementFactory) as PsiClassType,
+ type2.toPsiType(elementFactory) as PsiClassType
+ )?.descriptor ?: error("Could not intersect types $type1 and $type2!")
+ )
+ }
+
+ // Copied from ClassInfo
+ private fun getCommonSuperClass(
+ project: Project,
+ type1: PsiType,
+ type2: PsiType
+ ): PsiClassType? {
+ val left = (type1 as? PsiClassType)?.resolve() ?: return null
+ val right = (type2 as? PsiClassType)?.resolve() ?: return null
+
+ fun objectType() = PsiType.getJavaLangObject(PsiManager.getInstance(project), left.resolveScope)
+ fun PsiClass.type() = PsiElementFactory.getInstance(project).createType(this)
+
+ if (left.isInheritor(right, true)) {
+ return right.type()
+ }
+ if (right.isInheritor(left, true)) {
+ return left.type()
+ }
+ if (left.isInterface || right.isInterface) {
+ return objectType()
+ }
+
+ return generateSequence(left) { it.superClass }
+ .firstOrNull { right.isInheritor(it, true) }
+ ?.type()
+ ?: objectType()
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt
new file mode 100644
index 000000000..a44fdc28c
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionSyntaxHighlighter.kt
@@ -0,0 +1,198 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionTokenSets
+import com.intellij.openapi.editor.DefaultLanguageHighlighterColors
+import com.intellij.openapi.editor.HighlighterColors
+import com.intellij.openapi.editor.colors.TextAttributesKey
+import com.intellij.openapi.editor.colors.TextAttributesKey.createTextAttributesKey
+import com.intellij.openapi.fileTypes.SyntaxHighlighterBase
+import com.intellij.openapi.fileTypes.SyntaxHighlighterFactory
+import com.intellij.openapi.project.Project
+import com.intellij.openapi.vfs.VirtualFile
+import com.intellij.psi.TokenType
+import com.intellij.psi.tree.IElementType
+
+class MEExpressionSyntaxHighlighter : SyntaxHighlighterBase() {
+ companion object {
+ val STRING = createTextAttributesKey(
+ "MEEXPRESSION_STRING",
+ DefaultLanguageHighlighterColors.STRING
+ )
+ val STRING_ESCAPE = createTextAttributesKey(
+ "MEEXPRESSION_STRING_ESCAPE",
+ DefaultLanguageHighlighterColors.VALID_STRING_ESCAPE
+ )
+ val NUMBER = createTextAttributesKey(
+ "MEEXPRESSION_NUMBER",
+ DefaultLanguageHighlighterColors.NUMBER
+ )
+ val KEYWORD = createTextAttributesKey(
+ "MEEXPRESSION_KEYWORD",
+ DefaultLanguageHighlighterColors.KEYWORD,
+ )
+ val OPERATOR = createTextAttributesKey(
+ "MEEXPRESSION_OPERATOR",
+ DefaultLanguageHighlighterColors.OPERATION_SIGN
+ )
+ val PARENS = createTextAttributesKey(
+ "MEEXPRESSION_PARENS",
+ DefaultLanguageHighlighterColors.PARENTHESES
+ )
+ val BRACKETS = createTextAttributesKey(
+ "MEEXPRESSION_BRACKETS",
+ DefaultLanguageHighlighterColors.BRACKETS
+ )
+ val BRACES = createTextAttributesKey(
+ "MEEXPRESSION_BRACES",
+ DefaultLanguageHighlighterColors.BRACES
+ )
+ val DOT = createTextAttributesKey(
+ "MEEXPRESSION_DOT",
+ DefaultLanguageHighlighterColors.DOT
+ )
+ val METHOD_REFERENCE = createTextAttributesKey(
+ "MEEXPRESSION_METHOD_REFERENCE",
+ DefaultLanguageHighlighterColors.DOT
+ )
+ val COMMA = createTextAttributesKey(
+ "MEEXPRESSION_COMMA",
+ DefaultLanguageHighlighterColors.COMMA
+ )
+ val CAPTURE = createTextAttributesKey(
+ "MEEXPRESSION_CAPTURE",
+ DefaultLanguageHighlighterColors.OPERATION_SIGN
+ )
+ val WILDCARD = createTextAttributesKey(
+ "MEEXPRESSION_WILDCARD",
+ DefaultLanguageHighlighterColors.OPERATION_SIGN
+ )
+ val IDENTIFIER = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER",
+ DefaultLanguageHighlighterColors.IDENTIFIER
+ )
+ val IDENTIFIER_CALL = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER_CALL",
+ DefaultLanguageHighlighterColors.FUNCTION_CALL
+ )
+ val IDENTIFIER_CLASS_NAME = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER_CLASS_NAME",
+ DefaultLanguageHighlighterColors.CLASS_REFERENCE
+ )
+ val IDENTIFIER_PRIMITIVE_TYPE = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER_PRIMITIVE_TYPE",
+ DefaultLanguageHighlighterColors.KEYWORD
+ )
+ val IDENTIFIER_MEMBER_NAME = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER_MEMBER_NAME",
+ DefaultLanguageHighlighterColors.INSTANCE_FIELD
+ )
+ val IDENTIFIER_VARIABLE = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER_VARIABLE",
+ DefaultLanguageHighlighterColors.LOCAL_VARIABLE
+ )
+ val IDENTIFIER_TYPE_DECLARATION = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER_TYPE_DECLARATION",
+ DefaultLanguageHighlighterColors.CLASS_NAME
+ )
+ val IDENTIFIER_DECLARATION = createTextAttributesKey(
+ "MEEXPRESSION_IDENTIFIER_DECLARATION",
+ DefaultLanguageHighlighterColors.FUNCTION_DECLARATION
+ )
+ val BAD_CHAR = createTextAttributesKey(
+ "MEEXPRESSION_BAD_CHARACTER",
+ HighlighterColors.BAD_CHARACTER
+ )
+
+ val STRING_KEYS = arrayOf(STRING)
+ val STRING_ESCAPE_KEYS = arrayOf(STRING_ESCAPE)
+ val NUMBER_KEYS = arrayOf(NUMBER)
+ val KEYWORD_KEYS = arrayOf(KEYWORD)
+ val OPERATOR_KEYS = arrayOf(OPERATOR)
+ val PARENS_KEYS = arrayOf(PARENS)
+ val BRACKETS_KEYS = arrayOf(BRACKETS)
+ val BRACES_KEYS = arrayOf(BRACES)
+ val DOT_KEYS = arrayOf(DOT)
+ val METHOD_REFERENCE_KEYS = arrayOf(METHOD_REFERENCE)
+ val COMMA_KEYS = arrayOf(COMMA)
+ val CAPTURE_KEYS = arrayOf(CAPTURE)
+ val WILDCARD_KEYS = arrayOf(WILDCARD)
+ val IDENTIFIER_KEYS = arrayOf(IDENTIFIER)
+ val BAD_CHAR_KEYS = arrayOf(BAD_CHAR)
+ }
+
+ override fun getHighlightingLexer() = MEExpressionLexerAdapter()
+ override fun getTokenHighlights(tokenType: IElementType): Array {
+ if (tokenType == MEExpressionTypes.TOKEN_STRING_ESCAPE) {
+ return STRING_ESCAPE_KEYS
+ }
+ if (MEExpressionTokenSets.STRINGS.contains(tokenType)) {
+ return STRING_KEYS
+ }
+ if (tokenType == MEExpressionTypes.TOKEN_IDENTIFIER) {
+ return IDENTIFIER_KEYS
+ }
+ if (MEExpressionTokenSets.NUMBERS.contains(tokenType)) {
+ return NUMBER_KEYS
+ }
+ if (MEExpressionTokenSets.KEYWORDS.contains(tokenType)) {
+ return KEYWORD_KEYS
+ }
+ if (MEExpressionTokenSets.OPERATORS.contains(tokenType)) {
+ return OPERATOR_KEYS
+ }
+ if (MEExpressionTokenSets.PARENS.contains(tokenType)) {
+ return PARENS_KEYS
+ }
+ if (MEExpressionTokenSets.BRACKETS.contains(tokenType)) {
+ return BRACKETS_KEYS
+ }
+ if (MEExpressionTokenSets.BRACES.contains(tokenType)) {
+ return BRACES_KEYS
+ }
+ if (tokenType == MEExpressionTypes.TOKEN_DOT) {
+ return DOT_KEYS
+ }
+ if (tokenType == MEExpressionTypes.TOKEN_METHOD_REF) {
+ return METHOD_REFERENCE_KEYS
+ }
+ if (tokenType == MEExpressionTypes.TOKEN_COMMA) {
+ return COMMA_KEYS
+ }
+ if (tokenType == MEExpressionTypes.TOKEN_AT) {
+ return CAPTURE_KEYS
+ }
+ if (tokenType == MEExpressionTypes.TOKEN_WILDCARD) {
+ return WILDCARD_KEYS
+ }
+ if (tokenType == TokenType.BAD_CHARACTER) {
+ return BAD_CHAR_KEYS
+ }
+
+ return TextAttributesKey.EMPTY_ARRAY
+ }
+}
+
+class MEExpressionSyntaxHighlighterFactory : SyntaxHighlighterFactory() {
+ override fun getSyntaxHighlighter(project: Project?, virtualFile: VirtualFile?) = MEExpressionSyntaxHighlighter()
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt b/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt
new file mode 100644
index 000000000..6d714b43b
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEExpressionTypedHandlerDelegate.kt
@@ -0,0 +1,42 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.intellij.codeInsight.AutoPopupController
+import com.intellij.codeInsight.editorActions.TypedHandlerDelegate
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiFile
+import com.intellij.psi.util.elementType
+
+class MEExpressionTypedHandlerDelegate : TypedHandlerDelegate() {
+ override fun checkAutoPopup(charTyped: Char, project: Project, editor: Editor, file: PsiFile): Result {
+ if (charTyped == ':' && file.language == MEExpressionLanguage) {
+ AutoPopupController.getInstance(project).autoPopupMemberLookup(editor) {
+ val offset = editor.caretModel.offset
+ it.findElementAt(offset - 1).elementType == MEExpressionTypes.TOKEN_METHOD_REF
+ }
+ return Result.STOP
+ }
+ return Result.CONTINUE
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt b/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt
new file mode 100644
index 000000000..e7d22f578
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MEFlowContext.kt
@@ -0,0 +1,26 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.intellij.openapi.project.Project
+import com.llamalad7.mixinextras.expression.impl.flow.FlowContext
+
+class MEFlowContext(val project: Project) : FlowContext
diff --git a/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt
new file mode 100644
index 000000000..8a6312d24
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/MESourceMatchContext.kt
@@ -0,0 +1,98 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.platform.mixin.util.LocalInfo
+import com.demonwav.mcdev.util.MemberReference
+import com.intellij.openapi.project.Project
+import com.intellij.psi.PsiElement
+
+class MESourceMatchContext(val project: Project) {
+ @PublishedApi
+ internal var realElement: PsiElement? = null
+ private val capturesInternal = mutableListOf()
+ val captures: List get() = capturesInternal
+
+ private val types = mutableMapOf>()
+ private val fields = mutableMapOf>()
+ private val methods = mutableMapOf>()
+ private val localInfos = mutableMapOf>()
+
+ init {
+ addType("byte", "B")
+ addType("char", "C")
+ addType("double", "D")
+ addType("float", "F")
+ addType("int", "I")
+ addType("long", "J")
+ addType("short", "S")
+ }
+
+ fun addCapture(capturedElement: PsiElement) {
+ val element = realElement ?: capturedElement
+ capturesInternal += element
+ }
+
+ fun getTypes(key: String): List = types[key] ?: emptyList()
+
+ fun addType(key: String, desc: String) {
+ types.getOrPut(key, ::mutableListOf) += desc
+ }
+
+ fun getFields(key: String): List = fields[key] ?: emptyList()
+
+ fun addField(key: String, field: MemberReference) {
+ fields.getOrPut(key, ::mutableListOf) += field
+ }
+
+ fun getMethods(key: String): List = methods[key] ?: emptyList()
+
+ fun addMethod(key: String, method: MemberReference) {
+ methods.getOrPut(key, ::mutableListOf) += method
+ }
+
+ fun getLocalInfos(key: String): List = localInfos[key] ?: emptyList()
+
+ fun addLocalInfo(key: String, localInfo: LocalInfo) {
+ localInfos.getOrPut(key, ::mutableListOf) += localInfo
+ }
+
+ fun reset() {
+ capturesInternal.clear()
+ }
+
+ inline fun fakeElementScope(
+ isFake: Boolean,
+ realElement: PsiElement,
+ action: () -> T
+ ): T {
+ if (this.realElement != null || !isFake) {
+ return action()
+ }
+
+ this.realElement = realElement
+ try {
+ return action()
+ } finally {
+ this.realElement = null
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt
new file mode 100644
index 000000000..697faee8b
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionElementType.kt
@@ -0,0 +1,27 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage
+import com.intellij.psi.tree.IElementType
+import org.jetbrains.annotations.NonNls
+
+class MEExpressionElementType(@NonNls debugName: String) : IElementType(debugName, MEExpressionLanguage)
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt
new file mode 100644
index 000000000..1f9002488
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionFile.kt
@@ -0,0 +1,41 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.asset.PlatformAssets
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionFileType
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEItem
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatementItem
+import com.intellij.extapi.psi.PsiFileBase
+import com.intellij.psi.FileViewProvider
+
+class MEExpressionFile(viewProvider: FileViewProvider) : PsiFileBase(viewProvider, MEExpressionLanguage) {
+ override fun getFileType() = MEExpressionFileType
+ override fun toString() = "MixinExtras Expression File"
+ override fun getIcon(flags: Int) = PlatformAssets.MIXIN_ICON
+
+ val items: Array get() = findChildrenByClass(MEItem::class.java)
+ val declarations: List get() = items.filterIsInstance()
+ val statements: List get() = items.mapNotNull { (it as? MEStatementItem)?.statement }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt
new file mode 100644
index 000000000..2b6c41702
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionParserUtil.kt
@@ -0,0 +1,45 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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 .
+ */
+
+@file:JvmName("MEExpressionParserUtil")
+
+package com.demonwav.mcdev.platform.mixin.expression.psi
+
+import com.intellij.lang.PsiBuilder
+import com.intellij.lang.parser.GeneratedParserUtilBase.* // ktlint-disable no-wildcard-imports
+
+fun parseToRightBracket(
+ builder: PsiBuilder,
+ level: Int,
+ recoverParser: Parser,
+ rightBracketParser: Parser
+): Boolean {
+ recursion_guard_(builder, level, "parseToRightBracket")
+
+ // continue over any stuff inside the brackets as error elements. We need to find our precious right bracket.
+ var marker = enter_section_(builder, level, _NONE_)
+ exit_section_(builder, level, marker, false, false, recoverParser)
+
+ // consume our right bracket.
+ marker = enter_section_(builder)
+ val result = rightBracketParser.parse(builder, level)
+ exit_section_(builder, marker, null, result)
+ return result
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt
new file mode 100644
index 000000000..cd4a1842e
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenSets.kt
@@ -0,0 +1,73 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.intellij.psi.tree.TokenSet
+
+object MEExpressionTokenSets {
+ val STRINGS = TokenSet.create(
+ MEExpressionTypes.TOKEN_STRING,
+ MEExpressionTypes.TOKEN_STRING_ESCAPE,
+ MEExpressionTypes.TOKEN_STRING_TERMINATOR,
+ )
+ val NUMBERS = TokenSet.create(
+ MEExpressionTypes.TOKEN_INT_LIT,
+ MEExpressionTypes.TOKEN_DEC_LIT,
+ )
+ val KEYWORDS = TokenSet.create(
+ MEExpressionTypes.TOKEN_BOOL_LIT,
+ MEExpressionTypes.TOKEN_NULL_LIT,
+ MEExpressionTypes.TOKEN_DO,
+ MEExpressionTypes.TOKEN_INSTANCEOF,
+ MEExpressionTypes.TOKEN_NEW,
+ MEExpressionTypes.TOKEN_RETURN,
+ MEExpressionTypes.TOKEN_THROW,
+ MEExpressionTypes.TOKEN_THIS,
+ MEExpressionTypes.TOKEN_SUPER,
+ MEExpressionTypes.TOKEN_CLASS,
+ MEExpressionTypes.TOKEN_RESERVED,
+ )
+ val OPERATORS = TokenSet.create(
+ MEExpressionTypes.TOKEN_BITWISE_NOT,
+ MEExpressionTypes.TOKEN_MULT,
+ MEExpressionTypes.TOKEN_DIV,
+ MEExpressionTypes.TOKEN_MOD,
+ MEExpressionTypes.TOKEN_PLUS,
+ MEExpressionTypes.TOKEN_MINUS,
+ MEExpressionTypes.TOKEN_SHL,
+ MEExpressionTypes.TOKEN_SHR,
+ MEExpressionTypes.TOKEN_USHR,
+ MEExpressionTypes.TOKEN_LT,
+ MEExpressionTypes.TOKEN_LE,
+ MEExpressionTypes.TOKEN_GT,
+ MEExpressionTypes.TOKEN_GE,
+ MEExpressionTypes.TOKEN_EQ,
+ MEExpressionTypes.TOKEN_NE,
+ MEExpressionTypes.TOKEN_BITWISE_AND,
+ MEExpressionTypes.TOKEN_BITWISE_XOR,
+ MEExpressionTypes.TOKEN_BITWISE_OR,
+ MEExpressionTypes.TOKEN_ASSIGN,
+ )
+ val PARENS = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_PAREN, MEExpressionTypes.TOKEN_RIGHT_PAREN)
+ val BRACKETS = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_BRACKET, MEExpressionTypes.TOKEN_RIGHT_BRACKET)
+ val BRACES = TokenSet.create(MEExpressionTypes.TOKEN_LEFT_BRACE, MEExpressionTypes.TOKEN_RIGHT_BRACE)
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt
new file mode 100644
index 000000000..54a746b88
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MEExpressionTokenType.kt
@@ -0,0 +1,29 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionLanguage
+import com.intellij.psi.tree.IElementType
+import org.jetbrains.annotations.NonNls
+
+class MEExpressionTokenType(@NonNls debugName: String) : IElementType(debugName, MEExpressionLanguage) {
+ override fun toString() = "MEExpressionTokenType.${super.toString()}"
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt b/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt
new file mode 100644
index 000000000..3474a3067
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MEMatchableElement.kt
@@ -0,0 +1,31 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.intellij.psi.PsiElement
+
+interface MEMatchableElement : PsiElement {
+ fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean
+
+ fun getInputExprs(): List
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt b/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt
new file mode 100644
index 000000000..ba971b0e2
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MENameElementManipulator.kt
@@ -0,0 +1,34 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory
+import com.intellij.openapi.util.TextRange
+import com.intellij.psi.AbstractElementManipulator
+
+class MENameElementManipulator : AbstractElementManipulator() {
+ override fun handleContentChange(element: MEName, range: TextRange, newContent: String): MEName {
+ val text = element.text
+ val newText = text.substring(0, range.startOffset) + newContent + text.substring(range.endOffset)
+ return element.project.meExpressionElementFactory.createName(newText)
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt
new file mode 100644
index 000000000..b9856a13e
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MEPsiUtil.kt
@@ -0,0 +1,58 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEAssignStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression
+
+object MEPsiUtil {
+ fun isAccessedForReading(expr: MEExpression): Boolean {
+ return !isAccessedForWriting(expr)
+ }
+
+ fun isAccessedForWriting(expr: MEExpression): Boolean {
+ val parent = expr.parent
+ return parent is MEAssignStatement && expr == parent.targetExpr
+ }
+
+ fun skipParenthesizedExprDown(expr: MEExpression): MEExpression? {
+ var e: MEExpression? = expr
+ while (e is MEParenthesizedExpression) {
+ e = e.expression
+ }
+ return e
+ }
+
+ fun isWildcardExpression(expr: MEExpression): Boolean {
+ val actualExpr = skipParenthesizedExprDown(expr) ?: return false
+ return actualExpr is MENameExpression && actualExpr.meName.isWildcard
+ }
+
+ fun isIdentifierStart(char: Char): Boolean {
+ return char in 'a'..'z' || char in 'A'..'Z' || char == '_'
+ }
+
+ fun isIdentifierPart(char: Char): Boolean {
+ return isIdentifierStart(char) || char in '0'..'9'
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt b/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt
new file mode 100644
index 000000000..40f6beab5
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/MERecursiveWalkingVisitor.kt
@@ -0,0 +1,45 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEVisitor
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiRecursiveVisitor
+import com.intellij.psi.PsiWalkingState
+
+abstract class MERecursiveWalkingVisitor : MEVisitor(), PsiRecursiveVisitor {
+ private val walkingState = object : PsiWalkingState(this) {
+ override fun elementFinished(element: PsiElement) {
+ this@MERecursiveWalkingVisitor.elementFinished(element)
+ }
+ }
+
+ override fun visitElement(element: PsiElement) {
+ walkingState.elementStarted(element)
+ }
+
+ open fun elementFinished(element: PsiElement) {
+ }
+
+ fun stopWalking() {
+ walkingState.stopWalking()
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt b/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt
new file mode 100644
index 000000000..95c047703
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/METypeUtil.kt
@@ -0,0 +1,125 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArrayAccessExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEBinaryExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECastExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MENameExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory
+import com.intellij.patterns.ObjectPattern
+import com.intellij.patterns.PatternCondition
+import com.intellij.psi.PsiElement
+import com.intellij.psi.util.parentOfType
+import com.intellij.util.ProcessingContext
+
+object METypeUtil {
+ fun convertExpressionToType(expr: MEExpression): METype? {
+ return if (isExpressionValidType(expr)) {
+ expr.project.meExpressionElementFactory.createType(expr.text)
+ } else {
+ null
+ }
+ }
+
+ private fun isExpressionValidType(expr: MEExpression): Boolean {
+ var e = expr
+ while (true) {
+ when (e) {
+ is MEArrayAccessExpression -> {
+ if (e.indexExpr != null || e.rightBracketToken == null) {
+ return false
+ }
+ e = e.arrayExpr
+ }
+ is MENameExpression -> return true
+ else -> return false
+ }
+ }
+ }
+
+ fun isExpressionDirectlyInTypePosition(expr: MEExpression): Boolean {
+ var e: PsiElement? = expr
+ while (e != null) {
+ val parent = e.parent
+ when (parent) {
+ is MEArrayAccessExpression -> {}
+ is MEParenthesizedExpression -> {
+ val grandparent = parent.parent
+ return grandparent is MECastExpression && e == grandparent.castTypeExpr
+ }
+ is MEBinaryExpression -> {
+ return parent.operator == MEExpressionTypes.TOKEN_INSTANCEOF && e == parent.rightExpr
+ }
+ else -> return false
+ }
+ e = parent
+ }
+
+ return false
+ }
+
+ fun isExpressionInTypePosition(expr: MEExpression): Boolean {
+ var e: PsiElement? = expr
+ while (e != null) {
+ val parent = e.parent
+ when (parent) {
+ is MEParenthesizedExpression -> {
+ val grandparent = parent.parent
+ if (grandparent is MECastExpression && e == grandparent.castTypeExpr) {
+ return true
+ }
+ }
+ is MEBinaryExpression -> {
+ if (parent.operator == MEExpressionTypes.TOKEN_INSTANCEOF && e == parent.rightExpr) {
+ return true
+ }
+ }
+ is MEStatement -> return false
+ }
+ e = parent
+ }
+
+ return false
+ }
+
+ fun > ObjectPattern.inTypePosition(): Self =
+ with(InTypePositionCondition)
+ fun > ObjectPattern.notInTypePosition(): Self =
+ without(InTypePositionCondition)
+ fun > ObjectPattern.validType(): Self =
+ with(ValidTypeCondition)
+
+ private object InTypePositionCondition : PatternCondition("inTypePosition") {
+ override fun accepts(t: PsiElement, context: ProcessingContext?) =
+ t.parentOfType()?.let(::isExpressionInTypePosition) == true
+ }
+
+ private object ValidTypeCondition : PatternCondition("validType") {
+ override fun accepts(t: PsiElement, context: ProcessingContext?) =
+ t.parentOfType(withSelf = true)?.let(::isExpressionValidType) == true
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt
new file mode 100644
index 000000000..2ef6ed9f3
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArgumentsMixin.kt
@@ -0,0 +1,34 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiExpression
+import com.intellij.psi.PsiExpressionList
+
+interface MEArgumentsMixin : PsiElement {
+ fun matchesJava(java: PsiExpressionList, context: MESourceMatchContext): Boolean {
+ return matchesJava(java.expressions, context)
+ }
+
+ fun matchesJava(java: Array, context: MESourceMatchContext): Boolean
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt
new file mode 100644
index 000000000..c026499db
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEArrayAccessExpressionMixin.kt
@@ -0,0 +1,29 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.intellij.psi.PsiElement
+
+interface MEArrayAccessExpressionMixin : MEExpression {
+ val leftBracketToken: PsiElement
+ val rightBracketToken: PsiElement?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt
new file mode 100644
index 000000000..1631852dd
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEBinaryExpressionMixin.kt
@@ -0,0 +1,30 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+import com.intellij.psi.tree.IElementType
+
+interface MEBinaryExpressionMixin : MEExpression {
+ val operator: IElementType
+ val castType: METype?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt
new file mode 100644
index 000000000..3e61e8c13
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MECastExpressionMixin.kt
@@ -0,0 +1,30 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+
+interface MECastExpressionMixin : MEExpression {
+ val castType: METype?
+ val castTypeExpr: MEExpression?
+ val castedExpr: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt
new file mode 100644
index 000000000..cb8d52136
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEDeclarationItemMixin.kt
@@ -0,0 +1,27 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.intellij.psi.PsiElement
+
+interface MEDeclarationItemMixin : PsiElement {
+ val isType: Boolean
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt
new file mode 100644
index 000000000..e1a376343
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MELitExpressionMixin.kt
@@ -0,0 +1,31 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.intellij.lang.ASTNode
+
+interface MELitExpressionMixin : MEExpression {
+ val value: Any?
+ val isNull: Boolean
+ val isString: Boolean
+ val minusToken: ASTNode?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt
new file mode 100644
index 000000000..5e344d9d2
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENameMixin.kt
@@ -0,0 +1,28 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.intellij.psi.PsiElement
+
+interface MENameMixin : PsiElement {
+ val isWildcard: Boolean
+ val identifierElement: PsiElement?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt
new file mode 100644
index 000000000..31b29b4a9
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MENewExpressionMixin.kt
@@ -0,0 +1,35 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.intellij.psi.PsiElement
+
+interface MENewExpressionMixin : PsiElement {
+ val isArrayCreation: Boolean
+ val hasConstructorArguments: Boolean
+ val dimensions: Int
+ val dimExprTokens: List
+ val arrayInitializer: MEArguments?
+
+ class DimExprTokens(val leftBracket: PsiElement, val expr: MEExpression?, val rightBracket: PsiElement?)
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt
new file mode 100644
index 000000000..30d404f6c
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/METypeMixin.kt
@@ -0,0 +1,32 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiType
+
+interface METypeMixin : PsiElement {
+ val isArray: Boolean
+ val dimensions: Int
+
+ fun matchesJava(java: PsiType, context: MESourceMatchContext): Boolean
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt
new file mode 100644
index 000000000..21cb6abf6
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/MEUnaryExpressionMixin.kt
@@ -0,0 +1,28 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.intellij.psi.tree.IElementType
+
+interface MEUnaryExpressionMixin : MEExpression {
+ val operator: IElementType
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt
new file mode 100644
index 000000000..a83224344
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArgumentsImplMixin.kt
@@ -0,0 +1,44 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArgumentsMixin
+import com.intellij.extapi.psi.ASTWrapperPsiElement
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiExpression
+import com.intellij.psi.util.PsiUtil
+
+abstract class MEArgumentsImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEArgumentsMixin {
+ override fun matchesJava(java: Array, context: MESourceMatchContext): Boolean {
+ val exprs = expressionList
+ if (exprs.size != java.size) {
+ return false
+ }
+ return exprs.asSequence().zip(java.asSequence()).all { (expr, javaExpr) ->
+ val actualJavaExpr = PsiUtil.skipParenthesizedExprDown(javaExpr) ?: return@all false
+ expr.matchesJava(actualJavaExpr, context)
+ }
+ }
+
+ protected abstract val expressionList: List
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt
new file mode 100644
index 000000000..dcc6f6f9f
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEArrayAccessExpressionImplMixin.kt
@@ -0,0 +1,58 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEArrayAccessExpressionMixin
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiArrayAccessExpression
+import com.intellij.psi.PsiElement
+import com.intellij.psi.util.PsiUtil
+
+abstract class MEArrayAccessExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEArrayAccessExpressionMixin {
+ override val leftBracketToken get() = findNotNullChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET)
+ override val rightBracketToken get() = findChildByType(MEExpressionTypes.TOKEN_RIGHT_BRACKET)
+
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiArrayAccessExpression) {
+ return false
+ }
+
+ val readMatch = MEPsiUtil.isAccessedForReading(this) && PsiUtil.isAccessedForReading(java)
+ val writeMatch = MEPsiUtil.isAccessedForWriting(this) && PsiUtil.isAccessedForWriting(java)
+ if (!readMatch && !writeMatch) {
+ return false
+ }
+
+ val javaArray = PsiUtil.skipParenthesizedExprDown(java.arrayExpression) ?: return false
+ val javaIndex = PsiUtil.skipParenthesizedExprDown(java.indexExpression) ?: return false
+ return arrayExpr.matchesJava(javaArray, context) && indexExpr?.matchesJava(javaIndex, context) == true
+ }
+
+ override fun getInputExprs() = listOfNotNull(arrayExpr, indexExpr)
+
+ protected abstract val arrayExpr: MEExpression
+ protected abstract val indexExpr: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt
new file mode 100644
index 000000000..f3fbfb396
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEAssignStatementImplMixin.kt
@@ -0,0 +1,57 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.JavaTokenType
+import com.intellij.psi.PsiAssignmentExpression
+import com.intellij.psi.PsiElement
+import com.intellij.psi.util.PsiUtil
+import com.siyeh.ig.PsiReplacementUtil
+
+abstract class MEAssignStatementImplMixin(node: ASTNode) : MEStatementImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiAssignmentExpression) {
+ return false
+ }
+ val isOperatorAssignment = java.operationTokenType != JavaTokenType.EQ
+ val expandedJava = if (isOperatorAssignment) {
+ PsiReplacementUtil.replaceOperatorAssignmentWithAssignmentExpression(java.copy() as PsiAssignmentExpression)
+ as PsiAssignmentExpression
+ } else {
+ java
+ }
+
+ val leftJava = PsiUtil.skipParenthesizedExprDown(expandedJava.lExpression) ?: return false
+ val rightJava = PsiUtil.skipParenthesizedExprDown(expandedJava.rExpression) ?: return false
+ context.fakeElementScope(isOperatorAssignment, java) {
+ return targetExpr.matchesJava(leftJava, context) && rightExpr?.matchesJava(rightJava, context) == true
+ }
+ }
+
+ override fun getInputExprs() = targetExpr.getInputExprs() + listOfNotNull(rightExpr)
+
+ protected abstract val targetExpr: MEExpression
+ protected abstract val rightExpr: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt
new file mode 100644
index 000000000..96c6ae245
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBinaryExpressionImplMixin.kt
@@ -0,0 +1,124 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEBinaryExpressionMixin
+import com.intellij.lang.ASTNode
+import com.intellij.psi.JavaTokenType
+import com.intellij.psi.PsiBinaryExpression
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiInstanceOfExpression
+import com.intellij.psi.PsiTypeTestPattern
+import com.intellij.psi.tree.TokenSet
+import com.intellij.psi.util.JavaPsiPatternUtil
+import com.intellij.psi.util.PsiUtil
+
+abstract class MEBinaryExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEBinaryExpressionMixin {
+ override val operator get() = node.findChildByType(operatorTokens)!!.elementType
+ override val castType get() = rightExpr
+ ?.takeIf { operator == MEExpressionTypes.TOKEN_INSTANCEOF }
+ ?.let(METypeUtil::convertExpressionToType)
+
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (operator == MEExpressionTypes.TOKEN_INSTANCEOF) {
+ if (java !is PsiInstanceOfExpression) {
+ return false
+ }
+ if (!leftExpr.matchesJava(java.operand, context)) {
+ return false
+ }
+ val javaType = java.checkType?.type
+ ?: (JavaPsiPatternUtil.skipParenthesizedPatternDown(java.pattern) as? PsiTypeTestPattern)
+ ?.checkType?.type
+ ?: return false
+ return castType?.matchesJava(javaType, context) == true
+ } else {
+ if (java !is PsiBinaryExpression) {
+ return false
+ }
+
+ val operatorMatches = when (java.operationTokenType) {
+ JavaTokenType.ASTERISK -> operator == MEExpressionTypes.TOKEN_MULT
+ JavaTokenType.DIV -> operator == MEExpressionTypes.TOKEN_DIV
+ JavaTokenType.PERC -> operator == MEExpressionTypes.TOKEN_MOD
+ JavaTokenType.PLUS -> operator == MEExpressionTypes.TOKEN_PLUS
+ JavaTokenType.MINUS -> operator == MEExpressionTypes.TOKEN_MINUS
+ JavaTokenType.LTLT -> operator == MEExpressionTypes.TOKEN_SHL
+ JavaTokenType.GTGT -> operator == MEExpressionTypes.TOKEN_SHR
+ JavaTokenType.GTGTGT -> operator == MEExpressionTypes.TOKEN_USHR
+ JavaTokenType.LT -> operator == MEExpressionTypes.TOKEN_LT
+ JavaTokenType.LE -> operator == MEExpressionTypes.TOKEN_LE
+ JavaTokenType.GT -> operator == MEExpressionTypes.TOKEN_GT
+ JavaTokenType.GE -> operator == MEExpressionTypes.TOKEN_GE
+ JavaTokenType.EQEQ -> operator == MEExpressionTypes.TOKEN_EQ
+ JavaTokenType.NE -> operator == MEExpressionTypes.TOKEN_NE
+ JavaTokenType.AND -> operator == MEExpressionTypes.TOKEN_BITWISE_AND
+ JavaTokenType.XOR -> operator == MEExpressionTypes.TOKEN_BITWISE_XOR
+ JavaTokenType.OR -> operator == MEExpressionTypes.TOKEN_BITWISE_OR
+ else -> false
+ }
+ if (!operatorMatches) {
+ return false
+ }
+
+ val javaLeft = PsiUtil.skipParenthesizedExprDown(java.lOperand) ?: return false
+ val javaRight = PsiUtil.skipParenthesizedExprDown(java.rOperand) ?: return false
+ return leftExpr.matchesJava(javaLeft, context) && rightExpr?.matchesJava(javaRight, context) == true
+ }
+ }
+
+ override fun getInputExprs() = if (operator == MEExpressionTypes.TOKEN_INSTANCEOF) {
+ listOf(leftExpr)
+ } else {
+ listOfNotNull(leftExpr, rightExpr)
+ }
+
+ protected abstract val leftExpr: MEExpression
+ protected abstract val rightExpr: MEExpression?
+
+ companion object {
+ private val operatorTokens = TokenSet.create(
+ MEExpressionTypes.TOKEN_MULT,
+ MEExpressionTypes.TOKEN_DIV,
+ MEExpressionTypes.TOKEN_MOD,
+ MEExpressionTypes.TOKEN_PLUS,
+ MEExpressionTypes.TOKEN_MINUS,
+ MEExpressionTypes.TOKEN_SHL,
+ MEExpressionTypes.TOKEN_SHR,
+ MEExpressionTypes.TOKEN_USHR,
+ MEExpressionTypes.TOKEN_LT,
+ MEExpressionTypes.TOKEN_LE,
+ MEExpressionTypes.TOKEN_GT,
+ MEExpressionTypes.TOKEN_GE,
+ MEExpressionTypes.TOKEN_EQ,
+ MEExpressionTypes.TOKEN_NE,
+ MEExpressionTypes.TOKEN_BITWISE_AND,
+ MEExpressionTypes.TOKEN_BITWISE_XOR,
+ MEExpressionTypes.TOKEN_BITWISE_OR,
+ MEExpressionTypes.TOKEN_INSTANCEOF,
+ )
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt
new file mode 100644
index 000000000..08e035b61
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEBoundReferenceExpressionImplMixin.kt
@@ -0,0 +1,64 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiMethodReferenceExpression
+import com.intellij.psi.util.PsiUtil
+
+abstract class MEBoundReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiMethodReferenceExpression) {
+ return false
+ }
+
+ if (java.isConstructor) {
+ return false
+ }
+
+ val qualifier = PsiUtil.skipParenthesizedExprDown(java.qualifierExpression) ?: return false
+ if (!receiverExpr.matchesJava(qualifier, context)) {
+ return false
+ }
+
+ val memberName = this.memberName ?: return false
+ if (memberName.isWildcard) {
+ return true
+ }
+
+ val method = java.resolve() as? PsiMethod ?: return false
+ val qualifierClass = QualifiedMember.resolveQualifier(java) ?: method.containingClass ?: return false
+ return context.getMethods(memberName.text).any { reference ->
+ reference.matchMethod(method, qualifierClass)
+ }
+ }
+
+ override fun getInputExprs() = listOf(receiverExpr)
+
+ abstract val receiverExpr: MEExpression
+ abstract val memberName: MEName?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt
new file mode 100644
index 000000000..3401aab82
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECapturingExpressionImplMixin.kt
@@ -0,0 +1,38 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+
+abstract class MECapturingExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ context.addCapture(java)
+ return expression?.matchesJava(java, context) == true
+ }
+
+ override fun getInputExprs() = listOfNotNull(expression)
+
+ protected abstract val expression: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt
new file mode 100644
index 000000000..866999357
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MECastExpressionImplMixin.kt
@@ -0,0 +1,66 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEParenthesizedExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEPsiUtil
+import com.demonwav.mcdev.platform.mixin.expression.psi.METypeUtil
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MECastExpressionMixin
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiInstanceOfExpression
+import com.intellij.psi.PsiTypeCastExpression
+import com.intellij.psi.PsiTypeTestPattern
+import com.intellij.psi.util.JavaPsiPatternUtil
+import com.intellij.psi.util.PsiUtil
+
+abstract class MECastExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MECastExpressionMixin {
+ override val castType get() = castTypeExpr?.let(METypeUtil::convertExpressionToType)
+ override val castTypeExpr get() =
+ (expressionList.let { it.getOrNull(it.size - 2) } as? MEParenthesizedExpression)?.expression
+ override val castedExpr get() = expressionList.lastOrNull()
+
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ return when (java) {
+ is PsiTypeCastExpression -> {
+ val javaType = java.castType?.type ?: return false
+ val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false
+ castType?.matchesJava(javaType, context) == true &&
+ castedExpr?.matchesJava(javaOperand, context) == true
+ }
+ is PsiInstanceOfExpression -> {
+ val pattern = JavaPsiPatternUtil.skipParenthesizedPatternDown(java.pattern) as? PsiTypeTestPattern
+ ?: return false
+ val javaType = pattern.checkType?.type ?: return false
+ val castedExpr = this.castedExpr ?: return false
+ return MEPsiUtil.isWildcardExpression(castedExpr) && castType?.matchesJava(javaType, context) == true
+ }
+ else -> false
+ }
+ }
+
+ override fun getInputExprs() = listOfNotNull(castedExpr)
+
+ protected abstract val expressionList: List
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt
new file mode 100644
index 000000000..8415f7cd0
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEClassConstantExpressionImplMixin.kt
@@ -0,0 +1,60 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiClassObjectAccessExpression
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiField
+import com.intellij.psi.PsiReferenceExpression
+import com.intellij.psi.util.PsiTypesUtil
+
+abstract class MEClassConstantExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ return when (java) {
+ is PsiClassObjectAccessExpression -> type.matchesJava(java.operand.type, context)
+ is PsiReferenceExpression -> {
+ if (java.referenceName != "TYPE") {
+ return false
+ }
+ val field = java.resolve() as? PsiField ?: return false
+ val containingClass = field.containingClass?.qualifiedName ?: return false
+ val unboxedType = PsiTypesUtil.unboxIfPossible(containingClass)
+ if (unboxedType == null || unboxedType == containingClass) {
+ return false
+ }
+ val javaType = JavaPsiFacade.getElementFactory(context.project).createPrimitiveTypeFromText(unboxedType)
+ type.matchesJava(javaType, context)
+ }
+ else -> false
+ }
+ }
+
+ override fun getInputExprs() = emptyList()
+
+ protected abstract val type: METype
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt
new file mode 100644
index 000000000..ecaecca53
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEConstructorReferenceExpressionImplMixin.kt
@@ -0,0 +1,47 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.METype
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethodReferenceExpression
+
+abstract class MEConstructorReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiMethodReferenceExpression) {
+ return false
+ }
+
+ if (!java.isConstructor) {
+ return false
+ }
+
+ val qualifierType = java.qualifierType?.type ?: return false
+ return className.matchesJava(qualifierType, context)
+ }
+
+ override fun getInputExprs() = emptyList()
+
+ abstract val className: METype
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt
new file mode 100644
index 000000000..6201fbb9c
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationImplMixin.kt
@@ -0,0 +1,63 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.asset.PlatformAssets
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclarationItem
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEItemImpl
+import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory
+import com.intellij.lang.ASTNode
+import com.intellij.navigation.ItemPresentation
+import com.intellij.openapi.util.Iconable
+import com.intellij.psi.NavigatablePsiElement
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiNameIdentifierOwner
+import com.intellij.psi.PsiNamedElement
+import com.intellij.psi.search.LocalSearchScope
+import com.intellij.util.PlatformIcons
+import javax.swing.Icon
+
+abstract class MEDeclarationImplMixin(
+ node: ASTNode
+) : MEItemImpl(node), PsiNamedElement, PsiNameIdentifierOwner, NavigatablePsiElement {
+ override fun getName(): String = nameIdentifier.text
+
+ override fun setName(name: String): PsiElement {
+ this.nameIdentifier.replace(project.meExpressionElementFactory.createIdentifier(name))
+ return this
+ }
+
+ override fun getNameIdentifier(): PsiElement = firstChild
+
+ override fun getUseScope() = containingFile?.let(::LocalSearchScope) ?: super.getUseScope()
+
+ override fun getPresentation() = object : ItemPresentation {
+ override fun getPresentableText() = name
+
+ override fun getIcon(unused: Boolean) = this@MEDeclarationImplMixin.getIcon(Iconable.ICON_FLAG_VISIBILITY)
+ }
+
+ override fun getIcon(flags: Int): Icon = if ((parent as? MEDeclarationItem)?.isType == true) {
+ PlatformIcons.CLASS_ICON
+ } else {
+ PlatformAssets.MIXIN_ICON
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt
new file mode 100644
index 000000000..fbd21db66
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEDeclarationItemImplMixin.kt
@@ -0,0 +1,32 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEDeclarationItemMixin
+import com.intellij.extapi.psi.ASTWrapperPsiElement
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+
+abstract class MEDeclarationItemImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEDeclarationItemMixin {
+ override val isType: Boolean
+ get() = findChildByType(MEExpressionTypes.TOKEN_BOOL_LIT)?.text == "true"
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt
new file mode 100644
index 000000000..9cc9b64a6
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionImplMixin.kt
@@ -0,0 +1,38 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement
+import com.intellij.extapi.psi.ASTWrapperPsiElement
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+
+abstract class MEExpressionImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEMatchableElement {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ throw UnsupportedOperationException("Please implement matchesJava for your expression type")
+ }
+
+ override fun getInputExprs(): List {
+ throw UnsupportedOperationException("Please implement getInputExprs for your expression type")
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt
new file mode 100644
index 000000000..e895d32ba
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEExpressionStatementImplMixin.kt
@@ -0,0 +1,37 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+
+abstract class MEExpressionStatementImplMixin(node: ASTNode) : MEStatementImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ return expression.matchesJava(java, context)
+ }
+
+ override fun getInputExprs() = listOf(expression)
+
+ protected abstract val expression: MEExpression
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt
new file mode 100644
index 000000000..d264eb4ce
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEFreeMethodReferenceExpressionImplMixin.kt
@@ -0,0 +1,60 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethod
+import com.intellij.psi.PsiMethodReferenceExpression
+
+abstract class MEFreeMethodReferenceExpressionImplMixin(node: ASTNode) : MEExpressionImplMixin(node), MEExpression {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiMethodReferenceExpression) {
+ return false
+ }
+
+ if (java.isConstructor) {
+ return false
+ }
+
+ val qualifierClass = (java.qualifierType?.type as? PsiClassType)?.resolve() ?: return false
+
+ // check wildcard after checking for the qualifier class, otherwise the reference could have been qualified by
+ // an expression.
+ val memberName = this.memberName ?: return false
+ if (memberName.isWildcard) {
+ return true
+ }
+
+ val method = java.resolve() as? PsiMethod ?: return false
+ return context.getMethods(memberName.text).any { reference ->
+ reference.matchMethod(method, qualifierClass)
+ }
+ }
+
+ override fun getInputExprs() = emptyList()
+
+ abstract val memberName: MEName?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt
new file mode 100644
index 000000000..733f689f8
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MELitExpressionImplMixin.kt
@@ -0,0 +1,124 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MELitExpressionMixin
+import com.intellij.lang.ASTNode
+import com.intellij.psi.JavaTokenType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiLiteral
+import com.intellij.psi.PsiUnaryExpression
+import com.intellij.psi.util.PsiUtil
+import com.intellij.util.IncorrectOperationException
+
+abstract class MELitExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MELitExpressionMixin {
+ override val value get() = when (node.firstChildNode.elementType) {
+ MEExpressionTypes.TOKEN_NULL_LIT -> null
+ MEExpressionTypes.TOKEN_MINUS -> {
+ when (node.lastChildNode.elementType) {
+ MEExpressionTypes.TOKEN_INT_LIT -> {
+ val text = node.lastChildNode.text
+ if (text.startsWith("0x")) {
+ "-${text.substring(2)}".toLongOrNull(16)
+ } else {
+ "-$text".toLongOrNull()
+ }
+ }
+ MEExpressionTypes.TOKEN_DEC_LIT -> {
+ "-${node.lastChildNode.text}".toDoubleOrNull()
+ }
+ else -> throw IncorrectOperationException("Invalid number literal format")
+ }
+ }
+ MEExpressionTypes.TOKEN_BOOL_LIT -> node.chars[0] == 't'
+ MEExpressionTypes.TOKEN_INT_LIT -> {
+ val text = this.text
+ if (text.startsWith("0x")) {
+ text.substring(2).toLongOrNull(16)
+ } else {
+ text.toLongOrNull()
+ }
+ }
+ MEExpressionTypes.TOKEN_DEC_LIT -> text.toDoubleOrNull()
+ else -> {
+ val text = this.text
+ if (text.length >= 2) {
+ text.substring(1, text.length - 1).replace("\\'", "'").replace("\\\\", "\\")
+ } else {
+ null
+ }
+ }
+ }
+
+ override val isNull get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_NULL_LIT
+ override val isString get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_STRING_TERMINATOR
+
+ override val minusToken get() = node.firstChildNode.takeIf { it.elementType == MEExpressionTypes.TOKEN_MINUS }
+
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ return when (java) {
+ is PsiLiteral -> {
+ val value = this.value
+ val javaValue = java.value.widened
+ // MixinExtras compares floats as strings
+ when (value) {
+ is Double -> javaValue is Double && value.toString() == javaValue.toString()
+ is String -> {
+ val matchesChar =
+ value.length == 1 && javaValue is Long && value.firstOrNull()?.code?.toLong() == javaValue
+ matchesChar || value == javaValue
+ }
+ else -> value == javaValue
+ }
+ }
+ is PsiUnaryExpression -> {
+ if (java.operationTokenType != JavaTokenType.MINUS) {
+ return false
+ }
+ val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false
+ if (javaOperand !is PsiLiteral) {
+ return false
+ }
+ val value = this.value
+ val javaValue = javaOperand.value.widened
+ when (value) {
+ is Long -> javaValue == -value
+ is Double -> javaValue is Double && javaValue.toString() == (-value).toString()
+ else -> false
+ }
+ }
+ else -> false
+ }
+ }
+
+ override fun getInputExprs() = emptyList()
+
+ private val Any?.widened: Any? get() = when (this) {
+ is Int -> toLong()
+ is Float -> toDouble()
+ is Char -> code.toLong()
+ else -> this
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt
new file mode 100644
index 000000000..4c4c4a11e
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMemberAccessExpressionImplMixin.kt
@@ -0,0 +1,71 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember
+import com.intellij.lang.ASTNode
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiField
+import com.intellij.psi.PsiModifier
+import com.intellij.psi.PsiReferenceExpression
+import com.intellij.psi.util.PsiUtil
+import com.siyeh.ig.psiutils.ExpressionUtils
+
+abstract class MEMemberAccessExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiReferenceExpression) {
+ return false
+ }
+
+ val arrayFromLength = ExpressionUtils.getArrayFromLengthExpression(java)
+ if (arrayFromLength != null) {
+ if (memberName.isWildcard || memberName.text == "length") {
+ return true
+ }
+ }
+
+ val resolved = java.resolve() as? PsiField ?: return false
+ if (resolved.hasModifierProperty(PsiModifier.STATIC)) {
+ return false
+ }
+
+ val javaReceiver = PsiUtil.skipParenthesizedExprDown(java.qualifierExpression)
+ ?: JavaPsiFacade.getElementFactory(context.project).createExpressionFromText("this", null)
+ context.fakeElementScope(java.qualifierExpression == null, java) {
+ if (!receiverExpr.matchesJava(javaReceiver, context)) {
+ return false
+ }
+ }
+
+ val qualifier = QualifiedMember.resolveQualifier(java) ?: resolved.containingClass ?: return false
+ return context.getFields(memberName.text).any { it.matchField(resolved, qualifier) }
+ }
+
+ override fun getInputExprs() = listOf(receiverExpr)
+
+ protected abstract val receiverExpr: MEExpression
+ protected abstract val memberName: MEName
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt
new file mode 100644
index 000000000..2f397998e
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEMethodCallExpressionImplMixin.kt
@@ -0,0 +1,77 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember
+import com.intellij.lang.ASTNode
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethodCallExpression
+import com.intellij.psi.PsiModifier
+import com.intellij.psi.util.PsiUtil
+import com.siyeh.ig.psiutils.MethodCallUtils
+
+abstract class MEMethodCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiMethodCallExpression) {
+ return false
+ }
+
+ if (MethodCallUtils.hasSuperQualifier(java)) {
+ return false
+ }
+
+ val method = java.resolveMethod() ?: return false
+ if (method.hasModifierProperty(PsiModifier.STATIC)) {
+ return false
+ }
+
+ if (!memberName.isWildcard) {
+ val methodId = memberName.text
+ val qualifier =
+ QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false
+ if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) {
+ return false
+ }
+ }
+
+ val javaReceiver = PsiUtil.skipParenthesizedExprDown(java.methodExpression.qualifierExpression)
+ ?: JavaPsiFacade.getElementFactory(context.project).createExpressionFromText("this", null)
+ context.fakeElementScope(java.methodExpression.qualifierExpression == null, java.methodExpression) {
+ if (!receiverExpr.matchesJava(javaReceiver, context)) {
+ return false
+ }
+ }
+
+ return arguments?.matchesJava(java.argumentList, context) == true
+ }
+
+ override fun getInputExprs() = listOf(receiverExpr) + (arguments?.expressionList ?: emptyList())
+
+ protected abstract val receiverExpr: MEExpression
+ protected abstract val memberName: MEName
+ protected abstract val arguments: MEArguments?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt
new file mode 100644
index 000000000..decc485d4
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameExpressionImplMixin.kt
@@ -0,0 +1,78 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember
+import com.demonwav.mcdev.platform.mixin.util.LocalVariables
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiField
+import com.intellij.psi.PsiReferenceExpression
+import com.intellij.psi.PsiVariable
+import com.intellij.psi.util.PsiUtil
+
+abstract class MENameExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (MEName.isWildcard) {
+ return true
+ }
+
+ if (java !is PsiReferenceExpression) {
+ return false
+ }
+ val variable = java.resolve() as? PsiVariable ?: return false
+
+ val name = MEName.text
+
+ // match against fields
+ if (variable is PsiField) {
+ val qualifier = QualifiedMember.resolveQualifier(java) ?: variable.containingClass ?: return false
+ return context.getFields(name).any { it.matchField(variable, qualifier) }
+ }
+
+ // match against local variables
+ val sourceArgs by lazy {
+ LocalVariables.guessLocalsAt(java, true, !PsiUtil.isAccessedForWriting(java))
+ }
+ val sourceVariables by lazy {
+ LocalVariables.guessLocalsAt(java, false, !PsiUtil.isAccessedForWriting(java))
+ }
+ for (localInfo in context.getLocalInfos(name)) {
+ val sourceLocals = if (localInfo.argsOnly) sourceArgs else sourceVariables
+ for (local in localInfo.matchSourceLocals(sourceLocals)) {
+ if (local.variable == variable) {
+ return true
+ }
+ }
+ }
+
+ return false
+ }
+
+ override fun getInputExprs() = emptyList()
+
+ @Suppress("PropertyName")
+ protected abstract val MEName: MEName
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt
new file mode 100644
index 000000000..50b02d3c7
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENameImplMixin.kt
@@ -0,0 +1,41 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENameMixin
+import com.demonwav.mcdev.platform.mixin.expression.reference.MEDefinitionReference
+import com.intellij.extapi.psi.ASTWrapperPsiElement
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiReference
+
+abstract class MENameImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MENameMixin {
+ override val isWildcard get() = node.firstChildNode.elementType == MEExpressionTypes.TOKEN_WILDCARD
+ override val identifierElement get() = if (isWildcard) null else firstChild
+
+ override fun getReference(): PsiReference? {
+ if (isWildcard) {
+ return null
+ }
+ return MEDefinitionReference(this as MEName)
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt
new file mode 100644
index 000000000..1f72ada47
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MENewExpressionImplMixin.kt
@@ -0,0 +1,138 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MENewExpressionMixin
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiNewExpression
+import com.intellij.psi.util.PsiUtil
+import com.intellij.psi.util.siblings
+
+abstract class MENewExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MENewExpressionMixin {
+ override val isArrayCreation get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) != null
+
+ override val hasConstructorArguments get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_PAREN) != null
+
+ override val dimensions get() = findChildrenByType(MEExpressionTypes.TOKEN_LEFT_BRACKET).size
+
+ override val dimExprTokens: List get() {
+ val result = mutableListOf()
+
+ var leftBracket: PsiElement? = findNotNullChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET)
+ while (leftBracket != null) {
+ var expr: MEExpression? = null
+ var rightBracket: PsiElement? = null
+ var nextLeftBracket: PsiElement? = null
+ for (child in leftBracket.siblings(withSelf = false)) {
+ if (child is MEExpression) {
+ expr = child
+ } else {
+ when (child.node.elementType) {
+ MEExpressionTypes.TOKEN_RIGHT_BRACKET -> rightBracket = child
+ MEExpressionTypes.TOKEN_LEFT_BRACKET -> {
+ nextLeftBracket = child
+ break
+ }
+ }
+ }
+ }
+ result += MENewExpressionMixin.DimExprTokens(leftBracket, expr, rightBracket)
+ leftBracket = nextLeftBracket
+ }
+
+ return result
+ }
+
+ override val arrayInitializer get() = if (isArrayCreation) {
+ arguments
+ } else {
+ null
+ }
+
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiNewExpression) {
+ return false
+ }
+
+ if (isArrayCreation) {
+ if (!java.isArrayCreation) {
+ return false
+ }
+
+ val javaArrayType = java.type as? PsiArrayType ?: return false
+ if (javaArrayType.arrayDimensions != dimensions) {
+ return false
+ }
+
+ val matchesType = context.project.meExpressionElementFactory.createType(type)
+ .matchesJava(javaArrayType.deepComponentType, context)
+ if (!matchesType) {
+ return false
+ }
+
+ val javaArrayDims = java.arrayDimensions
+ val arrayDims = dimExprs
+ if (javaArrayDims.size != arrayDims.size) {
+ return false
+ }
+ if (!javaArrayDims.asSequence().zip(arrayDims.asSequence()).all { (javaArrayDim, arrayDim) ->
+ val actualJavaDim = PsiUtil.skipParenthesizedExprDown(javaArrayDim) ?: return@all false
+ arrayDim.matchesJava(actualJavaDim, context)
+ }
+ ) {
+ return false
+ }
+
+ val javaArrayInitializer = java.arrayInitializer
+ val arrayInitializer = this.arrayInitializer
+ return if (javaArrayInitializer == null) {
+ arrayInitializer == null
+ } else {
+ arrayInitializer?.matchesJava(javaArrayInitializer.initializers, context) == true
+ }
+ } else { // !isArrayCreation
+ if (java.isArrayCreation) {
+ return false
+ }
+
+ val javaType = java.type ?: return false
+ val javaArgs = java.argumentList ?: return false
+
+ return context.project.meExpressionElementFactory.createType(type).matchesJava(javaType, context) &&
+ arguments?.matchesJava(javaArgs, context) == true
+ }
+ }
+
+ override fun getInputExprs() = dimExprs + (arguments?.expressionList ?: emptyList())
+
+ protected abstract val type: MEName
+ protected abstract val dimExprs: List
+ protected abstract val arguments: MEArguments?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt
new file mode 100644
index 000000000..4061c6c6a
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEParenthesizedExpressionImplMixin.kt
@@ -0,0 +1,37 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+
+abstract class MEParenthesizedExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ return expression?.matchesJava(java, context) == true
+ }
+
+ override fun getInputExprs() = listOfNotNull(expression)
+
+ protected abstract val expression: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt
new file mode 100644
index 000000000..0113a7b86
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEReturnStatementImplMixin.kt
@@ -0,0 +1,43 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiReturnStatement
+import com.intellij.psi.util.PsiUtil
+
+abstract class MEReturnStatementImplMixin(node: ASTNode) : MEStatementImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiReturnStatement) {
+ return false
+ }
+ val javaReturnValue = PsiUtil.skipParenthesizedExprDown(java.returnValue) ?: return false
+ return valueExpr?.matchesJava(javaReturnValue, context) == true
+ }
+
+ override fun getInputExprs() = listOfNotNull(valueExpr)
+
+ protected abstract val valueExpr: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt
new file mode 100644
index 000000000..39f8ad153
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStatementImplMixin.kt
@@ -0,0 +1,38 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEMatchableElement
+import com.intellij.extapi.psi.ASTWrapperPsiElement
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+
+abstract class MEStatementImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), MEMatchableElement {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ throw UnsupportedOperationException("Please implement matchesJava for your statement type")
+ }
+
+ override fun getInputExprs(): List {
+ throw UnsupportedOperationException("Please implement getInputExprs for your statement type")
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt
new file mode 100644
index 000000000..7a578b242
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEStaticMethodCallExpressionImplMixin.kt
@@ -0,0 +1,60 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethodCallExpression
+import com.intellij.psi.PsiModifier
+
+abstract class MEStaticMethodCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiMethodCallExpression) {
+ return false
+ }
+
+ val method = java.resolveMethod() ?: return false
+ if (!method.hasModifierProperty(PsiModifier.STATIC)) {
+ return false
+ }
+
+ if (!memberName.isWildcard) {
+ val methodId = memberName.text
+ val qualifier =
+ QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false
+ if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) {
+ return false
+ }
+ }
+
+ return arguments?.matchesJava(java.argumentList, context) == true
+ }
+
+ override fun getInputExprs() = arguments?.expressionList ?: emptyList()
+
+ protected abstract val memberName: MEName
+ protected abstract val arguments: MEArguments?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt
new file mode 100644
index 000000000..2dee0b9c7
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MESuperCallExpressionImplMixin.kt
@@ -0,0 +1,60 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEArguments
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.QualifiedMember
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiMethodCallExpression
+import com.siyeh.ig.psiutils.MethodCallUtils
+
+abstract class MESuperCallExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiMethodCallExpression) {
+ return false
+ }
+ if (!MethodCallUtils.hasSuperQualifier(java)) {
+ return false
+ }
+
+ val memberName = this.memberName ?: return false
+ if (!memberName.isWildcard) {
+ val method = java.resolveMethod() ?: return false
+ val methodId = memberName.text
+ val qualifier =
+ QualifiedMember.resolveQualifier(java.methodExpression) ?: method.containingClass ?: return false
+ if (context.getMethods(methodId).none { it.matchMethod(method, qualifier) }) {
+ return false
+ }
+ }
+
+ return arguments?.matchesJava(java.argumentList, context) == true
+ }
+
+ override fun getInputExprs() = arguments?.expressionList ?: emptyList()
+
+ protected abstract val memberName: MEName?
+ protected abstract val arguments: MEArguments?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt
new file mode 100644
index 000000000..6a4d25339
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhisExpressionImplMixin.kt
@@ -0,0 +1,36 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiThisExpression
+
+abstract class METhisExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ return java is PsiThisExpression && java.qualifier == null
+ }
+
+ override fun getInputExprs() = emptyList()
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt
new file mode 100644
index 000000000..4226d24d3
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METhrowStatementImplMixin.kt
@@ -0,0 +1,44 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEStatementImpl
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiThrowStatement
+import com.intellij.psi.util.PsiUtil
+
+abstract class METhrowStatementImplMixin(node: ASTNode) : MEStatementImpl(node) {
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiThrowStatement) {
+ return false
+ }
+
+ val javaException = PsiUtil.skipParenthesizedExprDown(java.exception) ?: return false
+ return valueExpr?.matchesJava(javaException, context) == true
+ }
+
+ override fun getInputExprs() = listOfNotNull(valueExpr)
+
+ protected abstract val valueExpr: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt
new file mode 100644
index 000000000..f41550915
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/METypeImplMixin.kt
@@ -0,0 +1,53 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.METypeMixin
+import com.demonwav.mcdev.util.descriptor
+import com.intellij.extapi.psi.ASTWrapperPsiElement
+import com.intellij.lang.ASTNode
+import com.intellij.psi.PsiArrayType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiType
+
+abstract class METypeImplMixin(node: ASTNode) : ASTWrapperPsiElement(node), METypeMixin {
+ override val isArray get() = findChildByType(MEExpressionTypes.TOKEN_LEFT_BRACKET) != null
+ override val dimensions get() = findChildrenByType(MEExpressionTypes.TOKEN_LEFT_BRACKET).size
+
+ override fun matchesJava(java: PsiType, context: MESourceMatchContext): Boolean {
+ if (MEName.isWildcard) {
+ return java.arrayDimensions >= dimensions
+ } else {
+ var unwrappedElementType = java
+ repeat(dimensions) {
+ unwrappedElementType = (unwrappedElementType as? PsiArrayType)?.componentType ?: return false
+ }
+ val descriptor = unwrappedElementType.descriptor
+ return context.getTypes(MEName.text).any { it == descriptor }
+ }
+ }
+
+ @Suppress("PropertyName")
+ protected abstract val MEName: MEName
+}
diff --git a/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt
new file mode 100644
index 000000000..8238e13d8
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/psi/mixins/impl/MEUnaryExpressionImplMixin.kt
@@ -0,0 +1,65 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.psi.mixins.impl
+
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEExpressionTypes
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.impl.MEExpressionImpl
+import com.demonwav.mcdev.platform.mixin.expression.psi.mixins.MEUnaryExpressionMixin
+import com.intellij.lang.ASTNode
+import com.intellij.psi.JavaTokenType
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiLiteral
+import com.intellij.psi.PsiUnaryExpression
+import com.intellij.psi.util.PsiUtil
+
+abstract class MEUnaryExpressionImplMixin(node: ASTNode) : MEExpressionImpl(node), MEUnaryExpressionMixin {
+ override val operator get() = node.firstChildNode.elementType
+
+ override fun matchesJava(java: PsiElement, context: MESourceMatchContext): Boolean {
+ if (java !is PsiUnaryExpression) {
+ return false
+ }
+
+ val operatorMatches = when (java.operationTokenType) {
+ JavaTokenType.MINUS -> operator == MEExpressionTypes.TOKEN_MINUS
+ JavaTokenType.TILDE -> operator == MEExpressionTypes.TOKEN_BITWISE_NOT
+ else -> false
+ }
+ if (!operatorMatches) {
+ return false
+ }
+
+ val javaOperand = PsiUtil.skipParenthesizedExprDown(java.operand) ?: return false
+
+ if (operator == MEExpressionTypes.TOKEN_MINUS && javaOperand is PsiLiteral) {
+ // avoid matching "-1" etc
+ return false
+ }
+
+ return expression?.matchesJava(javaOperand, context) == true
+ }
+
+ override fun getInputExprs() = listOfNotNull(expression)
+
+ protected abstract val expression: MEExpression?
+}
diff --git a/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt b/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt
new file mode 100644
index 000000000..2b281f093
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/reference/MEDefinitionReference.kt
@@ -0,0 +1,70 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.reference
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEName
+import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile
+import com.intellij.openapi.util.TextRange
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiReference
+import com.intellij.psi.util.parentOfType
+import com.intellij.util.ArrayUtilRt
+import com.intellij.util.IncorrectOperationException
+
+class MEDefinitionReference(private var name: MEName) : PsiReference {
+ override fun getElement() = name
+
+ override fun getRangeInElement() = TextRange(0, name.textLength)
+
+ override fun resolve(): PsiElement? {
+ val file = element.parentOfType() ?: return null
+ val name = element.text
+ for (declItem in file.declarations) {
+ val declaration = declItem.declaration
+ if (declaration?.name == name) {
+ return declaration
+ }
+ }
+
+ return null
+ }
+
+ override fun getCanonicalText(): String = name.text
+
+ override fun handleElementRename(newElementName: String): PsiElement {
+ name = name.replace(name.project.meExpressionElementFactory.createName(newElementName)) as MEName
+ return name
+ }
+
+ override fun bindToElement(element: PsiElement): PsiElement {
+ throw IncorrectOperationException()
+ }
+
+ override fun isReferenceTo(element: PsiElement) = element.manager.areElementsEquivalent(element, resolve())
+
+ override fun isSoft() = false
+
+ override fun getVariants(): Array {
+ return (name.containingFile as? MEExpressionFile)?.declarations?.mapNotNull { it.declaration }?.toTypedArray()
+ ?: ArrayUtilRt.EMPTY_OBJECT_ARRAY
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt b/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt
new file mode 100644
index 000000000..c20296b7c
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/expression/reference/MEExpressionFindUsagesProvider.kt
@@ -0,0 +1,38 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression.reference
+
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEDeclaration
+import com.intellij.lang.findUsages.FindUsagesProvider
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiNamedElement
+
+class MEExpressionFindUsagesProvider : FindUsagesProvider {
+ override fun canFindUsagesFor(psiElement: PsiElement) = psiElement is MEDeclaration
+
+ override fun getHelpId(psiElement: PsiElement) = null
+
+ override fun getType(element: PsiElement) = "Definition"
+
+ override fun getDescriptiveName(element: PsiElement) = (element as? PsiNamedElement)?.name ?: "null"
+
+ override fun getNodeText(element: PsiElement, useFullName: Boolean) = getDescriptiveName(element)
+}
diff --git a/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt b/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt
index 7f359fd58..44ccb2c9a 100644
--- a/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt
+++ b/src/main/kotlin/platform/mixin/folding/MixinFoldingOptionsProvider.kt
@@ -56,5 +56,15 @@ class MixinFoldingOptionsProvider :
{ settings.state.foldAccessorMethodCalls },
{ b -> settings.state.foldAccessorMethodCalls = b },
)
+ checkBox(
+ "Fold MixinExtras expression definitions",
+ { settings.state.foldDefinitions },
+ { b -> settings.state.foldDefinitions = b },
+ )
+ checkBox(
+ "Fold MixinExtras expression definition fields and methods",
+ { settings.state.foldDefinitionFieldsAndMethods },
+ { b -> settings.state.foldDefinitionFieldsAndMethods = b },
+ )
}
}
diff --git a/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt b/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt
index 081cc03aa..61fee79af 100644
--- a/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt
+++ b/src/main/kotlin/platform/mixin/folding/MixinFoldingSettings.kt
@@ -35,6 +35,8 @@ class MixinFoldingSettings : PersistentStateComponent)
companion object {
@@ -217,4 +220,6 @@ object DefaultInjectorAnnotationHandler : InjectorAnnotationHandler() {
) = null
override val isSoft = true
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM
}
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt
index d34c4aad2..69a4197e6 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgHandler.kt
@@ -30,6 +30,7 @@ import com.demonwav.mcdev.util.descriptor
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiMethod
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
@@ -135,4 +136,6 @@ class ModifyArgHandler : InjectorAnnotationHandler() {
}
}
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARG
}
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt
index 446e9a20f..0d3b5476e 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyArgsHandler.kt
@@ -27,6 +27,7 @@ import com.demonwav.mcdev.util.Parameter
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiTypes
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodInsnNode
@@ -58,4 +59,6 @@ class ModifyArgsHandler : InjectorAnnotationHandler() {
),
)
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_ARGS
}
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt
index eebd528de..839d9f832 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyConstantHandler.kt
@@ -31,6 +31,7 @@ import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
import com.intellij.psi.util.parentOfType
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
@@ -131,4 +132,6 @@ class ModifyConstantHandler : InjectorAnnotationHandler() {
override fun isInsnAllowed(insn: AbstractInsnNode): Boolean {
return insn.opcode in allowedOpcodes
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_CONSTANT
}
diff --git a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt
index d9e4eeae9..42398c23d 100644
--- a/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/ModifyVariableHandler.kt
@@ -32,6 +32,7 @@ import com.demonwav.mcdev.util.findContainingMethod
import com.demonwav.mcdev.util.findModule
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
@@ -85,4 +86,6 @@ class ModifyVariableHandler : InjectorAnnotationHandler() {
return result
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_VARIABLE
}
diff --git a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt
index 81e494291..70d2d7fd2 100644
--- a/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/RedirectInjectorHandler.kt
@@ -39,6 +39,7 @@ import com.intellij.psi.PsiElementFactory
import com.intellij.psi.PsiManager
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
@@ -105,6 +106,8 @@ class RedirectInjectorHandler : InjectorAnnotationHandler() {
override val allowCoerce = true
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.REDIRECT
+
private interface RedirectType {
fun isInsnAllowed(node: AbstractInsnNode) = true
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
index c1b1df6ac..ec61f4a4c 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/AtResolver.kt
@@ -210,6 +210,7 @@ class AtResolver(
val targetPsiClass = targetElement.parentOfType() ?: return emptyList()
val navigationVisitor = injectionPoint.createNavigationVisitor(at, target, targetPsiClass) ?: return emptyList()
+ navigationVisitor.configureBytecodeTarget(targetClass, targetMethod)
targetElement.accept(navigationVisitor)
return bytecodeResults.mapNotNull { bytecodeResult ->
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt
index 8939f36ae..17a917eef 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/InjectionPoint.kt
@@ -321,6 +321,9 @@ abstract class NavigationVisitor : JavaRecursiveElementVisitor() {
result += element
}
+ open fun configureBytecodeTarget(classNode: ClassNode, methodNode: MethodNode) {
+ }
+
open fun visitStart(executableElement: PsiElement) {
}
@@ -407,6 +410,7 @@ abstract class CollectVisitor(protected val mode: Mode) {
insn: AbstractInsnNode,
element: T,
qualifier: String? = null,
+ decorations: Map = emptyMap(),
) {
// apply shift.
// being able to break out of the shift loops is important to prevent IDE freezes in case of large shift bys.
@@ -427,7 +431,14 @@ abstract class CollectVisitor(protected val mode: Mode) {
}
}
- val result = Result(nextIndex++, insn, shiftedInsn ?: return, element, qualifier)
+ val result = Result(
+ nextIndex++,
+ insn,
+ shiftedInsn ?: return,
+ element,
+ qualifier,
+ if (insn === shiftedInsn) decorations else emptyMap()
+ )
var isFiltered = false
for ((name, filter) in resultFilters) {
if (!filter(result, method)) {
@@ -463,6 +474,7 @@ abstract class CollectVisitor(protected val mode: Mode) {
val insn: AbstractInsnNode,
val target: T,
val qualifier: String? = null,
+ val decorations: Map
)
enum class Mode { MATCH_ALL, MATCH_FIRST, COMPLETION }
diff --git a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt
index 4e7378045..7ad828179 100644
--- a/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt
+++ b/src/main/kotlin/platform/mixin/handlers/injectionPoint/LoadInjectionPoint.kt
@@ -28,7 +28,6 @@ import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.MODIFY_
import com.demonwav.mcdev.util.constantValue
import com.demonwav.mcdev.util.findContainingMethod
import com.demonwav.mcdev.util.findModule
-import com.demonwav.mcdev.util.isErasureEquivalentTo
import com.intellij.codeInsight.lookup.LookupElementBuilder
import com.intellij.openapi.module.Module
import com.intellij.psi.JavaPsiFacade
@@ -166,13 +165,13 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio
val parentExpr = PsiUtil.skipParenthesizedExprUp(expression.parent)
val isIincUnary = parentExpr is PsiUnaryExpression &&
(
- parentExpr.operationSign.tokenType == JavaTokenType.PLUSPLUS ||
- parentExpr.operationSign.tokenType == JavaTokenType.MINUSMINUS
+ parentExpr.operationTokenType == JavaTokenType.PLUSPLUS ||
+ parentExpr.operationTokenType == JavaTokenType.MINUSMINUS
)
val isIincAssignment = parentExpr is PsiAssignmentExpression &&
(
- parentExpr.operationSign.tokenType == JavaTokenType.PLUSEQ ||
- parentExpr.operationSign.tokenType == JavaTokenType.MINUSEQ
+ parentExpr.operationTokenType == JavaTokenType.PLUSEQ ||
+ parentExpr.operationTokenType == JavaTokenType.MINUSEQ
) &&
PsiUtil.isConstantExpression(parentExpr.rExpression) &&
(parentExpr.rExpression?.constantValue as? Number)?.toInt()
@@ -239,42 +238,10 @@ abstract class AbstractLoadInjectionPoint(private val store: Boolean) : Injectio
name: String,
localsHere: List,
) {
- if (info.ordinal != null) {
- val local = localsHere.asSequence().filter {
- it.type.isErasureEquivalentTo(info.type)
- }.drop(info.ordinal).firstOrNull()
- if (name == local?.name) {
+ for (local in info.matchSourceLocals(localsHere)) {
+ if (name == local.name) {
addResult(location)
}
- return
- }
-
- if (info.index != null) {
- val local = localsHere.getOrNull(info.index)
- if (name == local?.name) {
- addResult(location)
- }
- return
- }
-
- if (info.names.isNotEmpty()) {
- val matchingLocals = localsHere.filter {
- info.names.contains(it.mixinName)
- }
- for (local in matchingLocals) {
- if (local.name == name) {
- addResult(location)
- }
- }
- return
- }
-
- // implicit mode
- val local = localsHere.singleOrNull {
- it.type.isErasureEquivalentTo(info.type)
- }
- if (local != null && local.name == name) {
- addResult(location)
}
}
}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt
new file mode 100644
index 000000000..680d8ea19
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ExpressionInjectionPoint.kt
@@ -0,0 +1,284 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.handlers.mixinextras
+
+import com.demonwav.mcdev.platform.mixin.expression.IdentifierPoolFactory
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil
+import com.demonwav.mcdev.platform.mixin.expression.MESourceMatchContext
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MECapturingExpression
+import com.demonwav.mcdev.platform.mixin.expression.gen.psi.MEStatement
+import com.demonwav.mcdev.platform.mixin.expression.meExpressionElementFactory
+import com.demonwav.mcdev.platform.mixin.expression.psi.MEExpressionFile
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.AtResolver
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.InjectionPoint
+import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.NavigationVisitor
+import com.demonwav.mcdev.platform.mixin.reference.MixinSelector
+import com.demonwav.mcdev.platform.mixin.util.LocalInfo
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.util.MemberReference
+import com.demonwav.mcdev.util.computeStringArray
+import com.demonwav.mcdev.util.constantStringValue
+import com.demonwav.mcdev.util.descriptor
+import com.demonwav.mcdev.util.findAnnotations
+import com.demonwav.mcdev.util.findContainingModifierList
+import com.demonwav.mcdev.util.findModule
+import com.demonwav.mcdev.util.findMultiInjectionHost
+import com.demonwav.mcdev.util.ifEmpty
+import com.demonwav.mcdev.util.parseArray
+import com.demonwav.mcdev.util.resolveType
+import com.demonwav.mcdev.util.resolveTypeArray
+import com.intellij.codeInsight.lookup.LookupElementBuilder
+import com.intellij.lang.injection.InjectedLanguageManager
+import com.intellij.openapi.editor.Editor
+import com.intellij.openapi.project.Project
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiLiteral
+import com.intellij.psi.PsiModifierList
+import com.intellij.psi.codeStyle.CodeStyleManager
+import com.intellij.psi.codeStyle.JavaCodeStyleManager
+import com.intellij.psi.util.parentOfType
+import com.llamalad7.mixinextras.expression.impl.ast.expressions.Expression
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
+import java.util.IdentityHashMap
+import org.objectweb.asm.tree.AbstractInsnNode
+import org.objectweb.asm.tree.ClassNode
+import org.objectweb.asm.tree.MethodNode
+
+class ExpressionInjectionPoint : InjectionPoint() {
+ override fun onCompleted(editor: Editor, reference: PsiLiteral) {
+ val modifierList = reference.findContainingModifierList() ?: return
+ if (modifierList.hasAnnotation(MixinConstants.MixinExtras.EXPRESSION)) {
+ return
+ }
+
+ val project = reference.project
+
+ val exprAnnotation = modifierList.addAfter(
+ JavaPsiFacade.getElementFactory(project)
+ .createAnnotationFromText("@${MixinConstants.MixinExtras.EXPRESSION}(\"\")", reference),
+ null
+ )
+
+ // add imports and reformat
+ JavaCodeStyleManager.getInstance(project).shortenClassReferences(exprAnnotation)
+ JavaCodeStyleManager.getInstance(project).optimizeImports(modifierList.containingFile)
+ val formattedModifierList = CodeStyleManager.getInstance(project).reformat(modifierList) as PsiModifierList
+
+ // move the caret to @Expression("")
+ val formattedExprAnnotation = formattedModifierList.findAnnotation(MixinConstants.MixinExtras.EXPRESSION)
+ ?: return
+ val exprLiteral = formattedExprAnnotation.findDeclaredAttributeValue(null) ?: return
+ editor.caretModel.moveToOffset(exprLiteral.textRange.startOffset + 1)
+ }
+
+ override fun createNavigationVisitor(
+ at: PsiAnnotation,
+ target: MixinSelector?,
+ targetClass: PsiClass
+ ): NavigationVisitor? {
+ val project = at.project
+
+ val atId = at.findDeclaredAttributeValue("id")?.constantStringValue ?: ""
+
+ val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null
+ val modifierList = injectorAnnotation.parent as? PsiModifierList ?: return null
+ val parsedExprs = parseExpressions(project, modifierList, atId)
+ parsedExprs.ifEmpty { return null }
+
+ val sourceMatchContext = createSourceMatchContext(project, modifierList)
+
+ return MyNavigationVisitor(parsedExprs.map { it.second }, sourceMatchContext)
+ }
+
+ private fun createSourceMatchContext(
+ project: Project,
+ modifierList: PsiModifierList
+ ): MESourceMatchContext {
+ val matchContext = MESourceMatchContext(project)
+
+ for (annotation in modifierList.annotations) {
+ if (!annotation.hasQualifiedName(MixinConstants.MixinExtras.DEFINITION)) {
+ continue
+ }
+
+ val definitionId = annotation.findDeclaredAttributeValue("id")?.constantStringValue ?: ""
+
+ val fields = annotation.findDeclaredAttributeValue("field")?.computeStringArray() ?: emptyList()
+ for (field in fields) {
+ val fieldRef = MemberReference.parse(field) ?: continue
+ matchContext.addField(definitionId, fieldRef)
+ }
+
+ val methods = annotation.findDeclaredAttributeValue("method")?.computeStringArray() ?: emptyList()
+ for (method in methods) {
+ val methodRef = MemberReference.parse(method) ?: continue
+ matchContext.addMethod(definitionId, methodRef)
+ }
+
+ val types = annotation.findDeclaredAttributeValue("type")?.resolveTypeArray() ?: emptyList()
+ for (type in types) {
+ matchContext.addType(definitionId, type.descriptor)
+ }
+
+ val locals = annotation.findDeclaredAttributeValue("local")?.findAnnotations() ?: emptyList()
+ for (localAnnotation in locals) {
+ val localType = localAnnotation.findDeclaredAttributeValue("type")?.resolveType()
+ val localInfo = LocalInfo.fromAnnotation(localType, localAnnotation)
+ matchContext.addLocalInfo(definitionId, localInfo)
+ }
+ }
+
+ return matchContext
+ }
+
+ override fun doCreateCollectVisitor(
+ at: PsiAnnotation,
+ target: MixinSelector?,
+ targetClass: ClassNode,
+ mode: CollectVisitor.Mode
+ ): CollectVisitor? {
+ val project = at.project
+
+ val atId = at.findDeclaredAttributeValue("id")?.constantStringValue ?: ""
+
+ val contextType = MEExpressionMatchUtil.getContextType(project, at.parentOfType()?.qualifiedName)
+
+ val injectorAnnotation = AtResolver.findInjectorAnnotation(at) ?: return null
+ val modifierList = injectorAnnotation.parent as? PsiModifierList ?: return null
+ val parsedExprs = parseExpressions(project, modifierList, atId)
+ parsedExprs.ifEmpty { return null }
+
+ val module = at.findModule() ?: return null
+
+ val poolFactory = MEExpressionMatchUtil.createIdentifierPoolFactory(module, targetClass, modifierList)
+
+ return MyCollectVisitor(mode, project, targetClass, parsedExprs, poolFactory, contextType)
+ }
+
+ private fun parseExpressions(
+ project: Project,
+ modifierList: PsiModifierList,
+ atId: String
+ ): List> {
+ return modifierList.annotations.asSequence()
+ .filter { exprAnnotation ->
+ exprAnnotation.hasQualifiedName(MixinConstants.MixinExtras.EXPRESSION) &&
+ (exprAnnotation.findDeclaredAttributeValue("id")?.constantStringValue ?: "") == atId
+ }
+ .flatMap { exprAnnotation ->
+ val expressionElements = exprAnnotation.findDeclaredAttributeValue("value")?.parseArray { it }
+ ?: return@flatMap emptySequence>()
+ expressionElements.asSequence().mapNotNull { expressionElement ->
+ val text = expressionElement.constantStringValue ?: return@mapNotNull null
+ val rootStatementPsi = InjectedLanguageManager.getInstance(project)
+ .getInjectedPsiFiles(expressionElement)?.firstOrNull()
+ ?.let {
+ (it.first as? MEExpressionFile)?.statements?.firstOrNull { stmt ->
+ stmt.findMultiInjectionHost()?.parentOfType() == exprAnnotation
+ }
+ }
+ ?: project.meExpressionElementFactory.createFile("do {$text}").statements.singleOrNull()
+ ?: project.meExpressionElementFactory.createStatement("empty")
+ MEExpressionMatchUtil.createExpression(text)?.let { it to rootStatementPsi }
+ }
+ }
+ .toList()
+ }
+
+ override fun createLookup(
+ targetClass: ClassNode,
+ result: CollectVisitor.Result
+ ): LookupElementBuilder? {
+ return null
+ }
+
+ private class MyCollectVisitor(
+ mode: Mode,
+ private val project: Project,
+ private val targetClass: ClassNode,
+ private val expressions: List>,
+ private val poolFactory: IdentifierPoolFactory,
+ private val contextType: ExpressionContext.Type,
+ ) : CollectVisitor(mode) {
+ override fun accept(methodNode: MethodNode) {
+ val insns = methodNode.instructions ?: return
+
+ val pool = poolFactory(methodNode)
+ val flows = MEExpressionMatchUtil.getFlowMap(project, targetClass, methodNode) ?: return
+
+ val result = IdentityHashMap>>()
+
+ for ((expr, psiExpr) in expressions) {
+ MEExpressionMatchUtil.findMatchingInstructions(
+ targetClass,
+ methodNode,
+ pool,
+ flows,
+ expr,
+ flows.keys,
+ contextType,
+ false
+ ) { match ->
+ val capturedExpr = psiExpr.findElementAt(match.startOffset)
+ ?.parentOfType(withSelf = true)
+ ?.expression
+ ?: psiExpr
+ result.putIfAbsent(match.originalInsn, capturedExpr to match.decorations)
+ }
+ }
+
+ if (result.isEmpty()) {
+ return
+ }
+
+ for (insn in insns) {
+ val (element, decorations) = result[insn] ?: continue
+ addResult(insn, element, decorations = decorations)
+ }
+ }
+ }
+
+ private class MyNavigationVisitor(
+ private val statements: List,
+ private val matchContext: MESourceMatchContext
+ ) : NavigationVisitor() {
+ override fun visitElement(element: PsiElement) {
+ for (statement in statements) {
+ if (statement.matchesJava(element, matchContext)) {
+ if (matchContext.captures.isNotEmpty()) {
+ for (capture in matchContext.captures) {
+ addResult(capture)
+ }
+ } else {
+ addResult(element)
+ }
+ }
+ matchContext.reset()
+ }
+
+ super.visitElement(element)
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt
index a0cb1ba40..2e76bcb9b 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/MixinExtrasInjectorAnnotationHandler.kt
@@ -35,8 +35,10 @@ import com.demonwav.mcdev.util.Parameter
import com.demonwav.mcdev.util.toJavaIdentifier
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
+import com.intellij.psi.PsiElement
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
+import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
@@ -52,30 +54,43 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler(
enum class InstructionType {
METHOD_CALL {
- override fun matches(insn: AbstractInsnNode) = insn is MethodInsnNode && insn.name != ""
+ override fun matches(target: TargetInsn) = target.insn is MethodInsnNode && target.insn.name != ""
},
FIELD_GET {
- override fun matches(insn: AbstractInsnNode) =
- insn.opcode == Opcodes.GETFIELD || insn.opcode == Opcodes.GETSTATIC
+ override fun matches(target: TargetInsn) =
+ target.insn.opcode == Opcodes.GETFIELD || target.insn.opcode == Opcodes.GETSTATIC
},
FIELD_SET {
- override fun matches(insn: AbstractInsnNode) =
- insn.opcode == Opcodes.PUTFIELD || insn.opcode == Opcodes.PUTSTATIC
+ override fun matches(target: TargetInsn) =
+ target.insn.opcode == Opcodes.PUTFIELD || target.insn.opcode == Opcodes.PUTSTATIC
},
INSTANTIATION {
- override fun matches(insn: AbstractInsnNode) = insn.opcode == Opcodes.NEW
+ override fun matches(target: TargetInsn) = target.insn.opcode == Opcodes.NEW
},
INSTANCEOF {
- override fun matches(insn: AbstractInsnNode) = insn.opcode == Opcodes.INSTANCEOF
+ override fun matches(target: TargetInsn) = target.insn.opcode == Opcodes.INSTANCEOF
},
CONSTANT {
- override fun matches(insn: AbstractInsnNode) = isConstant(insn)
+ override fun matches(target: TargetInsn) = isConstant(target.insn)
},
RETURN {
- override fun matches(insn: AbstractInsnNode) = insn.opcode in Opcodes.IRETURN..Opcodes.ARETURN
+ override fun matches(target: TargetInsn) = target.insn.opcode in Opcodes.IRETURN..Opcodes.ARETURN
+ },
+ SIMPLE_OPERATION {
+ override fun matches(target: TargetInsn) =
+ target.hasDecoration(ExpressionDecorations.SIMPLE_OPERATION_ARGS) &&
+ target.hasDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE)
+ },
+ SIMPLE_EXPRESSION {
+ override fun matches(target: TargetInsn) =
+ target.hasDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE)
+ },
+ STRING_CONCAT_EXPRESSION {
+ override fun matches(target: TargetInsn) =
+ target.hasDecoration(ExpressionDecorations.IS_STRING_CONCAT_EXPRESSION)
};
- abstract fun matches(insn: AbstractInsnNode): Boolean
+ abstract fun matches(target: TargetInsn): Boolean
}
abstract val supportedInstructionTypes: Collection
@@ -86,9 +101,13 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler(
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insn: AbstractInsnNode
+ target: TargetInsn,
): Pair?
+ open fun intLikeTypePositions(
+ target: TargetInsn
+ ): List = emptyList()
+
override val allowCoerce = true
override fun expectedMethodSignature(
@@ -98,26 +117,82 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler(
): List? {
val insns = resolveInstructions(annotation, targetClass, targetMethod)
.ifEmpty { return emptyList() }
- .map { it.insn }
+ .map { TargetInsn(it.insn, it.decorations) }
if (insns.any { insn -> supportedInstructionTypes.none { it.matches(insn) } }) return emptyList()
- val signatures = insns.map { expectedMethodSignature(annotation, targetClass, targetMethod, it) }
+ val signatures = insns.map { insn ->
+ expectedMethodSignature(annotation, targetClass, targetMethod, insn)
+ }
val firstMatch = signatures[0] ?: return emptyList()
if (signatures.drop(1).any { it != firstMatch }) return emptyList()
- return listOf(
- MethodSignature(
- listOf(
- firstMatch.first,
- ParameterGroup(
- collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
- required = ParameterGroup.RequiredLevel.OPTIONAL,
- isVarargs = true,
- ),
- ),
- firstMatch.second
- )
+ val intLikeTypePositions = insns.map { intLikeTypePositions(it) }.distinct().singleOrNull().orEmpty()
+ return allPossibleSignatures(
+ annotation,
+ targetClass,
+ targetMethod,
+ firstMatch.first,
+ firstMatch.second,
+ intLikeTypePositions
)
}
+ private fun allPossibleSignatures(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ params: ParameterGroup,
+ returnType: PsiType,
+ intLikeTypePositions: List
+ ): List {
+ if (intLikeTypePositions.isEmpty()) {
+ return listOf(
+ makeSignature(annotation, targetClass, targetMethod, params, returnType, intLikeTypePositions)
+ )
+ }
+ return buildList {
+ for (actualType in intLikePsiTypes) {
+ val newParams = params.parameters.toMutableList()
+ var newReturnType = returnType
+ for (pos in intLikeTypePositions) {
+ when (pos) {
+ is MethodSignature.TypePosition.Return -> newReturnType = actualType
+ is MethodSignature.TypePosition.Param ->
+ newParams[pos.index] = newParams[pos.index].copy(type = actualType)
+ }
+ }
+ add(
+ makeSignature(
+ annotation,
+ targetClass,
+ targetMethod,
+ ParameterGroup(newParams),
+ newReturnType,
+ intLikeTypePositions
+ )
+ )
+ }
+ }
+ }
+
+ private fun makeSignature(
+ annotation: PsiAnnotation,
+ targetClass: ClassNode,
+ targetMethod: MethodNode,
+ params: ParameterGroup,
+ returnType: PsiType,
+ intLikeTypePositions: List
+ ) = MethodSignature(
+ listOf(
+ params,
+ ParameterGroup(
+ collectTargetMethodParameters(annotation.project, targetClass, targetMethod),
+ required = ParameterGroup.RequiredLevel.OPTIONAL,
+ isVarargs = true,
+ ),
+ ),
+ returnType,
+ intLikeTypePositions
+ )
+
protected fun getInsnReturnType(insn: AbstractInsnNode): Type? {
return when {
insn is MethodInsnNode -> Type.getReturnType(insn.desc)
@@ -287,7 +362,12 @@ abstract class MixinExtrasInjectorAnnotationHandler : InjectorAnnotationHandler(
}
else -> null
- } ?: getInsnArgTypes(insn, targetClass)?.map { Parameter(null, it.toPsiType(elementFactory)) }
+ } ?: getInsnArgTypes(insn, targetClass)?.toParameters(annotation)
+ }
+
+ protected fun List.toParameters(context: PsiElement, names: Array? = null): List {
+ val elementFactory = JavaPsiFacade.getElementFactory(context.project)
+ return mapIndexed { i, it -> Parameter(names?.getOrNull(i), it.toPsiType(elementFactory)) }
}
}
@@ -348,3 +428,7 @@ private fun getConstantType(insn: AbstractInsnNode?): Type? {
}
}
}
+
+private val intLikePsiTypes = listOf(
+ PsiTypes.intType(), PsiTypes.booleanType(), PsiTypes.charType(), PsiTypes.byteType(), PsiTypes.shortType()
+)
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt
index 791423584..f16bf4924 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyExpressionValueHandler.kt
@@ -20,10 +20,16 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
+import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
+import com.demonwav.mcdev.platform.mixin.util.toPsiType
import com.demonwav.mcdev.util.Parameter
+import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
+import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils
+import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
@@ -31,7 +37,8 @@ import org.objectweb.asm.tree.MethodNode
class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() {
override val supportedInstructionTypes = listOf(
- InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.INSTANTIATION, InstructionType.CONSTANT
+ InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.INSTANTIATION, InstructionType.CONSTANT,
+ InstructionType.SIMPLE_EXPRESSION, InstructionType.STRING_CONCAT_EXPRESSION
)
override fun extraTargetRestrictions(insn: AbstractInsnNode): Boolean {
@@ -43,9 +50,36 @@ class ModifyExpressionValueHandler : MixinExtrasInjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insn: AbstractInsnNode
+ target: TargetInsn
): Pair? {
- val psiType = getPsiReturnType(insn, annotation) ?: return null
+ val psiType = getReturnType(target, annotation) ?: return null
return ParameterGroup(listOf(Parameter("original", psiType))) to psiType
}
+
+ override fun intLikeTypePositions(target: TargetInsn): List {
+ val expressionType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE)
+ if (expressionType == ExpressionASMUtils.INTLIKE_TYPE) {
+ return listOf(MethodSignature.TypePosition.Return, MethodSignature.TypePosition.Param(0))
+ }
+ return emptyList()
+ }
+
+ private fun getReturnType(
+ target: TargetInsn,
+ annotation: PsiAnnotation
+ ): PsiType? {
+ if (target.hasDecoration(ExpressionDecorations.IS_STRING_CONCAT_EXPRESSION)) {
+ return PsiType.getJavaLangString(annotation.manager, annotation.resolveScope)
+ }
+ val psiReturnType = getPsiReturnType(target.insn, annotation)
+ val rawReturnType = getInsnReturnType(target.insn)
+ val exprType = target.getDecoration(ExpressionDecorations.SIMPLE_EXPRESSION_TYPE)
+ if (exprType != null && rawReturnType != exprType) {
+ // The expression knows more than the standard logic does.
+ return exprType.toPsiType(JavaPsiFacade.getElementFactory(annotation.project))
+ }
+ return psiReturnType
+ }
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_EXPRESSION_VALUE
}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt
index 0c3c3c564..38ec7fc8a 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReceiverHandler.kt
@@ -23,6 +23,7 @@ package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Opcodes
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
@@ -44,9 +45,11 @@ class ModifyReceiverHandler : MixinExtrasInjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insn: AbstractInsnNode
+ target: TargetInsn
): Pair? {
- val params = getPsiParameters(insn, targetClass, annotation) ?: return null
+ val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null
return ParameterGroup(params) to params[0].type
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RECEIVER
}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt
index 8c2706c33..df9157186 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/ModifyReturnValueHandler.kt
@@ -25,7 +25,7 @@ import com.demonwav.mcdev.platform.mixin.util.getGenericReturnType
import com.demonwav.mcdev.util.Parameter
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
-import org.objectweb.asm.tree.AbstractInsnNode
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
@@ -36,9 +36,11 @@ class ModifyReturnValueHandler : MixinExtrasInjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insn: AbstractInsnNode
- ): Pair? {
+ target: TargetInsn
+ ): Pair {
val returnType = targetMethod.getGenericReturnType(targetClass, annotation.project)
return ParameterGroup(listOf(Parameter("original", returnType))) to returnType
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.MODIFY_RETURN_VALUE
}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt
new file mode 100644
index 000000000..1a03ff012
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/TargetInsn.kt
@@ -0,0 +1,30 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.handlers.mixinextras
+
+import org.objectweb.asm.tree.AbstractInsnNode
+
+class TargetInsn(val insn: AbstractInsnNode, private val decorations: Map) {
+ fun hasDecoration(key: String) = key in decorations
+
+ @Suppress("UNCHECKED_CAST")
+ fun getDecoration(key: String): T? = decorations[key] as T
+}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
index cd2011c19..938f0103e 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapMethodHandler.kt
@@ -31,6 +31,7 @@ import com.demonwav.mcdev.util.Parameter
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiElement
import com.intellij.psi.search.GlobalSearchScope
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
@@ -80,4 +81,6 @@ class WrapMethodHandler : InjectorAnnotationHandler() {
canDecompile = true
)?.let(::listOf).orEmpty()
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.CUSTOM
}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
index a5e2242e8..ef1726cc8 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapOperationHandler.kt
@@ -20,19 +20,25 @@
package com.demonwav.mcdev.platform.mixin.handlers.mixinextras
+import com.demonwav.mcdev.platform.mixin.inspection.injector.MethodSignature
import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
import com.demonwav.mcdev.platform.mixin.util.mixinExtrasOperationType
+import com.demonwav.mcdev.platform.mixin.util.toPsiType
import com.demonwav.mcdev.util.Parameter
+import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
-import org.objectweb.asm.tree.AbstractInsnNode
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
+import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils
+import com.llamalad7.mixinextras.expression.impl.utils.ExpressionDecorations
+import org.objectweb.asm.Type
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.MethodNode
class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() {
override val supportedInstructionTypes = listOf(
InstructionType.METHOD_CALL, InstructionType.FIELD_GET, InstructionType.FIELD_SET, InstructionType.INSTANCEOF,
- InstructionType.INSTANTIATION
+ InstructionType.INSTANTIATION, InstructionType.SIMPLE_OPERATION
)
override fun getAtKey(annotation: PsiAnnotation): String {
@@ -43,13 +49,51 @@ class WrapOperationHandler : MixinExtrasInjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insn: AbstractInsnNode
+ target: TargetInsn
): Pair? {
- val params = getPsiParameters(insn, targetClass, annotation) ?: return null
- val returnType = getPsiReturnType(insn, annotation) ?: return null
+ val params = getParameterTypes(target, targetClass, annotation) ?: return null
+ val returnType = getReturnType(target, annotation) ?: return null
val operationType = mixinExtrasOperationType(annotation, returnType) ?: return null
return ParameterGroup(
params + Parameter("original", operationType)
) to returnType
}
+
+ override fun intLikeTypePositions(target: TargetInsn) = buildList {
+ if (
+ target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE)
+ == ExpressionASMUtils.INTLIKE_TYPE
+ ) {
+ add(MethodSignature.TypePosition.Return)
+ }
+ target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS)?.forEachIndexed { i, it ->
+ if (it == ExpressionASMUtils.INTLIKE_TYPE) {
+ add(MethodSignature.TypePosition.Param(i))
+ }
+ }
+ }
+
+ private fun getParameterTypes(
+ target: TargetInsn,
+ targetClass: ClassNode,
+ annotation: PsiAnnotation
+ ): List? {
+ getPsiParameters(target.insn, targetClass, annotation)?.let { return it }
+ val args = target.getDecoration>(ExpressionDecorations.SIMPLE_OPERATION_ARGS) ?: return null
+ return args.toList().toParameters(
+ annotation,
+ target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_PARAM_NAMES)
+ )
+ }
+
+ private fun getReturnType(
+ target: TargetInsn,
+ annotation: PsiAnnotation
+ ): PsiType? {
+ getPsiReturnType(target.insn, annotation)?.let { return it }
+ val type = target.getDecoration(ExpressionDecorations.SIMPLE_OPERATION_RETURN_TYPE) ?: return null
+ return type.toPsiType(JavaPsiFacade.getElementFactory(annotation.project))
+ }
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_OPERATION
}
diff --git a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt
index 8a92d6bc6..df64324b7 100644
--- a/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt
+++ b/src/main/kotlin/platform/mixin/handlers/mixinextras/WrapWithConditionHandler.kt
@@ -24,6 +24,7 @@ import com.demonwav.mcdev.platform.mixin.inspection.injector.ParameterGroup
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypes
+import com.llamalad7.mixinextras.expression.impl.point.ExpressionContext
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
import org.objectweb.asm.tree.ClassNode
@@ -42,9 +43,11 @@ class WrapWithConditionHandler : MixinExtrasInjectorAnnotationHandler() {
annotation: PsiAnnotation,
targetClass: ClassNode,
targetMethod: MethodNode,
- insn: AbstractInsnNode
+ target: TargetInsn
): Pair? {
- val params = getPsiParameters(insn, targetClass, annotation) ?: return null
+ val params = getPsiParameters(target.insn, targetClass, annotation) ?: return null
return ParameterGroup(params) to PsiTypes.booleanType()
}
+
+ override val mixinExtrasExpressionContextType = ExpressionContext.Type.WRAP_WITH_CONDITION
}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt
index a51fd6db3..bc168040a 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/InvalidInjectorMethodSignatureInspection.kt
@@ -20,7 +20,6 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
-import com.demonwav.mcdev.platform.mixin.handlers.InjectAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.InjectorAnnotationHandler
import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
import com.demonwav.mcdev.platform.mixin.inspection.MixinInspection
@@ -34,20 +33,35 @@ import com.demonwav.mcdev.platform.mixin.util.isConstructor
import com.demonwav.mcdev.platform.mixin.util.isMixinExtrasSugar
import com.demonwav.mcdev.util.Parameter
import com.demonwav.mcdev.util.fullQualifiedName
-import com.demonwav.mcdev.util.normalize
+import com.demonwav.mcdev.util.invokeLater
import com.demonwav.mcdev.util.synchronize
+import com.intellij.codeInsight.FileModificationService
import com.intellij.codeInsight.intention.FileModifier.SafeFieldForPreview
import com.intellij.codeInsight.intention.QuickFixFactory
-import com.intellij.codeInspection.LocalQuickFix
-import com.intellij.codeInspection.ProblemDescriptor
+import com.intellij.codeInsight.lookup.LookupElement
+import com.intellij.codeInsight.lookup.LookupElementBuilder
+import com.intellij.codeInsight.template.Expression
+import com.intellij.codeInsight.template.ExpressionContext
+import com.intellij.codeInsight.template.Template
+import com.intellij.codeInsight.template.TemplateBuilderImpl
+import com.intellij.codeInsight.template.TemplateManager
+import com.intellij.codeInsight.template.TextResult
+import com.intellij.codeInsight.template.impl.VariableNode
+import com.intellij.codeInspection.LocalQuickFixAndIntentionActionOnPsiElement
import com.intellij.codeInspection.ProblemHighlightType
import com.intellij.codeInspection.ProblemsHolder
+import com.intellij.openapi.application.runWriteAction
+import com.intellij.openapi.command.WriteCommandAction
+import com.intellij.openapi.editor.Editor
import com.intellij.openapi.project.Project
+import com.intellij.openapi.util.TextRange
import com.intellij.psi.JavaElementVisitor
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiClassType
+import com.intellij.psi.PsiElement
import com.intellij.psi.PsiElementVisitor
import com.intellij.psi.PsiEllipsisType
+import com.intellij.psi.PsiFile
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiNameHelper
@@ -57,6 +71,9 @@ import com.intellij.psi.PsiType
import com.intellij.psi.codeStyle.JavaCodeStyleManager
import com.intellij.psi.codeStyle.VariableKind
import com.intellij.psi.util.PsiUtil
+import com.intellij.psi.util.TypeConversionUtil
+import com.intellij.psi.util.parentOfType
+import com.intellij.refactoring.suggested.startOffset
import org.objectweb.asm.Opcodes
class InvalidInjectorMethodSignatureInspection : MixinInspection() {
@@ -166,50 +183,42 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
}
if (!isValid) {
- val (expectedParameters, expectedReturnType) = possibleSignatures[0]
-
- val checkResult = checkParameters(parameters, expectedParameters, handler.allowCoerce)
- if (checkResult != CheckResult.OK) {
- reportedSignature = true
-
- val description =
- "Method parameters do not match expected parameters for $annotationName"
- val quickFix = ParametersQuickFix(
- expectedParameters,
- handler is InjectAnnotationHandler,
- )
- if (checkResult == CheckResult.ERROR) {
- holder.registerProblem(parameters, description, quickFix)
- } else {
- holder.registerProblem(
- parameters,
- description,
- ProblemHighlightType.WARNING,
- quickFix,
- )
- }
+ val (expectedParameters, expectedReturnType, intLikeTypePositions) = possibleSignatures[0]
+ val normalizedReturnType = when (expectedReturnType) {
+ is PsiEllipsisType -> expectedReturnType.toArrayType()
+ else -> expectedReturnType
}
+ val paramsCheck = checkParameters(parameters, expectedParameters, handler.allowCoerce)
+ val isWarning = paramsCheck == CheckResult.WARNING
val methodReturnType = method.returnType
- if (methodReturnType == null ||
- !checkReturnType(expectedReturnType, methodReturnType, method, handler.allowCoerce)
- ) {
+ val returnTypeOk = methodReturnType != null &&
+ checkReturnType(normalizedReturnType, methodReturnType, method, handler.allowCoerce)
+ val isError = paramsCheck == CheckResult.ERROR || !returnTypeOk
+ if (isWarning || isError) {
reportedSignature = true
- val normalizedExpected = when (expectedReturnType) {
- is PsiEllipsisType -> expectedReturnType.toArrayType()
- else -> expectedReturnType
- }
-
+ val description =
+ "Method signature does not match expected signature for $annotationName"
+ val quickFix = SignatureQuickFix(
+ method,
+ expectedParameters.takeUnless { paramsCheck == CheckResult.OK },
+ normalizedReturnType.takeUnless { returnTypeOk },
+ intLikeTypePositions
+ )
+ val highlightType =
+ if (isError)
+ ProblemHighlightType.GENERIC_ERROR_OR_WARNING
+ else
+ ProblemHighlightType.WARNING
+ val declarationStart = (method.returnTypeElement ?: identifier).startOffsetInParent
+ val declarationEnd = method.parameterList.textRangeInParent.endOffset
holder.registerProblem(
- method.returnTypeElement ?: identifier,
- "Expected return type '${normalizedExpected.presentableText}' " +
- "for $annotationName method",
- QuickFixFactory.getInstance().createMethodReturnFix(
- method,
- normalizedExpected,
- false,
- ),
+ method,
+ description,
+ highlightType,
+ TextRange.create(declarationStart, declarationEnd),
+ quickFix
)
}
}
@@ -224,9 +233,9 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
method: PsiMethod,
allowCoerce: Boolean,
): Boolean {
- val normalizedExpected = expectedReturnType.normalize()
- val normalizedReturn = methodReturnType.normalize()
- if (normalizedExpected == normalizedReturn) {
+ val expectedErasure = TypeConversionUtil.erasure(expectedReturnType)
+ val returnErasure = TypeConversionUtil.erasure(methodReturnType)
+ if (expectedErasure == returnErasure) {
return true
}
if (!allowCoerce || !method.hasAnnotation(COERCE)) {
@@ -289,22 +298,43 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
OK, WARNING, ERROR
}
- private class ParametersQuickFix(
+ private class SignatureQuickFix(
+ method: PsiMethod,
@SafeFieldForPreview
- private val expected: List,
- isInject: Boolean,
- ) : LocalQuickFix {
-
- private val fixName = if (isInject) {
- "Fix method parameters"
- } else {
- "Fix method parameters (won't keep captured locals)"
- }
+ private val expectedParams: List?,
+ @SafeFieldForPreview
+ private val expectedReturnType: PsiType?,
+ private val intLikeTypePositions: List
+ ) : LocalQuickFixAndIntentionActionOnPsiElement(method) {
+
+ private val fixName = "Fix method signature"
override fun getFamilyName() = fixName
- override fun applyFix(project: Project, descriptor: ProblemDescriptor) {
- val parameters = descriptor.psiElement as PsiParameterList
+ override fun getText() = familyName
+
+ override fun startInWriteAction() = false
+
+ override fun invoke(
+ project: Project,
+ file: PsiFile,
+ editor: Editor?,
+ startElement: PsiElement,
+ endElement: PsiElement,
+ ) {
+ if (!FileModificationService.getInstance().preparePsiElementForWrite(startElement)) {
+ return
+ }
+ val method = startElement as PsiMethod
+ fixParameters(project, method.parameterList)
+ fixReturnType(method)
+ fixIntLikeTypes(method, editor ?: return)
+ }
+
+ private fun fixParameters(project: Project, parameters: PsiParameterList) {
+ if (expectedParams == null) {
+ return
+ }
// We want to preserve captured locals
val locals = parameters.parameters.dropWhile {
val fqname = (it.type as? PsiClassType)?.fullQualifiedName ?: return@dropWhile true
@@ -316,7 +346,7 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
// We want to preserve sugars, and while we're at it, we might as well move them all to the end
val sugars = parameters.parameters.filter { it.isMixinExtrasSugar }
- val newParams = expected.flatMapTo(mutableListOf()) {
+ val newParams = expectedParams.flatMapTo(mutableListOf()) {
if (it.default) {
val nameHelper = PsiNameHelper.getInstance(project)
val languageLevel = PsiUtil.getLanguageLevel(parameters)
@@ -335,7 +365,81 @@ class InvalidInjectorMethodSignatureInspection : MixinInspection() {
// Restore the captured locals and sugars before applying the fix
newParams.addAll(locals)
newParams.addAll(sugars)
- parameters.synchronize(newParams)
+ runWriteAction {
+ parameters.synchronize(newParams)
+ }
}
+
+ private fun fixReturnType(method: PsiMethod) {
+ if (expectedReturnType == null) {
+ return
+ }
+ QuickFixFactory.getInstance()
+ .createMethodReturnFix(method, expectedReturnType, false)
+ .applyFix()
+ }
+
+ private fun fixIntLikeTypes(method: PsiMethod, editor: Editor) {
+ if (intLikeTypePositions.isEmpty()) {
+ return
+ }
+ invokeLater {
+ WriteCommandAction.runWriteCommandAction(
+ method.project,
+ "Choose Int-Like Type",
+ null,
+ {
+ val template = makeIntLikeTypeTemplate(method, intLikeTypePositions)
+ if (template != null) {
+ editor.caretModel.moveToOffset(method.startOffset)
+ TemplateManager.getInstance(method.project)
+ .startTemplate(editor, template)
+ }
+ },
+ method.parentOfType()!!
+ )
+ }
+ }
+
+ private fun makeIntLikeTypeTemplate(
+ method: PsiMethod,
+ positions: List
+ ): Template? {
+ val builder = TemplateBuilderImpl(method)
+ builder.replaceElement(
+ positions.first().getElement(method) ?: return null,
+ "intliketype",
+ ChooseIntLikeTypeExpression(),
+ true
+ )
+ for (pos in positions.drop(1)) {
+ builder.replaceElement(
+ pos.getElement(method) ?: return null,
+ VariableNode("intliketype", null),
+ false
+ )
+ }
+ return builder.buildInlineTemplate()
+ }
+ }
+}
+
+private class ChooseIntLikeTypeExpression : Expression() {
+ private val lookupItems: Array = intLikeTypes.map(LookupElementBuilder::create).toTypedArray()
+
+ override fun calculateLookupItems(context: ExpressionContext) = if (lookupItems.size > 1) lookupItems else null
+
+ override fun calculateQuickResult(context: ExpressionContext) = calculateResult(context)
+
+ override fun calculateResult(context: ExpressionContext) = TextResult("int")
+
+ private companion object {
+ private val intLikeTypes = listOf(
+ "int",
+ "char",
+ "boolean",
+ "byte",
+ "short"
+ )
}
}
diff --git a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt
index 167782cbe..631e1acf9 100644
--- a/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt
+++ b/src/main/kotlin/platform/mixin/inspection/injector/MethodSignature.kt
@@ -20,6 +20,24 @@
package com.demonwav.mcdev.platform.mixin.inspection.injector
+import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypeElement
-data class MethodSignature(val parameters: List, val returnType: PsiType)
+data class MethodSignature(
+ val parameters: List,
+ val returnType: PsiType,
+ val intLikeTypes: List = emptyList()
+) {
+ sealed interface TypePosition {
+ fun getElement(method: PsiMethod): PsiTypeElement?
+
+ data object Return : TypePosition {
+ override fun getElement(method: PsiMethod) = method.returnTypeElement
+ }
+
+ data class Param(val index: Int) : TypePosition {
+ override fun getElement(method: PsiMethod) = method.parameterList.parameters[index].typeElement
+ }
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt b/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt
index 720a4084a..79ea61bfb 100644
--- a/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt
+++ b/src/main/kotlin/platform/mixin/inspection/suppress/MixinClassCastInspectionSuppressor.kt
@@ -38,6 +38,7 @@ import com.intellij.psi.PsiInstanceOfExpression
import com.intellij.psi.PsiType
import com.intellij.psi.PsiTypeCastExpression
import com.intellij.psi.PsiTypeTestPattern
+import com.intellij.psi.util.JavaPsiPatternUtil
import com.intellij.psi.util.PsiUtil
/**
@@ -54,7 +55,8 @@ class MixinClassCastInspectionSuppressor : InspectionSuppressor {
// check instanceof
if (element is PsiInstanceOfExpression) {
val castType = element.checkType?.type
- ?: (element.pattern as? PsiTypeTestPattern)?.checkType?.type
+ ?: (JavaPsiPatternUtil.skipParenthesizedPatternDown(element.pattern) as? PsiTypeTestPattern)
+ ?.checkType?.type
?: return false
var operand = PsiUtil.skipParenthesizedExprDown(element.operand) ?: return false
while (operand is PsiTypeCastExpression) {
diff --git a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt
index 416eacdf7..58d68e12e 100644
--- a/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt
+++ b/src/main/kotlin/platform/mixin/reference/MixinReferenceContributor.kt
@@ -20,6 +20,8 @@
package com.demonwav.mcdev.platform.mixin.reference
+import com.demonwav.mcdev.platform.mixin.reference.target.FieldDefinitionReference
+import com.demonwav.mcdev.platform.mixin.reference.target.MethodDefinitionReference
import com.demonwav.mcdev.platform.mixin.reference.target.TargetReference
import com.demonwav.mcdev.platform.mixin.util.MixinConstants.Annotations.AT
import com.demonwav.mcdev.util.insideAnnotationAttribute
@@ -65,5 +67,15 @@ class MixinReferenceContributor : PsiReferenceContributor() {
InvokerReference.ELEMENT_PATTERN,
InvokerReference,
)
+
+ // Definition references
+ registrar.registerReferenceProvider(
+ FieldDefinitionReference.ELEMENT_PATTERN,
+ FieldDefinitionReference,
+ )
+ registrar.registerReferenceProvider(
+ MethodDefinitionReference.ELEMENT_PATTERN,
+ MethodDefinitionReference,
+ )
}
}
diff --git a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt
index 4e8d34f4f..88e912674 100644
--- a/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt
+++ b/src/main/kotlin/platform/mixin/reference/MixinSelectors.kt
@@ -48,7 +48,6 @@ import com.demonwav.mcdev.util.resolveTypeArray
import com.intellij.openapi.extensions.ExtensionPointName
import com.intellij.openapi.project.Project
import com.intellij.openapi.util.RecursionManager
-import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.CommonClassNames
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.PsiAnnotation
@@ -228,71 +227,7 @@ fun MemberReference.toMixinString(): String {
}
class MixinMemberParser : MixinSelectorParser {
- override fun parse(value: String, context: PsiElement): MixinSelector? {
- val reference = value.replace(" ", "")
- val owner: String?
-
- var pos = reference.lastIndexOf('.')
- if (pos != -1) {
- // Everything before the dot is the qualifier/owner
- owner = reference.substring(0, pos).replace('/', '.')
- } else {
- pos = reference.indexOf(';')
- if (pos != -1 && reference.startsWith('L')) {
- val internalOwner = reference.substring(1, pos)
- if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) {
- // Invalid: Qualifier should only contain slashes
- return null
- }
-
- owner = internalOwner.replace('/', '.')
-
- // if owner is all there is to the selector, match anything with the owner
- if (pos == reference.length - 1) {
- return MemberReference("", null, owner, matchAllNames = true, matchAllDescs = true)
- }
- } else {
- // No owner/qualifier specified
- pos = -1
- owner = null
- }
- }
-
- val descriptor: String?
- val name: String
- val matchAllNames = reference.getOrNull(pos + 1) == '*'
- val matchAllDescs: Boolean
-
- // Find descriptor separator
- val methodDescPos = reference.indexOf('(', pos + 1)
- if (methodDescPos != -1) {
- // Method descriptor
- descriptor = reference.substring(methodDescPos)
- name = reference.substring(pos + 1, methodDescPos)
- matchAllDescs = false
- } else {
- val fieldDescPos = reference.indexOf(':', pos + 1)
- if (fieldDescPos != -1) {
- descriptor = reference.substring(fieldDescPos + 1)
- name = reference.substring(pos + 1, fieldDescPos)
- matchAllDescs = false
- } else {
- descriptor = null
- matchAllDescs = reference.endsWith('*')
- name = if (matchAllDescs) {
- reference.substring(pos + 1, reference.lastIndex)
- } else {
- reference.substring(pos + 1)
- }
- }
- }
-
- if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") {
- return null
- }
-
- return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs)
- }
+ override fun parse(value: String, context: PsiElement) = MemberReference.parse(value)
}
// Regex reference
diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt
new file mode 100644
index 000000000..4489eff4e
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferenceGTDHandler.kt
@@ -0,0 +1,45 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.reference.target
+
+import com.intellij.codeInsight.navigation.actions.GotoDeclarationHandler
+import com.intellij.openapi.editor.Editor
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiLiteral
+import com.intellij.psi.util.parentOfType
+
+class DefinitionReferenceGTDHandler : GotoDeclarationHandler {
+ override fun getGotoDeclarationTargets(
+ sourceElement: PsiElement?,
+ offset: Int,
+ editor: Editor?
+ ): Array? {
+ if (sourceElement == null) return null
+ val stringLiteral = sourceElement.parentOfType() ?: return null
+ if (FieldDefinitionReference.ELEMENT_PATTERN.accepts(stringLiteral)) {
+ return FieldDefinitionReference.resolveForNavigation(stringLiteral)
+ }
+ if (MethodDefinitionReference.ELEMENT_PATTERN.accepts(stringLiteral)) {
+ return MethodDefinitionReference.resolveForNavigation(stringLiteral)
+ }
+ return null
+ }
+}
diff --git a/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt
new file mode 100644
index 000000000..a6927b8a8
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/reference/target/DefinitionReferences.kt
@@ -0,0 +1,182 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.reference.target
+
+import com.demonwav.mcdev.platform.mixin.expression.MEExpressionMatchUtil
+import com.demonwav.mcdev.platform.mixin.handlers.MixinAnnotationHandler
+import com.demonwav.mcdev.platform.mixin.reference.MixinReference
+import com.demonwav.mcdev.platform.mixin.util.MethodTargetMember
+import com.demonwav.mcdev.platform.mixin.util.MixinConstants
+import com.demonwav.mcdev.util.MemberReference
+import com.demonwav.mcdev.util.constantStringValue
+import com.demonwav.mcdev.util.findContainingModifierList
+import com.demonwav.mcdev.util.findField
+import com.demonwav.mcdev.util.findMethods
+import com.demonwav.mcdev.util.insideAnnotationAttribute
+import com.demonwav.mcdev.util.mapFirstNotNull
+import com.demonwav.mcdev.util.mapToArray
+import com.demonwav.mcdev.util.reference.PolyReferenceResolver
+import com.demonwav.mcdev.util.toTypedArray
+import com.intellij.codeInsight.lookup.LookupElementBuilder
+import com.intellij.openapi.project.Project
+import com.intellij.patterns.PsiJavaPatterns
+import com.intellij.patterns.StandardPatterns
+import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiClass
+import com.intellij.psi.PsiElement
+import com.intellij.psi.PsiElementResolveResult
+import com.intellij.psi.PsiMember
+import com.intellij.psi.ResolveResult
+import com.intellij.psi.search.GlobalSearchScope
+import com.intellij.util.containers.sequenceOfNotNull
+import com.llamalad7.mixinextras.expression.impl.flow.FlowValue
+import com.llamalad7.mixinextras.expression.impl.flow.postprocessing.LMFInfo
+import com.llamalad7.mixinextras.expression.impl.utils.FlowDecorations
+import org.objectweb.asm.tree.FieldInsnNode
+import org.objectweb.asm.tree.MethodInsnNode
+
+abstract class AbstractDefinitionReference : PolyReferenceResolver(), MixinReference {
+ abstract fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference?
+ abstract fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass): Sequence
+ abstract fun referenceToString(memberReference: MemberReference): String
+
+ override fun isUnresolved(context: PsiElement) = resolveInBytecode(context).isNotEmpty()
+
+ override fun isValidAnnotation(name: String, project: Project) = name == MixinConstants.MixinExtras.DEFINITION
+
+ override fun resolveReference(context: PsiElement): Array {
+ return resolveForNavigation(context).mapToArray(::PsiElementResolveResult)
+ }
+
+ fun resolveForNavigation(context: PsiElement): Array {
+ val project = context.project
+ val facade = JavaPsiFacade.getInstance(project)
+ return resolveInBytecode(context).asSequence().flatMap { memberReference ->
+ val ownerClass = facade.findClass(
+ memberReference.owner!!.replace('$', '.'),
+ GlobalSearchScope.allScope(project)
+ ) ?: return@flatMap emptySequence()
+ getMatchesInClass(memberReference.withoutOwner, ownerClass)
+ }.toTypedArray()
+ }
+
+ override fun collectVariants(context: PsiElement) =
+ resolveInBytecode(
+ context,
+ MemberReference("*", null, null, matchAllNames = true, matchAllDescs = true)
+ ).mapToArray {
+ LookupElementBuilder.create(referenceToString(it))
+ .withPresentableText(it.presentableText)
+ .withLookupString(it.name)
+ }
+
+ fun resolveInBytecode(context: PsiElement): List {
+ val memberReference = context.constantStringValue?.let(MemberReference::parse) ?: return emptyList()
+ return resolveInBytecode(context, memberReference)
+ }
+
+ private fun resolveInBytecode(context: PsiElement, memberReference: MemberReference): List {
+ val project = context.project
+ val modifierList = context.findContainingModifierList() ?: return emptyList()
+ val (annotation, handler) = modifierList.annotations.mapFirstNotNull { annotation ->
+ val qName = annotation.qualifiedName ?: return@mapFirstNotNull null
+ val handler = MixinAnnotationHandler.forMixinAnnotation(qName, project) ?: return@mapFirstNotNull null
+ annotation to handler
+ } ?: return emptyList()
+
+ val result = mutableListOf()
+
+ for (target in handler.resolveTarget(annotation)) {
+ if (target !is MethodTargetMember) {
+ continue
+ }
+
+ if (target.classAndMethod.method.instructions == null) {
+ continue
+ }
+
+ val flow = MEExpressionMatchUtil.getFlowMap(
+ project,
+ target.classAndMethod.clazz,
+ target.classAndMethod.method
+ ) ?: continue
+
+ for (node in flow.values) {
+ val fullReference = getFullReferenceIfMatches(memberReference, node) ?: continue
+ result += fullReference
+ }
+ }
+
+ return result
+ }
+}
+
+object FieldDefinitionReference : AbstractDefinitionReference() {
+ val ELEMENT_PATTERN = PsiJavaPatterns.psiLiteral(StandardPatterns.string())
+ .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "field")
+
+ override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? {
+ val insn = node.insn
+ if (insn !is FieldInsnNode || !memberReference.matchField(insn.owner, insn.name, insn.desc)) {
+ return null
+ }
+
+ return MemberReference(insn.name, insn.desc, insn.owner.replace('/', '.'))
+ }
+
+ override fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass) =
+ sequenceOfNotNull(clazz.findField(memberReference, checkBases = true))
+
+ override fun referenceToString(memberReference: MemberReference) =
+ "L${memberReference.owner?.replace('.', '/')};${memberReference.name}:${memberReference.descriptor}"
+
+ override val description = "defined field '%s'"
+}
+
+object MethodDefinitionReference : AbstractDefinitionReference() {
+ val ELEMENT_PATTERN = PsiJavaPatterns.psiLiteral(StandardPatterns.string())
+ .insideAnnotationAttribute(MixinConstants.MixinExtras.DEFINITION, "method")
+
+ override fun getFullReferenceIfMatches(memberReference: MemberReference, node: FlowValue): MemberReference? {
+ val info = node.getDecoration(FlowDecorations.LMF_INFO)
+ val insn = node.insn
+ val (owner, name, desc) = when {
+ info != null && (info.type == LMFInfo.Type.FREE_METHOD || info.type == LMFInfo.Type.BOUND_METHOD) ->
+ Triple(info.impl.owner, info.impl.name, info.impl.desc)
+
+ insn is MethodInsnNode -> Triple(insn.owner, insn.name, insn.desc)
+ else -> return null
+ }
+ if (!memberReference.matchMethod(owner, name, desc)) {
+ return null
+ }
+
+ return MemberReference(name, desc, owner.replace('/', '.'))
+ }
+
+ override fun getMatchesInClass(memberReference: MemberReference, clazz: PsiClass) =
+ clazz.findMethods(memberReference, checkBases = true)
+
+ override fun referenceToString(memberReference: MemberReference) =
+ "L${memberReference.owner?.replace('.', '/')};${memberReference.name}${memberReference.descriptor}"
+
+ override val description = "defined method '%s'"
+}
diff --git a/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt b/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt
index 3e800c643..842be4863 100644
--- a/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt
+++ b/src/main/kotlin/platform/mixin/util/AsmDfaUtil.kt
@@ -41,8 +41,8 @@ import org.objectweb.asm.tree.analysis.SimpleVerifier
object AsmDfaUtil {
private val LOGGER = thisLogger()
- fun analyzeMethod(project: Project, clazz: ClassNode, method: MethodNode): Array?>? {
- return method.cached(clazz, project) {
+ fun analyzeMethod(project: Project, classIn: ClassNode, methodIn: MethodNode): Array?>? {
+ return methodIn.cached(classIn, project) { clazz, method ->
try {
Analyzer(
PsiBytecodeInterpreter(
diff --git a/src/main/kotlin/platform/mixin/util/AsmUtil.kt b/src/main/kotlin/platform/mixin/util/AsmUtil.kt
index da1de2c4a..981be7320 100644
--- a/src/main/kotlin/platform/mixin/util/AsmUtil.kt
+++ b/src/main/kotlin/platform/mixin/util/AsmUtil.kt
@@ -32,6 +32,7 @@ import com.demonwav.mcdev.util.findQualifiedClass
import com.demonwav.mcdev.util.fullQualifiedName
import com.demonwav.mcdev.util.hasSyntheticMethod
import com.demonwav.mcdev.util.isErasureEquivalentTo
+import com.demonwav.mcdev.util.lockedCached
import com.demonwav.mcdev.util.loggerForTopLevel
import com.demonwav.mcdev.util.mapToArray
import com.demonwav.mcdev.util.realName
@@ -42,6 +43,7 @@ import com.intellij.openapi.module.Module
import com.intellij.openapi.progress.ProcessCanceledException
import com.intellij.openapi.project.Project
import com.intellij.openapi.roots.CompilerModuleExtension
+import com.intellij.openapi.util.Key
import com.intellij.openapi.util.RecursionManager
import com.intellij.psi.JavaPsiFacade
import com.intellij.psi.JavaRecursiveElementWalkingVisitor
@@ -67,19 +69,27 @@ import com.intellij.psi.PsiModifierList
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiParameterList
import com.intellij.psi.PsiType
+import com.intellij.psi.PsiTypes
import com.intellij.psi.impl.compiled.ClsElementImpl
import com.intellij.psi.search.GlobalSearchScope
+import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.PsiUtil
import com.intellij.refactoring.util.LambdaRefactoringUtil
import com.intellij.util.CommonJavaRefactoringUtil
+import com.llamalad7.mixinextras.expression.impl.utils.ExpressionASMUtils
+import java.io.PrintWriter
+import java.io.StringWriter
import java.lang.reflect.InvocationTargetException
import java.lang.reflect.Method
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentMap
import org.objectweb.asm.ClassReader
import org.objectweb.asm.Handle
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.signature.SignatureReader
import org.objectweb.asm.tree.AbstractInsnNode
+import org.objectweb.asm.tree.AnnotationNode
import org.objectweb.asm.tree.ClassNode
import org.objectweb.asm.tree.FieldInsnNode
import org.objectweb.asm.tree.FieldNode
@@ -89,6 +99,10 @@ import org.objectweb.asm.tree.InvokeDynamicInsnNode
import org.objectweb.asm.tree.MethodInsnNode
import org.objectweb.asm.tree.MethodNode
import org.objectweb.asm.tree.VarInsnNode
+import org.objectweb.asm.util.Textifier
+import org.objectweb.asm.util.TraceAnnotationVisitor
+import org.objectweb.asm.util.TraceClassVisitor
+import org.objectweb.asm.util.TraceMethodVisitor
private val LOGGER = loggerForTopLevel()
@@ -129,10 +143,25 @@ private fun hasModifier(access: Int, @PsiModifier.ModifierConstant modifier: Str
}
fun Type.toPsiType(elementFactory: PsiElementFactory, context: PsiElement? = null): PsiType {
+ if (this == ExpressionASMUtils.INTLIKE_TYPE) {
+ return PsiTypes.intType()
+ }
val javaClassName = className.replace("(\\$)(\\D)".toRegex()) { "." + it.groupValues[2] }
return elementFactory.createTypeFromText(javaClassName, context)
}
+val Type.canonicalName get() = computeCanonicalName(this)
+
+private fun computeCanonicalName(type: Type): String {
+ return when (type.sort) {
+ Type.ARRAY -> computeCanonicalName(type.elementType) + "[]".repeat(type.dimensions)
+ Type.OBJECT -> type.className.replace('$', '.')
+ else -> type.className
+ }
+}
+
+val Type.isPrimitive get() = sort != Type.ARRAY && sort != Type.OBJECT && sort != Type.METHOD
+
private fun hasAccess(access: Int, flag: Int) = (access and flag) != 0
// ClassNode
@@ -152,13 +181,10 @@ private val LOAD_CLASS_FILE_BYTES: Method? = runCatching {
.let { it.isAccessible = true; it }
}.getOrNull()
+private val INNER_CLASS_NODES_KEY = Key.create>>("mcdev.innerClassNodes")
+
/**
* Tries to find the bytecode for the class for the given qualified name.
- *
- * ### Implementation note:
- * First attempts to resolve the class using [findQualifiedClass]. This may fail in the case of anonymous classes, which
- * don't exist inside `PsiCompiledElement`s, so it then creates a fake `PsiClass` based on the qualified name and
- * attempts to resolve it from that.
*/
fun findClassNodeByQualifiedName(project: Project, module: Module?, fqn: String): ClassNode? {
val psiClass = findQualifiedClass(project, fqn)
@@ -166,52 +192,70 @@ fun findClassNodeByQualifiedName(project: Project, module: Module?, fqn: String)
return findClassNodeByPsiClass(psiClass, module)
}
- // try to find it by a fake one
- val fakeClassNode = ClassNode()
- fakeClassNode.name = fqn.replace('.', '/')
- val fakePsiClass = fakeClassNode.constructClass(project, "") ?: return null
- return findClassNodeByPsiClass(fakePsiClass, module)
+ fun resolveViaFakeClass(): ClassNode? {
+ val fakeClassNode = ClassNode()
+ fakeClassNode.name = fqn.replace('.', '/')
+ val fakePsiClass = fakeClassNode.constructClass(project, "") ?: return null
+ return findClassNodeByPsiClass(fakePsiClass, module)
+ }
+
+ val outerClass = findQualifiedClass(project, fqn.substringBefore('$'))
+ if (outerClass != null) {
+ val innerClasses = outerClass.lockedCached(
+ INNER_CLASS_NODES_KEY,
+ compute = ::ConcurrentHashMap
+ )
+ return innerClasses.computeIfAbsent(fqn) { resolveViaFakeClass() }
+ }
+
+ return resolveViaFakeClass()
}
+private val NODE_BY_PSI_CLASS_KEY = Key.create>("mcdev.nodeByPsiClass")
+
fun findClassNodeByPsiClass(psiClass: PsiClass, module: Module? = psiClass.findModule()): ClassNode? {
- return try {
- val bytes = LOAD_CLASS_FILE_BYTES?.invoke(null, psiClass) as? ByteArray
- if (bytes == null) {
- // find compiler output
- if (module == null) return null
- val fqn = psiClass.fullQualifiedName ?: return null
- var parentDir = CompilerModuleExtension.getInstance(module)?.compilerOutputPath ?: return null
- val packageName = fqn.substringBeforeLast('.', "")
- if (packageName.isNotEmpty()) {
- for (dir in packageName.split('.')) {
- parentDir = parentDir.findChild(dir) ?: return null
+ return psiClass.lockedCached(NODE_BY_PSI_CLASS_KEY) {
+ try {
+ val bytes = LOAD_CLASS_FILE_BYTES?.invoke(null, psiClass) as? ByteArray
+ if (bytes == null) {
+ // find compiler output
+ if (module == null) return@lockedCached null
+ val fqn = psiClass.fullQualifiedName ?: return@lockedCached null
+ var parentDir = CompilerModuleExtension.getInstance(module)?.compilerOutputPath
+ ?: return@lockedCached null
+ val packageName = fqn.substringBeforeLast('.', "")
+ if (packageName.isNotEmpty()) {
+ for (dir in packageName.split('.')) {
+ parentDir = parentDir.findChild(dir) ?: return@lockedCached null
+ }
}
+ val classFile = parentDir.findChild("${fqn.substringAfterLast('.')}.class")
+ ?: return@lockedCached null
+ val node = ClassNode()
+ classFile.inputStream.use { ClassReader(it).accept(node, 0) }
+ node
+ } else {
+ val node = ClassNode()
+ ClassReader(bytes).accept(node, 0)
+ node
+ }
+ } catch (e: Throwable) {
+ val actualThrowable = if (e is InvocationTargetException) e.cause ?: e else e
+ if (actualThrowable is ProcessCanceledException) {
+ throw actualThrowable
}
- val classFile = parentDir.findChild("${fqn.substringAfterLast('.')}.class") ?: return null
- val node = ClassNode()
- classFile.inputStream.use { ClassReader(it).accept(node, 0) }
- node
- } else {
- val node = ClassNode()
- ClassReader(bytes).accept(node, 0)
- node
- }
- } catch (e: Throwable) {
- val actualThrowable = if (e is InvocationTargetException) e.cause ?: e else e
- if (actualThrowable is ProcessCanceledException) {
- throw actualThrowable
- }
- if (actualThrowable is NoSuchFileException) {
- return null
- }
+ if (actualThrowable is NoSuchFileException) {
+ return@lockedCached null
+ }
- val message = actualThrowable.message
- // TODO: display an error to the user?
- if (message == null || !message.contains("Unsupported class file major version")) {
- LOGGER.error(actualThrowable)
+ val message = actualThrowable.message
+ // TODO: display an error to the user?
+ if (message == null || !message.contains("Unsupported class file major version")) {
+ LOGGER.error(actualThrowable)
+ }
+ null
}
- null
}
}
@@ -325,8 +369,11 @@ private fun ClassNode.constructClass(project: Project, body: String): PsiClass?
return clazz
}
-inline fun ClassNode.cached(project: Project, vararg dependencies: Any, crossinline compute: () -> T): T {
- return findStubClass(project)?.cached(*dependencies, compute = compute) ?: compute()
+fun ClassNode.cached(project: Project, vararg dependencies: Any, compute: (ClassNode) -> T): T {
+ val unsafeClass = UnsafeCachedValueCapture(this)
+ return findStubClass(project)?.cached(*dependencies) {
+ compute(unsafeClass.value)
+ } ?: compute(this)
}
/**
@@ -452,13 +499,17 @@ fun FieldNode.getGenericType(
return Type.getType(this.desc).toPsiType(elementFactory)
}
-inline fun FieldNode.cached(
+fun FieldNode.cached(
clazz: ClassNode,
project: Project,
vararg dependencies: Any,
- crossinline compute: () -> T,
+ compute: (ClassNode, FieldNode) -> T,
): T {
- return findStubField(clazz, project)?.cached(*dependencies, compute = compute) ?: compute()
+ val unsafeClass = UnsafeCachedValueCapture(clazz)
+ val unsafeField = UnsafeCachedValueCapture(this)
+ return findStubField(clazz, project)?.cached(*dependencies) {
+ compute(unsafeClass.value, unsafeField.value)
+ } ?: compute(clazz, this)
}
fun FieldNode.findStubField(clazz: ClassNode, project: Project): PsiField? {
@@ -693,13 +744,17 @@ private fun findAssociatedLambda(psiClass: PsiClass, clazz: ClassNode, lambdaMet
}
}
-inline fun MethodNode.cached(
+fun MethodNode.cached(
clazz: ClassNode,
project: Project,
vararg dependencies: Array,
- crossinline compute: () -> T,
+ compute: (ClassNode, MethodNode) -> T,
): T {
- return findStubMethod(clazz, project)?.cached(*dependencies, compute = compute) ?: compute()
+ val unsafeClass = UnsafeCachedValueCapture(clazz)
+ val unsafeMethod = UnsafeCachedValueCapture(this)
+ return findStubMethod(clazz, project)?.cached(*dependencies) {
+ compute(unsafeClass.value, unsafeMethod.value)
+ } ?: compute(clazz, this)
}
fun MethodNode.findStubMethod(clazz: ClassNode, project: Project): PsiMethod? {
@@ -932,3 +987,43 @@ fun MethodInsnNode.fakeResolve(): ClassAndMethodNode {
addConstructorToFakeClass(clazz)
return ClassAndMethodNode(clazz, method)
}
+
+// Textifier
+
+fun ClassNode.textify(): String {
+ val sw = StringWriter()
+ accept(TraceClassVisitor(PrintWriter(sw)))
+ return sw.toString().replaceIndent().trimEnd()
+}
+
+fun FieldNode.textify(): String {
+ val cv = TraceClassVisitor(null)
+ accept(cv)
+ val sw = StringWriter()
+ cv.p.print(PrintWriter(sw))
+ return sw.toString().replaceIndent().trimEnd()
+}
+
+fun MethodNode.textify(): String {
+ val cv = TraceClassVisitor(null)
+ accept(cv)
+ val sw = StringWriter()
+ cv.p.print(PrintWriter(sw))
+ return sw.toString().replaceIndent().trimEnd()
+}
+
+fun AnnotationNode.textify(): String {
+ val textifier = Textifier()
+ accept(TraceAnnotationVisitor(textifier))
+ val sw = StringWriter()
+ textifier.print(PrintWriter(sw))
+ return sw.toString().replaceIndent().trimEnd()
+}
+
+fun AbstractInsnNode.textify(): String {
+ val mv = TraceMethodVisitor(Textifier())
+ accept(mv)
+ val sw = StringWriter()
+ mv.p.print(PrintWriter(sw))
+ return sw.toString().replaceIndent().trimEnd()
+}
diff --git a/src/main/kotlin/platform/mixin/util/LocalInfo.kt b/src/main/kotlin/platform/mixin/util/LocalInfo.kt
index 710e7834e..fc684799d 100644
--- a/src/main/kotlin/platform/mixin/util/LocalInfo.kt
+++ b/src/main/kotlin/platform/mixin/util/LocalInfo.kt
@@ -24,9 +24,11 @@ import com.demonwav.mcdev.platform.mixin.handlers.injectionPoint.CollectVisitor
import com.demonwav.mcdev.util.computeStringArray
import com.demonwav.mcdev.util.constantValue
import com.demonwav.mcdev.util.descriptor
+import com.demonwav.mcdev.util.isErasureEquivalentTo
import com.intellij.openapi.module.Module
import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiType
+import com.intellij.util.containers.sequenceOfNotNull
import org.objectweb.asm.Opcodes
import org.objectweb.asm.Type
import org.objectweb.asm.tree.AbstractInsnNode
@@ -128,6 +130,27 @@ class LocalInfo(
}
}
+ fun matchSourceLocals(
+ sourceLocals: List
+ ): Sequence {
+ if (ordinal != null) {
+ return sequenceOfNotNull(
+ sourceLocals.asSequence().filter { it.type.isErasureEquivalentTo(type) }.drop(ordinal).firstOrNull()
+ )
+ }
+ if (index != null) {
+ return sequenceOfNotNull(sourceLocals.getOrNull(index))
+ }
+ if (names.isNotEmpty()) {
+ return sourceLocals.asSequence().filter { it.mixinName in names }
+ }
+
+ // implicit mode
+ return sequenceOfNotNull(
+ sourceLocals.singleOrNull { it.type.isErasureEquivalentTo(type) }
+ )
+ }
+
companion object {
/**
* Gets a [LocalInfo] from an annotation which declares the following attributes:
diff --git a/src/main/kotlin/platform/mixin/util/LocalVariables.kt b/src/main/kotlin/platform/mixin/util/LocalVariables.kt
index 59ee23686..6c5b15441 100644
--- a/src/main/kotlin/platform/mixin/util/LocalVariables.kt
+++ b/src/main/kotlin/platform/mixin/util/LocalVariables.kt
@@ -119,7 +119,13 @@ object LocalVariables {
for (parameter in method.parameterList.parameters) {
val mixinName = if (argsOnly) "var$argsIndex" else parameter.name
- args += SourceLocalVariable(parameter.name, parameter.type, argsIndex, mixinName = mixinName)
+ args += SourceLocalVariable(
+ parameter.name,
+ parameter.type,
+ argsIndex,
+ mixinName = mixinName,
+ variable = parameter
+ )
argsIndex++
if (parameter.isDoubleSlot) {
argsIndex++
@@ -207,7 +213,12 @@ object LocalVariables {
localsHere = localsHere.copyOf(localIndex + 1)
}
val name = instruction.variable.name ?: return
- localsHere[localIndex] = SourceLocalVariable(name, instruction.variable.type, localIndex)
+ localsHere[localIndex] = SourceLocalVariable(
+ name,
+ instruction.variable.type,
+ localIndex,
+ variable = instruction.variable
+ )
if (instruction.variable.isDoubleSlot && localIndex + 1 < localsHere.size) {
localsHere[localIndex + 1] = null
}
@@ -850,11 +861,16 @@ object LocalVariables {
}
}
+ /**
+ * Represents a local variable in source code and its probable relationship to the bytecode. Don't store instances
+ * of this class.
+ */
data class SourceLocalVariable(
val name: String,
val type: PsiType,
val index: Int,
val mixinName: String = name,
+ val variable: PsiVariable? = null,
val implicitLoadCountBefore: Int = 0,
val implicitLoadCountAfter: Int = 0,
val implicitStoreCountBefore: Int = 0,
diff --git a/src/main/kotlin/platform/mixin/util/MixinConstants.kt b/src/main/kotlin/platform/mixin/util/MixinConstants.kt
index 93ae8408d..173fc2050 100644
--- a/src/main/kotlin/platform/mixin/util/MixinConstants.kt
+++ b/src/main/kotlin/platform/mixin/util/MixinConstants.kt
@@ -84,11 +84,14 @@ object MixinConstants {
}
object MixinExtras {
+ const val PACKAGE = "com.llamalad7.mixinextras."
const val OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.Operation"
const val WRAP_OPERATION = "com.llamalad7.mixinextras.injector.wrapoperation.WrapOperation"
const val WRAP_METHOD = "com.llamalad7.mixinextras.injector.wrapmethod.WrapMethod"
const val LOCAL = "com.llamalad7.mixinextras.sugar.Local"
const val LOCAL_REF_PACKAGE = "com.llamalad7.mixinextras.sugar.ref."
+ const val EXPRESSION = "com.llamalad7.mixinextras.expression.Expression"
+ const val DEFINITION = "com.llamalad7.mixinextras.expression.Definition"
fun PsiType.unwrapLocalRef(): PsiType {
if (this !is PsiClassType) {
diff --git a/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt b/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt
new file mode 100644
index 000000000..bcc1c15d4
--- /dev/null
+++ b/src/main/kotlin/platform/mixin/util/UnsafeCachedValueCapture.kt
@@ -0,0 +1,28 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.util
+
+// See CachedValueStabilityChecker
+class UnsafeCachedValueCapture(val value: T) {
+ override fun hashCode() = 0
+ override fun equals(other: Any?) = other is UnsafeCachedValueCapture<*>
+ override fun toString() = value.toString()
+}
diff --git a/src/main/kotlin/util/BeforeOrAfter.kt b/src/main/kotlin/util/BeforeOrAfter.kt
new file mode 100644
index 000000000..448be3062
--- /dev/null
+++ b/src/main/kotlin/util/BeforeOrAfter.kt
@@ -0,0 +1,32 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.util
+
+import com.demonwav.mcdev.asset.MCDevBundle
+import java.util.function.Supplier
+
+enum class BeforeOrAfter(private val myDisplayName: Supplier) {
+ BEFORE(MCDevBundle.pointer("minecraft.before")),
+ AFTER(MCDevBundle.pointer("minecraft.after"));
+
+ val displayName get() = myDisplayName.get()
+ override fun toString() = displayName
+}
diff --git a/src/main/kotlin/util/MemberReference.kt b/src/main/kotlin/util/MemberReference.kt
index 5b5921e73..945746fa5 100644
--- a/src/main/kotlin/util/MemberReference.kt
+++ b/src/main/kotlin/util/MemberReference.kt
@@ -21,14 +21,12 @@
package com.demonwav.mcdev.util
import com.demonwav.mcdev.platform.mixin.reference.MixinSelector
-import com.google.gson.JsonDeserializationContext
-import com.google.gson.JsonDeserializer
-import com.google.gson.JsonElement
+import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiField
import com.intellij.psi.PsiMethod
import java.io.Serializable
-import java.lang.reflect.Type
+import org.objectweb.asm.Type
/**
* Represents a reference to a class member (a method or a field). It may
@@ -65,6 +63,19 @@ data class MemberReference(
override val fieldDescriptor = descriptor?.takeUnless { it.contains("(") }
override val displayName = name
+ val presentableText: String get() = buildString {
+ if (owner != null) {
+ append(owner.substringAfterLast('.'))
+ append('.')
+ }
+ append(name)
+ if (descriptor != null && descriptor.startsWith("(")) {
+ append('(')
+ append(Type.getArgumentTypes(descriptor).joinToString { it.className.substringAfterLast('.') })
+ append(')')
+ }
+ }
+
override fun canEverMatch(name: String): Boolean {
return matchAllNames || this.name == name
}
@@ -88,13 +99,71 @@ data class MemberReference(
(this.descriptor == null || this.descriptor == desc)
}
- object Deserializer : JsonDeserializer {
- override fun deserialize(json: JsonElement, type: Type, ctx: JsonDeserializationContext): MemberReference {
- val ref = json.asString
- val className = ref.substringBefore('#')
- val methodName = ref.substring(className.length + 1, ref.indexOf("("))
- val methodDesc = ref.substring(className.length + methodName.length + 1)
- return MemberReference(methodName, methodDesc, className)
+ companion object {
+ fun parse(value: String): MemberReference? {
+ val reference = value.replace(" ", "")
+ val owner: String?
+
+ var pos = reference.lastIndexOf('.')
+ if (pos != -1) {
+ // Everything before the dot is the qualifier/owner
+ owner = reference.substring(0, pos).replace('/', '.')
+ } else {
+ pos = reference.indexOf(';')
+ if (pos != -1 && reference.startsWith('L')) {
+ val internalOwner = reference.substring(1, pos)
+ if (!StringUtil.isJavaIdentifier(internalOwner.replace('/', '_'))) {
+ // Invalid: Qualifier should only contain slashes
+ return null
+ }
+
+ owner = internalOwner.replace('/', '.')
+
+ // if owner is all there is to the selector, match anything with the owner
+ if (pos == reference.length - 1) {
+ return MemberReference("", null, owner, matchAllNames = true, matchAllDescs = true)
+ }
+ } else {
+ // No owner/qualifier specified
+ pos = -1
+ owner = null
+ }
+ }
+
+ val descriptor: String?
+ val name: String
+ val matchAllNames = reference.getOrNull(pos + 1) == '*'
+ val matchAllDescs: Boolean
+
+ // Find descriptor separator
+ val methodDescPos = reference.indexOf('(', pos + 1)
+ if (methodDescPos != -1) {
+ // Method descriptor
+ descriptor = reference.substring(methodDescPos)
+ name = reference.substring(pos + 1, methodDescPos)
+ matchAllDescs = false
+ } else {
+ val fieldDescPos = reference.indexOf(':', pos + 1)
+ if (fieldDescPos != -1) {
+ descriptor = reference.substring(fieldDescPos + 1)
+ name = reference.substring(pos + 1, fieldDescPos)
+ matchAllDescs = false
+ } else {
+ descriptor = null
+ matchAllDescs = reference.endsWith('*')
+ name = if (matchAllDescs) {
+ reference.substring(pos + 1, reference.lastIndex)
+ } else {
+ reference.substring(pos + 1)
+ }
+ }
+ }
+
+ if (!matchAllNames && !StringUtil.isJavaIdentifier(name) && name != "" && name != "") {
+ return null
+ }
+
+ return MemberReference(if (matchAllNames) "*" else name, descriptor, owner, matchAllNames, matchAllDescs)
}
}
}
diff --git a/src/main/kotlin/util/bytecode-utils.kt b/src/main/kotlin/util/bytecode-utils.kt
index 5eab8bbcf..777210c54 100644
--- a/src/main/kotlin/util/bytecode-utils.kt
+++ b/src/main/kotlin/util/bytecode-utils.kt
@@ -69,7 +69,9 @@ fun getPrimitiveType(internalName: Char): PsiPrimitiveType? {
}
val PsiType.descriptor
- get() = appendDescriptor(StringBuilder()).toString()
+ get() = erasure().appendDescriptor(StringBuilder()).toString()
+
+private fun PsiType.erasure() = TypeConversionUtil.erasure(this)!!
fun getPrimitiveWrapperClass(internalName: Char, project: Project): PsiClass? {
val type = getPrimitiveType(internalName) ?: return null
diff --git a/src/main/kotlin/util/psi-utils.kt b/src/main/kotlin/util/psi-utils.kt
index af3363228..548ea40ed 100644
--- a/src/main/kotlin/util/psi-utils.kt
+++ b/src/main/kotlin/util/psi-utils.kt
@@ -24,6 +24,7 @@ import com.demonwav.mcdev.facet.MinecraftFacet
import com.demonwav.mcdev.platform.mcp.McpModule
import com.demonwav.mcdev.platform.mcp.McpModuleType
import com.intellij.codeInsight.lookup.LookupElementBuilder
+import com.intellij.lang.injection.InjectedLanguageManager
import com.intellij.openapi.module.Module
import com.intellij.openapi.module.ModuleManager
import com.intellij.openapi.module.ModuleUtilCore
@@ -32,10 +33,12 @@ import com.intellij.openapi.roots.ModuleRootManager
import com.intellij.openapi.roots.ProjectFileIndex
import com.intellij.openapi.roots.impl.OrderEntryUtil
import com.intellij.openapi.util.Key
+import com.intellij.openapi.util.UserDataHolderEx
import com.intellij.openapi.util.text.StringUtil
import com.intellij.psi.ElementManipulator
import com.intellij.psi.ElementManipulators
import com.intellij.psi.JavaPsiFacade
+import com.intellij.psi.PsiAnnotation
import com.intellij.psi.PsiClass
import com.intellij.psi.PsiDirectory
import com.intellij.psi.PsiElement
@@ -45,12 +48,14 @@ import com.intellij.psi.PsiEllipsisType
import com.intellij.psi.PsiExpression
import com.intellij.psi.PsiFile
import com.intellij.psi.PsiKeyword
+import com.intellij.psi.PsiLanguageInjectionHost
import com.intellij.psi.PsiMember
import com.intellij.psi.PsiMethod
import com.intellij.psi.PsiMethodReferenceExpression
import com.intellij.psi.PsiModifier
import com.intellij.psi.PsiModifier.ModifierConstant
import com.intellij.psi.PsiModifierList
+import com.intellij.psi.PsiNameValuePair
import com.intellij.psi.PsiParameter
import com.intellij.psi.PsiParameterList
import com.intellij.psi.PsiReference
@@ -58,14 +63,21 @@ import com.intellij.psi.PsiReferenceExpression
import com.intellij.psi.PsiType
import com.intellij.psi.ResolveResult
import com.intellij.psi.filters.ElementFilter
+import com.intellij.psi.util.CachedValue
import com.intellij.psi.util.CachedValueProvider
import com.intellij.psi.util.CachedValuesManager
import com.intellij.psi.util.PsiTreeUtil
import com.intellij.psi.util.PsiTypesUtil
import com.intellij.psi.util.TypeConversionUtil
+import com.intellij.psi.util.parentOfType
import com.intellij.refactoring.changeSignature.ChangeSignatureUtil
import com.intellij.util.IncorrectOperationException
import com.siyeh.ig.psiutils.ImportUtils
+import java.util.concurrent.ConcurrentHashMap
+import java.util.concurrent.ConcurrentMap
+import java.util.concurrent.locks.ReentrantReadWriteLock
+import kotlin.concurrent.read
+import kotlin.concurrent.write
// Parent
fun PsiElement.findModule(): Module? = ModuleUtilCore.findModuleForPsiElement(this)
@@ -82,6 +94,10 @@ fun PsiElement.findContainingMethod(): PsiMethod? = findParent(resolveReferences
fun PsiElement.findContainingModifierList(): PsiModifierList? = findParent(resolveReferences = false) { it is PsiClass }
+fun PsiElement.findContainingNameValuePair(): PsiNameValuePair? = findParent(resolveReferences = false) {
+ it is PsiClass || it is PsiMethod || it is PsiAnnotation
+}
+
private val PsiElement.ancestors: Sequence
get() = generateSequence(this) { if (it is PsiFile) null else it.parent }
@@ -174,6 +190,18 @@ inline fun PsiElement.childrenOfType(): Collection =
inline fun PsiElement.childOfType(): T? =
PsiTreeUtil.findChildOfType(this, T::class.java)
+/**
+ * [InjectedLanguageManager.getInjectionHost] returns the first host of a multi-host injection for some reason.
+ * Use this method as a workaround.
+ */
+fun PsiElement.findMultiInjectionHost(): PsiLanguageInjectionHost? {
+ val injectedLanguageManager = InjectedLanguageManager.getInstance(project)
+ val hostFile = injectedLanguageManager.getInjectionHost(this)?.containingFile ?: return null
+ val hostOffset = injectedLanguageManager.injectedToHost(this, textRange.startOffset)
+ val hostElement = hostFile.findElementAt(hostOffset) ?: return null
+ return hostElement.parentOfType(withSelf = true)
+}
+
fun Sequence.filter(filter: ElementFilter?, context: PsiElement): Sequence {
filter ?: return this
return filter { filter.isAcceptable(it, context) }
@@ -226,6 +254,36 @@ inline fun PsiElement.cached(vararg dependencies: Any, crossinline compute:
}
}
+@PublishedApi
+internal val CACHE_LOCKS_KEY = Key.create, ReentrantReadWriteLock>>("mcdev.cacheLock")
+
+inline fun PsiElement.lockedCached(
+ key: Key>,
+ vararg dependencies: Any,
+ crossinline compute: () -> T,
+): T {
+ val cacheLocks = (this as UserDataHolderEx).putUserDataIfAbsent(CACHE_LOCKS_KEY, ConcurrentHashMap())
+ val cacheLock = cacheLocks.computeIfAbsent(key) { ReentrantReadWriteLock() }
+
+ cacheLock.read {
+ val value = getUserData(key)?.upToDateOrNull
+ if (value != null) {
+ return value.get()
+ }
+ }
+
+ cacheLock.write {
+ val value = getUserData(key)?.upToDateOrNull
+ if (value != null) {
+ return value.get()
+ }
+
+ return CachedValuesManager.getCachedValue(this, key) {
+ CachedValueProvider.Result.create(compute(), *(dependencies.toList() + this).toTypedArray())
+ }
+ }
+}
+
fun LookupElementBuilder.withImportInsertion(toImport: List): LookupElementBuilder =
this.withInsertHandler { insertionContext, _ ->
toImport.forEach { ImportUtils.addImportIfNeeded(it, insertionContext.file) }
diff --git a/src/main/resources/META-INF/plugin.xml b/src/main/resources/META-INF/plugin.xml
index 28d579ca5..b15efd6b2 100644
--- a/src/main/resources/META-INF/plugin.xml
+++ b/src/main/resources/META-INF/plugin.xml
@@ -195,6 +195,7 @@
+
@@ -249,6 +250,10 @@
id="Settings.Minecraft"
groupId="language"
instance="com.demonwav.mcdev.MinecraftConfigurable"/>
+
+
@@ -492,6 +498,7 @@
+
@@ -518,6 +525,35 @@
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
diff --git a/src/main/resources/messages/MinecraftDevelopment.properties b/src/main/resources/messages/MinecraftDevelopment.properties
index 2ee0ef24d..7d8526e81 100644
--- a/src/main/resources/messages/MinecraftDevelopment.properties
+++ b/src/main/resources/messages/MinecraftDevelopment.properties
@@ -260,16 +260,9 @@ translation_sort.title=Select Sort Order
translation_sort.order=Sort Order
translation_sort.keep_comment=Keep Comment
-minecraft.settings.display_name=Minecraft Development
-minecraft.settings.title=Minecraft Development Settings
minecraft.settings.change_update_channel=Change Plugin Update Channel
-minecraft.settings.show_project_platform_icons=Show project platform icons
-minecraft.settings.show_event_listener_gutter_icons=Show event listener gutter icons
-minecraft.settings.show_chat_color_gutter_icons=Show chat color gutter icons
-minecraft.settings.show_chat_color_underlines=Show chat color underlines
minecraft.settings.chat_color_underline_style=Chat color underline style:
-minecraft.settings.mixin=Mixin
-minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line
+minecraft.settings.display_name=Minecraft Development
minecraft.settings.creator=Creator
minecraft.settings.creator.repos=Template Repositories:
minecraft.settings.creator.repos.column.name=Name
@@ -284,10 +277,59 @@ minecraft.settings.lang_template.project_must_be_selected=You must have selected
minecraft.settings.lang_template.comment=You may edit the template used for translation key sorting here.\
Each line may be empty, a comment (with #) or a glob pattern for matching translation keys (like "item.*").\
Note: Empty lines are respected and will be put into the sorting result.
+minecraft.settings.mixin.definition_pos_relative_to_expression=@Definition position relative to @Expression
+minecraft.settings.mixin.shadow_annotation_same_line=@Shadow annotations on same line
+minecraft.settings.mixin=Mixin
+minecraft.settings.project.display_name=Project-Specific Settings
+minecraft.settings.show_chat_color_gutter_icons=Show chat color gutter icons
+minecraft.settings.show_chat_color_underlines=Show chat color underlines
+minecraft.settings.show_event_listener_gutter_icons=Show event listener gutter icons
+minecraft.settings.show_project_platform_icons=Show project platform icons
+minecraft.settings.title=Minecraft Development Settings
minecraft.settings.translation=Translation
minecraft.settings.translation.force_json_translation_file=Force JSON translation file (1.13+)
minecraft.settings.translation.use_custom_convert_template=Use custom template for convert literal to translation
+minecraft.before=Before
+minecraft.after=After
+
+mixinextras.expression.lang.errors.array_access_missing_index=Missing index
+mixinextras.expression.lang.errors.array_length_after_empty=Cannot specify array length after an unspecified array length
+mixinextras.expression.lang.errors.empty_array_initializer=Array initializer cannot be empty
+mixinextras.expression.lang.errors.index_not_expected_in_type=Index not expected in type
+mixinextras.expression.lang.errors.instanceof_non_type=Expected type
+mixinextras.expression.lang.errors.invalid_number=Invalid number
+mixinextras.expression.lang.errors.missing_array_length=Array construction must contain a length
+mixinextras.expression.lang.errors.new_array_dim_expr_with_initializer=Cannot use initializer for array with specified length
+mixinextras.expression.lang.errors.new_no_constructor_args_or_array=Expected constructor arguments or array creation
+mixinextras.expression.lang.errors.unresolved_symbol=Unresolved symbol
+mixinextras.expression.lang.errors.unused_definition=Unused definition
+mixinextras.expression.lang.errors.unused_symbol.fix=Remove definition
+
+mixinextras.expression.lang.display_name=MixinExtras Expressions
+mixinextras.expression.lang.highlighting.bad_char.display_name=Bad character
+mixinextras.expression.lang.highlighting.braces.display_name=Braces
+mixinextras.expression.lang.highlighting.brackets.display_name=Brackets
+mixinextras.expression.lang.highlighting.call_identifier.display_name=Identifier//Method call
+mixinextras.expression.lang.highlighting.capture.display_name=Capture
+mixinextras.expression.lang.highlighting.class_name_identifier.display_name=Identifier//Class name
+mixinextras.expression.lang.highlighting.comma.display_name=Comma
+mixinextras.expression.lang.highlighting.declaration_identifier.display_name=Identifier//Declaration
+mixinextras.expression.lang.highlighting.dot.display_name=Dot
+mixinextras.expression.lang.highlighting.identifier.display_name=Identifier
+mixinextras.expression.lang.highlighting.keyword.display_name=Keyword
+mixinextras.expression.lang.highlighting.member_name_identifier.display_name=Identifier//Member name
+mixinextras.expression.lang.highlighting.method_reference.display_name=Method reference
+mixinextras.expression.lang.highlighting.number.display_name=Number
+mixinextras.expression.lang.highlighting.operator.display_name=Operator
+mixinextras.expression.lang.highlighting.parens.display_name=Parentheses
+mixinextras.expression.lang.highlighting.primitive_type_identifier.display_name=Identifier//Primitive type
+mixinextras.expression.lang.highlighting.string.display_name=String
+mixinextras.expression.lang.highlighting.string_escape.display_name=String escape
+mixinextras.expression.lang.highlighting.type_declaration_identifier.display_name=Identifier//Type declaration
+mixinextras.expression.lang.highlighting.variable_identifier.display_name=Identifier//Variable
+mixinextras.expression.lang.highlighting.wildcard.display_name=Wildcard
+
template.provider.builtin.label=Built In
template.provider.remote.label=Remote
template.provider.local.label=Local
diff --git a/src/test/kotlin/platform/mixin/BaseMixinTest.kt b/src/test/kotlin/platform/mixin/BaseMixinTest.kt
index 4b9dbd0ee..caac9bc5d 100644
--- a/src/test/kotlin/platform/mixin/BaseMixinTest.kt
+++ b/src/test/kotlin/platform/mixin/BaseMixinTest.kt
@@ -34,20 +34,23 @@ import org.junit.jupiter.api.BeforeEach
abstract class BaseMixinTest : BaseMinecraftTest(PlatformType.MIXIN) {
private var mixinLibrary: Library? = null
+ private var mixinExtrasLibrary: Library? = null
private var testDataLibrary: Library? = null
@BeforeEach
fun initMixin() {
runWriteTask {
mixinLibrary = createLibrary(project, "mixin")
+ mixinExtrasLibrary = createLibrary(project, "mixinextras-common")
testDataLibrary = createLibrary(project, "mixin-test-data")
}
ModuleRootModificationUtil.updateModel(module) { model ->
model.addLibraryEntry(mixinLibrary ?: throw IllegalStateException("Mixin library not created"))
+ model.addLibraryEntry(mixinExtrasLibrary ?: throw IllegalStateException("MixinExtras library not created"))
model.addLibraryEntry(testDataLibrary ?: throw IllegalStateException("Test data library not created"))
val orderEntries = model.orderEntries
- orderEntries.rotate(2)
+ orderEntries.rotate(3)
model.rearrangeOrderEntries(orderEntries)
}
}
diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt
index ae88d95a5..dfa58058f 100644
--- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt
+++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureFixTest.kt
@@ -33,7 +33,7 @@ class InvalidInjectorMethodSignatureFixTest : BaseMixinTest() {
private fun doTest(testName: String) {
fixture.enableInspections(InvalidInjectorMethodSignatureInspection::class)
- testInspectionFix(fixture, "invalidInjectorMethodSignature/$testName", "Fix method parameters")
+ testInspectionFix(fixture, "invalidInjectorMethodSignature/$testName", "Fix method signature")
}
@Test
diff --git a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt
index 769b3894c..ae97fb26e 100644
--- a/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt
+++ b/src/test/kotlin/platform/mixin/InvalidInjectorMethodSignatureInspectionTest.kt
@@ -98,7 +98,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() {
}
@Inject(method = "(Lcom/demonwav/mcdev/mixintestdata/invalidInjectorMethodSignatureInspection/MixedInOuter;Ljava/lang/String;)V", at = @At("RETURN"))
- private void injectCtor(String string, CallbackInfo ci) {
+ private void injectCtor(String string, CallbackInfo ci) {
}
}
""",
@@ -122,7 +122,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() {
public class TestMixin {
@Inject(method = "()V", at = @At("RETURN"))
- private void injectCtorWrong(MixedInOuter outer, CallbackInfo ci) {
+ private void injectCtorWrong(MixedInOuter outer, CallbackInfo ci) {
}
@Inject(method = "", at = @At("RETURN"))
@@ -130,7 +130,7 @@ class InvalidInjectorMethodSignatureInspectionTest : BaseMixinTest() {
}
@Inject(method = "(Ljava/lang/String;)V", at = @At("RETURN"))
- private void injectCtor(MixedInOuter outer, String string, CallbackInfo ci) {
+ private void injectCtor(MixedInOuter outer, String string, CallbackInfo ci) {
}
@Inject(method = "(Ljava/lang/String;)V", at = @At("RETURN"))
diff --git a/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt b/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt
new file mode 100644
index 000000000..5c12d882d
--- /dev/null
+++ b/src/test/kotlin/platform/mixin/expression/MEExpressionCompletionTest.kt
@@ -0,0 +1,645 @@
+/*
+ * Minecraft Development for IntelliJ
+ *
+ * https://mcdev.io/
+ *
+ * Copyright (C) 2024 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.expression
+
+import com.demonwav.mcdev.MinecraftProjectSettings
+import com.demonwav.mcdev.framework.EdtInterceptor
+import com.demonwav.mcdev.platform.mixin.BaseMixinTest
+import com.demonwav.mcdev.util.BeforeOrAfter
+import com.intellij.codeInsight.lookup.impl.LookupImpl
+import org.intellij.lang.annotations.Language
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.Assertions.assertFalse
+import org.junit.jupiter.api.Assertions.assertNotEquals
+import org.junit.jupiter.api.Assertions.assertNotNull
+import org.junit.jupiter.api.Assertions.assertNull
+import org.junit.jupiter.api.Assertions.assertTrue
+import org.junit.jupiter.api.Assertions.fail
+import org.junit.jupiter.api.DisplayName
+import org.junit.jupiter.api.Test
+import org.junit.jupiter.api.extension.ExtendWith
+
+@ExtendWith(EdtInterceptor::class)
+@DisplayName("MixinExtras expression completion test")
+class MEExpressionCompletionTest : BaseMixinTest() {
+ private fun assertLookupAppears(
+ lookupString: String,
+ @Language("JAVA") code: String,
+ shouldAppear: Boolean = true
+ ) {
+ buildProject {
+ dir("test") {
+ java("MEExpressionCompletionTest.java", code)
+ }
+ }
+
+ fixture.completeBasic()
+
+ val lookups = fixture.lookupElementStrings
+ if (lookups != null) {
+ if (shouldAppear) {
+ assertTrue(lookupString in lookups)
+ } else {
+ assertFalse(lookupString in lookups)
+ }
+ } else {
+ if (shouldAppear) {
+ assertEquals(lookupString, fixture.elementAtCaret.text)
+ } else {
+ assertNotEquals(lookupString, fixture.elementAtCaret.text)
+ }
+ }
+ }
+
+ private fun doBeforeAfterTest(
+ lookupString: String,
+ @Language("JAVA") code: String,
+ @Language("JAVA") expectedAfter: String?
+ ) {
+ buildProject {
+ dir("test") {
+ java("MEExpressionCompletionTest.java", code)
+ }
+ }
+
+ MinecraftProjectSettings.getInstance(fixture.project).definitionPosRelativeToExpression = BeforeOrAfter.BEFORE
+
+ val possibleItems = fixture.completeBasic()
+ if (possibleItems != null) {
+ val itemToComplete = possibleItems.firstOrNull { it.lookupString == lookupString }
+ if (expectedAfter != null) {
+ assertNotNull(itemToComplete, "Expected a completion matching \"$lookupString\"")
+ (fixture.lookup as LookupImpl).finishLookup('\n', itemToComplete)
+ } else {
+ assertNull(itemToComplete, "Expected no completions matching \"$lookupString\"")
+ return
+ }
+ } else if (expectedAfter == null) {
+ fail("Expected no completions matching \"$lookupString\"")
+ return
+ }
+
+ fixture.checkResult(expectedAfter)
+ }
+
+ @Test
+ @DisplayName("Local Variable Implicit Completion Test")
+ fun localVariableImplicitCompletionTest() {
+ doBeforeAfterTest(
+ "one",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import com.llamalad7.mixinextras.sugar.Local;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "one", local = @Local(type = int.class))
+ @Expression("one")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ @DisplayName("Local Variable Ordinal Completion Test")
+ fun localVariableOrdinalCompletionTest() {
+ doBeforeAfterTest(
+ "local1",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import com.llamalad7.mixinextras.sugar.Local;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "local1", local = @Local(type = String.class, ordinal = 0))
+ @Expression("local1")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ @DisplayName("Local Variable Inaccessible Type Completion Test")
+ fun localVariableInaccessibleTypeCompletionTest() {
+ doBeforeAfterTest(
+ "varOfInaccessibleType",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V")
+ @Expression("acceptInaccessibleType()")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import com.llamalad7.mixinextras.sugar.Local;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V")
+ @Definition(id = "varOfInaccessibleType", local = @Local(ordinal = 0))
+ @Expression("acceptInaccessibleType(varOfInaccessibleType)")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ @DisplayName("Field Completion Test")
+ fun fieldCompletionTest() {
+ doBeforeAfterTest(
+ "out",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "out", field = "Ljava/lang/System;out:Ljava/io/PrintStream;")
+ @Expression("out")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ @DisplayName("Method Completion Test")
+ fun methodCompletionTest() {
+ doBeforeAfterTest(
+ "acceptInaccessibleType",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "acceptInaccessibleType", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;acceptInaccessibleType(Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}InaccessibleType;)V")
+ @Expression("acceptInaccessibleType()")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ @DisplayName("Method No-Arg Completion Test")
+ fun methodNoArgCompletionTest() {
+ doBeforeAfterTest(
+ "noArgMethod",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "noArgMethod", method = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;noArgMethod()V")
+ @Expression("noArgMethod()")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ @DisplayName("Type Completion Test")
+ fun typeCompletionTest() {
+ doBeforeAfterTest(
+ "ArrayList",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("new ")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ import java.util.ArrayList;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "ArrayList", type = ArrayList.class)
+ @Expression("new ArrayList()")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ )
+ }
+
+ @Test
+ @DisplayName("Inaccessible Type Completion Test")
+ fun inaccessibleTypeCompletionTest() {
+ doBeforeAfterTest(
+ "InaccessibleType",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("new ")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ null,
+ )
+ }
+
+ @Test
+ @DisplayName("Array Creation Completion Test")
+ fun arrayCreationCompletionTest() {
+ doBeforeAfterTest(
+ "String",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("new ")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent(),
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "String", type = String.class)
+ @Expression("new String[]")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("LHS Of Complete Assignment Test")
+ fun lhsOfCompleteAssignmentTest() {
+ assertLookupAppears(
+ "local1",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression(" = 'Hello'")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("Cast Test")
+ fun castTest() {
+ assertLookupAppears(
+ "Integer",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("()")
+ @Inject(method = "getStingerCount", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("Member Function Test")
+ fun memberFunctionTest() {
+ assertLookupAppears(
+ "get",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "Integer", type = Integer.class)
+ @Definition(id = "synchedData", field = "Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData;synchedData:Lcom/demonwav/mcdev/mixintestdata/meExpression/MEExpressionTestData${'$'}SynchedDataManager;")
+ @Expression("(Integer) this.synchedData.")
+ @Inject(method = "getStingerCount", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("Array Length Test")
+ fun arrayLengthTest() {
+ assertLookupAppears(
+ "one",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "String", type = String.class)
+ @Expression("new String[]")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("Array Element Test")
+ fun arrayElementTest() {
+ assertLookupAppears(
+ "local2",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Definition;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Definition(id = "String", type = String.class)
+ @Expression("new String[]{?, }")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("Static Method Reference Test")
+ fun staticMethodReferenceTest() {
+ assertLookupAppears(
+ "staticMapper",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("::")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("Non Static Method Reference Test")
+ fun nonStaticMethodReferenceTest() {
+ assertLookupAppears(
+ "nonStaticMapper",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("this::")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+
+ @Test
+ @DisplayName("Constructor Method Reference Test")
+ fun constructorMethodReferenceTest() {
+ assertLookupAppears(
+ "ConstructedByMethodReference",
+ """
+ package test;
+
+ import com.demonwav.mcdev.mixintestdata.meExpression.MEExpressionTestData;
+ import com.llamalad7.mixinextras.expression.Expression;
+ import org.spongepowered.asm.mixin.Mixin;
+ import org.spongepowered.asm.mixin.injection.At;
+ import org.spongepowered.asm.mixin.injection.Inject;
+
+ @Mixin(MEExpressionTestData.class)
+ class MEExpressionCompletionTest {
+ @Expression("")
+ @Inject(method = "complexFunction", at = @At("MIXINEXTRAS:EXPRESSION"))
+ }
+ """.trimIndent()
+ )
+ }
+}