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() + ) + } +}