From 569d1e4e0ed2a5a341a75affa8058985b0184164 Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Sun, 28 Sep 2025 15:46:48 -0500 Subject: [PATCH 01/16] Fix for static initializers --- .../vineflower/EnigmaTextTokenCollector.java | 99 ++++++++++++++++--- 1 file changed, 88 insertions(+), 11 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index a24e818d7..823b5ceac 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -13,18 +13,20 @@ import org.jetbrains.java.decompiler.util.Pair; import org.jetbrains.java.decompiler.util.token.TextRange; -import java.util.HashMap; -import java.util.LinkedHashMap; -import java.util.Map; +import java.util.*; import java.util.function.UnaryOperator; public class EnigmaTextTokenCollector extends TextTokenVisitor { private String content; - private MethodEntry currentMethod; + private final Deque classStack = new ArrayDeque<>(); + private final Deque methodStack = new ArrayDeque<>(); private final Map> declarations = new HashMap<>(); private final Map, Entry>> references = new HashMap<>(); private final Map tokens = new LinkedHashMap<>(); + private final List syntheticMethods = new ArrayList<>(); + private final Deque openSynthetic = new ArrayDeque<>(); + private final Map syntheticEntryBySpan = new HashMap<>(); public EnigmaTextTokenCollector(TextTokenVisitor next) { super(next); @@ -80,21 +82,82 @@ public void addTokensToIndex(SourceIndex index, UnaryOperator tokenProces } } + private void findStaticInitializers() { + int index = 0; + while ((index = this.content.indexOf("static {", index)) >= 0) { + int startIndex = index; + index += "static {".length(); + int depth = 1; + while (depth > 0 && index < this.content.length()) { + char c = this.content.charAt(index++); + if (c == '{') { + depth++; + } else if (c == '}') { + depth--; + } + + if (depth == 0) { + this.syntheticMethods.add(new SyntheticMethodSpan(startIndex, index, false)); + } + } + } + } + + private void updateMethodStack(TextRange range) { + while (!this.openSynthetic.isEmpty() && encloses(this.openSynthetic.peek(), range)) { + SyntheticMethodSpan span = this.openSynthetic.pop(); + this.syntheticEntryBySpan.remove(span); + this.methodStack.pop(); + } + + List enclosing = this.syntheticMethods.stream() + .filter(span -> span.range.start < range.start && span.range.getEnd() > range.getEnd()) + .sorted(Comparator.comparingInt(span -> span.range.length)) + .toList(); + + for (SyntheticMethodSpan method : enclosing) { + if (!this.openSynthetic.contains(method)) { + MethodEntry entry = this.syntheticEntryBySpan.computeIfAbsent(method, this::getSyntheticMethodEntry); + if (this.methodStack.isEmpty() || !this.methodStack.peek().equals(entry)) { + this.methodStack.push(entry); + } + } + } + } + + private static boolean encloses(SyntheticMethodSpan outer, TextRange inner) { + return outer.range.start < inner.start && outer.range.getEnd() > inner.getEnd(); + } + + private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { + if (method.isLambda) { + //TODO add lambda logic + throw new UnsupportedOperationException("lambda handling not implemented yet"); + } else { + return getMethodEntry(this.classStack.peek().getFullName(), "", MethodDescriptor.parseDescriptor("()V")); + } + } + @Override public void start(String content) { this.content = content; - this.currentMethod = null; + this.classStack.clear(); + this.methodStack.clear(); + findStaticInitializers(); } + //TODO pop classStack when leaving class @Override public void visitClass(TextRange range, boolean declaration, String name) { super.visitClass(range, declaration, name); Token token = this.getToken(range); + updateMethodStack(range); if (declaration) { this.addDeclaration(token, getClassEntry(name)); + this.classStack.push(getClassEntry(name)); } else { - this.addReference(token, getClassEntry(name), this.currentMethod); + this.addReference(token, getClassEntry(name), this.methodStack.peek()); } } @@ -102,11 +165,12 @@ public void visitClass(TextRange range, boolean declaration, String name) { public void visitField(TextRange range, boolean declaration, String className, String name, FieldDescriptor descriptor) { super.visitField(range, declaration, className, name, descriptor); Token token = this.getToken(range); + updateMethodStack(range); if (declaration) { this.addDeclaration(token, getFieldEntry(className, name, descriptor)); } else { - this.addReference(token, getFieldEntry(className, name, descriptor), this.currentMethod); + this.addReference(token, getFieldEntry(className, name, descriptor), this.methodStack.peek()); } } @@ -114,13 +178,18 @@ public void visitField(TextRange range, boolean declaration, String className, S public void visitMethod(TextRange range, boolean declaration, String className, String name, MethodDescriptor descriptor) { super.visitMethod(range, declaration, className, name, descriptor); Token token = this.getToken(range); + updateMethodStack(range); MethodEntry entry = getMethodEntry(className, name, descriptor); if (declaration) { this.addDeclaration(token, entry); - this.currentMethod = entry; + if (!this.methodStack.isEmpty()) { + this.methodStack.pop(); + } + + this.methodStack.push(entry); } else { - this.addReference(token, entry, this.currentMethod); + this.addReference(token, entry, this.methodStack.peek()); } } @@ -128,12 +197,13 @@ public void visitMethod(TextRange range, boolean declaration, String className, public void visitParameter(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitParameter(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); + updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); if (declaration) { this.addDeclaration(token, getParameterEntry(parent, idx, name)); } else { - this.addReference(token, getParameterEntry(parent, idx, name), this.currentMethod); + this.addReference(token, getParameterEntry(parent, idx, name), this.methodStack.peek()); } } @@ -141,12 +211,19 @@ public void visitParameter(TextRange range, boolean declaration, String classNam public void visitLocal(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitLocal(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); + updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); if (declaration) { this.addDeclaration(token, getVariableEntry(parent, idx, name)); } else { - this.addReference(token, getVariableEntry(parent, idx, name), this.currentMethod); + this.addReference(token, getVariableEntry(parent, idx, name), this.methodStack.peek()); + } + } + + private record SyntheticMethodSpan(TextRange range, boolean isLambda) { + public SyntheticMethodSpan(int start, int end, boolean isLambda) { + this(new TextRange(start, end - start), isLambda); } } } From ad972e42181c18106756d1cd8b6581f9764ff439 Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Sun, 28 Sep 2025 16:53:17 -0500 Subject: [PATCH 02/16] Bug fixes --- .../vineflower/EnigmaTextTokenCollector.java | 24 ++++++++++++------- 1 file changed, 16 insertions(+), 8 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 823b5ceac..ef39767e6 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -18,7 +18,8 @@ public class EnigmaTextTokenCollector extends TextTokenVisitor { private String content; - private final Deque classStack = new ArrayDeque<>(); + private ClassEntry outerClass; + private ClassEntry currentClass; private final Deque methodStack = new ArrayDeque<>(); private final Map> declarations = new HashMap<>(); @@ -88,12 +89,15 @@ private void findStaticInitializers() { int startIndex = index; index += "static {".length(); int depth = 1; + boolean inString = false; while (depth > 0 && index < this.content.length()) { char c = this.content.charAt(index++); - if (c == '{') { + if (c == '{' && !inString) { depth++; - } else if (c == '}') { + } else if (c == '}' && !inString) { depth--; + } else if (c == '\"') { + inString = !inString; } if (depth == 0) { @@ -132,21 +136,19 @@ private static boolean encloses(SyntheticMethodSpan outer, TextRange inner) { private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { if (method.isLambda) { //TODO add lambda logic - throw new UnsupportedOperationException("lambda handling not implemented yet"); + throw new UnsupportedOperationException("Lambda handling is not implemented yet"); } else { - return getMethodEntry(this.classStack.peek().getFullName(), "", MethodDescriptor.parseDescriptor("()V")); + return getMethodEntry(this.currentClass.getFullName(), "", MethodDescriptor.parseDescriptor("()V")); } } @Override public void start(String content) { this.content = content; - this.classStack.clear(); this.methodStack.clear(); findStaticInitializers(); } - //TODO pop classStack when leaving class @Override public void visitClass(TextRange range, boolean declaration, String name) { super.visitClass(range, declaration, name); @@ -154,8 +156,14 @@ public void visitClass(TextRange range, boolean declaration, String name) { updateMethodStack(range); if (declaration) { + if (this.outerClass == null) { + this.outerClass = getClassEntry(name); + this.currentClass = this.outerClass; + } else { + this.currentClass = getClassEntry(this.outerClass.getFullName() + '$' + name); + } + this.addDeclaration(token, getClassEntry(name)); - this.classStack.push(getClassEntry(name)); } else { this.addReference(token, getClassEntry(name), this.methodStack.peek()); } From eb1a612e53eab2bd24b29fe02d09f5e1ec00895f Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Mon, 29 Sep 2025 12:37:45 -0500 Subject: [PATCH 03/16] Migrate to JavaParser --- enigma/build.gradle | 1 + .../vineflower/EnigmaTextTokenCollector.java | 103 ++++++++++++------ gradle/libs.versions.toml | 2 + 3 files changed, 70 insertions(+), 36 deletions(-) diff --git a/enigma/build.gradle b/enigma/build.gradle index 7d2e26205..a73b993ea 100644 --- a/enigma/build.gradle +++ b/enigma/build.gradle @@ -19,6 +19,7 @@ dependencies { implementation libs.vineflower implementation libs.cfr implementation libs.procyon + implementation libs.javaparser implementation libs.quilt.config diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index ef39767e6..7ebb1bb35 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -1,5 +1,17 @@ package org.quiltmc.enigma.impl.source.vineflower; +import com.github.javaparser.ParserConfiguration; +import com.github.javaparser.Position; +import com.github.javaparser.Range; +import com.github.javaparser.StaticJavaParser; +import com.github.javaparser.ast.CompilationUnit; +import com.github.javaparser.ast.body.InitializerDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; +import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; +import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor; +import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor; +import org.jetbrains.java.decompiler.util.Pair; +import org.jetbrains.java.decompiler.util.token.TextRange; import org.quiltmc.enigma.api.source.SourceIndex; import org.quiltmc.enigma.api.source.Token; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; @@ -7,24 +19,19 @@ import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; -import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; -import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor; -import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor; -import org.jetbrains.java.decompiler.util.Pair; -import org.jetbrains.java.decompiler.util.token.TextRange; import java.util.*; import java.util.function.UnaryOperator; public class EnigmaTextTokenCollector extends TextTokenVisitor { private String content; - private ClassEntry outerClass; - private ClassEntry currentClass; + private final Deque classStack = new ArrayDeque<>(); private final Deque methodStack = new ArrayDeque<>(); private final Map> declarations = new HashMap<>(); private final Map, Entry>> references = new HashMap<>(); private final Map tokens = new LinkedHashMap<>(); + private final Map classRanges = new HashMap<>(); private final List syntheticMethods = new ArrayList<>(); private final Deque openSynthetic = new ArrayDeque<>(); private final Map syntheticEntryBySpan = new HashMap<>(); @@ -83,28 +90,43 @@ public void addTokensToIndex(SourceIndex index, UnaryOperator tokenProces } } - private void findStaticInitializers() { - int index = 0; - while ((index = this.content.indexOf("static {", index)) >= 0) { - int startIndex = index; - index += "static {".length(); - int depth = 1; - boolean inString = false; - while (depth > 0 && index < this.content.length()) { - char c = this.content.charAt(index++); - if (c == '{' && !inString) { - depth++; - } else if (c == '}' && !inString) { - depth--; - } else if (c == '\"') { - inString = !inString; - } + private void parseSource() { + StaticJavaParser.getParserConfiguration() + .setStoreTokens(true) + .setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); //todo see if we can get source information instead of hardcoding this + CompilationUnit unit = StaticJavaParser.parse(this.content); + List initializers = unit.findAll(InitializerDeclaration.class, InitializerDeclaration::isStatic); + for (InitializerDeclaration decl : initializers) { + Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for initializer")); + int start = positionToIndex(range.begin); + int end = positionToIndex(range.end); + syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); + } + String pkgPrefix = unit.getPackageDeclaration().map(decl -> decl.getNameAsString().replace('.', '/') + "/").orElse(""); + for (TypeDeclaration decl : unit.getTypes()) { + addClassAndChildren(decl, pkgPrefix + decl.getNameAsString()); + } + } - if (depth == 0) { - this.syntheticMethods.add(new SyntheticMethodSpan(startIndex, index, false)); - } + private void addClassAndChildren(TypeDeclaration decl, String name) { + Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for type declaration")); + TextRange textRange = new TextRange(positionToIndex(range.begin), positionToIndex(range.end)); + classRanges.put(getClassEntry(name), textRange); + decl.getMembers().forEach(member -> { + if (member instanceof TypeDeclaration child) { + addClassAndChildren(child, name + "$" + child.getNameAsString()); } + }); + } + + private int positionToIndex(Position position) { + int index = 0; + int linesLeft = position.line; + while (linesLeft > 0) { + index = this.content.indexOf('\n', index) + 1; + linesLeft--; } + return index + position.column; } private void updateMethodStack(TextRange range) { @@ -115,7 +137,7 @@ private void updateMethodStack(TextRange range) { } List enclosing = this.syntheticMethods.stream() - .filter(span -> span.range.start < range.start && span.range.getEnd() > range.getEnd()) + .filter(span -> encloses(span, range)) .sorted(Comparator.comparingInt(span -> span.range.length)) .toList(); @@ -129,6 +151,12 @@ private void updateMethodStack(TextRange range) { } } + private void pruneExitedClass(TextRange range) { + while (!classStack.isEmpty() && classRanges.get(classStack.peek()).getEnd() < range.start) { + classStack.pop(); + } + } + private static boolean encloses(SyntheticMethodSpan outer, TextRange inner) { return outer.range.start < inner.start && outer.range.getEnd() > inner.getEnd(); } @@ -138,31 +166,30 @@ private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { //TODO add lambda logic throw new UnsupportedOperationException("Lambda handling is not implemented yet"); } else { - return getMethodEntry(this.currentClass.getFullName(), "", MethodDescriptor.parseDescriptor("()V")); + return getMethodEntry(this.classStack.peek().getFullName(), "", MethodDescriptor.parseDescriptor("()V")); } } @Override public void start(String content) { this.content = content; + this.classRanges.clear(); this.methodStack.clear(); - findStaticInitializers(); + this.openSynthetic.clear(); + this.syntheticMethods.clear(); + this.syntheticEntryBySpan.clear(); + parseSource(); } @Override public void visitClass(TextRange range, boolean declaration, String name) { super.visitClass(range, declaration, name); Token token = this.getToken(range); + pruneExitedClass(range); updateMethodStack(range); if (declaration) { - if (this.outerClass == null) { - this.outerClass = getClassEntry(name); - this.currentClass = this.outerClass; - } else { - this.currentClass = getClassEntry(this.outerClass.getFullName() + '$' + name); - } - + this.classStack.push(getClassEntry(name)); this.addDeclaration(token, getClassEntry(name)); } else { this.addReference(token, getClassEntry(name), this.methodStack.peek()); @@ -173,6 +200,7 @@ public void visitClass(TextRange range, boolean declaration, String name) { public void visitField(TextRange range, boolean declaration, String className, String name, FieldDescriptor descriptor) { super.visitField(range, declaration, className, name, descriptor); Token token = this.getToken(range); + pruneExitedClass(range); updateMethodStack(range); if (declaration) { @@ -186,6 +214,7 @@ public void visitField(TextRange range, boolean declaration, String className, S public void visitMethod(TextRange range, boolean declaration, String className, String name, MethodDescriptor descriptor) { super.visitMethod(range, declaration, className, name, descriptor); Token token = this.getToken(range); + pruneExitedClass(range); updateMethodStack(range); MethodEntry entry = getMethodEntry(className, name, descriptor); @@ -205,6 +234,7 @@ public void visitMethod(TextRange range, boolean declaration, String className, public void visitParameter(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitParameter(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); + pruneExitedClass(range); updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); @@ -219,6 +249,7 @@ public void visitParameter(TextRange range, boolean declaration, String classNam public void visitLocal(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitLocal(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); + pruneExitedClass(range); updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index ac23c5adf..4e4509d24 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -11,6 +11,7 @@ swing_dpi = "0.10" fontchooser = "2.5.2" tinylog = "2.6.2" quilt_config = "1.3.2" +javaparser = "3.27.0" vineflower = "1.11.0" cfr = "0.2.2" @@ -42,6 +43,7 @@ flatlaf_extras = { module = "com.formdev:flatlaf-extras", version.ref = "flatlaf syntaxpain = { module = "org.quiltmc:syntaxpain", version.ref = "syntaxpain" } swing_dpi = { module = "com.github.lukeu:swing-dpi", version.ref = "swing_dpi" } fontchooser = { module = "org.drjekyll:fontchooser", version.ref = "fontchooser" } +javaparser = { module = "com.github.javaparser:javaparser-core", version.ref = "javaparser" } vineflower = { module = "org.vineflower:vineflower", version.ref = "vineflower" } cfr = { module = "net.fabricmc:cfr", version.ref = "cfr" } From 91e4ba7a2f7215b36f9bc395b6f4a2b054d831b4 Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Mon, 29 Sep 2025 16:12:54 -0500 Subject: [PATCH 04/16] Add lambda support --- .../vineflower/EnigmaTextTokenCollector.java | 106 +++++++++++++++++- 1 file changed, 100 insertions(+), 6 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 7ebb1bb35..42bd5246a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -5,9 +5,13 @@ import com.github.javaparser.Range; import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; -import com.github.javaparser.ast.body.InitializerDeclaration; -import com.github.javaparser.ast.body.TypeDeclaration; +import com.github.javaparser.ast.Node; +import com.github.javaparser.ast.body.*; +import com.github.javaparser.ast.expr.LambdaExpr; +import org.jetbrains.java.decompiler.main.DecompilerContext; import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; +import org.jetbrains.java.decompiler.struct.StructClass; +import org.jetbrains.java.decompiler.struct.StructMethod; import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor; import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor; import org.jetbrains.java.decompiler.util.Pair; @@ -20,7 +24,15 @@ import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; -import java.util.*; +import java.util.ArrayDeque; +import java.util.ArrayList; +import java.util.Comparator; +import java.util.Deque; +import java.util.HashMap; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; import java.util.function.UnaryOperator; public class EnigmaTextTokenCollector extends TextTokenVisitor { @@ -102,10 +114,92 @@ private void parseSource() { int end = positionToIndex(range.end); syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); } + + for (FieldDeclaration decl : unit.findAll(FieldDeclaration.class)) { + Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for field declaration")); + int start = positionToIndex(range.begin); + int end = positionToIndex(range.end); + syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); + } + String pkgPrefix = unit.getPackageDeclaration().map(decl -> decl.getNameAsString().replace('.', '/') + "/").orElse(""); for (TypeDeclaration decl : unit.getTypes()) { addClassAndChildren(decl, pkgPrefix + decl.getNameAsString()); } + + for (ClassEntry entry : classRanges.keySet()) { + String[] parts = entry.getContextualName().split("\\$"); + TypeDeclaration type = null; + for (TypeDeclaration decl : unit.getTypes()) { + if (decl.getNameAsString().equals(parts[0])) { + type = decl; + break; + } + } + + for (int i = 1; i < parts.length; i++) { + if (type != null) { + TypeDeclaration finalType = type; + String name = parts[i]; + type = type.findFirst(TypeDeclaration.class, t -> t != finalType && t.getNameAsString().equals(name)).orElse(null); + } + } + + if (type == null) { + throw new IllegalStateException("Could not find type " + entry.getContextualName() + " in parsed source"); + } + + Map lambdaRanges = new HashMap<>(); + int lambdaOrdinal = 0; + for (var member : type.getMembers()) { + if (member instanceof TypeDeclaration) { + continue; + } + + List lambdas = member.findAll(LambdaExpr.class); + for (LambdaExpr lambda : lambdas) { + Node node = lambda; + String context; + while (true) { + Optional nodeOptional = node.getParentNode(); + if (nodeOptional.isEmpty()) { + throw new IllegalStateException("Lambda has no valid parent"); + } + + node = nodeOptional.get(); + if (node instanceof MethodDeclaration method) { + context = method.getNameAsString(); + break; + } else if (node instanceof ConstructorDeclaration) { + context = "new"; + break; + } else if (node instanceof TypeDeclaration) { + context = "static"; + break; + } + } + + String name = "lambda$" + context + "$" + lambdaOrdinal++; + Range range = lambda.getRange().orElseThrow(() -> new IllegalStateException("No range for lambda")); + int start = positionToIndex(range.begin); + int end = positionToIndex(range.end); + TextRange textRange = new TextRange(start, end - start); + lambdaRanges.put(name, textRange); + } + } + + StructClass clazz = DecompilerContext.getStructContext().getClass(entry.getFullName()); + for (StructMethod method : clazz.getMethods()) { + TextRange range = lambdaRanges.get(method.getName()); + if (range == null) { + continue; + } + + SyntheticMethodSpan span = new SyntheticMethodSpan(range, true); + syntheticMethods.add(span); + syntheticEntryBySpan.put(span, getMethodEntry(entry.getFullName(), method.getName(), MethodDescriptor.parseDescriptor(method.getDescriptor()))); + } + } } private void addClassAndChildren(TypeDeclaration decl, String name) { @@ -163,8 +257,7 @@ private static boolean encloses(SyntheticMethodSpan outer, TextRange inner) { private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { if (method.isLambda) { - //TODO add lambda logic - throw new UnsupportedOperationException("Lambda handling is not implemented yet"); + throw new IllegalStateException("Method entries for lambdas should have already been fetched"); } else { return getMethodEntry(this.classStack.peek().getFullName(), "", MethodDescriptor.parseDescriptor("()V")); } @@ -226,7 +319,8 @@ public void visitMethod(TextRange range, boolean declaration, String className, this.methodStack.push(entry); } else { - this.addReference(token, entry, this.methodStack.peek()); + MethodEntry context = !this.methodStack.isEmpty() ? this.methodStack.peek() : getMethodEntry(className, "", MethodDescriptor.parseDescriptor("()V")); + this.addReference(token, entry, context); } } From d9ceddf783a28506a2a00e77446d078f1de59ffc Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Mon, 29 Sep 2025 16:31:51 -0500 Subject: [PATCH 05/16] Switch to RAW language level so its language level agnostic --- .../impl/source/vineflower/EnigmaTextTokenCollector.java | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 42bd5246a..7f6bb9476 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -105,7 +105,7 @@ public void addTokensToIndex(SourceIndex index, UnaryOperator tokenProces private void parseSource() { StaticJavaParser.getParserConfiguration() .setStoreTokens(true) - .setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_21); //todo see if we can get source information instead of hardcoding this + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); CompilationUnit unit = StaticJavaParser.parse(this.content); List initializers = unit.findAll(InitializerDeclaration.class, InitializerDeclaration::isStatic); for (InitializerDeclaration decl : initializers) { @@ -115,7 +115,7 @@ private void parseSource() { syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); } - for (FieldDeclaration decl : unit.findAll(FieldDeclaration.class)) { + for (FieldDeclaration decl : unit.findAll(FieldDeclaration.class, FieldDeclaration::isStatic)) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for field declaration")); int start = positionToIndex(range.begin); int end = positionToIndex(range.end); From 1ddfdc931d571cb1482475192f7258ba4823e30a Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Mon, 29 Sep 2025 16:46:13 -0500 Subject: [PATCH 06/16] Checkstyle --- .../vineflower/EnigmaTextTokenCollector.java | 63 ++++++++++--------- 1 file changed, 34 insertions(+), 29 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 7f6bb9476..1db0b57dd 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -6,7 +6,11 @@ import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Node; -import com.github.javaparser.ast.body.*; +import com.github.javaparser.ast.body.ConstructorDeclaration; +import com.github.javaparser.ast.body.FieldDeclaration; +import com.github.javaparser.ast.body.InitializerDeclaration; +import com.github.javaparser.ast.body.MethodDeclaration; +import com.github.javaparser.ast.body.TypeDeclaration; import com.github.javaparser.ast.expr.LambdaExpr; import org.jetbrains.java.decompiler.main.DecompilerContext; import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; @@ -110,24 +114,24 @@ private void parseSource() { List initializers = unit.findAll(InitializerDeclaration.class, InitializerDeclaration::isStatic); for (InitializerDeclaration decl : initializers) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for initializer")); - int start = positionToIndex(range.begin); - int end = positionToIndex(range.end); - syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); + int start = this.positionToIndex(range.begin); + int end = this.positionToIndex(range.end); + this.syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); } for (FieldDeclaration decl : unit.findAll(FieldDeclaration.class, FieldDeclaration::isStatic)) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for field declaration")); - int start = positionToIndex(range.begin); - int end = positionToIndex(range.end); - syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); + int start = this.positionToIndex(range.begin); + int end = this.positionToIndex(range.end); + this.syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); } String pkgPrefix = unit.getPackageDeclaration().map(decl -> decl.getNameAsString().replace('.', '/') + "/").orElse(""); for (TypeDeclaration decl : unit.getTypes()) { - addClassAndChildren(decl, pkgPrefix + decl.getNameAsString()); + this.addClassAndChildren(decl, pkgPrefix + decl.getNameAsString()); } - for (ClassEntry entry : classRanges.keySet()) { + for (ClassEntry entry : this.classRanges.keySet()) { String[] parts = entry.getContextualName().split("\\$"); TypeDeclaration type = null; for (TypeDeclaration decl : unit.getTypes()) { @@ -181,8 +185,8 @@ private void parseSource() { String name = "lambda$" + context + "$" + lambdaOrdinal++; Range range = lambda.getRange().orElseThrow(() -> new IllegalStateException("No range for lambda")); - int start = positionToIndex(range.begin); - int end = positionToIndex(range.end); + int start = this.positionToIndex(range.begin); + int end = this.positionToIndex(range.end); TextRange textRange = new TextRange(start, end - start); lambdaRanges.put(name, textRange); } @@ -196,19 +200,19 @@ private void parseSource() { } SyntheticMethodSpan span = new SyntheticMethodSpan(range, true); - syntheticMethods.add(span); - syntheticEntryBySpan.put(span, getMethodEntry(entry.getFullName(), method.getName(), MethodDescriptor.parseDescriptor(method.getDescriptor()))); + this.syntheticMethods.add(span); + this.syntheticEntryBySpan.put(span, getMethodEntry(entry.getFullName(), method.getName(), MethodDescriptor.parseDescriptor(method.getDescriptor()))); } } } private void addClassAndChildren(TypeDeclaration decl, String name) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for type declaration")); - TextRange textRange = new TextRange(positionToIndex(range.begin), positionToIndex(range.end)); - classRanges.put(getClassEntry(name), textRange); + TextRange textRange = new TextRange(this.positionToIndex(range.begin), this.positionToIndex(range.end)); + this.classRanges.put(getClassEntry(name), textRange); decl.getMembers().forEach(member -> { if (member instanceof TypeDeclaration child) { - addClassAndChildren(child, name + "$" + child.getNameAsString()); + this.addClassAndChildren(child, name + "$" + child.getNameAsString()); } }); } @@ -220,6 +224,7 @@ private int positionToIndex(Position position) { index = this.content.indexOf('\n', index) + 1; linesLeft--; } + return index + position.column; } @@ -246,8 +251,8 @@ private void updateMethodStack(TextRange range) { } private void pruneExitedClass(TextRange range) { - while (!classStack.isEmpty() && classRanges.get(classStack.peek()).getEnd() < range.start) { - classStack.pop(); + while (!this.classStack.isEmpty() && this.classRanges.get(this.classStack.peek()).getEnd() < range.start) { + this.classStack.pop(); } } @@ -271,15 +276,15 @@ public void start(String content) { this.openSynthetic.clear(); this.syntheticMethods.clear(); this.syntheticEntryBySpan.clear(); - parseSource(); + this.parseSource(); } @Override public void visitClass(TextRange range, boolean declaration, String name) { super.visitClass(range, declaration, name); Token token = this.getToken(range); - pruneExitedClass(range); - updateMethodStack(range); + this.pruneExitedClass(range); + this.updateMethodStack(range); if (declaration) { this.classStack.push(getClassEntry(name)); @@ -293,8 +298,8 @@ public void visitClass(TextRange range, boolean declaration, String name) { public void visitField(TextRange range, boolean declaration, String className, String name, FieldDescriptor descriptor) { super.visitField(range, declaration, className, name, descriptor); Token token = this.getToken(range); - pruneExitedClass(range); - updateMethodStack(range); + this.pruneExitedClass(range); + this.updateMethodStack(range); if (declaration) { this.addDeclaration(token, getFieldEntry(className, name, descriptor)); @@ -307,8 +312,8 @@ public void visitField(TextRange range, boolean declaration, String className, S public void visitMethod(TextRange range, boolean declaration, String className, String name, MethodDescriptor descriptor) { super.visitMethod(range, declaration, className, name, descriptor); Token token = this.getToken(range); - pruneExitedClass(range); - updateMethodStack(range); + this.pruneExitedClass(range); + this.updateMethodStack(range); MethodEntry entry = getMethodEntry(className, name, descriptor); if (declaration) { @@ -328,8 +333,8 @@ public void visitMethod(TextRange range, boolean declaration, String className, public void visitParameter(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitParameter(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); - pruneExitedClass(range); - updateMethodStack(range); + this.pruneExitedClass(range); + this.updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); if (declaration) { @@ -343,8 +348,8 @@ public void visitParameter(TextRange range, boolean declaration, String classNam public void visitLocal(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitLocal(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); - pruneExitedClass(range); - updateMethodStack(range); + this.pruneExitedClass(range); + this.updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); if (declaration) { From affc7c3663fa79ad51281e81f214b27b0146e69a Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Mon, 29 Sep 2025 17:27:04 -0500 Subject: [PATCH 07/16] Checkstyle --- .../source/vineflower/EnigmaTextTokenCollector.java | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 1db0b57dd..f7e41deef 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -108,8 +108,8 @@ public void addTokensToIndex(SourceIndex index, UnaryOperator tokenProces private void parseSource() { StaticJavaParser.getParserConfiguration() - .setStoreTokens(true) - .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); + .setStoreTokens(true) + .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); CompilationUnit unit = StaticJavaParser.parse(this.content); List initializers = unit.findAll(InitializerDeclaration.class, InitializerDeclaration::isStatic); for (InitializerDeclaration decl : initializers) { @@ -236,9 +236,9 @@ private void updateMethodStack(TextRange range) { } List enclosing = this.syntheticMethods.stream() - .filter(span -> encloses(span, range)) - .sorted(Comparator.comparingInt(span -> span.range.length)) - .toList(); + .filter(span -> encloses(span, range)) + .sorted(Comparator.comparingInt(span -> span.range.length)) + .toList(); for (SyntheticMethodSpan method : enclosing) { if (!this.openSynthetic.contains(method)) { @@ -360,7 +360,7 @@ public void visitLocal(TextRange range, boolean declaration, String className, S } private record SyntheticMethodSpan(TextRange range, boolean isLambda) { - public SyntheticMethodSpan(int start, int end, boolean isLambda) { + SyntheticMethodSpan(int start, int end, boolean isLambda) { this(new TextRange(start, end - start), isLambda); } } From 1ef1381a486400c3243c5d49eca6a1dd4912e77f Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Mon, 29 Sep 2025 17:51:35 -0500 Subject: [PATCH 08/16] Gracefully handle parsing errors --- .../vineflower/EnigmaTextTokenCollector.java | 31 +++++++++++++------ 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index f7e41deef..f3b7cd2e1 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -1,9 +1,10 @@ package org.quiltmc.enigma.impl.source.vineflower; +import com.github.javaparser.JavaParser; +import com.github.javaparser.ParseResult; import com.github.javaparser.ParserConfiguration; import com.github.javaparser.Position; import com.github.javaparser.Range; -import com.github.javaparser.StaticJavaParser; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.body.ConstructorDeclaration; @@ -27,6 +28,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.tinylog.Logger; import java.util.ArrayDeque; import java.util.ArrayList; @@ -107,10 +109,17 @@ public void addTokensToIndex(SourceIndex index, UnaryOperator tokenProces } private void parseSource() { - StaticJavaParser.getParserConfiguration() + ParserConfiguration config = new ParserConfiguration() .setStoreTokens(true) .setLanguageLevel(ParserConfiguration.LanguageLevel.RAW); - CompilationUnit unit = StaticJavaParser.parse(this.content); + + ParseResult parseResult = new JavaParser(config).parse(this.content); + if (!parseResult.isSuccessful()) { + Logger.warn("Failed to parse source: {}", parseResult.getProblems()); + return; + } + + CompilationUnit unit = parseResult.getResult().get(); List initializers = unit.findAll(InitializerDeclaration.class, InitializerDeclaration::isStatic); for (InitializerDeclaration decl : initializers) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for initializer")); @@ -250,7 +259,11 @@ private void updateMethodStack(TextRange range) { } } - private void pruneExitedClass(TextRange range) { + private void pruneExitedClasses(TextRange range) { + if (this.classRanges.isEmpty()) { + return; // Parsing failed + } + while (!this.classStack.isEmpty() && this.classRanges.get(this.classStack.peek()).getEnd() < range.start) { this.classStack.pop(); } @@ -283,7 +296,7 @@ public void start(String content) { public void visitClass(TextRange range, boolean declaration, String name) { super.visitClass(range, declaration, name); Token token = this.getToken(range); - this.pruneExitedClass(range); + this.pruneExitedClasses(range); this.updateMethodStack(range); if (declaration) { @@ -298,7 +311,7 @@ public void visitClass(TextRange range, boolean declaration, String name) { public void visitField(TextRange range, boolean declaration, String className, String name, FieldDescriptor descriptor) { super.visitField(range, declaration, className, name, descriptor); Token token = this.getToken(range); - this.pruneExitedClass(range); + this.pruneExitedClasses(range); this.updateMethodStack(range); if (declaration) { @@ -312,7 +325,7 @@ public void visitField(TextRange range, boolean declaration, String className, S public void visitMethod(TextRange range, boolean declaration, String className, String name, MethodDescriptor descriptor) { super.visitMethod(range, declaration, className, name, descriptor); Token token = this.getToken(range); - this.pruneExitedClass(range); + this.pruneExitedClasses(range); this.updateMethodStack(range); MethodEntry entry = getMethodEntry(className, name, descriptor); @@ -333,7 +346,7 @@ public void visitMethod(TextRange range, boolean declaration, String className, public void visitParameter(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitParameter(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); - this.pruneExitedClass(range); + this.pruneExitedClasses(range); this.updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); @@ -348,7 +361,7 @@ public void visitParameter(TextRange range, boolean declaration, String classNam public void visitLocal(TextRange range, boolean declaration, String className, String methodName, MethodDescriptor methodDescriptor, int idx, String name) { super.visitLocal(range, declaration, className, methodName, methodDescriptor, idx, name); Token token = this.getToken(range); - this.pruneExitedClass(range); + this.pruneExitedClasses(range); this.updateMethodStack(range); MethodEntry parent = getMethodEntry(className, methodName, methodDescriptor); From 2ed1da90e71f1ecaf9471f95a9d5176436eff561 Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Mon, 29 Sep 2025 20:55:25 -0500 Subject: [PATCH 09/16] Bug fixes --- .../vineflower/EnigmaTextTokenCollector.java | 25 ++++++++++--------- 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index f3b7cd2e1..ea4bea6b5 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -217,7 +217,9 @@ private void parseSource() { private void addClassAndChildren(TypeDeclaration decl, String name) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for type declaration")); - TextRange textRange = new TextRange(this.positionToIndex(range.begin), this.positionToIndex(range.end)); + int start = this.positionToIndex(range.begin); + int end = this.positionToIndex(range.end); + TextRange textRange = new TextRange(start, end - start); this.classRanges.put(getClassEntry(name), textRange); decl.getMembers().forEach(member -> { if (member instanceof TypeDeclaration child) { @@ -226,19 +228,17 @@ private void addClassAndChildren(TypeDeclaration decl, String name) { }); } - private int positionToIndex(Position position) { - int index = 0; - int linesLeft = position.line; - while (linesLeft > 0) { - index = this.content.indexOf('\n', index) + 1; - linesLeft--; + private int positionToIndex(Position p) { + int idx = 0, line = 1; + while (line < p.line && idx >= 0) { + idx = this.content.indexOf('\n', idx) + 1; + line++; } - - return index + position.column; + return idx + (p.column - 1); } private void updateMethodStack(TextRange range) { - while (!this.openSynthetic.isEmpty() && encloses(this.openSynthetic.peek(), range)) { + while (!this.openSynthetic.isEmpty() && !encloses(this.openSynthetic.peek(), range)) { SyntheticMethodSpan span = this.openSynthetic.pop(); this.syntheticEntryBySpan.remove(span); this.methodStack.pop(); @@ -246,7 +246,7 @@ private void updateMethodStack(TextRange range) { List enclosing = this.syntheticMethods.stream() .filter(span -> encloses(span, range)) - .sorted(Comparator.comparingInt(span -> span.range.length)) + .sorted(Comparator.comparingInt(span -> span.range.length).reversed()) .toList(); for (SyntheticMethodSpan method : enclosing) { @@ -254,6 +254,7 @@ private void updateMethodStack(TextRange range) { MethodEntry entry = this.syntheticEntryBySpan.computeIfAbsent(method, this::getSyntheticMethodEntry); if (this.methodStack.isEmpty() || !this.methodStack.peek().equals(entry)) { this.methodStack.push(entry); + this.openSynthetic.push(method); } } } @@ -270,7 +271,7 @@ private void pruneExitedClasses(TextRange range) { } private static boolean encloses(SyntheticMethodSpan outer, TextRange inner) { - return outer.range.start < inner.start && outer.range.getEnd() > inner.getEnd(); + return outer.range.start <= inner.start && outer.range.getEnd() >= inner.getEnd(); } private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { From 2f06680cb02322136f48fc00884c299d8f90c7fd Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Wed, 1 Oct 2025 13:56:05 -0500 Subject: [PATCH 10/16] Don't assume lambda method names will match JVM format --- .../vineflower/EnigmaTextTokenCollector.java | 192 ++++++++++++++---- 1 file changed, 150 insertions(+), 42 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index ea4bea6b5..51ff60449 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -12,11 +12,15 @@ import com.github.javaparser.ast.body.InitializerDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.expr.LambdaExpr; +import com.github.javaparser.ast.expr.*; +import org.jetbrains.java.decompiler.code.*; import org.jetbrains.java.decompiler.main.DecompilerContext; import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; import org.jetbrains.java.decompiler.struct.StructClass; import org.jetbrains.java.decompiler.struct.StructMethod; +import org.jetbrains.java.decompiler.struct.attr.StructBootstrapMethodsAttribute; +import org.jetbrains.java.decompiler.struct.attr.StructGeneralAttribute; +import org.jetbrains.java.decompiler.struct.consts.*; import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor; import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor; import org.jetbrains.java.decompiler.util.Pair; @@ -30,6 +34,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; import org.tinylog.Logger; +import java.io.IOException; import java.util.ArrayDeque; import java.util.ArrayList; import java.util.Comparator; @@ -38,7 +43,6 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; -import java.util.Optional; import java.util.function.UnaryOperator; public class EnigmaTextTokenCollector extends TextTokenVisitor { @@ -140,8 +144,8 @@ private void parseSource() { this.addClassAndChildren(decl, pkgPrefix + decl.getNameAsString()); } - for (ClassEntry entry : this.classRanges.keySet()) { - String[] parts = entry.getContextualName().split("\\$"); + for (ClassEntry classEntry : this.classRanges.keySet()) { + String[] parts = classEntry.getContextualName().split("\\$"); TypeDeclaration type = null; for (TypeDeclaration decl : unit.getTypes()) { if (decl.getNameAsString().equals(parts[0])) { @@ -159,62 +163,144 @@ private void parseSource() { } if (type == null) { - throw new IllegalStateException("Could not find type " + entry.getContextualName() + " in parsed source"); + throw new IllegalStateException("Could not find type " + classEntry.getContextualName() + " in parsed source"); } - Map lambdaRanges = new HashMap<>(); - int lambdaOrdinal = 0; + Map rootNodes = new HashMap<>(); for (var member : type.getMembers()) { if (member instanceof TypeDeclaration) { continue; } - - List lambdas = member.findAll(LambdaExpr.class); - for (LambdaExpr lambda : lambdas) { - Node node = lambda; - String context; - while (true) { - Optional nodeOptional = node.getParentNode(); - if (nodeOptional.isEmpty()) { - throw new IllegalStateException("Lambda has no valid parent"); - } - - node = nodeOptional.get(); - if (node instanceof MethodDeclaration method) { - context = method.getNameAsString(); - break; - } else if (node instanceof ConstructorDeclaration) { - context = "new"; - break; - } else if (node instanceof TypeDeclaration) { - context = "static"; - break; + if (member instanceof ConstructorDeclaration constructor) { + LambdaNode rootNode = rootNodes.computeIfAbsent("", c -> new LambdaNode()); + this.findLambdasInSource(constructor.getBody(), rootNode); + } else if (member instanceof MethodDeclaration method) { + if (method.getBody().isPresent()) { + LambdaNode rootNode = rootNodes.computeIfAbsent(method.getNameAsString(), c -> new LambdaNode()); + this.findLambdasInSource(method.getBody().get(), rootNode); + } + } else { + LambdaNode rootNode = rootNodes.computeIfAbsent("", c -> new LambdaNode()); + for (LambdaExpr lambda : member.findAll(LambdaExpr.class)) { + if (lambda.findAncestor(LambdaExpr.class).isEmpty()) { + LambdaNode lambdaNode = new LambdaNode(lambda, false); + rootNode.children.add(lambdaNode); + this.findLambdasInSource(lambda, rootNode); } } - - String name = "lambda$" + context + "$" + lambdaOrdinal++; - Range range = lambda.getRange().orElseThrow(() -> new IllegalStateException("No range for lambda")); - int start = this.positionToIndex(range.begin); - int end = this.positionToIndex(range.end); - TextRange textRange = new TextRange(start, end - start); - lambdaRanges.put(name, textRange); } } - - StructClass clazz = DecompilerContext.getStructContext().getClass(entry.getFullName()); + StructClass clazz = DecompilerContext.getStructContext().getClass(classEntry.getFullName()); for (StructMethod method : clazz.getMethods()) { - TextRange range = lambdaRanges.get(method.getName()); - if (range == null) { + LambdaNode rootNode = rootNodes.get(method.getName()); + if (rootNode == null) { continue; } + this.pairContext(clazz, method, rootNode); + } + } + } + + private void pairContext(StructClass owner, StructMethod method, LambdaNode rootNode) { + List bytecodeLambdas = extractLambdasFromBytecode(owner, method); + int count = Math.min(bytecodeLambdas.size(), rootNode.children.size()); + for (int i = 0; i < count; i++) { + LambdaNode childNode = rootNode.children.get(i); + if (childNode.isMethodReference) { + continue; + } + MethodEntry entry = bytecodeLambdas.get(i); + SyntheticMethodSpan span = new SyntheticMethodSpan(childNode.range, true); + this.syntheticMethods.add(span); + this.syntheticEntryBySpan.put(span, entry); + StructClass entryClass = DecompilerContext.getStructContext().getClass(entry.getParent().getFullName()); + StructMethod entryMethod = entryClass.getMethod(entry.getName(), entry.getDesc().toString()); + this.pairContext(entryClass, entryMethod, childNode); + } + } - SyntheticMethodSpan span = new SyntheticMethodSpan(range, true); - this.syntheticMethods.add(span); - this.syntheticEntryBySpan.put(span, getMethodEntry(entry.getFullName(), method.getName(), MethodDescriptor.parseDescriptor(method.getDescriptor()))); + private void findLambdasInSource(Node method, LambdaNode parentNode) { + for (var member : method.getChildNodes()) { + if (member instanceof LambdaExpr lambda) { + LambdaNode lambdaNode = new LambdaNode(lambda, false); + parentNode.children.add(lambdaNode); + this.findLambdasInSource(lambda, lambdaNode); + } else if (member instanceof MethodReferenceExpr methodRef) { + LambdaNode lambdaNode = new LambdaNode(methodRef, true); + parentNode.children.add(lambdaNode); + } else { + this.findLambdasInSource(member, parentNode); } } } + private static List extractLambdasFromBytecode(StructClass clazz, StructMethod method) { + List lambdas = new ArrayList<>(); + ConstantPool pool = clazz.getPool(); + + StructBootstrapMethodsAttribute bootstrapAttr = clazz.getAttribute(StructGeneralAttribute.ATTRIBUTE_BOOTSTRAP_METHODS); + + if (bootstrapAttr == null) { + return lambdas; + } + + if (!method.containsCode()) { + return lambdas; + } + + try { + method.expandData(clazz); + } catch (IOException e) { + return lambdas; + } + + InstructionSequence seq = method.getInstructionSequence(); + if (seq == null) { + return lambdas; + } + + for (int i = 0; i < seq.length(); i++) { + Instruction instr = seq.getInstr(i); + if (instr.opcode != CodeConstants.opc_invokedynamic) { + continue; + } + + int indyIndex = instr.operand(0); + PooledConstant constant = pool.getConstant(indyIndex); + if (!(constant instanceof LinkConstant link) || link.type != LinkConstant.CONSTANT_InvokeDynamic) { + continue; + } + + int bsmIndex = link.index1; + LinkConstant bootstrapMethod = bootstrapAttr.getMethodReference(bsmIndex); + String methodOwner = bootstrapMethod.classname; + String methodName = bootstrapMethod.elementname; + boolean isLambda = "java/lang/invoke/LambdaMetafactory".equals(methodOwner) && + ("metafactory".equals(methodName) || "altMetafactory".equals(methodName)); + if (!isLambda) { + continue; + } + List args = bootstrapAttr.getMethodArguments(bsmIndex); + + if (args.size() < 3) { + continue; + } + + PooledConstant implConstant = args.get(1); + if (!(implConstant instanceof LinkConstant implMethod)) { + continue; + } + + String owner = implMethod.classname; + String name = implMethod.elementname; + String descriptor = implMethod.descriptor; + + lambdas.add(getMethodEntry(owner, name, MethodDescriptor.parseDescriptor(descriptor))); + } + + return lambdas; + } + private void addClassAndChildren(TypeDeclaration decl, String name) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for type declaration")); int start = this.positionToIndex(range.begin); @@ -237,6 +323,12 @@ private int positionToIndex(Position p) { return idx + (p.column - 1); } + private TextRange convertRange(Range range) { + int start = this.positionToIndex(range.begin); + int end = this.positionToIndex(range.end); + return new TextRange(start, end - start); + } + private void updateMethodStack(TextRange range) { while (!this.openSynthetic.isEmpty() && !encloses(this.openSynthetic.peek(), range)) { SyntheticMethodSpan span = this.openSynthetic.pop(); @@ -378,4 +470,20 @@ private record SyntheticMethodSpan(TextRange range, boolean isLambda) { this(new TextRange(start, end - start), isLambda); } } + + class LambdaNode { + final TextRange range; + final boolean isMethodReference; + final List children = new ArrayList<>(); + LambdaNode(Expression lambda, boolean isMethodReference) { + this.range = EnigmaTextTokenCollector.this.convertRange(lambda.getRange().orElseThrow(() -> new IllegalStateException("No range for lambda"))); + this.isMethodReference = isMethodReference; + } + + LambdaNode() { + this.range = null; + this.isMethodReference = false; + } + + } } From d1c183fe8e90b22d76fb5294e67b82359cfdc60c Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Wed, 1 Oct 2025 14:17:12 -0500 Subject: [PATCH 11/16] Use LineIndexer --- .../vineflower/EnigmaTextTokenCollector.java | 55 +++++++++---------- .../org/quiltmc/enigma/util/LineIndexer.java | 34 ++++++++++++ 2 files changed, 60 insertions(+), 29 deletions(-) create mode 100644 enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 51ff60449..6cae880fd 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -3,7 +3,6 @@ import com.github.javaparser.JavaParser; import com.github.javaparser.ParseResult; import com.github.javaparser.ParserConfiguration; -import com.github.javaparser.Position; import com.github.javaparser.Range; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Node; @@ -12,15 +11,21 @@ import com.github.javaparser.ast.body.InitializerDeclaration; import com.github.javaparser.ast.body.MethodDeclaration; import com.github.javaparser.ast.body.TypeDeclaration; -import com.github.javaparser.ast.expr.*; -import org.jetbrains.java.decompiler.code.*; +import com.github.javaparser.ast.expr.Expression; +import com.github.javaparser.ast.expr.LambdaExpr; +import com.github.javaparser.ast.expr.MethodReferenceExpr; +import org.jetbrains.java.decompiler.code.CodeConstants; +import org.jetbrains.java.decompiler.code.Instruction; +import org.jetbrains.java.decompiler.code.InstructionSequence; import org.jetbrains.java.decompiler.main.DecompilerContext; import org.jetbrains.java.decompiler.main.extern.TextTokenVisitor; import org.jetbrains.java.decompiler.struct.StructClass; import org.jetbrains.java.decompiler.struct.StructMethod; import org.jetbrains.java.decompiler.struct.attr.StructBootstrapMethodsAttribute; import org.jetbrains.java.decompiler.struct.attr.StructGeneralAttribute; -import org.jetbrains.java.decompiler.struct.consts.*; +import org.jetbrains.java.decompiler.struct.consts.ConstantPool; +import org.jetbrains.java.decompiler.struct.consts.LinkConstant; +import org.jetbrains.java.decompiler.struct.consts.PooledConstant; import org.jetbrains.java.decompiler.struct.gen.FieldDescriptor; import org.jetbrains.java.decompiler.struct.gen.MethodDescriptor; import org.jetbrains.java.decompiler.util.Pair; @@ -32,6 +37,7 @@ import org.quiltmc.enigma.api.translation.representation.entry.FieldEntry; import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry; import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry; +import org.quiltmc.enigma.util.LineIndexer; import org.tinylog.Logger; import java.io.IOException; @@ -47,6 +53,7 @@ public class EnigmaTextTokenCollector extends TextTokenVisitor { private String content; + private LineIndexer lineIndexer; private final Deque classStack = new ArrayDeque<>(); private final Deque methodStack = new ArrayDeque<>(); @@ -127,16 +134,12 @@ private void parseSource() { List initializers = unit.findAll(InitializerDeclaration.class, InitializerDeclaration::isStatic); for (InitializerDeclaration decl : initializers) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for initializer")); - int start = this.positionToIndex(range.begin); - int end = this.positionToIndex(range.end); - this.syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); + this.syntheticMethods.add(new SyntheticMethodSpan(this.convertRange(range), false)); } for (FieldDeclaration decl : unit.findAll(FieldDeclaration.class, FieldDeclaration::isStatic)) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for field declaration")); - int start = this.positionToIndex(range.begin); - int end = this.positionToIndex(range.end); - this.syntheticMethods.add(new SyntheticMethodSpan(start, end, false)); + this.syntheticMethods.add(new SyntheticMethodSpan(this.convertRange(range), false)); } String pkgPrefix = unit.getPackageDeclaration().map(decl -> decl.getNameAsString().replace('.', '/') + "/").orElse(""); @@ -191,6 +194,9 @@ private void parseSource() { } } StructClass clazz = DecompilerContext.getStructContext().getClass(classEntry.getFullName()); + if (clazz == null) { + throw new IllegalStateException("Class bytecode not found"); + } for (StructMethod method : clazz.getMethods()) { LambdaNode rootNode = rootNodes.get(method.getName()); if (rootNode == null) { @@ -298,14 +304,14 @@ private static List extractLambdasFromBytecode(StructClass clazz, S lambdas.add(getMethodEntry(owner, name, MethodDescriptor.parseDescriptor(descriptor))); } + method.releaseResources(); + return lambdas; } private void addClassAndChildren(TypeDeclaration decl, String name) { Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for type declaration")); - int start = this.positionToIndex(range.begin); - int end = this.positionToIndex(range.end); - TextRange textRange = new TextRange(start, end - start); + TextRange textRange = this.convertRange(range); this.classRanges.put(getClassEntry(name), textRange); decl.getMembers().forEach(member -> { if (member instanceof TypeDeclaration child) { @@ -314,18 +320,9 @@ private void addClassAndChildren(TypeDeclaration decl, String name) { }); } - private int positionToIndex(Position p) { - int idx = 0, line = 1; - while (line < p.line && idx >= 0) { - idx = this.content.indexOf('\n', idx) + 1; - line++; - } - return idx + (p.column - 1); - } - private TextRange convertRange(Range range) { - int start = this.positionToIndex(range.begin); - int end = this.positionToIndex(range.end); + int start = this.lineIndexer.getIndex(range.begin); + int end = this.lineIndexer.getIndex(range.end); return new TextRange(start, end - start); } @@ -370,6 +367,9 @@ private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { if (method.isLambda) { throw new IllegalStateException("Method entries for lambdas should have already been fetched"); } else { + if (this.classStack.isEmpty()) { + throw new IllegalStateException("No class on the stack for synthetic method at " + method.range); + } return getMethodEntry(this.classStack.peek().getFullName(), "", MethodDescriptor.parseDescriptor("()V")); } } @@ -377,6 +377,7 @@ private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { @Override public void start(String content) { this.content = content; + this.lineIndexer = new LineIndexer(content); this.classRanges.clear(); this.methodStack.clear(); this.openSynthetic.clear(); @@ -465,11 +466,7 @@ public void visitLocal(TextRange range, boolean declaration, String className, S } } - private record SyntheticMethodSpan(TextRange range, boolean isLambda) { - SyntheticMethodSpan(int start, int end, boolean isLambda) { - this(new TextRange(start, end - start), isLambda); - } - } + private record SyntheticMethodSpan(TextRange range, boolean isLambda) {} class LambdaNode { final TextRange range; diff --git a/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java new file mode 100644 index 000000000..e121895b2 --- /dev/null +++ b/enigma/src/main/java/org/quiltmc/enigma/util/LineIndexer.java @@ -0,0 +1,34 @@ +package org.quiltmc.enigma.util; + +import com.github.javaparser.Position; + +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Matcher; +import java.util.regex.Pattern; + +public class LineIndexer { + private static final Pattern LINE_END = Pattern.compile("\\r\\n?|\\n"); + + private final List indexesByLine = new ArrayList<>(); + private final Matcher lineEndMatcher; + + public LineIndexer(String string) { + // the first line always starts at 0 + this.indexesByLine.add(0); + this.lineEndMatcher = LINE_END.matcher(string); + } + + public int getStartIndex(int line) { + while (line >= this.indexesByLine.size() && this.lineEndMatcher.find()) { + this.indexesByLine.add(this.lineEndMatcher.end()); + } + + return line < this.indexesByLine.size() ? this.indexesByLine.get(line) : -1; + } + + public int getIndex(Position position) { + final int lineIndex = this.getStartIndex(position.line - Position.FIRST_LINE); + return lineIndex < 0 ? lineIndex : lineIndex + position.column - Position.FIRST_COLUMN; + } +} From ab8a669142281791ef63803759a74ac37b0f8ab4 Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Wed, 1 Oct 2025 14:29:15 -0500 Subject: [PATCH 12/16] Skip tokens that don't have a range --- .../vineflower/EnigmaTextTokenCollector.java | 37 +++++++++++++++---- 1 file changed, 29 insertions(+), 8 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 6cae880fd..552716f8a 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -49,6 +49,7 @@ import java.util.LinkedHashMap; import java.util.List; import java.util.Map; +import java.util.Optional; import java.util.function.UnaryOperator; public class EnigmaTextTokenCollector extends TextTokenVisitor { @@ -133,13 +134,19 @@ private void parseSource() { CompilationUnit unit = parseResult.getResult().get(); List initializers = unit.findAll(InitializerDeclaration.class, InitializerDeclaration::isStatic); for (InitializerDeclaration decl : initializers) { - Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for initializer")); - this.syntheticMethods.add(new SyntheticMethodSpan(this.convertRange(range), false)); + TextRange range = this.getTextRangeForNode(decl); + if (range == null) { + continue; + } + this.syntheticMethods.add(new SyntheticMethodSpan(range, false)); } for (FieldDeclaration decl : unit.findAll(FieldDeclaration.class, FieldDeclaration::isStatic)) { - Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for field declaration")); - this.syntheticMethods.add(new SyntheticMethodSpan(this.convertRange(range), false)); + TextRange range = this.getTextRangeForNode(decl); + if (range == null) { + continue; + } + this.syntheticMethods.add(new SyntheticMethodSpan(range, false)); } String pkgPrefix = unit.getPackageDeclaration().map(decl -> decl.getNameAsString().replace('.', '/') + "/").orElse(""); @@ -215,7 +222,12 @@ private void pairContext(StructClass owner, StructMethod method, LambdaNode root if (childNode.isMethodReference) { continue; } + MethodEntry entry = bytecodeLambdas.get(i); + if (childNode.range == null) { + continue; + } + SyntheticMethodSpan span = new SyntheticMethodSpan(childNode.range, true); this.syntheticMethods.add(span); this.syntheticEntryBySpan.put(span, entry); @@ -310,8 +322,11 @@ private static List extractLambdasFromBytecode(StructClass clazz, S } private void addClassAndChildren(TypeDeclaration decl, String name) { - Range range = decl.getRange().orElseThrow(() -> new IllegalStateException("No range for type declaration")); - TextRange textRange = this.convertRange(range); + TextRange textRange = this.getTextRangeForNode(decl); + if (textRange == null) { + return; + } + this.classRanges.put(getClassEntry(name), textRange); decl.getMembers().forEach(member -> { if (member instanceof TypeDeclaration child) { @@ -320,7 +335,13 @@ private void addClassAndChildren(TypeDeclaration decl, String name) { }); } - private TextRange convertRange(Range range) { + private TextRange getTextRangeForNode(Node node) { + Optional rangeOpt = node.getRange(); + if (rangeOpt.isEmpty()) { + Logger.error("No range for node of type {}", node.getClass().getSimpleName()); + return null; + } + Range range = rangeOpt.get(); int start = this.lineIndexer.getIndex(range.begin); int end = this.lineIndexer.getIndex(range.end); return new TextRange(start, end - start); @@ -473,7 +494,7 @@ class LambdaNode { final boolean isMethodReference; final List children = new ArrayList<>(); LambdaNode(Expression lambda, boolean isMethodReference) { - this.range = EnigmaTextTokenCollector.this.convertRange(lambda.getRange().orElseThrow(() -> new IllegalStateException("No range for lambda"))); + this.range = EnigmaTextTokenCollector.this.getTextRangeForNode(lambda); this.isMethodReference = isMethodReference; } From c7238d3c1bf939e3a209365016f449721132e63e Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Wed, 1 Oct 2025 14:35:51 -0500 Subject: [PATCH 13/16] Checkstyle --- .../vineflower/EnigmaTextTokenCollector.java | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index 552716f8a..c31625f7c 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -138,6 +138,7 @@ private void parseSource() { if (range == null) { continue; } + this.syntheticMethods.add(new SyntheticMethodSpan(range, false)); } @@ -146,6 +147,7 @@ private void parseSource() { if (range == null) { continue; } + this.syntheticMethods.add(new SyntheticMethodSpan(range, false)); } @@ -181,6 +183,7 @@ private void parseSource() { if (member instanceof TypeDeclaration) { continue; } + if (member instanceof ConstructorDeclaration constructor) { LambdaNode rootNode = rootNodes.computeIfAbsent("", c -> new LambdaNode()); this.findLambdasInSource(constructor.getBody(), rootNode); @@ -200,15 +203,18 @@ private void parseSource() { } } } + StructClass clazz = DecompilerContext.getStructContext().getClass(classEntry.getFullName()); if (clazz == null) { throw new IllegalStateException("Class bytecode not found"); } + for (StructMethod method : clazz.getMethods()) { LambdaNode rootNode = rootNodes.get(method.getName()); if (rootNode == null) { continue; } + this.pairContext(clazz, method, rootNode); } } @@ -293,11 +299,12 @@ private static List extractLambdasFromBytecode(StructClass clazz, S LinkConstant bootstrapMethod = bootstrapAttr.getMethodReference(bsmIndex); String methodOwner = bootstrapMethod.classname; String methodName = bootstrapMethod.elementname; - boolean isLambda = "java/lang/invoke/LambdaMetafactory".equals(methodOwner) && - ("metafactory".equals(methodName) || "altMetafactory".equals(methodName)); + boolean isLambda = "java/lang/invoke/LambdaMetafactory".equals(methodOwner) + && ("metafactory".equals(methodName) || "altMetafactory".equals(methodName)); if (!isLambda) { continue; } + List args = bootstrapAttr.getMethodArguments(bsmIndex); if (args.size() < 3) { @@ -341,6 +348,7 @@ private TextRange getTextRangeForNode(Node node) { Logger.error("No range for node of type {}", node.getClass().getSimpleName()); return null; } + Range range = rangeOpt.get(); int start = this.lineIndexer.getIndex(range.begin); int end = this.lineIndexer.getIndex(range.end); @@ -391,6 +399,7 @@ private MethodEntry getSyntheticMethodEntry(SyntheticMethodSpan method) { if (this.classStack.isEmpty()) { throw new IllegalStateException("No class on the stack for synthetic method at " + method.range); } + return getMethodEntry(this.classStack.peek().getFullName(), "", MethodDescriptor.parseDescriptor("()V")); } } @@ -502,6 +511,5 @@ class LambdaNode { this.range = null; this.isMethodReference = false; } - } } From 7b3c652f81896e4215c4d52d2e1ca51e9bfe51ea Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Thu, 2 Oct 2025 15:11:36 -0500 Subject: [PATCH 14/16] Bug fix --- .../enigma/api/translation/representation/entry/ClassEntry.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java index 44da2495d..a7b36ed7b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassEntry.java @@ -64,7 +64,7 @@ public String getSourceRemapName() { @Override public String getContextualName() { if (this.isInnerClass()) { - return this.parent.getSimpleName() + "$" + this.name; + return this.parent.getContextualName() + "$" + this.name; } return this.getSimpleName(); From 0cd9e6cdccb8fbf011ce3f6a7e6afd2e4846189a Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Sat, 11 Oct 2025 14:08:25 -0500 Subject: [PATCH 15/16] Handle more edge cases --- .../impl/source/vineflower/EnigmaTextTokenCollector.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index c31625f7c..e5b74ccc3 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -238,6 +238,10 @@ private void pairContext(StructClass owner, StructMethod method, LambdaNode root this.syntheticMethods.add(span); this.syntheticEntryBySpan.put(span, entry); StructClass entryClass = DecompilerContext.getStructContext().getClass(entry.getParent().getFullName()); + if (entryClass == null) { + continue; + } + StructMethod entryMethod = entryClass.getMethod(entry.getName(), entry.getDesc().toString()); this.pairContext(entryClass, entryMethod, childNode); } @@ -383,7 +387,9 @@ private void pruneExitedClasses(TextRange range) { return; // Parsing failed } - while (!this.classStack.isEmpty() && this.classRanges.get(this.classStack.peek()).getEnd() < range.start) { + while (!this.classStack.isEmpty() + && (!this.classRanges.containsKey(this.classStack.peek()) + || this.classRanges.get(this.classStack.peek()).getEnd() < range.start)) { this.classStack.pop(); } } From 49ce4634c4743a31fd9102b7c72491442e1cf4bf Mon Sep 17 00:00:00 2001 From: PiTheGuy Date: Tue, 14 Oct 2025 17:06:02 -0500 Subject: [PATCH 16/16] Minor code cleanup --- .../impl/source/vineflower/EnigmaTextTokenCollector.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java index e5b74ccc3..4ea27fe92 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java +++ b/enigma/src/main/java/org/quiltmc/enigma/impl/source/vineflower/EnigmaTextTokenCollector.java @@ -347,13 +347,12 @@ private void addClassAndChildren(TypeDeclaration decl, String name) { } private TextRange getTextRangeForNode(Node node) { - Optional rangeOpt = node.getRange(); - if (rangeOpt.isEmpty()) { + if (!node.hasRange()) { Logger.error("No range for node of type {}", node.getClass().getSimpleName()); return null; } - Range range = rangeOpt.get(); + Range range = node.getRange().orElseThrow(); int start = this.lineIndexer.getIndex(range.begin); int end = this.lineIndexer.getIndex(range.end); return new TextRange(start, end - start);