Skip to content
Open
Show file tree
Hide file tree
Changes from 1 commit
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 {@code entry}'s source is written inside another class' source, i.e. whether the jar
Comment thread
RDIL marked this conversation as resolved.
Outdated
* declares it nested with an {@code InnerClasses} record. A class whose name looks nested but that has no
* such record decompiles to a file of its own, and this returns {@code false} for it.
Comment thread
RDIL marked this conversation as resolved.
Outdated
*
* @param entry the class to check
*/
public boolean isNestedInSource(ClassEntry entry) {
return this.nestedInSource.contains(entry);
}

/**
* Returns the class whose source contains {@code entry}'s, which is {@code entry} itself unless it is
Comment thread
RDIL marked this conversation as resolved.
Outdated
* {@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,14 +45,30 @@ private void collectClassNames() {
}

this.classNames = new ArrayList<>();
String root = this.name.contains("$") ? this.name.substring(0, this.name.indexOf("$")) : this.name;
String root = this.sourceRoot(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 sourceRoot(String className) {
Comment thread
RDIL marked this conversation as resolved.
Outdated
String root = className;
while (root.lastIndexOf('$') > 0 && this.isNestedInSource(root)) {
Comment thread
RDIL marked this conversation as resolved.
Outdated
root = root.substring(0, root.lastIndexOf('$'));
}

return root;
}

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

@Override
Expand Down
Loading
Loading