Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion enigma-swing/src/main/java/org/quiltmc/enigma/gui/Gui.java
Original file line number Diff line number Diff line change
Expand Up @@ -367,7 +367,8 @@ public void closeEditor(EditorPanel editor) {
* @param reference the reference
*/
public void showReference(EntryReference<Entry<?>, Entry<?>> reference) {
this.editorTabbedPane.openClass(reference.getLocationClassEntry().getOutermostClass()).showReference(reference);
ClassEntry sourceRoot = this.controller.getProject().getSourceRoot(reference.getLocationClassEntry());
this.editorTabbedPane.openClass(sourceRoot).showReference(reference);
}

public void setObfClasses(Collection<ClassEntry> obfClasses) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -444,6 +444,10 @@ public void navigateTo(EntryReference<Entry<?>, Entry<?>> reference) {
this.openReference(reference);
}

public ClassEntry getSourceRoot(Entry<?> entry) {
return this.project.getSourceRoot(entry.getContainingClass());
}

public void refreshClasses() {
if (this.project == null) {
return;
Expand All @@ -461,7 +465,7 @@ public void addSeparatedClasses(List<ClassEntry> obfClasses, List<ClassEntry> de

Collection<ClassEntry> classes = this.project.getJarIndex().getIndex(EntryIndex.class).getClasses();
Stream<ClassEntry> visibleClasses = classes.stream()
.filter(entry -> !entry.isInnerClass());
.filter(entry -> !this.project.isNestedInSource(entry));

visibleClasses.forEach(entry -> {
TranslateResult<ClassEntry> result = mapper.extendedDeobfuscate(entry);
Expand Down Expand Up @@ -594,27 +598,27 @@ private void applyChange0(ValidationContext vc, EntryChange<?> change, boolean u

// local variable entries need to be propagated up the tree to update param names in javadoc
if (target instanceof LocalVariableEntry) {
this.chp.invalidateJavadoc(target.getTopLevelClass());
this.chp.invalidateJavadoc(this.getSourceRoot(target));

var children = this.project.getJarIndex().getIndex(InheritanceIndex.class).getChildren(target.getContainingClass());
for (ClassEntry child : children) {
this.chp.invalidateJavadoc(child.getTopLevelClass());
this.chp.invalidateJavadoc(this.getSourceRoot(child));
}
}
}

if (!Objects.equals(prev.javadoc(), mapping.javadoc())) {
this.chp.invalidateJavadoc(target.getTopLevelClass());
this.chp.invalidateJavadoc(this.getSourceRoot(target));
}

if (renamed && target instanceof ClassEntry classEntry && !classEntry.isInnerClass()) {
if (renamed && target instanceof ClassEntry classEntry && !this.project.isNestedInSource(classEntry)) {
boolean isOldOb = prev.targetName() == null;
boolean isNewOb = mapping.targetName() == null;
this.gui.moveClassTree(target.getContainingClass(), updateSwingState, isOldOb, isNewOb);
} else if (updateSwingState) {
// update stat icons for classes that could have had their mappings changed by this update
boolean propagate = target instanceof FieldEntry || target instanceof MethodEntry || target instanceof LocalVariableEntry;
this.gui.reloadStats(change.getTarget().getTopLevelClass(), propagate);
this.gui.reloadStats(this.getSourceRoot(change.getTarget()), propagate);
}
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -229,7 +229,7 @@ public void show(boolean clear, Type... types) {

switch (searchedType) {
case CLASS -> entryIndex.getClasses().parallelStream()
.filter(e -> !e.isInnerClass())
.filter(e -> !this.gui.getController().getProject().isNestedInSource(e))
.map(e -> SearchEntryImpl.from(e, this.gui.getController()))
.map(SearchUtil.Entry::from)
.sequential()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ public void done() {

@Override
public String toString() {
return this.deobfEntry.getSimpleName();
return this.deobfEntry.getContextualName();
Comment thread
supersaiyansubtlety marked this conversation as resolved.
}

@Override
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -319,7 +319,7 @@ public void mousePressed(MouseEvent e) {

{
final ClassHandle targetTopClassHandle = this.gui.getController().getClassHandleProvider()
.openClass(target.getTopLevelClass());
.openClass(this.gui.getController().getSourceRoot(target));

if (targetTopClassHandle != null) {
this.declarationSnippet = new DeclarationSnippetPanel(this.gui, target, targetTopClassHandle);
Expand Down
12 changes: 11 additions & 1 deletion enigma/src/main/java/org/quiltmc/enigma/api/EnigmaProject.java
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import org.quiltmc.enigma.api.analysis.EntryReference;
import org.quiltmc.enigma.api.analysis.index.jar.EnclosingMethodIndex;
import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex;
import org.quiltmc.enigma.api.analysis.index.jar.InnerClassIndex;
import org.quiltmc.enigma.api.analysis.index.jar.JarIndex;
import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex;
import org.quiltmc.enigma.api.service.ObfuscationTestService;
Expand All @@ -33,6 +34,7 @@
import org.quiltmc.enigma.api.translation.representation.entry.LocalVariableEntry;
import org.quiltmc.enigma.api.translation.representation.entry.MethodEntry;
import org.quiltmc.enigma.impl.translation.mapping.MappingsChecker;
import org.quiltmc.enigma.util.AsmUtil;
import org.quiltmc.enigma.util.I18n;
import org.tinylog.Logger;

Expand Down Expand Up @@ -306,6 +308,14 @@ public boolean isAnonymousOrLocal(ClassEntry classEntry) {
return enclosingMethodIndex.hasEnclosingMethod(classEntry);
}

public boolean isNestedInSource(ClassEntry classEntry) {
return this.jarIndex.getIndex(InnerClassIndex.class).isNestedInSource(classEntry);
}

public ClassEntry getSourceRoot(ClassEntry classEntry) {
return this.jarIndex.getIndex(InnerClassIndex.class).getSourceRoot(classEntry);
}

/**
* Verifies that the provided {@code parameter} has a valid index for its parent method.
* This method validates both the upper and lower bounds of the parent method's index range.
Expand Down Expand Up @@ -413,7 +423,7 @@ public SourceExport decompile(ProgressListener progress, DecompilerService decom

public Stream<ClassSource> decompileStream(ProgressListener progress, DecompilerService decompilerService, DecompileErrorStrategy errorStrategy) {
Collection<ClassNode> classes = this.compiled.values().stream()
.filter(classNode -> classNode.name.indexOf('$') == -1)
.filter(classNode -> !AsmUtil.isNestedInSource(classNode))
.toList();

progress.init(classes.size(), I18n.translate("progress.classes.decompiling"));
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
package org.quiltmc.enigma.api.analysis.index.jar;

import org.jspecify.annotations.NonNull;
import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry;
import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry;

import java.util.HashSet;
import java.util.Set;

/**
* An index of the classes a jar declares nested with the {@code InnerClasses} attribute.
*
* <p>Enigma takes nesting from class names, where {@code a/B$C} is an inner class of {@code a/B}.
* Decompilers take it from the {@code InnerClasses} attribute, and only put a class' source inside another
* class' source when that attribute says so. An obfuscator that strips the attribute without renaming makes
* the two disagree: the class still reads as nested, but it decompiles to a file of its own.
*
* <p>A class counts as {@linkplain #isNestedInSource nested in source} when any class in the jar declares
* an {@code InnerClasses} record naming it. The compiler writes that record in the nested class, in its
* enclosing class and in every class that references it, so one record anywhere in the jar is enough.
*/
public class InnerClassIndex implements JarIndexer {
private final Set<ClassEntry> nestedInSource = new HashSet<>();

@Override
public void indexInnerClass(ClassDefEntry classEntry, @NonNull InnerClassData innerClassData) {
this.nestedInSource.add(new ClassEntry(innerClassData.name()));
}

/**
* Returns whether the passed {@code entry}'s source is written inside another class' source, i.e. whether the jar
* declares it nested with an {@code InnerClasses} record. A class whose name looks nested but that has no
* such record yields {@code false}.
*
* @param entry the class to check
*/
public boolean isNestedInSource(ClassEntry entry) {
return this.nestedInSource.contains(entry);
}

/**
* Returns the class whose source contains the passed {@code entry}'s, which is {@code entry} itself unless it is
* {@linkplain #isNestedInSource nested in source}. This is the class to decompile, to open in an editor
* and to index tokens against when navigating to {@code entry}.
*
* @param entry the class to find the source of
*/
public ClassEntry getSourceRoot(ClassEntry entry) {
ClassEntry root = entry;
while (root.getOuterClass() != null && this.isNestedInSource(root)) {
root = root.getOuterClass();
}

return root;
}

@Override
public String getTranslationKey() {
return "progress.jar.indexing.process.inner_classes";
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package org.quiltmc.enigma.api.analysis.index.jar;

import org.jspecify.annotations.Nullable;
import org.quiltmc.enigma.api.analysis.ReferenceTargetType;
import org.quiltmc.enigma.api.translation.representation.Lambda;
import org.quiltmc.enigma.api.translation.representation.entry.ClassDefEntry;
Expand Down Expand Up @@ -34,6 +35,9 @@ default void indexLambda(MethodDefEntry callerEntry, Lambda lambda, ReferenceTar
default void indexEnclosingMethod(ClassDefEntry classEntry, EnclosingMethodData enclosingMethodData) {
}

default void indexInnerClass(ClassDefEntry classEntry, InnerClassData innerClassData) {
}

default void processIndex(JarIndex index) {
}

Expand All @@ -51,6 +55,10 @@ default Class<? extends JarIndexer> getType() {
return this.getClass();
}

// https://docs.oracle.com/javase/specs/jvms/se7/html/jvms-4.html#jvms-4.7.6
record InnerClassData(String name, @Nullable String outerName, @Nullable String innerName, int access) {
}

record EnclosingMethodData(String owner, String name, String descriptor) {
public MethodEntry getMethod() {
return MethodEntry.parse(this.owner, this.name, this.descriptor);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,10 +27,11 @@ public static MainJarIndex empty() {
BridgeMethodIndex bridgeMethodIndex = new IndependentBridgeMethodIndex(entryIndex, inheritanceIndex, referenceIndex);
PackageVisibilityIndex packageVisibilityIndex = new PackageVisibilityIndex();
EnclosingMethodIndex enclosingMethodIndex = new EnclosingMethodIndex();
InnerClassIndex innerClassIndex = new InnerClassIndex();
LambdaIndex lambdaIndex = new LambdaIndex();
return new MainJarIndex(
entryIndex, inheritanceIndex, referenceIndex, bridgeMethodIndex,
packageVisibilityIndex, enclosingMethodIndex, lambdaIndex
packageVisibilityIndex, enclosingMethodIndex, innerClassIndex, lambdaIndex
);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ public void invalidateJavadoc(ClassEntry entry) {
e.invalidateJavadoc();
}

if (entry.isInnerClass()) {
// only a class written inside another class' source affects that class' text
if (entry.isInnerClass() && this.project.isNestedInSource(entry)) {
this.invalidateJavadoc(entry.getOuterClass());
}
});
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,7 +128,7 @@ public ProjectStatsResult generate(ProgressListener progress, @Nullable ClassEnt
this.generationLatch = new CountDownLatch(1);

List<ClassEntry> classes = this.entryIndex.getClasses()
.stream().filter(entry -> !entry.isInnerClass()).toList();
.stream().filter(entry -> !this.project.isNestedInSource(entry)).toList();

int done = 0;
progress.init(classes.size() - 1, I18n.translate("progress.stats"));
Expand Down Expand Up @@ -162,7 +162,7 @@ public ProjectStatsResult generate(ProgressListener progress, @Nullable ClassEnt

private void addChildrenRecursively(List<Entry<?>> entries, Entry<?> toCheck) {
if (toCheck instanceof ClassEntry innerClassEntry) {
List<ParentedEntry<?>> classChildren = this.project.getJarIndex().getChildrenByClass().get(innerClassEntry);
List<ParentedEntry<?>> classChildren = this.ownChildren(innerClassEntry);
if (!classChildren.isEmpty()) {
entries.addAll(classChildren);
for (Entry<?> entry : classChildren) {
Expand All @@ -174,6 +174,16 @@ private void addChildrenRecursively(List<Entry<?>> entries, Entry<?> toCheck) {
}
}

/**
* Returns the members of {@code classEntry} and the classes written inside its source. A class that is
* not nested in source has its own stats, so including it here would count its members twice.
*/
private List<ParentedEntry<?>> ownChildren(ClassEntry classEntry) {
return this.project.getJarIndex().getChildrenByClass().get(classEntry).stream()
.filter(child -> !(child instanceof ClassEntry childClass) || this.project.isNestedInSource(childClass))
.toList();
}

/**
* Generates stats for the provided class.
* @param classEntry the class to generate stats for
Expand All @@ -192,7 +202,7 @@ private StatsResult generate(ClassEntry classEntry, GenerationParameters paramet
Map<StatType, Integer> mappableCounts = new EnumMap<>(StatType.class);
Map<StatType, Map<String, Integer>> unmappedCounts = new EnumMap<>(StatType.class);

List<ParentedEntry<?>> children = this.project.getJarIndex().getChildrenByClass().get(classEntry);
List<ParentedEntry<?>> children = this.ownChildren(classEntry);
List<Entry<?>> entries = new ArrayList<>(children);

for (Entry<?> entry : children) {
Expand Down Expand Up @@ -311,7 +321,8 @@ private void update(StatType type, Map<StatType, Integer> mappable, Map<StatType
if (this.project.isRenamable(entry)) {
if (this.project.isObfuscated(entry)
|| (!parameters.countFallback() && this.fallbackNameProposerIdCache.contains(this.project.getRemapper().getMapping(entry).sourcePluginId()))) { // fallback proposed mappings don't count
String parent = this.project.getRemapper().deobfuscate(entry.getTopLevelClass()).getName().replace('/', '.');
ClassEntry statsClass = this.project.getSourceRoot(entry.getContainingClass());
String parent = this.project.getRemapper().deobfuscate(statsClass).getFullName().replace('/', '.');

unmapped.computeIfAbsent(type, t -> new HashMap<>());
unmapped.get(type).put(parent, unmapped.get(type).getOrDefault(parent, 0) + 1);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -228,6 +228,11 @@ public void indexLambda(MethodDefEntry callerEntry, Lambda lambda, ReferenceTarg
this.indexers.forEach((key, indexer) -> indexer.indexLambda(callerEntry, lambda, targetType));
}

@Override
public void indexInnerClass(ClassDefEntry classEntry, InnerClassData innerClassData) {
this.indexers.forEach((key, indexer) -> indexer.indexInnerClass(classEntry, innerClassData));
}

@Override
public void indexEnclosingMethod(ClassDefEntry classEntry, EnclosingMethodData enclosingMethodData) {
this.indexers.forEach((key, indexer) -> indexer.indexEnclosingMethod(classEntry, enclosingMethodData));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,13 @@ public void visitOuterClass(String owner, String name, String descriptor) {
super.visitOuterClass(owner, name, descriptor);
}

@Override
public void visitInnerClass(String name, String outerName, String innerName, int access) {
this.indexer.indexInnerClass(this.classEntry, new JarIndexer.InnerClassData(name, outerName, innerName, access));

super.visitInnerClass(name, outerName, innerName, access);
}

@Override
public FieldVisitor visitField(int access, String name, String desc, String signature, Object value) {
this.indexer.indexField(FieldDefEntry.parse(this.classEntry, access, name, desc, signature));
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,11 @@ public IContextSource getExternalSource() {
return this.external;
}

/** The internal name of the class this source was created for. */
public String getClassName() {
return this.name;
}

@Override
public String getName() {
return "class " + this.name;
Expand All @@ -40,16 +45,35 @@ private void collectClassNames() {
}

this.classNames = new ArrayList<>();
String root = this.name.contains("$") ? this.name.substring(0, this.name.indexOf("$")) : this.name;
String root = this.getSourceRoot(this.name);
this.classNames.add(root);

Map<String, Object> options = VineflowerPreferences.getEffectiveOptions();
if (!options.containsKey(IFernflowerPreferences.DECOMPILE_INNER)
|| "1".equals(options.get(IFernflowerPreferences.DECOMPILE_INNER))) {
this.classNames.addAll(this.classProvider.getClasses(root).stream().filter(s -> s.contains("$")).toList());
this.classNames.addAll(this.classProvider.getClasses(root).stream()
.filter(s -> !s.equals(root) && this.isNestedInSource(s))
.toList());
}
}

private String getSourceRoot(String className) {
String root = className;
int separator = root.lastIndexOf('$');

while (separator > 0 && this.isNestedInSource(root)) {
root = root.substring(0, separator);
separator = root.lastIndexOf('$');
}

return root;
}

private boolean isNestedInSource(String className) {
ClassNode node = this.classProvider.get(className);
return node != null && AsmUtil.isNestedInSource(node);
}

@Override
public Entries getEntries() {
this.collectClassNames();
Expand Down
Loading
Loading