From dda4efe9d12570b35e82023db09013aad706ef80 Mon Sep 17 00:00:00 2001 From: ix0rai Date: Mon, 16 Dec 2024 18:57:27 -0600 Subject: [PATCH] fallback name proposal and name proposal save bypass (#240) * add methods and start work on tests * actually use that method oops * implement bypassValidation() * implement fallback for stats * checkstyle tests * store a full `EntryMapping` in `TranslateResult` * add highlighting for fallback mappings * fix crash with trailing commas in profile json * add test proposer for fallback - fix highlighting - only propose names for the first parameter of each method in test:parameters * orange! * good looking colors for fallback * checkstyle * checkstyle * rethrow exceptions in tests * fix dynamic proposal not accounting for unchecked * fix dynamic proposal putting obf mappings in the wrong tree * checkstyle --- .../enigma/gui/config/theme/ThemeUtil.java | 28 ++- .../properties/DarculaThemeProperties.java | 3 + .../composite/SyntaxPaneProperties.java | 28 +++ .../gui/element/ClassSelectorPopupMenu.java | 3 +- .../enigma/gui/element/EditorPopupMenu.java | 3 +- .../quiltmc/enigma/gui/panel/EditorPanel.java | 57 +++--- .../java/org/quiltmc/enigma/api/Enigma.java | 20 +- .../api/service/NameProposalService.java | 16 ++ .../api/source/DecompiledClassSource.java | 9 +- .../quiltmc/enigma/api/source/TokenStore.java | 32 +++- .../enigma/api/stats/StatsGenerator.java | 31 ++- .../api/translation/TranslateResult.java | 96 +++++++--- .../translation/mapping/EntryRemapper.java | 11 +- .../translation/representation/Lambda.java | 3 +- .../representation/entry/ClassDefEntry.java | 2 +- .../representation/entry/ClassEntry.java | 2 +- .../representation/entry/FieldDefEntry.java | 2 +- .../representation/entry/FieldEntry.java | 2 +- .../entry/LocalVariableDefEntry.java | 2 +- .../entry/LocalVariableEntry.java | 2 +- .../representation/entry/MethodDefEntry.java | 2 +- .../representation/entry/MethodEntry.java | 2 +- .../TestFallbackNameProposal.java | 172 +++++++++++++++++ .../TestNameProposalBypassValidation.java | 179 ++++++++++++++++++ .../records/TestRecordComponentProposal.java | 24 +-- .../enigma/test/plugin/TestEnigmaPlugin.java | 42 +++- .../src/testFixtures/resources/profile.json | 3 + 27 files changed, 668 insertions(+), 108 deletions(-) create mode 100644 enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java create mode 100644 enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposalBypassValidation.java diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/ThemeUtil.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/ThemeUtil.java index 0e3d57946..349fd7ae3 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/ThemeUtil.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/ThemeUtil.java @@ -4,14 +4,12 @@ import org.quiltmc.enigma.gui.config.Config; import org.quiltmc.enigma.gui.highlight.BoxHighlightPainter; import org.quiltmc.enigma.gui.util.ScaleUtil; -import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.syntaxpain.JavaSyntaxKit; import java.awt.Color; import java.awt.Dimension; import java.awt.Font; import java.awt.image.BufferedImage; -import java.util.Map; import javax.swing.JEditorPane; import javax.swing.JPanel; import javax.swing.UIManager; @@ -79,14 +77,24 @@ private static void setFonts() { UIManager.put("Button.font", bold); } - public static Map getBoxHighlightPainters() { - return Map.of( - TokenType.OBFUSCATED, BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().obfuscated.value(), Config.getCurrentSyntaxPaneColors().obfuscatedOutline.value()), - TokenType.JAR_PROPOSED, BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().proposed.value(), Config.getCurrentSyntaxPaneColors().proposedOutline.value()), - TokenType.DYNAMIC_PROPOSED, BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().proposed.value(), Config.getCurrentSyntaxPaneColors().proposedOutline.value()), - TokenType.DEOBFUSCATED, BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().deobfuscated.value(), Config.getCurrentSyntaxPaneColors().deobfuscatedOutline.value()), - TokenType.DEBUG, BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().debugToken.value(), Config.getCurrentSyntaxPaneColors().debugTokenOutline.value()) - ); + public static BoxHighlightPainter createObfuscatedPainter() { + return BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().obfuscated.value(), Config.getCurrentSyntaxPaneColors().obfuscatedOutline.value()); + } + + public static BoxHighlightPainter createProposedPainter() { + return BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().proposed.value(), Config.getCurrentSyntaxPaneColors().proposedOutline.value()); + } + + public static BoxHighlightPainter createDeobfuscatedPainter() { + return BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().deobfuscated.value(), Config.getCurrentSyntaxPaneColors().deobfuscatedOutline.value()); + } + + public static BoxHighlightPainter createDebugPainter() { + return BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().debugToken.value(), Config.getCurrentSyntaxPaneColors().debugTokenOutline.value()); + } + + public static BoxHighlightPainter createFallbackPainter() { + return BoxHighlightPainter.create(Config.getCurrentSyntaxPaneColors().fallback.value(), Config.getCurrentSyntaxPaneColors().fallbackOutline.value()); } public static void resetIfAbsent(TrackedValue value) { diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/DarculaThemeProperties.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/DarculaThemeProperties.java index 57d9c9d53..3898e0b20 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/DarculaThemeProperties.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/DarculaThemeProperties.java @@ -54,6 +54,9 @@ public Colors.Builder buildSyntaxPaneColors(Colors.Builder colors) { .deobfuscated(new SerializableColor(0x4D50FA7B)) .deobfuscatedOutline(new SerializableColor(0x8050FA7B)) + .fallback(new SerializableColor(0x4Daa5500)) + .fallbackOutline(new SerializableColor(0x80d86f06)) + .editorBackground(new SerializableColor(0xFF282A36)) .highlight(new SerializableColor(0xFFFF79C6)) .caret(new SerializableColor(0xFFF8F8F2)) diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/composite/SyntaxPaneProperties.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/composite/SyntaxPaneProperties.java index c288e6e92..1371fc70d 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/composite/SyntaxPaneProperties.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/config/theme/properties/composite/SyntaxPaneProperties.java @@ -49,6 +49,9 @@ public static class Colors implements Consumer { public final TrackedValue deobfuscated; public final TrackedValue deobfuscatedOutline; + public final TrackedValue fallback; + public final TrackedValue fallbackOutline; + public final TrackedValue editorBackground; public final TrackedValue highlight; public final TrackedValue caret; @@ -78,6 +81,9 @@ private Colors( ThemeProperties.SerializableColor deobfuscated, ThemeProperties.SerializableColor deobfuscatedOutline, + ThemeProperties.SerializableColor fallback, + ThemeProperties.SerializableColor fallbackOutline, + ThemeProperties.SerializableColor editorBackground, ThemeProperties.SerializableColor highlight, ThemeProperties.SerializableColor caret, @@ -106,6 +112,9 @@ private Colors( this.deobfuscated = TrackedValue.create(deobfuscated, "deobfuscated"); this.deobfuscatedOutline = TrackedValue.create(deobfuscatedOutline, "deobfuscated_outline"); + this.fallback = TrackedValue.create(fallback, "fallback"); + this.fallbackOutline = TrackedValue.create(fallbackOutline, "fallbackOutline"); + this.editorBackground = TrackedValue.create(editorBackground, "editor_background"); this.highlight = TrackedValue.create(highlight, "highlight"); this.caret = TrackedValue.create(caret, "caret"); @@ -142,6 +151,9 @@ public Stream> stream() { this.deobfuscated, this.deobfuscatedOutline, + this.fallback, + this.fallbackOutline, + this.editorBackground, this.highlight, this.caret, @@ -177,6 +189,9 @@ public static class Builder { private ThemeProperties.SerializableColor deobfuscated = new ThemeProperties.SerializableColor(0xFFDCFFDC); private ThemeProperties.SerializableColor deobfuscatedOutline = new ThemeProperties.SerializableColor(0xFF50A050); + private ThemeProperties.SerializableColor fallback = new ThemeProperties.SerializableColor(0xFFffddbb); + private ThemeProperties.SerializableColor fallbackOutline = new ThemeProperties.SerializableColor(0xFFd86f06); + private ThemeProperties.SerializableColor editorBackground = new ThemeProperties.SerializableColor(0xFFFFFFFF); private ThemeProperties.SerializableColor highlight = new ThemeProperties.SerializableColor(0xFF3333EE); private ThemeProperties.SerializableColor caret = new ThemeProperties.SerializableColor(0xFF000000); @@ -207,6 +222,9 @@ public Colors build() { this.deobfuscated, this.deobfuscatedOutline, + this.fallback, + this.fallbackOutline, + this.editorBackground, this.highlight, this.caret, @@ -270,6 +288,16 @@ public Builder deobfuscatedOutline(ThemeProperties.SerializableColor deobfuscate return this; } + public Builder fallback(ThemeProperties.SerializableColor fallback) { + this.fallback = fallback; + return this; + } + + public Builder fallbackOutline(ThemeProperties.SerializableColor fallbackOutline) { + this.fallbackOutline = fallbackOutline; + return this; + } + public Builder editorBackground(ThemeProperties.SerializableColor editorBackground) { this.editorBackground = editorBackground; return this; diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassSelectorPopupMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassSelectorPopupMenu.java index a128b3920..5c38091d8 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassSelectorPopupMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/ClassSelectorPopupMenu.java @@ -1,5 +1,6 @@ package org.quiltmc.enigma.gui.element; +import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.gui.ClassSelector; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.docker.ClassesDocker; @@ -126,7 +127,7 @@ public void show(ClassSelector selector, int x, int y) { // update toggle mapping text to match this.toggleMapping.setEnabled(selected != null); if (selected != null) { - if (this.gui.getController().getProject().getRemapper().extendedDeobfuscate(selected).isDeobfuscated()) { + if (this.gui.getController().getProject().getRemapper().extendedDeobfuscate(selected).getType() == TokenType.DEOBFUSCATED) { this.toggleMapping.setText(I18n.translate("popup_menu.reset_obfuscated")); } else { this.toggleMapping.setText(I18n.translate("popup_menu.mark_deobfuscated")); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorPopupMenu.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorPopupMenu.java index 51582eda1..e60c84ce7 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorPopupMenu.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/element/EditorPopupMenu.java @@ -1,6 +1,7 @@ package org.quiltmc.enigma.gui.element; import org.quiltmc.enigma.api.analysis.EntryReference; +import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.gui.EditableType; import org.quiltmc.enigma.gui.Gui; import org.quiltmc.enigma.gui.GuiController; @@ -171,7 +172,7 @@ public void updateUiState() { this.openNextItem.setEnabled(controller.hasNextReference()); this.toggleMappingItem.setEnabled(isRenamable && (type != null && this.gui.isEditable(type))); - if (referenceEntry != null && this.gui.getController().getProject().getRemapper().extendedDeobfuscate(referenceEntry).isDeobfuscated()) { + if (referenceEntry != null && this.gui.getController().getProject().getRemapper().extendedDeobfuscate(referenceEntry).getType() == TokenType.DEOBFUSCATED) { this.toggleMappingItem.setText(I18n.translate("popup_menu.reset_obfuscated")); } else { this.toggleMappingItem.setText(I18n.translate("popup_menu.mark_deobfuscated")); diff --git a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java index e4bb28c08..0176fd100 100644 --- a/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java +++ b/enigma-swing/src/main/java/org/quiltmc/enigma/gui/panel/EditorPanel.java @@ -5,6 +5,7 @@ import org.quiltmc.enigma.api.class_handle.ClassHandle; import org.quiltmc.enigma.api.class_handle.ClassHandleError; import org.quiltmc.enigma.api.event.ClassHandleListener; +import org.quiltmc.enigma.api.source.TokenStore; import org.quiltmc.enigma.gui.BrowserCaret; import org.quiltmc.enigma.gui.EditableType; import org.quiltmc.enigma.gui.Gui; @@ -96,7 +97,11 @@ public class EditorPanel { private boolean shouldNavigateOnClick; private int fontSize = 12; - private final Map boxHighlightPainters; + private final BoxHighlightPainter obfuscatedPainter; + private final BoxHighlightPainter proposedPainter; + private final BoxHighlightPainter deobfuscatedPainter; + private final BoxHighlightPainter debugPainter; + public final BoxHighlightPainter fallbackPainter; private final List listeners = new ArrayList<>(); @@ -132,7 +137,11 @@ public EditorPanel(Gui gui, NavigatorPanel navigator) { this.errorTextArea.setEditable(false); this.errorTextArea.setFont(ScaleUtil.getFont(Font.MONOSPACED, Font.PLAIN, 10)); - this.boxHighlightPainters = ThemeUtil.getBoxHighlightPainters(); + this.obfuscatedPainter = ThemeUtil.createObfuscatedPainter(); + this.proposedPainter = ThemeUtil.createProposedPainter(); + this.debugPainter = ThemeUtil.createDebugPainter(); + this.fallbackPainter = ThemeUtil.createFallbackPainter(); + this.deobfuscatedPainter = ThemeUtil.createDeobfuscatedPainter(); this.editor.addMouseListener(new MouseAdapter() { @Override @@ -464,7 +473,7 @@ public void setSource(DecompiledClassSource source) { this.editor.getHighlighter().removeAllHighlights(); this.editor.setText(source.toString()); - this.setHighlightedTokens(source.getHighlightedTokens()); + this.setHighlightedTokens(source.getTokenStore(), source.getHighlightedTokens()); if (this.source != null) { this.editor.setCaretPosition(newCaretPos); @@ -484,32 +493,30 @@ public void setSource(DecompiledClassSource source) { } } - public void setHighlightedTokens(Map> tokens) { + public void setHighlightedTokens(TokenStore tokenStore, Map> tokens) { // remove any old highlighters this.editor.getHighlighter().removeAllHighlights(); - if (this.boxHighlightPainters != null) { - BoxHighlightPainter proposedPainter = this.boxHighlightPainters.get(TokenType.JAR_PROPOSED); - - for (TokenType type : tokens.keySet()) { - BoxHighlightPainter painter = this.boxHighlightPainters.get(type); - - if (painter != null) { - for (Token token : tokens.get(type)) { - EntryReference, Entry> reference = this.getReference(token); - BoxHighlightPainter tokenPainter; - - if (reference != null) { - EditableType t = EditableType.fromEntry(reference.entry); - boolean editable = t == null || this.gui.isEditable(t); - tokenPainter = editable ? painter : proposedPainter; - } else { - tokenPainter = painter; - } - - this.addHighlightedToken(token, tokenPainter); - } + for (TokenType type : tokens.keySet()) { + BoxHighlightPainter typePainter = switch (type) { + case OBFUSCATED -> this.obfuscatedPainter; + case DEOBFUSCATED -> this.deobfuscatedPainter; + case DEBUG -> this.debugPainter; + case JAR_PROPOSED, DYNAMIC_PROPOSED -> this.proposedPainter; + }; + + for (Token token : tokens.get(type)) { + BoxHighlightPainter tokenPainter = typePainter; + EntryReference, Entry> reference = this.getReference(token); + + if (reference != null) { + EditableType t = EditableType.fromEntry(reference.entry); + boolean editable = t == null || this.gui.isEditable(t); + boolean fallback = tokenStore.isFallback(token); + tokenPainter = editable ? (fallback ? this.fallbackPainter : typePainter) : this.proposedPainter; } + + this.addHighlightedToken(token, tokenPainter); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java index 32ea23ba7..217d9556b 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/Enigma.java @@ -107,7 +107,7 @@ public EnigmaProject openJar(Path path, ClassProvider libraryClassProvider, Prog if (proposed != null) { for (var entry : proposed.entrySet()) { - if (entry.getValue() != null && entry.getValue().tokenType() != TokenType.JAR_PROPOSED) { + if (!service.bypassValidation() && entry.getValue() != null && entry.getValue().tokenType() != TokenType.JAR_PROPOSED) { throw new RuntimeException("Token type of mapping " + entry.getValue() + " for entry " + entry.getKey() + " was " + entry.getValue().tokenType() + ", but should be " + TokenType.JAR_PROPOSED + "!"); } @@ -197,6 +197,22 @@ public Optional getReadWriteService(Path path) { return this.parseFileType(path).flatMap(this::getReadWriteService); } + /** + * Searches for and returns a service with a matching id to the provided {@code id}. + * @param type the type of the searched service + * @param id the id of the service + * @return the optional service + */ + public Optional getService(EnigmaServiceType type, String id) { + for (T service : this.services.get(type)) { + if (service.getId().equals(id)) { + return Optional.of(service); + } + } + + return Optional.empty(); + } + public static void validatePluginId(String id) { if (id != null && !id.matches("([a-z0-9_]+):([a-z0-9_]+((/[a-z0-9_]+)+)?)")) { throw new IllegalArgumentException("Invalid plugin id: \"" + id + "\"\n" + "Refer to Javadoc on EnigmaService#getId for how to properly form a service ID."); @@ -310,7 +326,7 @@ public void registerService(EnigmaServiceType servi for (EnigmaProfile.Service serviceProfile : serviceProfiles) { T service = factory.create(this.getServiceContext(serviceProfile)); - if (serviceProfile.matches(service.getId())) { + if (serviceProfile != null && serviceProfile.matches(service.getId())) { this.putService(serviceType, service); break; } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java b/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java index a00ae418a..1d4b1c852 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/service/NameProposalService.java @@ -42,6 +42,22 @@ public interface NameProposalService extends EnigmaService { @Nullable Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping); + default boolean isFallback() { + return false; + } + + /** + * Disables validation of proposed mappings from this service. + * This allows you to return any kind of mapping you want from {@link #getDynamicProposedNames(EntryRemapper, Entry, EntryMapping, EntryMapping)} + * and {@link #getProposedNames(JarIndex)}, but should be used sparingly as it will allow creating mappings that can't be linked back to this proposer. + * Do not use this unless you're sure there's no other way to accomplish what you're looking to do! + * + * @return whether validation should be bypassed + */ + default boolean bypassValidation() { + return false; + } + /** * Creates a proposed mapping, with no javadoc and using {@link #getId()} as the source plugin ID. * @param name the name diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java index 6d3050167..87879f215 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/DecompiledClassSource.java @@ -4,6 +4,7 @@ import org.quiltmc.enigma.api.analysis.EntryReference; import org.quiltmc.enigma.api.translation.TranslateResult; import org.quiltmc.enigma.api.translation.Translator; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import org.quiltmc.enigma.api.translation.representation.TypeDescriptor; import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; import org.quiltmc.enigma.api.translation.representation.entry.Entry; @@ -57,14 +58,14 @@ private String remapToken(TokenStore target, EnigmaProject project, Token token, TranslateResult> translatedEntry = translator.extendedTranslate(entry); if (project.isRenamable(reference)) { - if (!translatedEntry.isObfuscated()) { - target.add(translatedEntry.getType(), movedToken); + if (translatedEntry != null && !translatedEntry.isObfuscated()) { + target.add(project, translatedEntry.getMapping(), movedToken); return translatedEntry.getValue().getSourceRemapName(); } else { - target.add(TokenType.OBFUSCATED, movedToken); + target.add(project, EntryMapping.OBFUSCATED, movedToken); } } else if (DEBUG_TOKEN_HIGHLIGHTS) { - target.add(TokenType.DEBUG, movedToken); + target.add(project, new EntryMapping(null, null, TokenType.DEBUG, null), movedToken); } return this.generateDefaultName(translatedEntry.getValue()); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/source/TokenStore.java b/enigma/src/main/java/org/quiltmc/enigma/api/source/TokenStore.java index 1950f11e3..e737d469c 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/source/TokenStore.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/source/TokenStore.java @@ -1,5 +1,9 @@ package org.quiltmc.enigma.api.source; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; + import java.util.Collections; import java.util.Comparator; import java.util.EnumMap; @@ -9,15 +13,17 @@ import java.util.TreeSet; public final class TokenStore { - private static final TokenStore EMPTY = new TokenStore(Collections.emptyNavigableSet(), Collections.emptyMap(), null); + private static final TokenStore EMPTY = new TokenStore(Collections.emptyNavigableSet(), Collections.emptyMap(), Collections.emptyNavigableSet(), null); private final NavigableSet tokens; private final Map> byType; + private final NavigableSet fallbackTokens; private final String obfSource; - private TokenStore(NavigableSet tokens, Map> byType, String obfSource) { + private TokenStore(NavigableSet tokens, Map> byType, NavigableSet fallbackTokens, String obfSource) { this.tokens = tokens; this.byType = byType; + this.fallbackTokens = fallbackTokens; this.obfSource = obfSource; } @@ -27,22 +33,36 @@ public static TokenStore create(SourceIndex obfuscatedIndex) { map.put(value, new TreeSet<>(Comparator.comparing(t -> t.start))); } - return new TokenStore(new TreeSet<>(Comparator.comparing(t -> t.start)), Collections.unmodifiableMap(map), obfuscatedIndex.getSource()); + return new TokenStore(new TreeSet<>(Comparator.comparing(t -> t.start)), Collections.unmodifiableMap(map), new TreeSet<>(Comparator.comparing(t -> t.start)), obfuscatedIndex.getSource()); } public static TokenStore empty() { return TokenStore.EMPTY; } - public void add(TokenType type, Token token) { + public void add(EnigmaProject project, EntryMapping mapping, Token token) { this.tokens.add(token); - this.byType.get(type).add(token); + this.byType.get(mapping.tokenType()).add(token); + + if (mapping.sourcePluginId() != null) { + var sourceServiceOptional = project.getEnigma().getService(NameProposalService.TYPE, mapping.sourcePluginId()); + sourceServiceOptional.ifPresent(service -> { + if (service.isFallback()) { + this.fallbackTokens.add(token); + } + }); + } + } + + public boolean isFallback(Token token) { + return this.fallbackTokens.contains(token); } public boolean isCompatible(TokenStore other) { return this.obfSource != null && other.obfSource != null && this.obfSource.equals(other.obfSource) - && this.tokens.size() == other.tokens.size(); + && this.tokens.size() == other.tokens.size() + && this.fallbackTokens.size() == other.fallbackTokens.size(); } public int mapPosition(TokenStore to, int position) { diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java b/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java index 1dbe8eebc..43756081d 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/stats/StatsGenerator.java @@ -4,6 +4,8 @@ import org.quiltmc.enigma.api.EnigmaProject; import org.quiltmc.enigma.api.ProgressListener; import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.service.EnigmaService; +import org.quiltmc.enigma.api.service.NameProposalService; import org.quiltmc.enigma.api.translation.mapping.EntryResolver; import org.quiltmc.enigma.api.translation.mapping.ResolutionStrategy; import org.quiltmc.enigma.api.translation.representation.ArgumentDescriptor; @@ -39,6 +41,8 @@ public class StatsGenerator { private ProgressListener overallListener; private CountDownLatch generationLatch = null; + private List fallbackNameProposerIdCache; + public StatsGenerator(EnigmaProject project) { this.project = project; this.entryIndex = project.getJarIndex().getIndex(EntryIndex.class); @@ -100,6 +104,8 @@ public ProjectStatsResult generate(ProgressListener progress, Set incl this.overallListener = progress; } + this.rebuildCache(); + includedTypes = EnumSet.copyOf(includedTypes); Map stats = this.result == null ? new HashMap<>() : this.result.getStats(); @@ -115,7 +121,7 @@ public ProjectStatsResult generate(ProgressListener progress, Set incl for (ClassEntry entry : classes) { progress.step(done++, I18n.translateFormatted("progress.stats.for", entry.getName())); - StatsResult result = this.generate(includedTypes, entry, includeSynthetic); + StatsResult result = this.generate(includedTypes, entry, includeSynthetic, false); stats.put(entry, result); } @@ -131,7 +137,7 @@ public ProjectStatsResult generate(ProgressListener progress, Set incl } } else { Preconditions.checkNotNull(classEntry, "Entry cannot be null after initial stat generation!"); - stats.put(classEntry, this.generate(includedTypes, classEntry, includeSynthetic)); + stats.put(classEntry, this.generate(includedTypes, classEntry, includeSynthetic, false)); this.result = new ProjectStatsResult(this.project, stats); } @@ -161,6 +167,14 @@ private void addChildrenRecursively(List> entries, Entry toCheck) { * @return the generated {@link StatsResult} */ public StatsResult generate(Set includedTypes, ClassEntry classEntry, boolean includeSynthetic) { + return this.generate(includedTypes, classEntry, includeSynthetic, true); + } + + private StatsResult generate(Set includedTypes, ClassEntry classEntry, boolean includeSynthetic, boolean rebuildCache) { + if (rebuildCache) { + this.rebuildCache(); + } + Map mappableCounts = new EnumMap<>(StatType.class); Map> unmappedCounts = new EnumMap<>(StatType.class); @@ -223,6 +237,10 @@ public StatsResult generate(Set includedTypes, ClassEntry classEntry, return StatsResult.create(mappableCounts, unmappedCounts, false); } + private void rebuildCache() { + this.fallbackNameProposerIdCache = this.project.getEnigma().getNameProposalServices().stream().filter(NameProposalService::isFallback).map(EnigmaService::getId).toList(); + } + private boolean isCanonicalConstructor(ClassDefEntry record, MethodEntry methodEntry) { if (!record.isRecord() || !methodEntry.isConstructor()) { return false; @@ -274,12 +292,9 @@ public StatsResult getStats(ClassEntry entry) { } private void update(StatType type, Map mappable, Map> unmapped, Entry entry) { - boolean obfuscated = this.project.isObfuscated(entry); - boolean renamable = this.project.isRenamable(entry); - boolean synthetic = this.project.isSynthetic(entry); - - if (renamable) { - if (obfuscated && !synthetic) { + if (this.project.isRenamable(entry)) { + if (this.project.isObfuscated(entry) && !this.project.isSynthetic(entry) + || 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('/', '.'); unmapped.computeIfAbsent(type, t -> new HashMap<>()); diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/TranslateResult.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/TranslateResult.java index e7298b9f8..fdb59dc7f 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/TranslateResult.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/TranslateResult.java @@ -1,57 +1,100 @@ package org.quiltmc.enigma.api.translation; import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; import java.util.Objects; import java.util.function.Function; -public final class TranslateResult { - private final TokenType type; +/** + * Represents the result of a translation operation on an arbitrary object. + * @param the type of the translated object + */ +public final class TranslateResult { + private final EntryMapping mapping; private final T value; - private TranslateResult(TokenType type, T value) { - this.type = type; + private TranslateResult(EntryMapping mapping, T value) { + this.mapping = mapping; this.value = value; } - public static TranslateResult of(TokenType type, T value) { - Objects.requireNonNull(type, "type must not be null"); - return new TranslateResult<>(type, value); + /** + * Creates a translation result for the given value. + * @param mapping the value's mapping + * @param value the translated value + * @return a result containing that value + */ + public static TranslateResult of(EntryMapping mapping, T value) { + Objects.requireNonNull(mapping, "mapping must not be null"); + return new TranslateResult<>(mapping, value); } - // Used for translatables that don't have a concept of - // obfuscated/deobfuscated (e.g. method descriptors) for example because - // they don't have an identifier attached to them - public static TranslateResult ungrouped(T value) { + /** + * Creates a translation result for values that cannot have an attached mapping, such as method descriptors. + * @param value the translated value + * @return a result containing that value + */ + public static TranslateResult ungrouped(T value) { return TranslateResult.obfuscated(value); } - public static TranslateResult obfuscated(T value) { - return TranslateResult.of(TokenType.OBFUSCATED, value); + /** + * Creates an obfuscated translation result. + * @param value the obfuscated value + * @return a result containing that value + */ + public static TranslateResult obfuscated(T value) { + return TranslateResult.of(EntryMapping.OBFUSCATED, value); } - public static TranslateResult deobfuscated(T value) { - return TranslateResult.of(TokenType.DEOBFUSCATED, value); + /** + * {@return the token type of the result mapping} + */ + public TokenType getType() { + return this.mapping.tokenType(); } - public TokenType getType() { - return this.type; + /** + * {@return the result mapping} + */ + public EntryMapping getMapping() { + return this.mapping; } + /** + * {@return the translated value} + */ public T getValue() { return this.value; } - public TranslateResult map(Function op) { - return TranslateResult.of(this.type, op.apply(this.value)); - } - + /** + * {@return whether this result is obfuscated} + */ public boolean isObfuscated() { - return this.type == TokenType.OBFUSCATED; + return this.getType() == TokenType.OBFUSCATED; } + /** + * {@return whether this result is manually mapped} + * + * @deprecated for removal in 3.0.0. This method's name is misleading, as it does not check that the mapping is not obfuscated, it checks whether the token type is equal to + * {@link TokenType#DEOBFUSCATED}, which equates to a manual mapping. For the old behaviour, you can manually compare {@link #getType()} with {@link TokenType#DEOBFUSCATED}. + * In order to check that the mapping is not obfuscated, use {@code !}{@link #isObfuscated()}. + */ + @Deprecated(forRemoval = true, since = "2.6.0") public boolean isDeobfuscated() { - return this.type == TokenType.DEOBFUSCATED; + return this.getType() == TokenType.DEOBFUSCATED; + } + + /** + * Creates a new result, applying {@code op} to the {@link #value} without changing the {@link #mapping}. + * @param op the operation to apply to the current value + * @return the new result + */ + public TranslateResult map(Function op) { + return TranslateResult.of(this.mapping, op.apply(this.value)); } @Override @@ -59,17 +102,16 @@ public boolean equals(Object o) { if (this == o) return true; if (o == null || this.getClass() != o.getClass()) return false; TranslateResult that = (TranslateResult) o; - return this.type == that.type - && Objects.equals(this.value, that.value); + return Objects.equals(this.mapping, that.mapping) && Objects.equals(this.value, that.value); } @Override public int hashCode() { - return Objects.hash(this.type, this.value); + return Objects.hash(this.mapping, this.value); } @Override public String toString() { - return String.format("TranslateResult { type: %s, value: %s }", this.type, this.value); + return String.format("TranslateResult { mapping: %s, value: %s }", this.mapping, this.value); } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java index 41172c434..8efbdbcbd 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/mapping/EntryRemapper.java @@ -4,6 +4,7 @@ import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; import org.quiltmc.enigma.api.analysis.index.mapping.MappingsIndex; import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.translation.MappingTranslator; import org.quiltmc.enigma.api.translation.Translatable; import org.quiltmc.enigma.api.translation.TranslateResult; @@ -142,7 +143,15 @@ public void insertDynamicallyProposedMappings(@Nullable Entry obfEntry, @Null for (var service : this.proposalServices) { var proposedNames = service.getDynamicProposedNames(this, obfEntry, oldMapping, newMapping); if (proposedNames != null) { - proposedNames.forEach(this.proposedMappings::insert); + // due to unchecked proposal, proposers are allowed to insert other token types + // when deobfuscated, they must be put in the main tree + proposedNames.forEach((entry, mapping) -> { + if (mapping.tokenType() == TokenType.DEOBFUSCATED) { + this.mappings.insert(entry, mapping); + } else { + this.proposedMappings.insert(entry, mapping); + } + }); } } } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java index 0858e17e0..145ed3531 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/Lambda.java @@ -1,6 +1,5 @@ package org.quiltmc.enigma.api.translation.representation; -import org.quiltmc.enigma.api.source.TokenType; import org.quiltmc.enigma.api.translation.Translatable; import org.quiltmc.enigma.api.translation.TranslateResult; import org.quiltmc.enigma.api.translation.Translator; @@ -21,7 +20,7 @@ public TranslateResult extendedTranslate(Translator translator, EntryRes EntryMapping samMethodMapping = this.resolveMapping(resolver, mappings, samMethod); return TranslateResult.of( - samMethodMapping.targetName() == null ? TokenType.OBFUSCATED : TokenType.DEOBFUSCATED, + samMethodMapping, new Lambda( samMethodMapping.targetName() != null ? samMethodMapping.targetName() : this.invokedName, this.invokedType.extendedTranslate(translator, resolver, mappings).getValue(), diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassDefEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassDefEntry.java index c3f201708..68c03e3e0 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassDefEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/ClassDefEntry.java @@ -77,7 +77,7 @@ public TranslateResult extendedTranslate(Translator translator, @ ClassEntry[] translatedInterfaces = Arrays.stream(this.interfaces).map(translator::translate).toArray(ClassEntry[]::new); String docs = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new ClassDefEntry(this.parent, translatedName, translatedSignature, this.access, translatedSuper, translatedInterfaces, docs) ); } 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 5fd9251aa..44da2495d 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 @@ -80,7 +80,7 @@ public TranslateResult extendedTranslate(Translator transl String translatedName = mapping.targetName() != null ? mapping.targetName() : this.name; String docs = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new ClassEntry(this.parent, translatedName, docs) ); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldDefEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldDefEntry.java index 25b3434e8..772bddcde 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldDefEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldDefEntry.java @@ -46,7 +46,7 @@ protected TranslateResult extendedTranslate(Translator translator, @ String translatedName = mapping.targetName() != null ? mapping.targetName() : this.name; String docs = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new FieldDefEntry(this.parent, translatedName, translatedDesc, translatedSignature, this.access, docs) ); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldEntry.java index 4dd3020c5..b721f6045 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/FieldEntry.java @@ -53,7 +53,7 @@ protected TranslateResult extendedTranslate(Translator translator, @ String translatedName = mapping.targetName() != null ? mapping.targetName() : this.name; String docs = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new FieldEntry(this.parent, translatedName, translator.translate(this.desc), docs) ); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableDefEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableDefEntry.java index b475fcfa3..afd2aa4ca 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableDefEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableDefEntry.java @@ -28,7 +28,7 @@ protected TranslateResult extendedTranslate(Translator trans String translatedName = mapping.targetName() != null ? mapping.targetName() : this.name; String javadoc = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new LocalVariableDefEntry(this.parent, this.index, translatedName, this.parameter, translatedDesc, javadoc) ); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableEntry.java index 3f62f350a..668757ade 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/LocalVariableEntry.java @@ -44,7 +44,7 @@ protected TranslateResult extendedTranslate(Translator trans String translatedName = mapping.targetName() != null ? mapping.targetName() : this.name; String javadoc = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new LocalVariableEntry(this.parent, this.index, translatedName, this.parameter, javadoc) ); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodDefEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodDefEntry.java index b3df6a0f3..77fb193f9 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodDefEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodDefEntry.java @@ -46,7 +46,7 @@ protected TranslateResult extendedTranslate(Translator translato String translatedName = mapping.targetName() != null ? mapping.targetName() : this.name; String docs = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new MethodDefEntry(this.parent, translatedName, translatedDesc, translatedSignature, this.access, docs) ); } diff --git a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java index d960c535a..2da41b338 100644 --- a/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java +++ b/enigma/src/main/java/org/quiltmc/enigma/api/translation/representation/entry/MethodEntry.java @@ -97,7 +97,7 @@ protected TranslateResult extendedTranslate(Translator tr String translatedName = mapping.targetName() != null ? mapping.targetName() : this.name; String docs = mapping.javadoc(); return TranslateResult.of( - mapping.tokenType(), + mapping, new MethodEntry(this.parent, translatedName, translator.translate(this.descriptor), docs) ); } diff --git a/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java new file mode 100644 index 000000000..ba42ba08d --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestFallbackNameProposal.java @@ -0,0 +1,172 @@ +package org.quiltmc.enigma.name_proposal; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.TestEntryFactory; +import org.quiltmc.enigma.TestUtil; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaPlugin; +import org.quiltmc.enigma.api.EnigmaPluginContext; +import org.quiltmc.enigma.api.EnigmaProfile; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; +import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.stats.StatType; +import org.quiltmc.enigma.api.stats.StatsGenerator; +import org.quiltmc.enigma.api.translation.TranslateResult; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; +import org.quiltmc.enigma.api.translation.representation.entry.ClassEntry; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.impl.plugin.BuiltinPlugin; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; +import static org.hamcrest.Matchers.is; +import static org.junit.jupiter.api.Assertions.assertEquals; + +public class TestFallbackNameProposal { + private static final Path JAR = TestUtil.obfJar("validation"); + private static EnigmaProject project; + + @BeforeAll + public static void setupEnigma() { + Reader r = new StringReader(""" + { + "services": { + "name_proposal": [ + { + "id": "test:name_all_fields_slay" + }, + { + "id": "test:name_all_methods_gaming" + } + ] + } + }"""); + + try { + EnigmaProfile profile = EnigmaProfile.parse(r); + Enigma enigma = Enigma.builder().setProfile(profile).setPlugins(List.of(new BuiltinPlugin(), new TestPlugin())).build(); + project = enigma.openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); + } catch (Exception e) { + throw new RuntimeException("Failed to open jar!", e); + } + } + + @Test + public void testFallbackStats() throws IOException { + ClassEntry bClass = TestEntryFactory.newClass("b"); + + // assert a couple mappings to make sure the test plugin works + assertMappingStartsWith(TestEntryFactory.newMethod(bClass, "c", "()V"), TestEntryFactory.newMethod(bClass, "gaming", "()V")); + assertMappingStartsWith(TestEntryFactory.newMethod(bClass, "a", "(I)V"), TestEntryFactory.newMethod(bClass, "gaming", "(I)V")); + + assertMappingStartsWith(TestEntryFactory.newField(bClass, "a", "I"), TestEntryFactory.newField(bClass, "slay", "I")); + assertMappingStartsWith(TestEntryFactory.newField(bClass, "a", "Ljava/lang/String;"), TestEntryFactory.newField(bClass, "slay", "Ljava/lang/String;")); + + var proposerFieldStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.FIELDS), bClass, false); + var proposerMethodStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.METHODS), bClass, false); + + project = Enigma.create().openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); + + var controlFieldStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.FIELDS), bClass, false); + var controlMethodStats = new StatsGenerator(project).generate(ProgressListener.createEmpty(), Set.of(StatType.METHODS), bClass, false); + + // method stats should be identical since fallback proposals don't affect stats + assertEquals(controlMethodStats.getMappable(), proposerMethodStats.getMappable()); + assertEquals(controlMethodStats.getMapped(), proposerMethodStats.getMapped()); + assertEquals(controlMethodStats.getUnmapped(), proposerMethodStats.getUnmapped()); + + // field stats should be fully mapped when proposed -- normal behaviour + assertEquals(controlFieldStats.getMappable(), proposerFieldStats.getMappable()); + assertEquals(0, proposerFieldStats.getUnmapped()); + assertEquals(controlFieldStats.getMappable(), proposerFieldStats.getMapped()); + } + + private static void assertMappingStartsWith(Entry obf, Entry deobf) { + TranslateResult> result = project.getRemapper().getDeobfuscator().extendedTranslate(obf); + assertThat(result, is(notNullValue())); + + String deobfName = result.getValue().getName(); + if (deobfName != null) { + assertThat(deobfName, startsWith(deobf.getName())); + } + } + + private static class TestPlugin implements EnigmaPlugin { + @Override + public void init(EnigmaPluginContext ctx) { + ctx.registerService(NameProposalService.TYPE, ctx1 -> new TestPlugin.TestFieldProposerNoFallback()); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new TestPlugin.TestMethodProposerWithFallback()); + } + + private static class TestFieldProposerNoFallback implements NameProposalService { + @Override + public Map, EntryMapping> getProposedNames(JarIndex index) { + Map, EntryMapping> mappings = new HashMap<>(); + AtomicInteger i = new AtomicInteger(); + + index.getIndex(EntryIndex.class).getFields().forEach( + field -> mappings.put(field, this.createMapping("slay" + i.getAndIncrement(), TokenType.JAR_PROPOSED)) + ); + + return mappings; + } + + @Override + public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { + return null; + } + + @Override + public String getId() { + return "test:name_all_fields_slay"; + } + } + + private static class TestMethodProposerWithFallback implements NameProposalService { + @Override + public Map, EntryMapping> getProposedNames(JarIndex index) { + Map, EntryMapping> mappings = new HashMap<>(); + AtomicInteger i = new AtomicInteger(); + + index.getIndex(EntryIndex.class).getMethods().forEach( + method -> mappings.put(method, this.createMapping("gaming" + i.getAndIncrement(), TokenType.JAR_PROPOSED)) + ); + + return mappings; + } + + @Override + public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { + return null; + } + + @Override + public boolean isFallback() { + return true; + } + + @Override + public String getId() { + return "test:name_all_methods_gaming"; + } + } + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposalBypassValidation.java b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposalBypassValidation.java new file mode 100644 index 000000000..56331c250 --- /dev/null +++ b/enigma/src/test/java/org/quiltmc/enigma/name_proposal/TestNameProposalBypassValidation.java @@ -0,0 +1,179 @@ +package org.quiltmc.enigma.name_proposal; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.quiltmc.enigma.TestEntryFactory; +import org.quiltmc.enigma.TestUtil; +import org.quiltmc.enigma.api.Enigma; +import org.quiltmc.enigma.api.EnigmaPlugin; +import org.quiltmc.enigma.api.EnigmaPluginContext; +import org.quiltmc.enigma.api.EnigmaProfile; +import org.quiltmc.enigma.api.EnigmaProject; +import org.quiltmc.enigma.api.ProgressListener; +import org.quiltmc.enigma.api.analysis.index.jar.EntryIndex; +import org.quiltmc.enigma.api.analysis.index.jar.JarIndex; +import org.quiltmc.enigma.api.class_provider.ClasspathClassProvider; +import org.quiltmc.enigma.api.service.NameProposalService; +import org.quiltmc.enigma.api.service.ReadWriteService; +import org.quiltmc.enigma.api.source.TokenType; +import org.quiltmc.enigma.api.translation.TranslateResult; +import org.quiltmc.enigma.api.translation.mapping.EntryMapping; +import org.quiltmc.enigma.api.translation.mapping.EntryRemapper; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingFileNameFormat; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingParseException; +import org.quiltmc.enigma.api.translation.mapping.serde.MappingSaveParameters; +import org.quiltmc.enigma.api.translation.representation.entry.Entry; +import org.quiltmc.enigma.impl.plugin.BuiltinPlugin; + +import javax.annotation.Nullable; +import java.io.IOException; +import java.io.Reader; +import java.io.StringReader; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.concurrent.atomic.AtomicInteger; + +import static org.hamcrest.MatcherAssert.assertThat; +import static org.hamcrest.Matchers.*; + +public class TestNameProposalBypassValidation { + private static final Path JAR = TestUtil.obfJar("validation"); + private static EnigmaProject project; + + @BeforeAll + public static void setupEnigma() { + Reader r = new StringReader(""" + { + "services": { + "name_proposal": [ + { + "id": "test:name_all_fields_slay" + }, + { + "id": "test:name_all_methods_gaming" + } + ] + } + }"""); + + try { + EnigmaProfile profile = EnigmaProfile.parse(r); + Enigma enigma = Enigma.builder().setProfile(profile).setPlugins(List.of(new BuiltinPlugin(), new TestPlugin())).build(); + project = enigma.openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); + } catch (Exception e) { + throw new RuntimeException("Failed to open jar!", e); + } + } + + @Test + public void test() throws IOException, MappingParseException { + // assert a couple mappings to make sure the test plugin works + assertMappingStartsWith(TestEntryFactory.newMethod("b", "c", "()V"), TestEntryFactory.newMethod("b", "gaming", "()V")); + assertMappingStartsWith(TestEntryFactory.newMethod("b", "a", "(I)V"), TestEntryFactory.newMethod("b", "gaming", "(I)V")); + + assertMappingStartsWith(TestEntryFactory.newField("b", "a", "I"), TestEntryFactory.newField("b", "slay", "I")); + assertMappingStartsWith(TestEntryFactory.newField("b", "a", "Ljava/lang/String;"), TestEntryFactory.newField("b", "slay", "Ljava/lang/String;")); + + // save mappings to temp file + Path tempFile = Files.createTempFile("temp", ".mapping"); + var service = getService(); + service.write(project.getRemapper().getMappings(), tempFile, new MappingSaveParameters(MappingFileNameFormat.BY_DEOBF, false, null, null)); + + // replace project with one that does not have the test plugin + Enigma enigma = Enigma.create(); + project = enigma.openJar(JAR, new ClasspathClassProvider(), ProgressListener.createEmpty()); + project.setMappings(service.read(tempFile), ProgressListener.createEmpty()); + + // check which mappings saved + assertUnmapped(TestEntryFactory.newField("b", "a", "I")); + assertUnmapped(TestEntryFactory.newField("b", "a", "Ljava/lang/String;")); + + assertMappingStartsWith(TestEntryFactory.newMethod("b", "c", "()V"), TestEntryFactory.newMethod("b", "gaming", "()V")); + assertMappingStartsWith(TestEntryFactory.newMethod("b", "a", "(I)V"), TestEntryFactory.newMethod("b", "gaming", "(I)V")); + } + + private static void assertMappingStartsWith(Entry obf, Entry deobf) { + TranslateResult> result = project.getRemapper().getDeobfuscator().extendedTranslate(obf); + assertThat(result, is(notNullValue())); + + String deobfName = result.getValue().getName(); + if (deobfName != null) { + assertThat(deobfName, startsWith(deobf.getName())); + } + } + + private static void assertUnmapped(Entry obf) { + TranslateResult> result = project.getRemapper().getDeobfuscator().extendedTranslate(obf); + assertThat(result, is(notNullValue())); + assertThat(result.getType(), is(TokenType.OBFUSCATED)); + } + + @SuppressWarnings("all") + private static ReadWriteService getService() { + return project.getEnigma().getReadWriteService(project.getEnigma().getSupportedFileTypes().stream().filter(file -> file.getExtensions().contains("mapping") && !file.isDirectory()).findFirst().get()).get(); + } + + private static class TestPlugin implements EnigmaPlugin { + @Override + public void init(EnigmaPluginContext ctx) { + ctx.registerService(NameProposalService.TYPE, ctx1 -> new TestFieldProposerNormal()); + ctx.registerService(NameProposalService.TYPE, ctx1 -> new TestMethodProposerWithBypass()); + } + + private static class TestFieldProposerNormal implements NameProposalService { + @Override + public Map, EntryMapping> getProposedNames(JarIndex index) { + Map, EntryMapping> mappings = new HashMap<>(); + AtomicInteger i = new AtomicInteger(); + + index.getIndex(EntryIndex.class).getFields().forEach( + field -> mappings.put(field, this.createMapping("slay" + i.getAndIncrement(), TokenType.JAR_PROPOSED)) + ); + + return mappings; + } + + @Override + public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { + return null; + } + + @Override + public String getId() { + return "test:name_all_fields_slay"; + } + } + + private static class TestMethodProposerWithBypass implements NameProposalService { + @Override + public Map, EntryMapping> getProposedNames(JarIndex index) { + Map, EntryMapping> mappings = new HashMap<>(); + AtomicInteger i = new AtomicInteger(); + + index.getIndex(EntryIndex.class).getMethods().forEach( + method -> mappings.put(method, new EntryMapping("gaming" + i.getAndIncrement(), null, TokenType.DEOBFUSCATED, null)) + ); + + return mappings; + } + + @Override + public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, @Nullable Entry obfEntry, @Nullable EntryMapping oldMapping, @Nullable EntryMapping newMapping) { + return null; + } + + @Override + public boolean bypassValidation() { + return true; + } + + @Override + public String getId() { + return "test:name_all_methods_gaming"; + } + } + } +} diff --git a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java index a89660c23..1fdb6ad4c 100644 --- a/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java +++ b/enigma/src/test/java/org/quiltmc/enigma/records/TestRecordComponentProposal.java @@ -1,7 +1,7 @@ package org.quiltmc.enigma.records; import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.quiltmc.enigma.TestEntryFactory; import org.quiltmc.enigma.TestUtil; @@ -25,8 +25,8 @@ public class TestRecordComponentProposal { private static final Path JAR = TestUtil.obfJar("records"); private static EnigmaProject project; - @BeforeAll - static void setupEnigma() throws IOException { + @BeforeEach + void setupEnigma() throws IOException { Reader r = new StringReader(""" { "services": { @@ -55,8 +55,8 @@ void testSimpleRecordComponentProposal() { FieldEntry aField = TestEntryFactory.newField(aClass, "a", "I"); MethodEntry aGetter = TestEntryFactory.newMethod(aClass, "a", "()I"); - Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); - Assertions.assertSame(project.getRemapper().getMapping(aGetter).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aGetter).tokenType()); project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); @@ -78,9 +78,9 @@ void testMismatchRecordComponentProposal() { MethodEntry fakeAGetter = TestEntryFactory.newMethod(cClass, "a", "()I"); MethodEntry realAGetter = TestEntryFactory.newMethod(cClass, "b", "()I"); - Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); - Assertions.assertSame(project.getRemapper().getMapping(fakeAGetter).tokenType(), TokenType.OBFUSCATED); - Assertions.assertSame(project.getRemapper().getMapping(realAGetter).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(fakeAGetter).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(realAGetter).tokenType()); project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); @@ -105,8 +105,8 @@ void testRecordComponentMappingRemoval() { FieldEntry aField = TestEntryFactory.newField(aClass, "a", "I"); MethodEntry aGetter = TestEntryFactory.newMethod(aClass, "a", "()I"); - Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); - Assertions.assertSame(project.getRemapper().getMapping(aGetter).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aGetter).tokenType()); // put name, make sure getter matches project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); @@ -132,8 +132,8 @@ void testTypedRecordComponentProposal() { FieldEntry aField = TestEntryFactory.newField(eClass, "a", "Ljava/lang/String;"); MethodEntry aGetter = TestEntryFactory.newMethod(eClass, "a", "()Ljava/lang/String;"); - Assertions.assertSame(project.getRemapper().getMapping(aField).tokenType(), TokenType.OBFUSCATED); - Assertions.assertSame(project.getRemapper().getMapping(aGetter).tokenType(), TokenType.OBFUSCATED); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aField).tokenType()); + Assertions.assertSame(TokenType.OBFUSCATED, project.getRemapper().getMapping(aGetter).tokenType()); project.getRemapper().putMapping(TestUtil.newVC(), aField, new EntryMapping("mapped")); diff --git a/enigma/src/testFixtures/java/org/quiltmc/enigma/test/plugin/TestEnigmaPlugin.java b/enigma/src/testFixtures/java/org/quiltmc/enigma/test/plugin/TestEnigmaPlugin.java index afac6ab12..338b35181 100644 --- a/enigma/src/testFixtures/java/org/quiltmc/enigma/test/plugin/TestEnigmaPlugin.java +++ b/enigma/src/testFixtures/java/org/quiltmc/enigma/test/plugin/TestEnigmaPlugin.java @@ -18,12 +18,50 @@ public class TestEnigmaPlugin implements EnigmaPlugin { @Override public void init(EnigmaPluginContext ctx) { this.registerParameterNamingService(ctx); + this.registerFieldNamingService(ctx); } private void registerParameterNamingService(EnigmaPluginContext ctx) { ctx.registerService(NameProposalService.TYPE, ctx1 -> new ParameterNameProposalService()); } + private void registerFieldNamingService(EnigmaPluginContext ctx) { + ctx.registerService(NameProposalService.TYPE, ctx2 -> new StringFieldNameProposalService()); + } + + public static class StringFieldNameProposalService implements NameProposalService { + @Override + public String getId() { + return "test:strings"; + } + + @Override + public Map, EntryMapping> getProposedNames(JarIndex index) { + EntryIndex entryIndex = index.getIndex(EntryIndex.class); + Map, EntryMapping> names = new HashMap<>(); + + int fieldIndex = 0; + for (var field : entryIndex.getFields()) { + if (field.getDesc().toString().equals("Ljava/lang/String;")) { + names.put(field, this.createMapping("string" + fieldIndex, TokenType.JAR_PROPOSED)); + fieldIndex++; + } + } + + return names; + } + + @Override + public boolean isFallback() { + return true; + } + + @Override + public Map, EntryMapping> getDynamicProposedNames(EntryRemapper remapper, Entry obfEntry, EntryMapping oldMapping, EntryMapping newMapping) { + return null; + } + } + public static class ParameterNameProposalService implements NameProposalService { private static final MethodDescriptor EQUALS_DESC = new MethodDescriptor("(Ljava/lang/Object;)Z"); @@ -41,7 +79,9 @@ public Map, EntryMapping> getProposedNames(JarIndex index) { var param = method.getParameters(entryIndex).get(0); names.put(param, this.createMapping("o", TokenType.JAR_PROPOSED)); } else { - for (var param : method.getParameters(entryIndex)) { + // only propose a name for the first parameter + if (!method.getParameters(index.getIndex(EntryIndex.class)).isEmpty()) { + var param = method.getParameters(entryIndex).get(0); names.put(param, this.createMapping("param" + param.getIndex(), TokenType.JAR_PROPOSED)); } } diff --git a/enigma/src/testFixtures/resources/profile.json b/enigma/src/testFixtures/resources/profile.json index ac5b8375a..e5c984681 100644 --- a/enigma/src/testFixtures/resources/profile.json +++ b/enigma/src/testFixtures/resources/profile.json @@ -21,6 +21,9 @@ { "id": "test:parameters" }, + { + "id": "test:strings" + }, { "id": "enigma:record_component_proposer" }