diff --git a/.gitattributes b/.gitattributes index 11aafdf28..b6d0ae55a 100644 --- a/.gitattributes +++ b/.gitattributes @@ -17,6 +17,8 @@ *.xml text eol=lf *.yaml text eol=lf *.yml text eol=lf +*.toml text eol=lf +*.lang text eol=lf # These files are binary and should be left untouched *.png binary diff --git a/README.md b/README.md index ae771d9a9..8b04585d1 100644 --- a/README.md +++ b/README.md @@ -69,4 +69,4 @@ Open the project in an IDE or generate the build with gradle. **Without IDE**: 1. Run `gradlew build` - - Output will be located at: `recaf-ui/build/recaf-ui-{VERSION}-all.jar` \ No newline at end of file + - Output will be located at: `recaf-ui/build/libs/recaf-ui-{VERSION}-all.jar` \ No newline at end of file diff --git a/build.gradle b/build.gradle index 4fccfa9d3..babeb3dc0 100644 --- a/build.gradle +++ b/build.gradle @@ -56,7 +56,7 @@ subprojects { // Append options for unchecked/deprecation tasks.withType(JavaCompile).configureEach { - options.compilerArgs << "-Xlint:unchecked" << "-Xlint:deprecation" + options.compilerArgs << '-Xlint:unchecked' << '-Xlint:deprecation' << '-g' << '-parameters' options.encoding = 'UTF-8' options.incremental = true } diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml index d65ce6f2b..b809ea327 100644 --- a/gradle/libs.versions.toml +++ b/gradle/libs.versions.toml @@ -10,14 +10,14 @@ dex-translator = "1.1.1" directories = "26" docking = "1.2.3" downgrader = "1.1.1" -extra-collections = "1.2.3" +extra-collections = "1.3.0" extra-observables = "1.3.0" gson = "2.10.1" ikonli = "12.3.1" -instrument-server = "1.4.0" +instrument-server = "1.4.1" jackson = "2.16.1" jakarta-annotation = "3.0.0-M1" -jasm = "913f24ec92" +jasm = "3db094eade" jlinker = "1.0.7" jphantom = "1.4.4" junit = "5.10.2" @@ -26,6 +26,7 @@ llzip = "2.5.0" logback-classic = { strictly = "1.4.11" } # newer releases break in jar releases mapping-io = "0.5.1" mockito = "5.11.0" +natural-order = "1.1" openrewrite = "8.19.0" picocli = "4.7.5" procyon = "0.6.0" @@ -44,7 +45,6 @@ peterabeles-gversion = "1.10.2" jgrapht = "1.5.2" [libraries] - asm-core = { module = "org.ow2.asm:asm", version.ref = "asm" } asm-analysis = { module = "org.ow2.asm:asm-analysis", version.ref = "asm" } asm-commons = { module = "org.ow2.asm:asm-commons", version.ref = "asm" } @@ -106,6 +106,8 @@ mapping-io = { module = "net.fabricmc:mapping-io", version.ref = "mapping-io" } mockito = { module = "org.mockito:mockito-core", version.ref = "mockito" } +natural-order = { module = "net.grey-panther:natural-comparator", version.ref = "natural-order" } + openrewrite = { module = "org.openrewrite:rewrite-java-17", version.ref = "openrewrite" } picocli = { module = "info.picocli:picocli", version.ref = "picocli" } diff --git a/recaf-core/build.gradle b/recaf-core/build.gradle index edde5fbbb..073f9bbd7 100644 --- a/recaf-core/build.gradle +++ b/recaf-core/build.gradle @@ -30,6 +30,7 @@ dependencies { api(libs.llzip) api(libs.bundles.logging) api(libs.mapping.io) + api(libs.natural.order) api(libs.picocli) api(libs.procyon) api(libs.jackson) diff --git a/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java b/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java index bea75d145..a234b68d6 100644 --- a/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java +++ b/recaf-core/src/main/java/software/coley/recaf/analytics/logging/Logging.java @@ -13,6 +13,7 @@ import java.util.List; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import static org.slf4j.LoggerFactory.getLogger; @@ -23,7 +24,7 @@ */ public class Logging { private static final Map loggers = new ConcurrentHashMap<>(); - private static final List> logConsumers = new ArrayList<>(); + private static final List> logConsumers = new CopyOnWriteArrayList<>(); private static Level interceptLevel = Level.INFO; /** @@ -50,7 +51,7 @@ public static DebuggingLogger get(Class cls) { * @param consumer * New log message consumer. */ - public static void addLogConsumer(LogConsumer consumer) { + public static void addLogConsumer(@Nonnull LogConsumer consumer) { logConsumers.add(consumer); } @@ -58,7 +59,7 @@ public static void addLogConsumer(LogConsumer consumer) { * @param consumer * Log message consumer to remove. */ - public static void removeLogConsumer(LogConsumer consumer) { + public static void removeLogConsumer(@Nonnull LogConsumer consumer) { logConsumers.remove(consumer); } diff --git a/recaf-core/src/main/java/software/coley/recaf/cdi/EagerInitialization.java b/recaf-core/src/main/java/software/coley/recaf/cdi/EagerInitialization.java index 9338a3a78..e1bc34ceb 100644 --- a/recaf-core/src/main/java/software/coley/recaf/cdi/EagerInitialization.java +++ b/recaf-core/src/main/java/software/coley/recaf/cdi/EagerInitialization.java @@ -10,6 +10,8 @@ /** * Applied to beans to enable eager initialization, which is to say they and their dependencies get created as soon as * possible depending on the {@link #value() value of the intended} {@link InitializationStage}. + *

+ * Beans are not eagerly initialized while in a test environment. * * @author Matt Coley */ diff --git a/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java b/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java index 70f4ff3f2..2876cc50f 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/BasicClassInfo.java @@ -6,14 +6,13 @@ import software.coley.recaf.info.builder.AbstractClassInfoBuilder; import software.coley.recaf.info.member.BasicMember; import software.coley.recaf.info.member.FieldMember; +import software.coley.recaf.info.member.LocalVariable; import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.info.properties.Property; import software.coley.recaf.info.properties.PropertyContainer; +import software.coley.recaf.util.Types; -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Objects; +import java.util.*; import java.util.stream.Stream; /** @@ -24,6 +23,9 @@ * @see BasicAndroidClassInfo */ public abstract class BasicClassInfo implements ClassInfo { + private static final int SIGS_VALID = 1; + private static final int SIGS_INVALID = 0; + private static final int SIGS_UNKNOWN = -1; private final PropertyContainer properties; private final String name; private final String superName; @@ -40,6 +42,7 @@ public abstract class BasicClassInfo implements ClassInfo { private final List fields; private final List methods; private List breadcrumbs; + private int sigCheck = SIGS_UNKNOWN; protected BasicClassInfo(AbstractClassInfoBuilder builder) { this(builder.getName(), @@ -60,14 +63,14 @@ protected BasicClassInfo(AbstractClassInfoBuilder builder) { } protected BasicClassInfo(@Nonnull String name, String superName, @Nonnull List interfaces, int access, - String signature, String sourceFileName, - @Nonnull List annotations, - @Nonnull List typeAnnotations, - String outerClassName, String outerMethodName, - String outerMethodDescriptor, - @Nonnull List innerClasses, - @Nonnull List fields, @Nonnull List methods, - @Nonnull PropertyContainer properties) { + String signature, String sourceFileName, + @Nonnull List annotations, + @Nonnull List typeAnnotations, + String outerClassName, String outerMethodName, + String outerMethodDescriptor, + @Nonnull List innerClasses, + @Nonnull List fields, @Nonnull List methods, + @Nonnull PropertyContainer properties) { this.name = name; this.superName = superName; this.interfaces = interfaces; @@ -117,6 +120,49 @@ public String getSignature() { return signature; } + @Override + public boolean hasValidSignatures() { + // Check cached value. + if (sigCheck != SIGS_UNKNOWN) return sigCheck == SIGS_VALID; + + // Check class level signature. + String classSignature = getSignature(); + if (classSignature != null && !Types.isValidSignature(classSignature, false)) { + sigCheck = SIGS_INVALID; + return false; + } + + // Check field signatures. + for (FieldMember field : getFields()) { + String fieldSignature = field.getSignature(); + if (fieldSignature != null && !Types.isValidSignature(field.getSignature(), true)) { + sigCheck = SIGS_INVALID; + return false; + } + } + + // Check method signatures. + for (MethodMember method : getMethods()) { + String methodSignature = method.getSignature(); + if (methodSignature != null && !Types.isValidSignature(methodSignature, false)) { + sigCheck = SIGS_INVALID; + return false; + } + + // And local variables. + for (LocalVariable variable : method.getLocalVariables()) { + String localSignature = variable.getSignature(); + if (localSignature != null && !Types.isValidSignature(localSignature, true)) { + sigCheck = SIGS_INVALID; + return false; + } + } + } + + sigCheck = SIGS_VALID; + return true; + } + @Override public String getSourceFileName() { return sourceFileName; @@ -153,16 +199,19 @@ public String getOuterMethodDescriptor() { @Override public List getOuterClassBreadcrumbs() { if (breadcrumbs == null) { + String currentOuter = getOuterClassName(); + if (currentOuter == null) + return breadcrumbs = Collections.emptyList(); + int maxOuterDepth = 10; breadcrumbs = new ArrayList<>(); - String currentOuter = getOuterClassName(); int counter = 0; while (currentOuter != null) { if (++counter > maxOuterDepth) { breadcrumbs.clear(); // assuming some obfuscator is at work, so breadcrumbs might be invalid. break; } - breadcrumbs.add(0, currentOuter); + breadcrumbs.addFirst(currentOuter); String targetOuter = currentOuter; currentOuter = innerClasses.stream() .filter(i -> i.getInnerClassName().equals(targetOuter)) @@ -236,7 +285,7 @@ public boolean equals(Object o) { public int hashCode() { // NOTE: Do NOT consider the properties since contents of the map can point back to this instance // or our containing resource, causing a cycle. - int result = name.hashCode(); + int result = name.hashCode(); result = 31 * result + (superName != null ? superName.hashCode() : 0); result = 31 * result + interfaces.hashCode(); result = 31 * result + access; diff --git a/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java b/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java index fba23535c..6d0de859f 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/ClassInfo.java @@ -6,6 +6,8 @@ import software.coley.recaf.info.member.ClassMember; import software.coley.recaf.info.member.FieldMember; import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.util.Types; +import software.coley.recaf.util.visitors.IllegalSignatureRemovingVisitor; import java.util.List; import java.util.function.Consumer; @@ -78,6 +80,16 @@ default Stream parentTypesStream() { @Nullable String getSignature(); + /** + * @return {@code true} when the {@link #getSignature() class signature} and all + * {@link #getFields() fields} and {@link #getMethods() methods} have valid {@link ClassMember#getSignature() signatures}. + * {@code false} when any of those values is malformed. + * + * @see IllegalSignatureRemovingVisitor Visitor for removing invalid signatures on JVM classes. + * @see Types#isValidSignature(String, boolean) Method for checking validity of a generic signature. + */ + boolean hasValidSignatures(); + /** * @return Name of outer class that this is declared in, if this is an inner class. * {@code null} when this class is not an inner class. diff --git a/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java b/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java index 5b5131c3a..6fbd30291 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/JvmClassInfo.java @@ -8,10 +8,9 @@ import software.coley.recaf.info.properties.builtin.ReferencedClassesProperty; import software.coley.recaf.info.properties.builtin.StringDefinitionsProperty; import software.coley.recaf.util.Types; +import software.coley.recaf.util.visitors.TypeVisitor; -import java.util.HashSet; -import java.util.Set; -import java.util.SortedSet; +import java.util.*; import java.util.function.Consumer; import java.util.function.Predicate; @@ -61,57 +60,87 @@ default JvmClassInfoBuilder toJvmClassBuilder() { * @return Set of all classes referenced in the constant pool. */ @Nonnull - default Set getReferencedClasses() { - SortedSet classes = ReferencedClassesProperty.get(this); + default NavigableSet getReferencedClasses() { + NavigableSet classes = ReferencedClassesProperty.get(this); if (classes != null) return classes; Set classNames = new HashSet<>(); - Consumer nameHandler = className -> { - if (className.indexOf(0) == '[') - className = className.substring(className.lastIndexOf('[') + 1, className.indexOf(';')); - classNames.add(className); - }; - Consumer typeConsumer = t -> { - if (t.getSort() == Type.ARRAY) - t = t.getElementType(); - if (!Types.isPrimitive(t)) - nameHandler.accept(t.getInternalName()); - }; - ClassReader reader = getClassReader(); + + // Iterate over pool entries. Supe fast way to discover most of the referenced types. int itemCount = reader.getItemCount(); char[] buffer = new char[reader.getMaxStringLength()]; for (int i = 1; i < itemCount; i++) { int offset = reader.getItem(i); if (offset >= 10) { - int itemTag = reader.readByte(offset - 1); - if (itemTag == ConstantPoolConstants.CLASS) { - String className = reader.readUTF8(offset, buffer); - if (className.isEmpty()) - continue; - if (className.indexOf(0) == '[') - className = className.substring(className.lastIndexOf('[') + 1, className.indexOf(';')); - classNames.add(className); - } else if (itemTag == ConstantPoolConstants.NAME_TYPE) { - String desc = reader.readUTF8(offset + 2, buffer); - if (desc.isEmpty()) - continue; - if (desc.charAt(0) == '(') { - Type methodType = Type.getMethodType(desc); - for (Type argumentType : methodType.getArgumentTypes()) - typeConsumer.accept(argumentType); - Type returnType = methodType.getReturnType(); - typeConsumer.accept(returnType); - } else { - Type type = Type.getType(desc); - typeConsumer.accept(type); + try { + int itemTag = reader.readByte(offset - 1); + if (itemTag == ConstantPoolConstants.CLASS) { + String className = reader.readUTF8(offset, buffer); + if (className.isEmpty()) + continue; + addName(className, classNames); + } else if (itemTag == ConstantPoolConstants.NAME_TYPE) { + String desc = reader.readUTF8(offset + 2, buffer); + if (desc.isEmpty()) + continue; + if (desc.charAt(0) == '(') { + addMethodType(Type.getMethodType(desc), classNames); + } else { + Type type = Type.getType(desc); + addType(type, classNames); + } + } else if (itemTag == ConstantPoolConstants.METHOD_TYPE) { + String methodDesc = reader.readUTF8(offset, buffer); + if (methodDesc.isEmpty() || methodDesc.charAt(0) != '(') + continue; + addMethodType(Type.getMethodType(methodDesc), classNames); } + } catch (Throwable ignored) { + // Exists only to catch situations where obfuscators put unused junk pool entries + // with malformed descriptors, which cause ASM's type parser to crash. } } } + + // In some cases like interface classes, there may be UTF8 pool entries outlining method descriptors which + // are not directly linked in NameType or MethodType pool entries. We need to iterate over fields and methods + // to get the descriptors in these cases. + reader.accept(new TypeVisitor(t -> { + if (t.getSort() == Type.METHOD) + addMethodType(t, classNames); + else + addType(t, classNames); + }), ClassReader.SKIP_DEBUG | ClassReader.SKIP_CODE); + ReferencedClassesProperty.set(this, classNames); - return classNames; + return Objects.requireNonNull(ReferencedClassesProperty.get(this)); + } + + private static void addMethodType(@Nonnull Type methodType, @Nonnull Set classNames) { + for (Type argumentType : methodType.getArgumentTypes()) + addType(argumentType, classNames); + Type returnType = methodType.getReturnType(); + addType(returnType, classNames); + } + + private static void addType(@Nonnull Type type, @Nonnull Set classNames) { + if (type.getSort() == Type.ARRAY) + type = type.getElementType(); + if (!Types.isPrimitive(type)) + addName(type.getInternalName(), classNames); + } + + private static void addName(@Nonnull String className, @Nonnull Set classNames) { + if (className.isEmpty()) + return; + if (className.indexOf(0) == '[' || className.charAt(className.length() - 1) == ';') + addType(Type.getType(className), classNames); + else if (className.indexOf(0) == '(') + addMethodType(Type.getMethodType(className), classNames); + else + classNames.add(className); } /** diff --git a/recaf-core/src/main/java/software/coley/recaf/info/annotation/AnnotationElement.java b/recaf-core/src/main/java/software/coley/recaf/info/annotation/AnnotationElement.java index 8f2998d92..7e8c5c8d6 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/annotation/AnnotationElement.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/annotation/AnnotationElement.java @@ -1,6 +1,7 @@ package software.coley.recaf.info.annotation; import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; import java.util.List; @@ -17,7 +18,8 @@ public interface AnnotationElement { String getElementName(); /** - * @return Element value. Can be a primitive, {@link String}, a {@link AnnotationElement}, or a {@link List} of any of the prior values. + * @return Element value. Can be a primitive, {@link String}, a {@link AnnotationInfo}, + * a {@link AnnotationEnumReference}, a {@link Type}, or a {@link List} of any of the prior values. */ @Nonnull Object getElementValue(); diff --git a/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java b/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java index a1b52abb9..c19451c23 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/builder/JvmClassInfoBuilder.java @@ -351,6 +351,7 @@ public void visitAttribute(Attribute attribute) { @Override public void visitEnd() { + super.visitEnd(); methods.add(getMethodMember()); } }; @@ -401,7 +402,7 @@ private static class FieldBuilderAdapter extends FieldVisitor { private final BasicFieldMember fieldMember; public FieldBuilderAdapter(int access, String name, String descriptor, - String signature, Object value) { + String signature, Object value) { super(getAsmVersion()); fieldMember = new BasicFieldMember(name, descriptor, signature, access, value); } @@ -425,12 +426,19 @@ public BasicFieldMember getFieldMember() { private static class MethodBuilderAdapter extends MethodVisitor { private final BasicMethodMember methodMember; + private final Type methodDescriptor; + private final List parameters; + private int parameterIndex; + private int parameterSlot; public MethodBuilderAdapter(int access, String name, String descriptor, - String signature, String[] exceptions) { + String signature, String[] exceptions) { super(getAsmVersion()); List exceptionList = exceptions == null ? Collections.emptyList() : Arrays.asList(exceptions); methodMember = new BasicMethodMember(name, descriptor, signature, access, exceptionList, new ArrayList<>()); + methodDescriptor = Type.getMethodType(descriptor); + parameterSlot = methodMember.hasStaticModifier() ? 0 : 1; + parameters = new ArrayList<>(methodDescriptor.getArgumentCount()); } @Override @@ -450,6 +458,44 @@ public void visitLocalVariable(String name, String descriptor, String signature, super.visitLocalVariable(name, descriptor, signature, start, end, index); } + @Override + public void visitParameter(String name, int access) { + super.visitParameter(name, access); + + Type[] argumentTypes = methodDescriptor.getArgumentTypes(); + if (parameterIndex < argumentTypes.length) { + Type argumentType = argumentTypes[parameterIndex]; + + // Only add when we have a name for the parameter. + if (name != null) + parameters.add(new BasicLocalVariable(parameterSlot, name, argumentType.getDescriptor(), null)); + + parameterIndex++; + parameterSlot += argumentType.getSize(); + } + } + + @Override + public AnnotationVisitor visitAnnotationDefault() { + return new DefaultAnnotationAdapter(anno -> { + AnnotationElement element = anno.getElements().get(DefaultAnnotationAdapter.KEY); + if (element != null) methodMember.setAnnotationDefault(element); + }); + } + + @Override + public void visitEnd() { + super.visitEnd(); + + // Add local variables generated from the visited parameters if the local variable table hasn't already + // provided variables for those indices. This assists in providing variable models for abstract methods. + // This only works when a 'MethodParameters' attribute is present on the method. The javac compiler + // emits this when passing '-parameters'. + for (LocalVariable parameter : parameters) + if (methodMember.getLocalVariable(parameter.getIndex()) == null) + methodMember.addLocalVariable(parameter); + } + @Nonnull public BasicMethodMember getMethodMember() { return methodMember; @@ -458,14 +504,14 @@ public BasicMethodMember getMethodMember() { private static class AnnotationBuilderAdapter extends AnnotationVisitor { private final Consumer annotationConsumer; - private final Map elements = new HashMap<>(); + protected final Map elements = new HashMap<>(); private final List arrayValues = new ArrayList<>(); private final List subAnnotations = new ArrayList<>(); private final boolean visible; private final String descriptor; protected AnnotationBuilderAdapter(boolean visible, String descriptor, - Consumer annotationConsumer) { + Consumer annotationConsumer) { super(getAsmVersion()); this.visible = visible; this.descriptor = descriptor; @@ -497,20 +543,19 @@ public void visitEnum(String name, String descriptor, String value) { @Override public AnnotationVisitor visitAnnotation(String name, String descriptor) { - AnnotationBuilderAdapter adapter = new AnnotationBuilderAdapter(true, descriptor, anno -> { + return new AnnotationBuilderAdapter(true, descriptor, anno -> { if (name == null) { - arrayValues.add(anno); + AnnotationBuilderAdapter.this.arrayValues.add(anno); } else { - elements.put(name, new BasicAnnotationElement(name, anno)); + AnnotationBuilderAdapter.this.elements.put(name, new BasicAnnotationElement(name, anno)); } }) { @Override protected void populate(@Nonnull BasicAnnotationInfo anno) { super.populate(anno); - subAnnotations.add(anno); + AnnotationBuilderAdapter.this.subAnnotations.add(anno); } }; - return adapter; } @Override @@ -537,4 +582,20 @@ protected void populate(@Nonnull BasicAnnotationInfo anno) { if (annotationConsumer != null) annotationConsumer.accept(anno); } } + + private static class DefaultAnnotationAdapter extends AnnotationBuilderAdapter { + private static final String KEY = "value"; + + protected DefaultAnnotationAdapter(Consumer annotationConsumer) { + super(true, "Ljava/lang/Object;", annotationConsumer); + } + + @Override + public void visitEnum(String name, String descriptor, String value) { + name = KEY; + + BasicAnnotationEnumReference enumRef = new BasicAnnotationEnumReference(descriptor, value); + elements.put(name, new BasicAnnotationElement(name, enumRef)); + } + } } diff --git a/recaf-core/src/main/java/software/coley/recaf/info/member/BasicMethodMember.java b/recaf-core/src/main/java/software/coley/recaf/info/member/BasicMethodMember.java index ab93a68a3..2c1a18da7 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/member/BasicMethodMember.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/member/BasicMethodMember.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import software.coley.recaf.info.annotation.AnnotationElement; import java.util.List; import java.util.Objects; @@ -14,6 +15,7 @@ public class BasicMethodMember extends BasicMember implements MethodMember { private final List thrownTypes; private final List variables; + private AnnotationElement annotationDefault; /** * @param name @@ -44,6 +46,13 @@ public void addLocalVariable(@Nonnull LocalVariable variable) { variables.add(variable); } + /** + * @param annotationDefault Element value to set. + */ + public void setAnnotationDefault(@Nonnull AnnotationElement annotationDefault) { + this.annotationDefault = annotationDefault; + } + @Nonnull @Override public List getThrownTypes() { @@ -56,6 +65,12 @@ public List getLocalVariables() { return variables; } + @Nullable + @Override + public AnnotationElement getAnnotationDefault() { + return annotationDefault; + } + @Override public String toString() { return "Method: " + getName() + getDescriptor(); diff --git a/recaf-core/src/main/java/software/coley/recaf/info/member/MethodMember.java b/recaf-core/src/main/java/software/coley/recaf/info/member/MethodMember.java index 805aeee86..441b13150 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/member/MethodMember.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/member/MethodMember.java @@ -3,6 +3,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.info.annotation.AnnotationElement; import java.util.List; @@ -24,6 +25,12 @@ public interface MethodMember extends ClassMember { @Nonnull List getLocalVariables(); + /** + * @return Element holding the default value for an annotation method. + */ + @Nullable + AnnotationElement getAnnotationDefault(); + /** * @param index * Local variable index. diff --git a/recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ReferencedClassesProperty.java b/recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ReferencedClassesProperty.java index bde2932a9..7d00dcb9e 100644 --- a/recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ReferencedClassesProperty.java +++ b/recaf-core/src/main/java/software/coley/recaf/info/properties/builtin/ReferencedClassesProperty.java @@ -3,12 +3,9 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import software.coley.recaf.info.ClassInfo; -import software.coley.recaf.info.Info; import software.coley.recaf.info.properties.BasicProperty; -import java.util.Collection; -import java.util.SortedSet; -import java.util.TreeSet; +import java.util.*; /** * Built in property to track what classes are referenced by this type. @@ -23,7 +20,7 @@ public class ReferencedClassesProperty extends BasicProperty> * Collection of referenced classes. */ public ReferencedClassesProperty(@Nonnull Collection classes) { - super(KEY, new TreeSet<>(classes)); + super(KEY, Collections.unmodifiableNavigableSet(new TreeSet<>(classes))); } /** @@ -33,7 +30,7 @@ public ReferencedClassesProperty(@Nonnull Collection classes) { * @return Set of referenced classes, or {@code null} when no association exists. */ @Nullable - public static SortedSet get(@Nonnull ClassInfo info) { + public static NavigableSet get(@Nonnull ClassInfo info) { return info.getPropertyValueOrNull(KEY); } diff --git a/recaf-core/src/main/java/software/coley/recaf/path/AbstractPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/AbstractPathNode.java index 52dd8a469..781e82a7b 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/AbstractPathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/AbstractPathNode.java @@ -2,7 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; -import software.coley.recaf.util.Unchecked; +import software.coley.collections.Unchecked; /** * Base implementation of {@link PathNode}. diff --git a/recaf-core/src/main/java/software/coley/recaf/path/BundlePathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/BundlePathNode.java index 61b306ecc..6ada13849 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/BundlePathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/BundlePathNode.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; import software.coley.recaf.workspace.model.bundle.Bundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; @@ -138,7 +139,7 @@ public int localCompare(PathNode o) { .map(Map.Entry::getKey) .findFirst() .orElse(null); - return String.CASE_INSENSITIVE_ORDER.compare(dexName, otherDexName); + return CaseInsensitiveSimpleNaturalComparator.getInstance().compare(dexName, otherDexName); } } return 0; diff --git a/recaf-core/src/main/java/software/coley/recaf/path/ClassMemberPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/ClassMemberPathNode.java index acd590463..acc2494c2 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/ClassMemberPathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/ClassMemberPathNode.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import org.objectweb.asm.tree.AbstractInsnNode; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.InnerClassInfo; @@ -188,7 +189,7 @@ public int localCompare(PathNode o) { // Just sort alphabetically if parent not known. String key = member.getName() + member.getDescriptor(); String otherKey = otherMember.getName() + member.getDescriptor(); - cmp = String.CASE_INSENSITIVE_ORDER.compare(key, otherKey); + cmp = CaseInsensitiveSimpleNaturalComparator.getInstance().compare(key, otherKey); } return cmp; } diff --git a/recaf-core/src/main/java/software/coley/recaf/path/ClassPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/ClassPathNode.java index 216e8b26e..3a0504025 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/ClassPathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/ClassPathNode.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.InnerClassInfo; import software.coley.recaf.info.annotation.AnnotationInfo; @@ -134,7 +135,7 @@ public int localCompare(PathNode o) { if (o instanceof ClassPathNode classPathNode) { String name = getValue().getName(); String otherName = classPathNode.getValue().getName(); - return String.CASE_INSENSITIVE_ORDER.compare(name, otherName); + return CaseInsensitiveSimpleNaturalComparator.getInstance().compare(name, otherName); } return 0; } diff --git a/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java index a3b635600..e981bd64a 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/DirectoryPathNode.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.FileInfo; import software.coley.recaf.workspace.model.bundle.Bundle; @@ -124,7 +125,7 @@ public int localCompare(PathNode o) { if (o instanceof DirectoryPathNode pathNode) { String name = getValue(); String otherName = pathNode.getValue(); - return String.CASE_INSENSITIVE_ORDER.compare(name, otherName); + return CaseInsensitiveSimpleNaturalComparator.getInstance().compare(name, otherName); } return 0; } diff --git a/recaf-core/src/main/java/software/coley/recaf/path/EmbeddedResourceContainerPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/EmbeddedResourceContainerPathNode.java new file mode 100644 index 000000000..a746a1d65 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/path/EmbeddedResourceContainerPathNode.java @@ -0,0 +1,61 @@ +package software.coley.recaf.path; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.resource.WorkspaceFileResource; +import software.coley.recaf.workspace.model.resource.WorkspaceResource; + +import java.util.Set; + +/** + * Path node for housing one or more embedded resources in another resource. + * + * @author Matt Coley + */ +public class EmbeddedResourceContainerPathNode extends AbstractPathNode { + /** + * Type identifier for embedded containers. + */ + public static final String TYPE_ID = "embedded-container"; + + /** + * Node with parent. + * + * @param parent + * Parent node. + * @param workspace + * Workspace containing the host resource. + */ + public EmbeddedResourceContainerPathNode(@Nullable ResourcePathNode parent, + @Nonnull Workspace workspace) { + super(TYPE_ID, parent, workspace); + } + + /** + * @param resource + * Resource to wrap into node. + * + * @return Path node of resource, with the current workspace as parent. + */ + @Nonnull + public ResourcePathNode child(@Nonnull WorkspaceResource resource) { + return new ResourcePathNode(this, resource); + } + + @Override + public ResourcePathNode getParent() { + return (ResourcePathNode) super.getParent(); + } + + @Nonnull + @Override + public Set directParentTypeIds() { + return Set.of(EmbeddedResourceContainerPathNode.TYPE_ID); + } + + @Override + public int localCompare(PathNode o) { + return 0; + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/path/FilePathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/FilePathNode.java index 78e9b0680..157c6e2fa 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/FilePathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/FilePathNode.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.FileInfo; @@ -87,7 +88,7 @@ public int localCompare(PathNode o) { if (o instanceof FilePathNode fileNode) { String name = getValue().getName(); String otherName = fileNode.getValue().getName(); - return String.CASE_INSENSITIVE_ORDER.compare(name, otherName); + return CaseInsensitiveSimpleNaturalComparator.getInstance().compare(name, otherName); } return 0; } diff --git a/recaf-core/src/main/java/software/coley/recaf/path/InnerClassPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/InnerClassPathNode.java index fe323e6a7..bf6a8f9a0 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/InnerClassPathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/InnerClassPathNode.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.InnerClassInfo; import software.coley.recaf.info.annotation.AnnotationInfo; @@ -63,7 +64,7 @@ public int localCompare(PathNode o) { if (o instanceof InnerClassPathNode innerClassPathNode) { String name = getValue().getInnerClassName(); String otherName = innerClassPathNode.getValue().getInnerClassName(); - return String.CASE_INSENSITIVE_ORDER.compare(name, otherName); + return CaseInsensitiveSimpleNaturalComparator.getInstance().compare(name, otherName); } // Show before members diff --git a/recaf-core/src/main/java/software/coley/recaf/path/LineNumberPathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/LineNumberPathNode.java index 717acd720..b4877241b 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/LineNumberPathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/LineNumberPathNode.java @@ -56,8 +56,18 @@ public Set directParentTypeIds() { public int localCompare(PathNode o) { if (this == o) return 0; - if (o instanceof LineNumberPathNode lineNode) - return getValue().compareTo(lineNode.getValue()); + if (o instanceof LineNumberPathNode lineNode) { + int i = getValue().compareTo(lineNode.getValue()); + if (i == 0) { + // Fall back to parent file comparison if the local line numbers are the same. + // Not ideal, but we can't validate anything else here. + FilePathNode parent = getParent(); + FilePathNode otherParent = lineNode.getParent(); + if (parent != null && otherParent != null) + i = parent.localCompare(otherParent); + } + return i; + } return 0; } diff --git a/recaf-core/src/main/java/software/coley/recaf/path/ResourcePathNode.java b/recaf-core/src/main/java/software/coley/recaf/path/ResourcePathNode.java index f201f20d6..c831fc338 100644 --- a/recaf-core/src/main/java/software/coley/recaf/path/ResourcePathNode.java +++ b/recaf-core/src/main/java/software/coley/recaf/path/ResourcePathNode.java @@ -2,11 +2,17 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; +import software.coley.collections.Maps; +import software.coley.collections.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.Bundle; +import software.coley.recaf.workspace.model.resource.WorkspaceFileResource; import software.coley.recaf.workspace.model.resource.WorkspaceResource; import java.util.List; +import java.util.Map; +import java.util.Objects; import java.util.Set; /** @@ -27,7 +33,7 @@ public class ResourcePathNode extends AbstractPathNode bundle) { return new BundlePathNode(this, bundle); } + /** + * @return Path node for a container of multiple embedded resources. + */ + @Nonnull + public EmbeddedResourceContainerPathNode embeddedChildContainer() { + Workspace valueOfType = Objects.requireNonNull(getValueOfType(Workspace.class), + "Path did not contain workspace in parent"); + return new EmbeddedResourceContainerPathNode(this, valueOfType); + } + /** * @return {@code true} when this resource node, wraps the primary resource of a workspace. */ public boolean isPrimary() { - WorkspacePathNode parent = getParent(); + PathNode parent = getParent(); if (parent == null) return false; return parent.getValue().getPrimaryResource() == getValue(); } - @Override - public WorkspacePathNode getParent() { - return (WorkspacePathNode) super.getParent(); - } - @Nonnull @Override public Set directParentTypeIds() { @@ -80,22 +105,32 @@ public int localCompare(PathNode o) { if (this == o) return 0; if (o instanceof ResourcePathNode resourcePathNode) { + PathNode parent = getParent(); Workspace workspace = parentValue(); WorkspaceResource resource = getValue(); WorkspaceResource otherResource = resourcePathNode.getValue(); - if (workspace != null) { - if (resource == otherResource) - return 0; - // Show in order as in the workspace. - List resources = workspace.getAllResources(false); - return Integer.compare(resources.indexOf(resource), resources.indexOf(otherResource)); + if (parent instanceof EmbeddedResourceContainerPathNode) { + PathNode parentOfParent = Unchecked.cast(parent.getParent()); + Map lookup = Maps.reverse(parentOfParent.getValue().getEmbeddedResources()); + String ourKey = lookup.getOrDefault(resource, "?"); + String otherKey = lookup.getOrDefault(otherResource, "?"); + return CaseInsensitiveSimpleNaturalComparator.getInstance().compare(ourKey, otherKey); } else { - // Enforce some ordering. Not ideal but works. - return String.CASE_INSENSITIVE_ORDER.compare( - resource.getClass().getSimpleName(), - otherResource.getClass().getSimpleName() - ); + if (workspace != null) { + if (resource == otherResource) + return 0; + + // Show in order as in the workspace. + List resources = workspace.getAllResources(false); + return Integer.compare(resources.indexOf(resource), resources.indexOf(otherResource)); + } else { + // Enforce some ordering. Not ideal but works. + return CaseInsensitiveSimpleNaturalComparator.getInstance().compare( + resource.getClass().getSimpleName(), + otherResource.getClass().getSimpleName() + ); + } } } return 0; diff --git a/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java b/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java index e145ab531..266969529 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/assembler/ExpressionCompiler.java @@ -4,13 +4,17 @@ import dev.xdark.blw.type.*; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; import me.darknet.assembler.printer.JvmClassPrinter; import me.darknet.assembler.printer.JvmMethodPrinter; import me.darknet.assembler.printer.PrintContext; import org.objectweb.asm.Opcodes; +import org.slf4j.Logger; import regexodus.Matcher; import regexodus.Pattern; +import software.coley.recaf.Bootstrap; +import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.cdi.WorkspaceScoped; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.member.BasicLocalVariable; @@ -35,8 +39,9 @@ * * @author Matt Coley */ -@WorkspaceScoped +@Dependent public class ExpressionCompiler { + private static final Logger logger = Logging.get(ExpressionCompiler.class); private static final Pattern IMPORT_EXTRACT_PATTERN = RegexUtil.pattern("^\\s*(import \\w.+;)"); private static final String EXPR_MARKER = "/* EXPR_START */"; private final JavacCompiler javac; @@ -330,24 +335,32 @@ else if (AccessFlag.isPrivate(methodFlags)) // Stub out fields / methods for (FieldMember field : fields) { + // Skip stubbing compiler-generated fields. + if (field.hasBridgeModifier() || field.hasSyntheticModifier()) + continue; + // Skip stubbing of illegally named fields. String name = field.getName(); if (!isSafeName(name)) continue; - NameType fieldInfo = getInfo(name, field.getDescriptor()); - if (!isSafeClassName(fieldInfo.className)) + NameType fieldNameType = getInfo(name, field.getDescriptor()); + if (!isSafeClassName(fieldNameType.className)) continue; // Skip enum constants, we added those earlier. - if (fieldInfo.className.equals(className.replace('/', '.')) && field.hasFinalModifier() && field.hasStaticModifier()) + if (fieldNameType.className.equals(className.replace('/', '.')) && field.hasFinalModifier() && field.hasStaticModifier()) continue; // Append the field. The only modifier that we care about here is if it is static or not. if (field.hasStaticModifier()) code.append("static "); - code.append(fieldInfo.className).append(' ').append(fieldInfo.name).append(";\n"); + code.append(fieldNameType.className).append(' ').append(fieldNameType.name).append(";\n"); } for (MethodMember method : methods) { + // Skip stubbing compiler-generated methods. + if (method.hasBridgeModifier() || method.hasSyntheticModifier()) + continue; + // Skip stubbing of illegally named methods. String name = method.getName(); boolean isCtor = false; @@ -480,7 +493,7 @@ private NameType getInfo(@Nonnull String name, @Nonnull String descriptor) throw if (componentReturnType instanceof PrimitiveType primitiveParameter) { className = primitiveParameter.name(); } else if (componentReturnType instanceof InstanceType instanceType) { - className = instanceType.internalName().replace('/', '.'); + className = instanceType.internalName().replace('/', '.').replace('$', '.'); } else { throw new ExpressionCompileException("Illegal component type: " + componentReturnType); } @@ -488,7 +501,7 @@ private NameType getInfo(@Nonnull String name, @Nonnull String descriptor) throw size = 1; } else { size = 1; - className = Types.instanceTypeFromDescriptor(descriptor).internalName().replace('/', '.'); + className = Types.instanceTypeFromDescriptor(descriptor).internalName().replace('/', '.').replace('$', '.'); } return new NameType(size, name, className); } @@ -519,8 +532,16 @@ private LocalVariable findVar(int index) { private LocalVariable getParameterVariable(int parameterVarIndex, int parameterIndex) { LocalVariable parameterVariable = findVar(parameterVarIndex); if (parameterVariable == null) { - ClassType parameterType = methodType.parameterTypes().get(parameterIndex); + List parameterTypes = methodType.parameterTypes(); + ClassType parameterType; + if (parameterIndex < parameterTypes.size()) { + parameterType = parameterTypes.get(parameterIndex); + } else { + logger.warn("Could not resolve parameter variable (pVar={}, pIndex={}) in {}", parameterVarIndex, parameterIndex, methodName); + parameterType = Types.OBJECT; + } parameterVariable = new BasicLocalVariable(parameterVarIndex, "p" + parameterIndex, parameterType.descriptor(), null); + } return parameterVariable; } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/attach/BasicAttachManager.java b/recaf-core/src/main/java/software/coley/recaf/services/attach/BasicAttachManager.java index 51353dfff..cf674a6fc 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/attach/BasicAttachManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/attach/BasicAttachManager.java @@ -16,6 +16,7 @@ import software.coley.instrument.sock.SocketAvailability; import software.coley.instrument.util.Discovery; import software.coley.recaf.analytics.logging.Logging; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.util.DevDetection; import software.coley.recaf.util.StringUtil; import software.coley.recaf.util.threading.ThreadUtil; @@ -34,6 +35,7 @@ import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.TimeUnit; import java.util.jar.JarFile; @@ -55,7 +57,7 @@ public class BasicAttachManager implements AttachManager { private final Map virtualMachineMainClassMap = new ConcurrentHashMap<>(); private final Map virtualMachineJmxConnMap = new ConcurrentHashMap<>(); private final ObservableList virtualMachineDescriptors = new ObservableList<>(); - private final List postScanListeners = new ArrayList<>(); + private final List postScanListeners = new CopyOnWriteArrayList<>(); private final AttachManagerConfig config; private static ExtractState extractState = ExtractState.DEFAULT; @@ -311,8 +313,8 @@ public void scan() { } // Call listeners - for (PostScanListener listener : postScanListeners) - listener.onScanCompleted(toAdd, toRemove); + CollectionUtil.safeForEach(postScanListeners, listener -> listener.onScanCompleted(toAdd, toRemove), + (listener, t) -> logger.error("Exception thrown after scan completion", t)); }); } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/CallGraph.java b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/CallGraph.java index aebb9887e..ba669edb7 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/CallGraph.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/CallGraph.java @@ -13,13 +13,14 @@ import software.coley.recaf.analytics.logging.DebuggingLogger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.cdi.WorkspaceScoped; +import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.services.Service; import software.coley.recaf.util.MultiMap; import software.coley.recaf.util.threading.ThreadPoolFactory; -import software.coley.recaf.workspace.model.WorkspaceModificationListener; import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.WorkspaceModificationListener; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import software.coley.recaf.workspace.model.resource.ResourceJvmClassListener; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -104,6 +105,13 @@ public ClassMethodsContainer getClassMethodsContainer(@Nonnull JvmClassInfo clas return classToMethodsContainer.computeIfAbsent(classInfo, c -> new ClassMethodsContainer(classInfo)); } + @Nullable + public MethodVertex getVertex(@Nonnull MethodMember method) { + ClassInfo declaringClass = method.getDeclaringClass(); + if (declaringClass == null) return null; + return getClassMethodsContainer(declaringClass.asJvmClass()).getVertex(method); + } + /** * @param classInfo * Class to wrap. @@ -209,15 +217,15 @@ public void visitInvokeDynamicInsn(String name, String descriptor, Handle bootst * @param isInterface * Method interface flag. */ - private void onMethodCalled(MutableMethodVertex methodVertex, int opcode, String owner, String name, - String descriptor, boolean isInterface) { + private void onMethodCalled(@Nonnull MutableMethodVertex methodVertex, int opcode, @Nonnull String owner, + @Nonnull String name, @Nonnull String descriptor, boolean isInterface) { MethodRef ref = new MethodRef(owner, name, descriptor); // Resolve the method Result> resolutionResult = resolve(opcode, owner, name, descriptor, isInterface); // Handle result - if (resolutionResult != null && resolutionResult.isSuccess()) { + if (resolutionResult.isSuccess()) { // Extract vertex from resolution Resolution resolution = resolutionResult.value(); ClassMethodsContainer resolvedClass = getClassMethodsContainer(resolution.owner().innerValue()); @@ -256,16 +264,17 @@ private void onMethodCalled(MutableMethodVertex methodVertex, int opcode, String * Invoke interface flag. * * @return Resolution result of the method within the owner. - * {@code null} when the {@code owner} could not be found. + * The result {@link Result#isError()} will be {@code true} when the {@code owner} could not be found. */ - @Nullable - public Result> resolve(int opcode, String owner, String name, String descriptor, boolean isInterface) { + @Nonnull + public Result> resolve(int opcode, @Nonnull String owner, @Nonnull String name, + @Nonnull String descriptor, boolean isInterface) { JvmClassInfo ownerClass = lookup.apply(owner); // Skip if we cannot resolve owner if (ownerClass == null) { unresolvedCalls.put(owner, new MethodRef(owner, name, descriptor)); - return null; + return Result.error(ResolutionError.NO_SUCH_METHOD); } Result> resolutionResult; @@ -372,7 +381,7 @@ static class MutableMethodVertex implements MethodVertex { private final MethodRef method; private final MethodMember resolvedMethod; - MutableMethodVertex(MethodRef method, MethodMember resolvedMethod) { + MutableMethodVertex(@Nonnull MethodRef method, @Nonnull MethodMember resolvedMethod) { this.method = method; this.resolvedMethod = resolvedMethod; } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassLookup.java b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassLookup.java index a398bcbd9..1f774988a 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassLookup.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassLookup.java @@ -25,7 +25,9 @@ public ClassLookup(@Nonnull Workspace workspace) { @Override public JvmClassInfo apply(String name) { + if (name == null) return null; ClassPathNode classPath = workspace.findJvmClass(name); + if (classPath == null) classPath = workspace.findLatestVersionedJvmClass(name); if (classPath == null) return null; return classPath.getValue().asJvmClass(); } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassMethodsContainer.java b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassMethodsContainer.java index 92b1150a5..0d3d7509c 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassMethodsContainer.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/ClassMethodsContainer.java @@ -32,6 +32,7 @@ public ClassMethodsContainer(@Nonnull JvmClassInfo jvmClass) { /** * @return Wrapped {@link JvmClassInfo}. */ + @Nonnull public JvmClassInfo getJvmClass() { return jvmClass; } @@ -39,6 +40,7 @@ public JvmClassInfo getJvmClass() { /** * @return Collection of method vertices within this class. */ + @Nonnull public Collection getVertices() { return methodVertices.values(); } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/LinkedClass.java b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/LinkedClass.java index 7b909e87a..0faf3453b 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/LinkedClass.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/LinkedClass.java @@ -3,6 +3,7 @@ import dev.xdark.jlinker.ClassInfo; import dev.xdark.jlinker.MemberInfo; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import software.coley.recaf.analytics.logging.DebuggingLogger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.JvmClassInfo; @@ -29,7 +30,6 @@ public class LinkedClass implements ClassInfo { private final BiFunction> fieldLookup; private final BiFunction> methodLookup; - @SuppressWarnings("all") // Do not 'optimize' by using <> since it crashes javac on Java 11 (JDK-8212586) public LinkedClass(@Nonnull ClassLookup lookup, @Nonnull JvmClassInfo info) { this.info = info; @@ -59,7 +59,7 @@ public LinkedClass(@Nonnull ClassLookup lookup, @Nonnull JvmClassInfo info) { return null; } - return new MemberInfo() { + return new MemberInfo<>() { @Override public FieldMember innerValue() { return declaredField; @@ -83,7 +83,7 @@ public boolean isPolymorphic() { logger.debugging(l -> l.warn("Missing declared method: {}{}", name, descriptor)); return null; } - return new MemberInfo() { + return new MemberInfo<>() { @Override public MethodMember innerValue() { return declaredMethod; @@ -112,10 +112,12 @@ public int accessFlags() { return info.getAccess(); } - @Nonnull + @Nullable @Override public ClassInfo superClass() { - return superClassLookup.apply(info.getSuperName()); + String superName = info.getSuperName(); + if (superName == null) return null; + return superClassLookup.apply(superName); } @Nonnull diff --git a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodRef.java b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodRef.java index 5412c4ea6..12d17357f 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodRef.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodRef.java @@ -5,77 +5,13 @@ /** * Basic method outline. * + * @param owner + * Method reference owner. + * @param name + * Method name. + * @param desc + * Method descriptor. + * * @author Amejonah */ -public class MethodRef { - private final String owner; - private final String name; - private final String desc; - - /** - * @param owner - * Method reference owner. - * @param name - * Method name. - * @param desc - * Method descriptor. - */ - public MethodRef(@Nonnull String owner, @Nonnull String name, @Nonnull String desc) { - this.owner = owner; - this.name = name; - this.desc = desc; - } - - /** - * @return Method reference owner. - */ - @Nonnull - public String getOwner() { - return owner; - } - - /** - * @return Method name. - */ - @Nonnull - public String getName() { - return name; - } - - /** - * @return Method descriptor. - */ - @Nonnull - public String getDesc() { - return desc; - } - - @Override - public boolean equals(Object o) { - if (this == o) return true; - if (o == null || getClass() != o.getClass()) return false; - - MethodRef methodRef = (MethodRef) o; - - if (!owner.equals(methodRef.owner)) return false; - if (!name.equals(methodRef.name)) return false; - return desc.equals(methodRef.desc); - } - - @Override - public int hashCode() { - int result = owner.hashCode(); - result = 31 * result + name.hashCode(); - result = 31 * result + desc.hashCode(); - return result; - } - - @Override - public String toString() { - return "MethodRef{" + - "owner='" + owner + '\'' + - ", name='" + name + '\'' + - ", desc='" + desc + '\'' + - '}'; - } -} +public record MethodRef(@Nonnull String owner, @Nonnull String name, @Nonnull String desc) {} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodVertex.java b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodVertex.java index 81822adeb..6216d36ae 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodVertex.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/callgraph/MethodVertex.java @@ -11,7 +11,7 @@ * * @author Amejonah */ -interface MethodVertex { +public interface MethodVertex { /** * @return Basic method details. */ diff --git a/recaf-core/src/main/java/software/coley/recaf/services/comment/CommentManager.java b/recaf-core/src/main/java/software/coley/recaf/services/comment/CommentManager.java index 1d3df387c..d898cc997 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/comment/CommentManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/comment/CommentManager.java @@ -28,6 +28,7 @@ import software.coley.recaf.services.json.GsonProvider; import software.coley.recaf.services.mapping.*; import software.coley.recaf.services.workspace.WorkspaceManager; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.util.StringUtil; import software.coley.recaf.util.TestEnvironment; import software.coley.recaf.workspace.model.Workspace; @@ -36,9 +37,11 @@ import java.nio.file.Files; import java.nio.file.Path; import java.util.HashSet; +import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.CopyOnWriteArrayList; /** * Manager for comment tracking on {@link ClassInfo} and {@link ClassMember} content in workspaces. @@ -54,8 +57,8 @@ public class CommentManager implements Service, CommentUpdateListener, CommentCo private final Map delegatingMap = new ConcurrentHashMap<>(); /** Map of workspace comment impls modeling only data. Used for persistence. */ private final Map persistMap = new ConcurrentHashMap<>(); - private final Set commentUpdateListeners = new HashSet<>(); - private final Set commentContainerListeners = new HashSet<>(); + private final List commentUpdateListeners = new CopyOnWriteArrayList<>(); + private final List commentContainerListeners = new CopyOnWriteArrayList<>(); private final WorkspaceManager workspaceManager; private final RecafDirectoriesConfig directoriesConfig; private final CommentManagerConfig config; @@ -63,8 +66,8 @@ public class CommentManager implements Service, CommentUpdateListener, CommentCo @Inject public CommentManager(@Nonnull DecompilerManager decompilerManager, @Nonnull WorkspaceManager workspaceManager, - @Nonnull MappingListeners mappingListeners, @Nonnull GsonProvider gsonProvider, - @Nonnull RecafDirectoriesConfig directoriesConfig, @Nonnull CommentManagerConfig config) { + @Nonnull MappingListeners mappingListeners, @Nonnull GsonProvider gsonProvider, + @Nonnull RecafDirectoriesConfig directoriesConfig, @Nonnull CommentManagerConfig config) { this.workspaceManager = workspaceManager; this.gsonProvider = gsonProvider; this.directoriesConfig = directoriesConfig; @@ -333,52 +336,32 @@ private void onShutdown() { @Override public void onClassCommentUpdated(@Nonnull ClassPathNode path, @Nullable String comment) { - for (CommentUpdateListener listener : commentUpdateListeners) - try { - listener.onClassCommentUpdated(path, comment); - } catch (Throwable t) { - logger.error("Uncaught exception in handling of comment update for '{}'", path.getValue().getName(), t); - } + CollectionUtil.safeForEach(commentUpdateListeners, listener -> listener.onClassCommentUpdated(path, comment), + (listener, t) -> logger.error("Exception thrown when updating class comment", t)); } @Override public void onFieldCommentUpdated(@Nonnull ClassMemberPathNode path, @Nullable String comment) { - for (CommentUpdateListener listener : commentUpdateListeners) - try { - listener.onFieldCommentUpdated(path, comment); - } catch (Throwable t) { - logger.error("Uncaught exception in handling of comment update for '{}'", path.getValue().getName(), t); - } + CollectionUtil.safeForEach(commentUpdateListeners, listener -> listener.onFieldCommentUpdated(path, comment), + (listener, t) -> logger.error("Exception thrown when updating field comment", t)); } @Override public void onMethodCommentUpdated(@Nonnull ClassMemberPathNode path, @Nullable String comment) { - for (CommentUpdateListener listener : commentUpdateListeners) - try { - listener.onMethodCommentUpdated(path, comment); - } catch (Throwable t) { - logger.error("Uncaught exception in handling of comment update for '{}'", path.getValue().getName(), t); - } + CollectionUtil.safeForEach(commentUpdateListeners, listener -> listener.onMethodCommentUpdated(path, comment), + (listener, t) -> logger.error("Exception thrown when updating method comment", t)); } @Override public void onClassContainerCreated(@Nonnull ClassPathNode path, @Nullable ClassComments comments) { - for (CommentContainerListener listener : commentContainerListeners) - try { - listener.onClassContainerCreated(path, comments); - } catch (Throwable t) { - logger.error("Uncaught exception in handling of comment container creation for '{}'", path.getValue().getName(), t); - } + CollectionUtil.safeForEach(commentContainerListeners, listener -> listener.onClassContainerCreated(path, comments), + (listener, t) -> logger.error("Exception thrown when creating class comment container", t)); } @Override public void onClassContainerRemoved(@Nonnull ClassPathNode path, @Nullable ClassComments comments) { - for (CommentContainerListener listener : commentContainerListeners) - try { - listener.onClassContainerRemoved(path, comments); - } catch (Throwable t) { - logger.error("Uncaught exception in handling of comment container removal for '{}'", path.getValue().getName(), t); - } + CollectionUtil.safeForEach(commentContainerListeners, listener -> listener.onClassContainerRemoved(path, comments), + (listener, t) -> logger.error("Exception thrown when removing class comment container", t)); } /** @@ -487,7 +470,7 @@ private Path getCommentsDirectory() { @Nonnull private DelegatingWorkspaceComments newDelegatingWorkspaceComments(@Nonnull Workspace workspace, - @Nonnull PersistWorkspaceComments persistComments) { + @Nonnull PersistWorkspaceComments persistComments) { DelegatingWorkspaceComments delegatingComments = new DelegatingWorkspaceComments(this, persistComments); // Initialize delegate class comment models for entries in the persist model. diff --git a/recaf-core/src/main/java/software/coley/recaf/services/comment/DelegatingWorkspaceComments.java b/recaf-core/src/main/java/software/coley/recaf/services/comment/DelegatingWorkspaceComments.java index 6cd112856..70041707d 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/comment/DelegatingWorkspaceComments.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/comment/DelegatingWorkspaceComments.java @@ -2,8 +2,8 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import software.coley.collections.Unchecked; import software.coley.recaf.path.ClassPathNode; -import software.coley.recaf.util.Unchecked; import java.util.Iterator; import java.util.Map; diff --git a/recaf-core/src/main/java/software/coley/recaf/services/comment/PersistWorkspaceComments.java b/recaf-core/src/main/java/software/coley/recaf/services/comment/PersistWorkspaceComments.java index 33d79b1eb..8ea81dbdd 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/comment/PersistWorkspaceComments.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/comment/PersistWorkspaceComments.java @@ -2,8 +2,8 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import software.coley.collections.Unchecked; import software.coley.recaf.path.ClassPathNode; -import software.coley.recaf.util.Unchecked; import java.util.Collection; import java.util.Iterator; diff --git a/recaf-core/src/main/java/software/coley/recaf/services/config/ConfigManager.java b/recaf-core/src/main/java/software/coley/recaf/services/config/ConfigManager.java index 8820d3f6d..9e7115583 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/config/ConfigManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/config/ConfigManager.java @@ -1,6 +1,9 @@ package software.coley.recaf.services.config; -import com.google.gson.*; +import com.google.gson.Gson; +import com.google.gson.JsonArray; +import com.google.gson.JsonElement; +import com.google.gson.JsonObject; import com.google.gson.stream.JsonReader; import com.google.gson.stream.JsonWriter; import jakarta.annotation.Nonnull; @@ -14,16 +17,18 @@ import software.coley.recaf.config.ConfigCollectionValue; import software.coley.recaf.config.ConfigContainer; import software.coley.recaf.config.ConfigValue; -import software.coley.recaf.services.json.GsonProvider; import software.coley.recaf.services.Service; import software.coley.recaf.services.ServiceConfig; import software.coley.recaf.services.file.RecafDirectoriesConfig; +import software.coley.recaf.services.json.GsonProvider; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.util.TestEnvironment; import java.io.IOException; import java.nio.file.Files; import java.nio.file.Path; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; /** * Tracker for all {@link ConfigContainer} instances. @@ -36,14 +41,14 @@ public class ConfigManager implements Service { public static final String SERVICE_ID = "config-manager"; private static final Logger logger = Logging.get(ConfigManager.class); private final Map containers = new TreeMap<>(); - private final List listeners = new ArrayList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); private final ConfigManagerConfig config; private final RecafDirectoriesConfig fileConfig; private final GsonProvider gsonProvider; @Inject public ConfigManager(@Nonnull ConfigManagerConfig config, @Nonnull RecafDirectoriesConfig fileConfig, - @Nonnull GsonProvider gsonProvider, @Nonnull Instance containers) { + @Nonnull GsonProvider gsonProvider, @Nonnull Instance containers) { this.config = config; this.fileConfig = fileConfig; this.gsonProvider = gsonProvider; @@ -159,8 +164,8 @@ public void registerContainer(@Nonnull ConfigContainer container) { containers.put(id, container); // Alert listeners when content added - for (ManagedConfigListener listener : listeners) - listener.onRegister(container); + CollectionUtil.safeForEach(listeners, listener -> listener.onRegister(container), + (listener, t) -> logger.error("Exception thrown when registering container '{}'", container.getId(), t)); } /** @@ -172,8 +177,8 @@ public void unregisterContainer(@Nonnull ConfigContainer container) { // Alert listeners when content removed if (removed != null) { - for (ManagedConfigListener listener : listeners) - listener.onUnregister(removed); + CollectionUtil.safeForEach(listeners, listener -> listener.onUnregister(container), + (listener, t) -> logger.error("Exception thrown when unregistering container '{}'", container.getId(), t)); } } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/AbstractJvmDecompiler.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/AbstractJvmDecompiler.java index e10c09d5a..a517f4159 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/decompile/AbstractJvmDecompiler.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/AbstractJvmDecompiler.java @@ -39,7 +39,7 @@ public boolean addJvmBytecodeFilter(@Nonnull JvmBytecodeFilter filter) { @Override public boolean removeJvmBytecodeFilter(@Nonnull JvmBytecodeFilter filter) { - return bytecodeFilters.add(filter); + return bytecodeFilters.remove(filter); } @Nonnull diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/FallbackConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/FallbackConfig.java new file mode 100644 index 000000000..28f5cc209 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/FallbackConfig.java @@ -0,0 +1,19 @@ +package software.coley.recaf.services.decompile.fallback; + +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import software.coley.recaf.services.decompile.BaseDecompilerConfig; + +/** + * Config for {@link FallbackDecompiler} + * + * @author Matt Coley + */ +@ApplicationScoped +public class FallbackConfig extends BaseDecompilerConfig { + @Inject + public FallbackConfig() { + super("decompiler-fallback" + CONFIG_SUFFIX); + registerConfigValuesHashUpdates(); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/FallbackDecompiler.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/FallbackDecompiler.java new file mode 100644 index 000000000..f7a3829a0 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/FallbackDecompiler.java @@ -0,0 +1,43 @@ +package software.coley.recaf.services.decompile.fallback; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.services.decompile.AbstractJvmDecompiler; +import software.coley.recaf.services.decompile.DecompileResult; +import software.coley.recaf.services.decompile.fallback.print.ClassPrinter; +import software.coley.recaf.services.text.TextFormatConfig; +import software.coley.recaf.workspace.model.Workspace; + +/** + * Fallback decompiler implementation. + * + * @author Matt Coley + */ +@ApplicationScoped +public class FallbackDecompiler extends AbstractJvmDecompiler { + public static final String NAME = "Fallback"; + private static final String VERSION = "1.0.0"; + private final TextFormatConfig formatConfig; + + /** + * New Procyon decompiler instance. + * + * @param config + * Config instance. + */ + @Inject + public FallbackDecompiler(@Nonnull FallbackConfig config, @Nonnull TextFormatConfig formatConfig) { + super(NAME, VERSION, config); + this.formatConfig = formatConfig; + } + + @Nonnull + @Override + protected DecompileResult decompileInternal(@Nonnull Workspace workspace, @Nonnull JvmClassInfo classInfo) { + String decompile = new ClassPrinter(formatConfig, classInfo).print(); + int configHash = getConfig().getHash(); + return new DecompileResult(decompile, configHash); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/ClassPrinter.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/ClassPrinter.java new file mode 100644 index 000000000..6cabe8d7f --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/ClassPrinter.java @@ -0,0 +1,559 @@ +package software.coley.recaf.services.decompile.fallback.print; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.info.annotation.AnnotationElement; +import software.coley.recaf.info.member.FieldMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.services.text.TextFormatConfig; +import software.coley.recaf.util.AccessFlag; +import software.coley.recaf.util.StringUtil; + +import java.util.*; +import java.util.stream.Collectors; + +/** + * Basic class printer. + * + * @author Matt Coley + */ +public class ClassPrinter { + private final TextFormatConfig format; + private final JvmClassInfo classInfo; + + /** + * @param format + * Format config. + * @param classInfo + * Class to print. + */ + public ClassPrinter(@Nonnull TextFormatConfig format, @Nonnull JvmClassInfo classInfo) { + this.format = format; + this.classInfo = classInfo; + } + + /** + * @return Formatted class output. + */ + @Nonnull + public String print() { + Printer out = new Printer(); + appendPackage(out); + appendImports(out); + appendDeclaration(out); + appendMembers(out); + return out.toString(); + } + + /** + * Appends the package name to the output. + * + * @param out + * Printer to write to. + */ + private void appendPackage(@Nonnull Printer out) { + String className = classInfo.getName(); + if (className.contains("/")) { + String packageName = format.filterEscape(className.substring(0, className.lastIndexOf('/'))); + out.appendLine("package " + packageName.replace('/', '.') + ";"); + out.newLine(); + } + } + + /** + * Appends each imported class to the output. + * + * @param out + * Printer to write to. + */ + private void appendImports(@Nonnull Printer out) { + String lastRootPackage = null; + NavigableSet referencedClasses = classInfo.getReferencedClasses(); + boolean hasImports = false; + for (String referencedClass : referencedClasses) { + // Skip classes in the default package. + if (!referencedClass.contains("/")) continue; + + // Skip core classes that are implicitly imported. + if (referencedClass.startsWith("java/lang/")) continue; + + // Skip self. + if (referencedClass.equals(classInfo.getName())) continue; + + // Break root package imports up for clarity. For example: + // - com.* + // - org.* + // Between these two import groups will be a blank line. + String rootPackage = referencedClass.substring(0, referencedClass.indexOf('/')); + if (lastRootPackage == null) lastRootPackage = rootPackage; + if (!rootPackage.equals(lastRootPackage)) { + out.newLine(); + lastRootPackage = rootPackage; + } + + // Add import + out.appendLine("import " + format.filterEscape(referencedClass.replace('/', '.')) + ";"); + hasImports = true; + + // TODO: Import names aren't always correct since '$' should also be escaped when it represents the separation of + // an outer and inner class. Since we have workspace and runtime access we 'should' check this + // and attempt to make more accurate output + } + if (hasImports) out.newLine(); + } + + /** + * Appends the class declaration to the output. + * + * @param out + * Printer to write to. + */ + private void appendDeclaration(@Nonnull Printer out) { + appendDeclarationAnnotations(out); + if (classInfo.hasEnumModifier()) { + appendEnumDeclaration(out); + } else if (classInfo.hasAnnotationModifier()) { + appendAnnotationDeclaration(out); + } else if (classInfo.hasInterfaceModifier()) { + appendInterfaceDeclaration(out); + } else { + appendStandardDeclaration(out); + } + } + + /** + * Appends class annotations to the output. + * + * @param out + * Printer to write to. + */ + private void appendDeclarationAnnotations(@Nonnull Printer out) { + String annotations = PrintUtils.annotationsToString(format, classInfo); + if (!annotations.isBlank()) out.appendMultiLine(annotations); + } + + /** + * Appends the enum formatted declaration to the output. + * + * @param out + * Printer to write to. + */ + private void appendEnumDeclaration(@Nonnull Printer out) { + int acc = classInfo.getAccess(); + + // Get flag-set and remove 'enum' and 'final'. + // We will add 'enum' ourselves, and 'final' is redundant. + Set flagSet = AccessFlag.getApplicableFlags(AccessFlag.Type.CLASS, acc); + flagSet.remove(AccessFlag.ACC_ENUM); + flagSet.remove(AccessFlag.ACC_FINAL); + String decFlagsString = AccessFlag.sortAndToString(AccessFlag.Type.CLASS, flagSet); + StringBuilder sb = new StringBuilder(); + if (decFlagsString.isBlank()) { + sb.append("enum "); + } else { + sb.append(decFlagsString).append(" enum "); + } + sb.append(format.filter(classInfo.getName())); + String superName = classInfo.getSuperName(); + + // Should normally extend enum. Technically bytecode allows for other types if those at runtime then + // inherit from Enum. + if (superName != null && !superName.equals("java/lang/Enum")) { + sb.append(" extends ").append(format.filter(superName)); + } + if (!classInfo.getInterfaces().isEmpty()) { + sb.append(" implements "); + String interfaces = classInfo.getInterfaces().stream() + .map(format::filter) + .collect(Collectors.joining(", ")); + sb.append(interfaces); + } + out.appendLine(sb.toString()); + } + + /** + * Appends the annotation formatted declaration to the output. + * + * @param out + * Printer to write to. + */ + private void appendAnnotationDeclaration(@Nonnull Printer out) { + int acc = classInfo.getAccess(); + + // Get flag-set and remove 'interface' and 'abstract'. + // We will add 'interface' ourselves, and 'abstract' is redundant. + Set flagSet = AccessFlag.getApplicableFlags(AccessFlag.Type.CLASS, acc); + flagSet.remove(AccessFlag.ACC_ANNOTATION); + flagSet.remove(AccessFlag.ACC_INTERFACE); + flagSet.remove(AccessFlag.ACC_ABSTRACT); + String decFlagsString = AccessFlag.sortAndToString(AccessFlag.Type.CLASS, flagSet); + StringBuilder sb = new StringBuilder(); + if (decFlagsString.isBlank()) { + sb.append("@interface "); + } else { + sb.append(decFlagsString).append(" @interface "); + } + sb.append(format.filter(classInfo.getName())); + out.appendLine(sb.toString()); + } + + /** + * Appends the interface formatted declaration to the output. + * + * @param out + * Printer to write to. + */ + private void appendInterfaceDeclaration(@Nonnull Printer out) { + int acc = classInfo.getAccess(); + + // Get flag-set and remove 'interface' and 'abstract'. + // We will add 'interface' ourselves, and 'abstract' is redundant. + Set flagSet = AccessFlag.getApplicableFlags(AccessFlag.Type.CLASS, acc); + flagSet.remove(AccessFlag.ACC_INTERFACE); + flagSet.remove(AccessFlag.ACC_ABSTRACT); + String decFlagsString = AccessFlag.sortAndToString(AccessFlag.Type.CLASS, flagSet); + StringBuilder sb = new StringBuilder(); + if (decFlagsString.isBlank()) { + sb.append("interface "); + } else { + sb.append(decFlagsString) + .append(" interface "); + } + sb.append(format.filter(classInfo.getName())); + if (!classInfo.getInterfaces().isEmpty()) { + // Interfaces use 'extends' rather than 'implements'. + sb.append(" extends "); + String interfaces = classInfo.getInterfaces().stream() + .map(format::filter) + .collect(Collectors.joining(", ")); + sb.append(interfaces); + } + out.appendLine(sb.toString()); + } + + /** + * Appends the class formatted declaration to the output. + * + * @param out + * Printer to write to. + */ + private void appendStandardDeclaration(@Nonnull Printer out) { + int acc = classInfo.getAccess(); + String decFlagsString = AccessFlag.sortAndToString(AccessFlag.Type.CLASS, acc); + StringBuilder sb = new StringBuilder(); + if (decFlagsString.isBlank()) { + sb.append("class "); + } else { + sb.append(decFlagsString).append(" class "); + } + sb.append(format.filter(classInfo.getName())); + String superName = classInfo.getSuperName(); + if (superName != null && !superName.equals("java/lang/Object")) { + sb.append(" extends ").append(format.filter(superName)); + } + if (!classInfo.getInterfaces().isEmpty()) { + sb.append(" implements "); + String interfaces = classInfo.getInterfaces().stream() + .map(format::filter) + .collect(Collectors.joining(", ")); + sb.append(interfaces); + } + out.appendLine(sb.toString()); + } + + /** + * Appends the class body (members). + * + * @param out + * Printer to write to. + */ + private void appendMembers(@Nonnull Printer out) { + out.appendLine("{"); + if (!classInfo.getFields().isEmpty()) { + Printer fieldPrinter = new Printer(); + fieldPrinter.setIndent(" "); + if (classInfo.hasEnumModifier()) { + appendEnumFieldMembers(fieldPrinter); + } else { + appendFieldMembers(fieldPrinter); + } + out.appendMultiLine(fieldPrinter.toString()); + out.appendLine(""); + } + if (!classInfo.getMethods().isEmpty()) { + Printer methodPrinter = new Printer(); + methodPrinter.setIndent(" "); + + // Some method types we'll want to handle a bit differently. + // Split them up: + // - Regular methods + // - The static initializer + // - Constructors + List methods = new ArrayList<>(classInfo.getMethods()); + MethodMember staticInitializer = classInfo.getDeclaredMethod("", "()V"); + List constructors = classInfo.methodStream() + .filter(m -> m.getName().equals("")) + .toList(); + methods.remove(staticInitializer); + methods.removeAll(constructors); + + // We'll place the static initializer first regardless of where its defined order-wise. + if (staticInitializer != null) { + appendStaticInitializer(methodPrinter, staticInitializer); + methodPrinter.newLine(); + } + + // Then the constructors. + for (MethodMember constructor : constructors) { + appendConstructor(methodPrinter, constructor); + methodPrinter.newLine(); + } + + // Then the rest of the methods, in whatever order they're defined in. + for (MethodMember method : methods) { + appendMethod(methodPrinter, method); + methodPrinter.newLine(); + } + + // Append them all to the output. + out.appendMultiLine(methodPrinter.toString()); + } + out.appendLine("}"); + } + + /** + * Appends all fields in the class. + * + * @param out + * Printer to write to. + * + * @see #appendEnumFieldMembers(Printer) To be used when the current class is an enum. + */ + private void appendFieldMembers(@Nonnull Printer out) { + for (FieldMember field : classInfo.getFields()) { + appendField(out, field); + } + } + + /** + * Appends all enum constants, then other fields in the class. + * + * @param out + * Printer to write to. + * + * @see #appendEnumFieldMembers(Printer) To be used when the current class is not an enum. + */ + private void appendEnumFieldMembers(@Nonnull Printer out) { + // Filter out enum constants + List enumConstFields = new ArrayList<>(); + List otherFields = new ArrayList<>(); + for (FieldMember field : classInfo.getFields()) { + if (isEnumConst(field)) { + enumConstFields.add(field); + } else { + otherFields.add(field); + } + } + + // Print enum constants first. + for (int i = 0; i < enumConstFields.size(); i++) { + String suffix = i == enumConstFields.size() - 1 ? ";\n" : ", "; + FieldMember enumConst = enumConstFields.get(i); + StringBuilder sb = new StringBuilder(); + String annotations = PrintUtils.annotationsToString(format, enumConst); + if (!annotations.isBlank()) + sb.append(annotations).append('\n'); + sb.append(enumConst.getName()).append(suffix); + out.appendMultiLine(sb.toString()); + } + out.newLine(); + + // And then the rest of the fields + for (FieldMember field : otherFields) { + appendField(out, field); + } + } + + /** + * Appends the given field. + * + * @param out + * Printer to write to. + * @param field + * Field to write to the given printer. + */ + private void appendField(@Nonnull Printer out, @Nonnull FieldMember field) { + StringBuilder declaration = new StringBuilder(); + + // Append annotations to builder. + String annotations = PrintUtils.annotationsToString(format, field); + if (!annotations.isBlank()) + declaration.append(annotations).append('\n'); + + // Append flags to builder. + Collection flags = AccessFlag.getApplicableFlags(AccessFlag.Type.FIELD, field.getAccess()); + flags.remove(AccessFlag.ACC_ENUM); // We don't want to print 'enum' as a flag + flags = AccessFlag.sort(AccessFlag.Type.FIELD, flags); + if (!flags.isEmpty()) + declaration.append(AccessFlag.toString(flags)).append(' '); + + // Append type + name to builder. + Type type = Type.getType(field.getDescriptor()); + String typeName = format.filter(type.getClassName()); + if (typeName.contains(".")) + typeName = typeName.substring(typeName.lastIndexOf(".") + 1); + declaration.append(typeName).append(' ').append(format.filter(field.getName())); + + // Append value to builder. + Object value = field.getDefaultValue(); + if (value != null) { + switch (value) { + case String s -> value = "\"" + format.filter(s) + "\""; + case Float v -> value = value + "F"; + case Long l -> value = value + "L"; + default -> { + // No change + } + } + declaration.append(" = ").append(value); + } + + // Cap it off. + declaration.append(';'); + out.appendMultiLine(declaration.toString()); + } + + /** + * @param field + * Field to check. + * + * @return {@code true} when it is an enum constant of the {@link #classInfo current class}. + */ + private boolean isEnumConst(@Nonnull FieldMember field) { + String descriptor = field.getDescriptor(); + if (descriptor.length() < 3) return false; + + String type = descriptor.substring(1, descriptor.length() - 1); + + // Must be same type as declaring class. + if (!type.equals(classInfo.getName())) return false; + + // Must have enum const flags + return AccessFlag.hasAll(field.getAccess(), AccessFlag.ACC_STATIC, AccessFlag.ACC_FINAL); + } + + /** + * Appends the given static initializer method. + * + * @param out + * Printer to write to. + * @param method + * Static initializer method. + */ + private void appendStaticInitializer(@Nonnull Printer out, @Nonnull MethodMember method) { + MethodPrinter clinitPrinter = new MethodPrinter(format, classInfo, method) { + @Override + protected void buildDeclarationFlags(@Nonnull StringBuilder sb) { + // Force only printing the modifier 'static' even if other flags are present + sb.append("static "); + } + + @Override + protected void buildDeclarationReturnType(@Nonnull StringBuilder sb) { + // no-op + } + + @Override + protected void buildDeclarationName(@Nonnull StringBuilder sb) { + // no-op + } + + @Override + protected void buildDeclarationArgs(@Nonnull StringBuilder sb) { + // no-op + } + + @Override + protected void buildDeclarationThrows(@Nonnull StringBuilder sb) { + // no-op + } + }; + out.appendMultiLine(clinitPrinter.print()); + } + + /** + * Appends the given constructor method. + * + * @param out + * Printer to write to. + * @param method + * Constructor method. + */ + private void appendConstructor(@Nonnull Printer out, @Nonnull MethodMember method) { + MethodPrinter constructorPrinter = new MethodPrinter(format, classInfo, method) { + @Override + protected void buildDeclarationReturnType(@Nonnull StringBuilder sb) { + // no-op + } + + @Override + protected void buildDeclarationName(@Nonnull StringBuilder sb) { + // The name is always the class name + sb.append(format.filterEscape(StringUtil.shortenPath(classInfo.getName()))); + } + }; + out.appendMultiLine(constructorPrinter.print()); + } + + /** + * Appends the given method. + * + * @param out + * Printer to write to. + * @param method + * Regular method. + */ + private void appendMethod(@Nonnull Printer out, @Nonnull MethodMember method) { + if (classInfo.hasAnnotationModifier()) { + MethodPrinter constructorPrinter = new MethodPrinter(format, classInfo, method) { + @Override + protected void buildDeclarationFlags(@Nonnull StringBuilder sb) { + // no-op since all methods are 'public abstract' per interface contract (with additional restrictions) + } + + @Override + protected void appendAbstractBody(@Nonnull StringBuilder sb) { + AnnotationElement annotationDefault = method.getAnnotationDefault(); + if (annotationDefault != null) { + sb.append(" default ").append(PrintUtils.elementToString(format, annotationDefault)).append(";"); + } else { + sb.append(";"); + } + } + }; + out.appendMultiLine(constructorPrinter.print()); + } else if (classInfo.hasInterfaceModifier()) { + MethodPrinter constructorPrinter = new MethodPrinter(format, classInfo, method) { + @Override + protected void buildDeclarationFlags(@Nonnull StringBuilder sb) { + Collection flags = AccessFlag.getApplicableFlags(AccessFlag.Type.METHOD, method.getAccess()); + flags = AccessFlag.sort(AccessFlag.Type.METHOD, flags); + flags.remove(AccessFlag.ACC_PUBLIC); + flags.remove(AccessFlag.ACC_ABSTRACT); + boolean isAbstract = AccessFlag.isAbstract(method.getAccess()); + if (!flags.isEmpty()) { + String flagsStr = AccessFlag.toString(flags); + if (!isAbstract) + sb.append("default "); + sb.append(flagsStr).append(' '); + } else if (!isAbstract) + sb.append("default "); + } + }; + out.appendMultiLine(constructorPrinter.print()); + } else { + out.appendMultiLine(new MethodPrinter(format, classInfo, method).print()); + } + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/MethodPrinter.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/MethodPrinter.java new file mode 100644 index 000000000..80ed0c06d --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/MethodPrinter.java @@ -0,0 +1,259 @@ +package software.coley.recaf.services.decompile.fallback.print; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.ClassVisitor; +import org.objectweb.asm.MethodVisitor; +import org.objectweb.asm.Type; +import org.objectweb.asm.util.Textifier; +import org.objectweb.asm.util.TraceMethodVisitor; +import software.coley.recaf.RecafConstants; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.info.member.LocalVariable; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.services.text.TextFormatConfig; +import software.coley.recaf.util.AccessFlag; +import software.coley.recaf.util.StringUtil; +import software.coley.recaf.util.visitors.MemberFilteringVisitor; + +import java.io.ByteArrayOutputStream; +import java.io.PrintWriter; +import java.nio.charset.StandardCharsets; +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * Utility for printing method bodies. + * + * @author Matt Coley + */ +public class MethodPrinter { + private final TextFormatConfig format; + private final JvmClassInfo classInfo; + private final MethodMember method; + + /** + * @param format + * Format config. + * @param classInfo + * Class containing the method. + * @param method + * Method to print. + */ + public MethodPrinter(@Nonnull TextFormatConfig format, @Nonnull JvmClassInfo classInfo, @Nonnull MethodMember method) { + this.format = format; + this.classInfo = classInfo; + this.method = method; + } + + /** + * @return Method string representation. + */ + @Nonnull + public String print() { + StringBuilder sb = new StringBuilder(); + appendAnnotations(sb); + appendDeclaration(sb); + if (AccessFlag.isNative(method.getAccess()) || AccessFlag.isAbstract(method.getAccess())) { + appendAbstractBody(sb); + } else { + appendBody(sb); + } + return sb.toString(); + } + + /** + * Appends annotations on the method declaration to the printer. + * + * @param sb + * Builder to add to. + */ + protected void appendAnnotations(@Nonnull StringBuilder sb) { + String annotations = PrintUtils.annotationsToString(format, method); + if (!annotations.isBlank()) sb.append(annotations).append('\n'); + } + + /** + * Appends the method declaration to the printer. + *
    + *
  1. {@link #buildDeclarationFlags(StringBuilder)}
  2. + *
  3. {@link #buildDeclarationReturnType(StringBuilder)}
  4. + *
  5. {@link #buildDeclarationName(StringBuilder)}
  6. + *
  7. {@link #buildDeclarationArgs(StringBuilder)}
  8. + *
+ * + * @param sb + * Builder to add to. + */ + protected void appendDeclaration(@Nonnull StringBuilder sb) { + buildDeclarationFlags(sb); + buildDeclarationReturnType(sb); + buildDeclarationName(sb); + buildDeclarationArgs(sb); + buildDeclarationThrows(sb); + } + + /** + * Appends the following pattern to the builder: + *
+	 * public static abstract...
+	 * 
+ * + * @param sb + * Builder to add to. + */ + protected void buildDeclarationFlags(@Nonnull StringBuilder sb) { + Collection flags = AccessFlag.getApplicableFlags(AccessFlag.Type.METHOD, method.getAccess()); + flags = AccessFlag.sort(AccessFlag.Type.METHOD, flags); + if (!flags.isEmpty()) { + sb.append(AccessFlag.toString(flags)).append(' '); + } + } + + /** + * Appends the following pattern to the builder: + *
+	 * ReturnType
+	 * 
+ * + * @param sb + * Builder to add to. + */ + protected void buildDeclarationReturnType(@Nonnull StringBuilder sb) { + Type methodType = Type.getMethodType(method.getDescriptor()); + String returnTypeName = format.filterEscape(methodType.getReturnType().getClassName()); + if (returnTypeName.contains(".")) + returnTypeName = returnTypeName.substring(returnTypeName.lastIndexOf(".") + 1); + sb.append(returnTypeName).append(' '); + } + + /** + * Appends the following pattern to the builder: + *
+	 * methodName
+	 * 
+ * + * @param sb + * Builder to add to. + */ + protected void buildDeclarationName(@Nonnull StringBuilder sb) { + sb.append(format.filter(method.getName())); + } + + /** + * Appends the following pattern to the builder: + *
+	 * (Type argName, Type argName)
+	 * 
+ * + * @param sb + * Builder to add to. + */ + protected void buildDeclarationArgs(@Nonnull StringBuilder sb) { + sb.append('('); + boolean isVarargs = AccessFlag.isVarargs(method.getAccess()); + int varIndex = AccessFlag.isStatic(method.getAccess()) ? 0 : 1; + Type methodType = Type.getMethodType(method.getDescriptor()); + Type[] argTypes = methodType.getArgumentTypes(); + for (int param = 0; param < argTypes.length; param++) { + // Get arg type text + Type argType = argTypes[param]; + String argTypeName = format.filterEscape(argType.getClassName()); + if (argTypeName.contains(".")) + argTypeName = argTypeName.substring(argTypeName.lastIndexOf(".") + 1); + boolean isLast = param == argTypes.length - 1; + if (isVarargs && isLast && argType.getSort() == Type.ARRAY) { + argTypeName = StringUtil.replaceLast(argTypeName, "[]", "..."); + } + + // Get arg name + String name = "p" + varIndex; + LocalVariable variable = method.getLocalVariable(varIndex); + if (variable != null) { + name = format.filter(variable.getName()); + } + + // Append to arg list + sb.append(argTypeName).append(' ').append(name); + if (!isLast) { + sb.append(", "); + } + + // Increment for next var + varIndex += argType.getSize(); + } + sb.append(')'); + } + + /** + * Appends the following pattern to the builder: + *
+	 * throws Item1, Item2, ...
+	 * 
+ * + * @param sb + * Builder to add to. + */ + protected void buildDeclarationThrows(@Nonnull StringBuilder sb) { + List thrownTypes = method.getThrownTypes(); + if (thrownTypes.isEmpty()) + return; + String shortNames = thrownTypes.stream() + .map(t -> format.filterEscape(StringUtil.shortenPath(t))) + .collect(Collectors.joining(", ")); + sb.append(" throws ").append(shortNames); + } + + /** + * Appends the abstract method body to the printer. + * + * @param sb + * Builder to add to. + */ + protected void appendAbstractBody(@Nonnull StringBuilder sb) { + sb.append(';'); + } + + /** + * Appends the method body to the printer. + * + * @param sb + * Builder to add to. + */ + protected void appendBody(@Nonnull StringBuilder sb) { + Textifier textifier = new Textifier(); + ClassVisitor printVisitor = new ClassVisitor(RecafConstants.getAsmVersion()) { + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + return new TraceMethodVisitor(textifier); + } + }; + classInfo.getClassReader().accept(new MemberFilteringVisitor(printVisitor, method), 0); + + sb.append(" {\n"); + if (!textifier.getText().isEmpty()) { + // Pipe ASM's text line model to an output. + ByteArrayOutputStream baos = new ByteArrayOutputStream(); + PrintWriter writer = new PrintWriter(baos); + textifier.print(writer); + writer.close(); + + // Cleanup the output text. + String asmDump = baos.toString(StandardCharsets.UTF_8); + int beginIndex = asmDump.indexOf(" L0"); + if (beginIndex > 0) + asmDump = asmDump.substring(beginIndex); + + // Indent it just a bit with our printer and append to the string builder. + Printer codePrinter = new Printer(); + codePrinter.setIndent(" "); + codePrinter.appendMultiLine(asmDump); + + sb.append(" /*\n"); + sb.append(codePrinter); + sb.append(" */\n"); + } + sb.append(" throw new RuntimeException(\"Stub method\");\n"); + sb.append("}\n"); + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/PrintUtils.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/PrintUtils.java new file mode 100644 index 000000000..603529637 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/PrintUtils.java @@ -0,0 +1,136 @@ +package software.coley.recaf.services.decompile.fallback.print; + +import jakarta.annotation.Nonnull; +import org.objectweb.asm.Type; +import software.coley.recaf.info.annotation.Annotated; +import software.coley.recaf.info.annotation.AnnotationElement; +import software.coley.recaf.info.annotation.AnnotationEnumReference; +import software.coley.recaf.info.annotation.AnnotationInfo; +import software.coley.recaf.services.text.TextFormatConfig; +import software.coley.recaf.util.StringUtil; +import software.coley.recaf.util.Types; + +import java.util.List; +import java.util.Map; +import java.util.stream.Collectors; + +/** + * Various printing utilities. + * + * @author Matt Coley + */ +public class PrintUtils { + /** + * @param format + * Format config. + * @param container + * Annotation container. Can be a class, field, or method. + * + * @return String display of the annotations on the given container. Empty string if there are no annotations. + */ + @Nonnull + public static String annotationsToString(@Nonnull TextFormatConfig format, @Nonnull Annotated container) { + // Skip if there are no annotations. + List annotations = container.getAnnotations(); + if (annotations.isEmpty()) + return ""; + + // Print all annotations. + StringBuilder sb = new StringBuilder(); + for (AnnotationInfo annotation : annotations) + sb.append(annotationToString(format, annotation)).append('\n'); + sb.setLength(sb.length() - 1); // Cut off ending '\n' + return sb.toString(); + } + + /** + * @param format + * Format config. + * @param annotation + * Annotation to represent. + * + * @return String display of the annotation. + */ + @Nonnull + private static String annotationToString(@Nonnull TextFormatConfig format, @Nonnull AnnotationInfo annotation) { + String annotationDesc = annotation.getDescriptor(); + if (Types.isValidDesc(annotationDesc)) { + Map elements = annotation.getElements(); + String annotationName = StringUtil.shortenPath(Type.getType(annotationDesc).getInternalName()); + StringBuilder sb = new StringBuilder("@"); + sb.append(format.filterEscape(annotationName)); + if (!elements.isEmpty()) { + if (elements.size() == 1 && elements.get("value") != null) { + // If we only have 'value' we can ommit the 'k=' portion of the standard 'k=v' + AnnotationElement element = elements.values().iterator().next(); + sb.append("(").append(elementToString(format, element)).append(")"); + } else { + // Print all args in k=v format + String args = elements.entrySet().stream() + .map(e -> e.getKey() + " = " + elementToString(format, e.getValue())) + .collect(Collectors.joining(", ")); + sb.append("(").append(args).append(")"); + } + } + return sb.toString(); + } else { + return "// Invalid annotation removed"; + } + } + + /** + * @param format + * Format config. + * @param element + * Annotation element to represent. + * + * @return String display of the annotation element. + */ + @Nonnull + public static String elementToString(@Nonnull TextFormatConfig format, @Nonnull AnnotationElement element) { + Object value = element.getElementValue(); + return elementValueToString(format, value); + } + + /** + * @param format + * Format config. + * @param value + * Annotation element value to represent. + * + * @return String display of the element value. + */ + @Nonnull + private static String elementValueToString(@Nonnull TextFormatConfig format, @Nonnull Object value) { + switch (value) { + case String str -> { + // String value + return '"' + str + '"'; + } + case Type type -> { + // Class value + return format.filter(type.getInternalName()) + ".class"; + } + case AnnotationInfo subAnnotation -> { + // Annotation value + return annotationToString(format, subAnnotation); + } + case AnnotationEnumReference enumReference -> { + // Enum value + String enumType = Type.getType(enumReference.getDescriptor()).getInternalName(); + return format.filter(enumType) + '.' + enumReference.getValue(); + } + case List list -> { + // List of values + String elements = list.stream() + .map(e -> elementValueToString(format, e)) + .collect(Collectors.joining(", ")); + return "{ " + elements + " }"; + } + default -> { + // Primitive + return value.toString(); + } + } + } +} \ No newline at end of file diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/Printer.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/Printer.java new file mode 100644 index 000000000..0df452c95 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/fallback/print/Printer.java @@ -0,0 +1,61 @@ +package software.coley.recaf.services.decompile.fallback.print; + +import jakarta.annotation.Nonnull; +import software.coley.recaf.util.StringUtil; + +/** + * String printing wrapper of {@link StringBuilder}. + * Helps with indentation and line-based print calls. + * + * @author Matt Coley + */ +public class Printer { + private final StringBuilder out = new StringBuilder(); + private String indent; + + /** + * @param indent + * New indentation prefix. + */ + public void setIndent(@Nonnull String indent) { + this.indent = indent; + } + + /** + * Appends a line with a {@link #setIndent(String) configurable indent}. + * + * @param line + * Line to print. + */ + public void appendLine(@Nonnull String line) { + if (indent != null) + out.append(indent); + out.append(line).append("\n"); + } + + /** + * Appends all lines in the multi-line text. + * + * @param text + * Multi-line text to append. + * + * @see #appendLine(String) + */ + public void appendMultiLine(@Nonnull String text) { + String[] lines = StringUtil.splitNewline(text); + for (String line : lines) + appendLine(line); + } + + /** + * Append blank new line. + */ + public void newLine() { + out.append('\n'); + } + + @Override + public String toString() { + return out.toString(); + } +} \ No newline at end of file diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/BaseSource.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/BaseSource.java index f367a61b2..b7fef84d9 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/BaseSource.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/BaseSource.java @@ -6,6 +6,7 @@ import software.coley.recaf.workspace.model.Workspace; import java.io.ByteArrayInputStream; +import java.io.IOException; import java.io.InputStream; /** @@ -33,7 +34,7 @@ public String getName() { public InputStream getInputStream(String resource) { String name = resource.substring(0, resource.length() - IContextSource.CLASS_SUFFIX.length()); ClassPathNode node = workspace.findClass(name); - if (node == null) return InputStream.nullInputStream(); + if (node == null) return null; // VF wants missing data to be null here, not an IOException or empty stream. return new ByteArrayInputStream(node.getValue().asJvmClass().getBytecode()); } } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/ClassSource.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/ClassSource.java index 0e7abaf09..02c3947ad 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/ClassSource.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/ClassSource.java @@ -48,8 +48,11 @@ public Entries getEntries() { // too much of a perf hit. List entries = new ArrayList<>(); entries.add(new Entry(info.getName(), Entry.BASE_VERSION)); - for (InnerClassInfo innerClass : info.getInnerClasses()) - entries.add(new Entry(innerClass.getName(), Entry.BASE_VERSION)); + for (InnerClassInfo innerClass : info.getInnerClasses()) { + // Only add entry if it exists in the workspace. + if (workspace.findClass(innerClass.getInnerClassName()) != null) + entries.add(new Entry(innerClass.getName(), Entry.BASE_VERSION)); + } return new Entries(entries, Collections.emptyList(), Collections.emptyList()); } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/VineflowerConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/VineflowerConfig.java index 2b5e572a8..5e7e72065 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/VineflowerConfig.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/decompile/vineflower/VineflowerConfig.java @@ -9,9 +9,9 @@ import software.coley.observables.ObservableBoolean; import software.coley.observables.ObservableObject; import software.coley.recaf.config.BasicConfigValue; -import software.coley.recaf.config.ConfigGroups; import software.coley.recaf.services.decompile.BaseDecompilerConfig; +import java.lang.reflect.Field; import java.util.HashMap; import java.util.Map; @@ -19,6 +19,7 @@ * Config for {@link VineflowerDecompiler} * * @author therathatter + * @see IFernflowerPreferences Source of value definitions. */ @ApplicationScoped @SuppressWarnings("all") // ignore unused refs / typos @@ -74,59 +75,68 @@ public class VineflowerConfig extends BaseDecompilerConfig { private final ObservableBoolean decompileComplexCondys = new ObservableBoolean(false); private final ObservableBoolean forceJsrInline = new ObservableBoolean(false); + public static void main(String[] args) { + for (Field field : IFernflowerPreferences.class.getDeclaredFields()) { + try { + IFernflowerPreferences.Name name = field.getDeclaredAnnotation(IFernflowerPreferences.Name.class); + String key = (String) field.get(null); + System.out.println("service.decompile.impl.decompiler-vineflower-config." + key + "=" + name.value()); + } catch (Throwable t) {} + } + } + @Inject public VineflowerConfig() { super("decompiler-vineflower" + CONFIG_SUFFIX); + addValue(new BasicConfigValue<>("logging-level", Level.class, loggingLevel)); - addValue(new BasicConfigValue<>("rbr", boolean.class, removeBridge)); - addValue(new BasicConfigValue<>("rsy", boolean.class, removeSynthetic)); - addValue(new BasicConfigValue<>("din", boolean.class, decompileInner)); - addValue(new BasicConfigValue<>("dc4", boolean.class, decompileClass_1_4)); - addValue(new BasicConfigValue<>("das", boolean.class, decompileAssertions)); - addValue(new BasicConfigValue<>("hes", boolean.class, hideEmptySuper)); - addValue(new BasicConfigValue<>("hdc", boolean.class, hideDefaultConstructor)); - addValue(new BasicConfigValue<>("dgs", boolean.class, decompileGenericSignatures)); - addValue(new BasicConfigValue<>("ner", boolean.class, noExceptionsReturn)); - addValue(new BasicConfigValue<>("esm", boolean.class, ensureSynchronizedMonitor)); - addValue(new BasicConfigValue<>("den", boolean.class, decompileEnum)); - addValue(new BasicConfigValue<>("dpr", boolean.class, decompilePreview)); - addValue(new BasicConfigValue<>("rgn", boolean.class, removeGetClassNew)); - addValue(new BasicConfigValue<>("lit", boolean.class, literalsAsIs)); - addValue(new BasicConfigValue<>("bto", boolean.class, booleanTrueOne)); - addValue(new BasicConfigValue<>("asc", boolean.class, asciiStringCharacters)); - addValue(new BasicConfigValue<>("nns", boolean.class, syntheticNotSet)); - addValue(new BasicConfigValue<>("uto", boolean.class, undefinedParamTypeObject)); - addValue(new BasicConfigValue<>("udv", boolean.class, useDebugVarNames)); - addValue(new BasicConfigValue<>("ump", boolean.class, useMethodParameters)); - addValue(new BasicConfigValue<>("rer", boolean.class, removeEmptyRanges)); - addValue(new BasicConfigValue<>("fdi", boolean.class, finallyDeinline)); - addValue(new BasicConfigValue<>("inn", boolean.class, ideaNotNullAnnotation)); - addValue(new BasicConfigValue<>("lac", boolean.class, lambdaToAnonymousClass)); - addValue(new BasicConfigValue<>("bsm", boolean.class, bytecodeSourceMapping)); - addValue(new BasicConfigValue<>("dcl", boolean.class, dumpCodeLines)); - addValue(new BasicConfigValue<>("iib", boolean.class, ignoreInvalidBytecode)); - addValue(new BasicConfigValue<>("vac", boolean.class, verifyAnonymousClasses)); - addValue(new BasicConfigValue<>("tcs", boolean.class, ternaryConstantSimplification)); - addValue(new BasicConfigValue<>("pam", boolean.class, patternMatching)); - addValue(new BasicConfigValue<>("tlf", boolean.class, tryLoopFix)); - addValue(new BasicConfigValue<>("tco", boolean.class, ternaryConditions)); - addValue(new BasicConfigValue<>("swe", boolean.class, switchExpressions)); - addValue(new BasicConfigValue<>("shs", boolean.class, showHiddenStatements)); - addValue(new BasicConfigValue<>("ovr", boolean.class, overrideAnnotation)); - addValue(new BasicConfigValue<>("ssp", boolean.class, simplifyStackSecondPass)); - addValue(new BasicConfigValue<>("vvm", boolean.class, verifyVariableMerges)); - addValue(new BasicConfigValue<>("ega", boolean.class, explicitGenericArguments)); - addValue(new BasicConfigValue<>("isl", boolean.class, inlineSimpleLambdas)); - addValue(new BasicConfigValue<>("jvn", boolean.class, useJadVarNaming)); - addValue(new BasicConfigValue<>("jpr", boolean.class, useJadParameterNaming)); - addValue(new BasicConfigValue<>("sef", boolean.class, skipExtraFiles)); - addValue(new BasicConfigValue<>("win", boolean.class, warnInconsistentInnerClasses)); - addValue(new BasicConfigValue<>("dbe", boolean.class, dumpBytecodeOnError)); - addValue(new BasicConfigValue<>("dee", boolean.class, dumpExceptionOnError)); - addValue(new BasicConfigValue<>("dec", boolean.class, decompilerComments)); - addValue(new BasicConfigValue<>("sfc", boolean.class, sourceFileComments)); - addValue(new BasicConfigValue<>("dcc", boolean.class, decompileComplexCondys)); - addValue(new BasicConfigValue<>("fji", boolean.class, forceJsrInline)); + addValue(new BasicConfigValue<>("remove-bridge", boolean.class, removeBridge)); + addValue(new BasicConfigValue<>("remove-synthetic", boolean.class, removeSynthetic)); + addValue(new BasicConfigValue<>("decompile-inner", boolean.class, decompileInner)); + addValue(new BasicConfigValue<>("decompile-java4", boolean.class, decompileClass_1_4)); + addValue(new BasicConfigValue<>("decompile-assert", boolean.class, decompileAssertions)); + addValue(new BasicConfigValue<>("hide-empty-super", boolean.class, hideEmptySuper)); + addValue(new BasicConfigValue<>("hide-default-constructor", boolean.class, hideDefaultConstructor)); + addValue(new BasicConfigValue<>("decompile-generics", boolean.class, decompileGenericSignatures)); + addValue(new BasicConfigValue<>("incorporate-returns", boolean.class, noExceptionsReturn)); + addValue(new BasicConfigValue<>("ensure-synchronized-monitors", boolean.class, ensureSynchronizedMonitor)); + addValue(new BasicConfigValue<>("decompile-enums", boolean.class, decompileEnum)); + addValue(new BasicConfigValue<>("decompile-preview", boolean.class, decompilePreview)); + addValue(new BasicConfigValue<>("remove-getclass", boolean.class, removeGetClassNew)); + addValue(new BasicConfigValue<>("keep-literals", boolean.class, literalsAsIs)); + addValue(new BasicConfigValue<>("boolean-as-int", boolean.class, booleanTrueOne)); + addValue(new BasicConfigValue<>("ascii-strings", boolean.class, asciiStringCharacters)); + addValue(new BasicConfigValue<>("synthetic-not-set", boolean.class, syntheticNotSet)); + addValue(new BasicConfigValue<>("undefined-as-object", boolean.class, undefinedParamTypeObject)); + addValue(new BasicConfigValue<>("use-lvt-names", boolean.class, useDebugVarNames)); + addValue(new BasicConfigValue<>("use-method-parameters", boolean.class, useMethodParameters)); + addValue(new BasicConfigValue<>("remove-empty-try-catch", boolean.class, removeEmptyRanges)); + addValue(new BasicConfigValue<>("decompile-finally", boolean.class, finallyDeinline)); + addValue(new BasicConfigValue<>("lambda-to-anonymous-class", boolean.class, lambdaToAnonymousClass)); + addValue(new BasicConfigValue<>("bytecode-source-mapping", boolean.class, bytecodeSourceMapping)); + addValue(new BasicConfigValue<>("dump-code-lines", boolean.class, dumpCodeLines)); + addValue(new BasicConfigValue<>("ignore-invalid-bytecode", boolean.class, ignoreInvalidBytecode)); + addValue(new BasicConfigValue<>("verify-anonymous-classes", boolean.class, verifyAnonymousClasses)); + addValue(new BasicConfigValue<>("ternary-constant-simplification", boolean.class, ternaryConstantSimplification)); + addValue(new BasicConfigValue<>("pattern-matching", boolean.class, patternMatching)); + addValue(new BasicConfigValue<>("try-loop-fix", boolean.class, tryLoopFix)); + addValue(new BasicConfigValue<>("ternary-in-if", boolean.class, ternaryConditions)); + addValue(new BasicConfigValue<>("decompile-switch-expressions", boolean.class, switchExpressions)); + addValue(new BasicConfigValue<>("show-hidden-statements", boolean.class, showHiddenStatements)); + addValue(new BasicConfigValue<>("override-annotation", boolean.class, overrideAnnotation)); + addValue(new BasicConfigValue<>("simplify-stack", boolean.class, simplifyStackSecondPass)); + addValue(new BasicConfigValue<>("verify-merges", boolean.class, verifyVariableMerges)); + addValue(new BasicConfigValue<>("explicit-generics", boolean.class, explicitGenericArguments)); + addValue(new BasicConfigValue<>("inline-simple-lambdas", boolean.class, inlineSimpleLambdas)); + addValue(new BasicConfigValue<>("skip-extra-files", boolean.class, skipExtraFiles)); + addValue(new BasicConfigValue<>("warn-inconsistent-inner-attributes", boolean.class, warnInconsistentInnerClasses)); + addValue(new BasicConfigValue<>("dump-bytecode-on-error", boolean.class, dumpBytecodeOnError)); + addValue(new BasicConfigValue<>("dump-exception-on-error", boolean.class, dumpExceptionOnError)); + addValue(new BasicConfigValue<>("decompiler-comments", boolean.class, decompilerComments)); + addValue(new BasicConfigValue<>("sourcefile-comments", boolean.class, sourceFileComments)); + addValue(new BasicConfigValue<>("decompile-complex-constant-dynamic", boolean.class, decompileComplexCondys)); + addValue(new BasicConfigValue<>("force-jsr-inline", boolean.class, forceJsrInline)); + registerConfigValuesHashUpdates(); } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceGraph.java b/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceGraph.java index 057cf1d25..364083ede 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceGraph.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceGraph.java @@ -5,6 +5,7 @@ import jakarta.inject.Inject; import software.coley.collections.Lists; import software.coley.recaf.cdi.AutoRegisterWorkspaceListeners; +import software.coley.recaf.cdi.EagerInitialization; import software.coley.recaf.cdi.WorkspaceScoped; import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.info.BasicJvmClassInfo; @@ -14,9 +15,12 @@ import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.path.ResourcePathNode; import software.coley.recaf.services.Service; +import software.coley.recaf.services.mapping.MappingApplicationListener; +import software.coley.recaf.services.mapping.MappingListeners; +import software.coley.recaf.services.mapping.MappingResults; import software.coley.recaf.services.workspace.WorkspaceCloseListener; -import software.coley.recaf.workspace.model.WorkspaceModificationListener; import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.WorkspaceModificationListener; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import software.coley.recaf.workspace.model.resource.ResourceAndroidClassListener; @@ -36,14 +40,17 @@ * @author Matt Coley */ @WorkspaceScoped +@EagerInitialization @AutoRegisterWorkspaceListeners public class InheritanceGraph implements Service, WorkspaceModificationListener, WorkspaceCloseListener, - ResourceJvmClassListener, ResourceAndroidClassListener { + ResourceJvmClassListener, ResourceAndroidClassListener, MappingApplicationListener { public static final String SERVICE_ID = "graph-inheritance"; + /** Vertex used for classes that are not found in the workspace. */ private static final InheritanceVertex STUB = new InheritanceStubVertex(); private static final String OBJECT = "java/lang/Object"; private final Map> parentToChild = new ConcurrentHashMap<>(); private final Map vertices = new ConcurrentHashMap<>(); + private final Set stubs = ConcurrentHashMap.newKeySet(); private final Function vertexProvider = createVertexProvider(); private final InheritanceGraphConfig config; private final Workspace workspace; @@ -57,7 +64,7 @@ public class InheritanceGraph implements Service, WorkspaceModificationListener, * Workspace to pull classes from. */ @Inject - public InheritanceGraph(@Nonnull InheritanceGraphConfig config, @Nonnull Workspace workspace) { + public InheritanceGraph(@Nonnull InheritanceGraphConfig config, @Nonnull MappingListeners mappingListeners, @Nonnull Workspace workspace) { this.config = config; this.workspace = workspace; @@ -66,6 +73,9 @@ public InheritanceGraph(@Nonnull InheritanceGraphConfig config, @Nonnull Workspa primaryResource.addResourceJvmClassListener(this); primaryResource.addResourceAndroidClassListener(this); + // Add listener to handle updating the graph when renaming is applied. + mappingListeners.addMappingApplicationListener(this); + // Populate downwards (parent --> child) lookup refreshChildLookup(); } @@ -180,6 +190,20 @@ private void removeParentToChildLookup(@Nonnull String name, @Nonnull String par if (childVertex != null) childVertex.clearCachedVertices(); } + /** + * Removes the given class from the graph. + * + * @param cls + * Class that was removed. + */ + private void removeClass(@Nonnull ClassInfo cls) { + removeParentToChildLookup(cls); + + String name = cls.getName(); + vertices.remove(name); + } + + /** * @param parent * Parent to find children of. @@ -199,8 +223,21 @@ private Set getDirectChildren(@Nonnull String parent) { */ @Nullable public InheritanceVertex getVertex(@Nonnull String name) { - InheritanceVertex vertex = vertices.computeIfAbsent(name, vertexProvider); - return vertex == STUB ? null : vertex; + InheritanceVertex vertex = vertices.get(name); + if (vertex == null && !stubs.contains(name)) { + // Vertex does not exist and was not marked as a stub. + // We want to look up the vertex for the given class and figure out if its valid or needs to be stubbed. + InheritanceVertex provided = vertexProvider.apply(name); + if (provided == STUB || provided == null) { + // Provider yielded either a stub OR no result. Discard it. + stubs.add(name); + } else { + // Provider yielded a valid vertex. Update the return value and record it in the map. + vertices.put(name, provided); + vertex = provided; + } + } + return vertex; } /** @@ -285,6 +322,10 @@ private Function createVertexProvider() { if (name == null) return null; + // Edge case handling for arrays. There is no object typing of arrays. + if (name.isEmpty() || name.charAt(0) == '[') + return null; + // Find class in workspace, if not found yield stub. ClassPathNode result = workspace.findClass(name); if (result == null) @@ -353,12 +394,12 @@ public void onUpdateClass(@Nonnull WorkspaceResource resource, @Nonnull AndroidC @Override public void onRemoveClass(@Nonnull WorkspaceResource resource, @Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo cls) { - removeParentToChildLookup(cls); + removeClass(cls); } @Override public void onRemoveClass(@Nonnull WorkspaceResource resource, @Nonnull AndroidClassBundle bundle, @Nonnull AndroidClassInfo cls) { - removeParentToChildLookup(cls); + removeClass(cls); } @Override @@ -375,6 +416,40 @@ public void onRemoveLibrary(@Nonnull Workspace workspace, @Nonnull WorkspaceReso public void onWorkspaceClosed(@Nonnull Workspace workspace) { parentToChild.clear(); vertices.clear(); + stubs.clear(); + } + + @Override + public void onPreApply(@Nonnull MappingResults mappingResults) { + // no-op + } + + @Override + public void onPostApply(@Nonnull MappingResults mappingResults) { + // Remove vertices and lookups of items that no longer exist. + mappingResults.getPreMappingPaths().forEach((name, path) -> { + InheritanceVertex vertex = vertexProvider.apply(name); + if (vertex == STUB) { + vertices.remove(name); + parentToChild.remove(name); + } + }); + + // While applying mappings, the graph does not perfectly refresh, so we need to clear out some state + // so that when the graph is used again the correct information will be fetched. + mappingResults.getPostMappingPaths().forEach((name, path) -> { + // Stub information for classes we know exist in the workspace should be removed. + stubs.remove(name); + + // Refresh the parent-->children mapping. + parentToChild.remove(name); + ClassInfo postPath = path.getValue(); + populateParentToChildLookup(postPath); + + // Clear cached parents inside the vertex. + InheritanceVertex vertex = vertices.get(name); + if (vertex != null) vertex.clearCachedVertices(); + }); } @Nonnull diff --git a/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceVertex.java b/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceVertex.java index 90387a3a8..cf57a61dd 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceVertex.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/inheritance/InheritanceVertex.java @@ -185,11 +185,13 @@ public boolean isLibraryMethod(String name, String desc) { // Check against this definition if (!isPrimary && hasMethod(name, desc)) return true; + // Check parents. // If we extend a class with a library definition then it should be considered a library method. for (InheritanceVertex parent : getParents()) if (parent.isLibraryMethod(name, desc)) return true; + // No library definition found, so its safe to rename. return false; } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/IntermediateMappings.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/IntermediateMappings.java index b1eea99f2..0e05771fc 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/IntermediateMappings.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/IntermediateMappings.java @@ -28,6 +28,7 @@ public class IntermediateMappings implements Mappings { * Post-mapping name. */ public void addClass(String oldName, String newName) { + if (Objects.equals(oldName, newName)) return; // Skip identity mappings classes.put(oldName, new ClassMapping(oldName, newName)); } @@ -42,6 +43,7 @@ public void addClass(String oldName, String newName) { * Post-mapping field name. */ public void addField(String ownerName, String desc, String oldName, String newName) { + if (Objects.equals(oldName, newName)) return; // Skip identity mappings fields.computeIfAbsent(ownerName, n -> new ArrayList<>()) .add(new FieldMapping(ownerName, oldName, desc, newName)); } @@ -57,6 +59,7 @@ public void addField(String ownerName, String desc, String oldName, String newNa * Post-mapping method name. */ public void addMethod(String ownerName, String desc, String oldName, String newName) { + if (Objects.equals(oldName, newName)) return; // Skip identity mappings methods.computeIfAbsent(ownerName, n -> new ArrayList<>()) .add(new MethodMapping(ownerName, oldName, desc, newName)); } @@ -80,6 +83,7 @@ public void addMethod(String ownerName, String desc, String oldName, String newN public void addVariable(String ownerName, String methodName, String methodDesc, String desc, String oldName, int index, String newName) { + if (Objects.equals(oldName, newName)) return; // Skip identity mappings String key = varKey(ownerName, methodName, methodDesc); variables.computeIfAbsent(key, n -> new ArrayList<>()) .add(new VariableMapping(ownerName, methodName, methodDesc, desc, oldName, index, newName)); diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingApplier.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingApplier.java index 812fb6439..2bf09b462 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingApplier.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingApplier.java @@ -3,6 +3,7 @@ import jakarta.annotation.Nonnull; import jakarta.inject.Inject; import org.objectweb.asm.ClassReader; +import org.objectweb.asm.ClassVisitor; import org.objectweb.asm.ClassWriter; import software.coley.recaf.cdi.WorkspaceScoped; import software.coley.recaf.info.JvmClassInfo; @@ -14,6 +15,7 @@ import software.coley.recaf.services.mapping.aggregate.AggregateMappingManager; import software.coley.recaf.util.threading.ThreadPoolFactory; import software.coley.recaf.util.threading.ThreadUtil; +import software.coley.recaf.util.visitors.IllegalSignatureRemovingVisitor; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -122,7 +124,11 @@ private Mappings enrich(@Nonnull Mappings mappings) { // Map intermediate mappings to the adapter so that we can pass in the inheritance graph for better coverage // of cases inherited field/method references. if (mappings instanceof IntermediateMappings intermediateMappings) { - MappingsAdapter adapter = new MappingsAdapter(true, true); + // Mapping formats that export to intermediate should mark whether they support + // differentiation of field and variable types. + boolean fieldDifferentiation = mappings.doesSupportFieldTypeDifferentiation(); + boolean varDifferentiation = mappings.doesSupportVariableTypeDifferentiation(); + MappingsAdapter adapter = new MappingsAdapter(fieldDifferentiation, varDifferentiation); adapter.importIntermediate(intermediateMappings); mappings = adapter; } @@ -167,7 +173,8 @@ private static void dumpIntoResults(@Nonnull MappingResults results, ClassWriter cw = new ClassWriter(0); ClassReader cr = classInfo.getClassReader(); WorkspaceClassRemapper remapVisitor = new WorkspaceClassRemapper(cw, workspace, mappings); - cr.accept(remapVisitor, 0); + ClassVisitor cv = classInfo.hasValidSignatures() ? remapVisitor : new IllegalSignatureRemovingVisitor(remapVisitor); // Because ASM crashes otherwise. + cr.accept(cv, 0); // Update class if it has any modified references if (remapVisitor.hasMappingBeenApplied()) { diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingListeners.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingListeners.java index ac279077b..c5c5c8098 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingListeners.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingListeners.java @@ -4,12 +4,16 @@ import jakarta.annotation.Nullable; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.slf4j.Logger; +import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.services.Service; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * Manages listeners for things like {@link MappingResults} application in an application-scoped, as opposed to @@ -20,7 +24,8 @@ @ApplicationScoped public class MappingListeners implements Service { public static final String SERVICE_ID = "mapping-listeners"; - private final List mappingApplicationListeners = new ArrayList<>(); + private static final Logger logger = Logging.get(MappingListeners.class); + private final List mappingApplicationListeners = new CopyOnWriteArrayList<>(); private final MappingListenersConfig config; @Inject @@ -39,7 +44,7 @@ public MappingListeners(@Nonnull MappingListenersConfig config) { * @param listener * Listener to add. */ - public void addMappingApplicationListener(@Nonnull MappingApplicationListener listener) { + public synchronized void addMappingApplicationListener(@Nonnull MappingApplicationListener listener) { mappingApplicationListeners.add(listener); } @@ -50,7 +55,7 @@ public void addMappingApplicationListener(@Nonnull MappingApplicationListener li * @return {@code true} when item was removed. * {@code false} when item was not in the list to begin with. */ - public boolean removeMappingApplicationListener(@Nonnull MappingApplicationListener listener) { + public synchronized boolean removeMappingApplicationListener(@Nonnull MappingApplicationListener listener) { return mappingApplicationListeners.remove(listener); } @@ -66,22 +71,20 @@ public MappingApplicationListener createBundledMappingApplicationListener() { if (listeners.isEmpty()) return null; else if (listeners.size() == 1) - return listeners.get(0); + return listeners.getFirst(); // Bundle multiple listeners. return new MappingApplicationListener() { @Override public void onPreApply(@Nonnull MappingResults mappingResults) { - for (MappingApplicationListener listener : listeners) { - listener.onPreApply(mappingResults); - } + CollectionUtil.safeForEach(listeners, listener -> listener.onPreApply(mappingResults), + (listener, t) -> logger.error("Exception thrown before applying mappings", t)); } @Override public void onPostApply(@Nonnull MappingResults mappingResults) { - for (MappingApplicationListener listener : listeners) { - listener.onPostApply(mappingResults); - } + CollectionUtil.safeForEach(listeners, listener -> listener.onPostApply(mappingResults), + (listener, t) -> logger.error("Exception thrown after applying mappings", t)); } }; } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingResults.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingResults.java index a41751c8d..c05982b3d 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingResults.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingResults.java @@ -147,6 +147,9 @@ public void apply() { for (ApplicationEntry entry : applicationEntries) entry.applicationRunnable().run(); + // Log in console how many classes got mapped. + logger.info("Applied mapping to {} classes", preMappingPaths.size()); + // Pass to handler again to notify of application of mappings has completed/ if (applicationHandler != null) try { diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/Mappings.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/Mappings.java index 60a9c7540..b1dba8092 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/Mappings.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/Mappings.java @@ -21,6 +21,30 @@ * @author Matt Coley */ public interface Mappings { + /** + * Some mapping formats do not include field types since name overloading is illegal at the source level of Java. + * It's valid in the bytecode but the mapping omits this info since it isn't necessary information for mapping + * that does not support name overloading. + *

+ * This is mostly only relevant for usage of {@link MappingsAdapter} which + * + * @return {@code true} when field mappings include the type descriptor in their lookup information. + */ + default boolean doesSupportFieldTypeDifferentiation() { + return true; + } + + /** + * Some mapping formats do not include variable types since name overloading is illegal at the source level of Java. + * Variable names are not used by the JVM at all so their names can be anything at the bytecode level. So including + * the type makes it easier to reverse mappings. + * + * @return {@code true} when variable mappings include the type descriptor in their lookup information. + */ + default boolean doesSupportVariableTypeDifferentiation() { + return true; + } + /** * @param classInfo * Class to lookup. diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingsAdapter.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingsAdapter.java index e0d8cb8eb..2543ccced 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingsAdapter.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/MappingsAdapter.java @@ -16,15 +16,14 @@ import java.util.function.Function; /** - * Basic groundwork for any mapping implementation.
- *
- * Mapping capabilities are defined in the constructor: - *

    - *
  • {@link #doesSupportFieldTypeDifferentiation()}
  • - *
  • {@link #doesSupportVariableTypeDifferentiation()}
  • - *
- * Allows hierarchy look-ups (More info given at: {@link #enableHierarchyLookup(InheritanceGraph)}
- * Handles inner class relations in class look-ups. + * A {@link Mappings} implementation with a number of additional operations to support usage beyond basic mapping info storage. + * Enhancements + *
    + *
  1. Import mapping entries from a {@link IntermediateMappings} instance.
  2. + *
  3. Enhance field/method lookups with inheritance info from {@link InheritanceGraph}, see {@link #enableHierarchyLookup(InheritanceGraph)}.
  4. + *
  5. Enhance inner/outer class mapping edge cases via {@link #enableClassLookup(Workspace)}.
  6. + *
  7. Adapt keys in cases where fields/vars do not have type info associated with them (for formats that suck).
  8. + *
* * @author Matt Coley */ @@ -42,7 +41,7 @@ public class MappingsAdapter implements Mappings { * {@code true} if the mapping format implementation includes type descriptors in variable mappings. */ public MappingsAdapter(boolean supportFieldTypeDifferentiation, - boolean supportVariableTypeDifferentiation) { + boolean supportVariableTypeDifferentiation) { this.supportFieldTypeDifferentiation = supportFieldTypeDifferentiation; this.supportVariableTypeDifferentiation = supportVariableTypeDifferentiation; } @@ -151,7 +150,7 @@ public String getMappedMethodName(@Nonnull String ownerName, @Nonnull String met @Nullable @Override public String getMappedVariableName(@Nonnull String className, @Nonnull String methodName, @Nonnull String methodDesc, - @Nullable String name, @Nullable String desc, int index) { + @Nullable String name, @Nullable String desc, int index) { MappingKey key = getVariableKey(className, methodName, methodDesc, name, desc, index); return mappings.get(key); } @@ -177,6 +176,16 @@ public IntermediateMappings exportIntermediate() { return intermediate; } + @Override + public boolean doesSupportFieldTypeDifferentiation() { + return supportFieldTypeDifferentiation; + } + + @Override + public boolean doesSupportVariableTypeDifferentiation() { + return supportVariableTypeDifferentiation; + } + /** * @param owner * Internal name of the class "defining" the member. @@ -241,28 +250,6 @@ public void enableClassLookup(@Nonnull Workspace workspace) { this.workspace = workspace; } - /** - * Some mapping formats do not include field types since name overloading is illegal at the source level of Java. - * It's valid in the bytecode but the mapping omits this info since it isn't necessary information for mapping - * that does not support name overloading. - * - * @return {@code true} when field mappings include the type descriptor in their lookup information. - */ - public boolean doesSupportFieldTypeDifferentiation() { - return supportFieldTypeDifferentiation; - } - - /** - * Some mapping forats do not include variable types since name overloading is illegal at the source level of Java. - * Variable names are not used by the JVM at all so their names can be anything at the bytecode level. So including - * the type makes it easier to reverse mappings. - * - * @return {@code true} when variable mappings include the type descriptor in their lookup information. - */ - public boolean doesSupportVariableTypeDifferentiation() { - return supportVariableTypeDifferentiation; - } - /** * Add mapping for class name. * @@ -352,7 +339,7 @@ public void addMethod(@Nonnull String owner, @Nonnull String originalName, @Nonn * New name of the variable. */ public void addVariable(@Nonnull String className, @Nonnull String methodName, @Nonnull String methodDesc, - @Nonnull String originalName, @Nullable String desc, int index, @Nonnull String renamedName) { + @Nonnull String originalName, @Nullable String desc, int index, @Nonnull String renamedName) { MappingKey key = getVariableKey(className, methodName, methodDesc, originalName, desc, index); mappings.put(key, renamedName); } @@ -416,7 +403,7 @@ protected MappingKey getMethodKey(@Nonnull String ownerName, @Nonnull String met */ @Nonnull protected MappingKey getVariableKey(@Nonnull String className, @Nonnull String methodName, @Nonnull String methodDesc, - @Nullable String name, @Nullable String desc, int index) { + @Nullable String name, @Nullable String desc, int index) { return new VariableMappingKey(className, methodName, methodDesc, name, desc); } } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/aggregate/AggregateMappingManager.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/aggregate/AggregateMappingManager.java index e2e33eda5..7464c4cdc 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/aggregate/AggregateMappingManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/aggregate/AggregateMappingManager.java @@ -2,15 +2,20 @@ import jakarta.annotation.Nonnull; import jakarta.inject.Inject; +import org.slf4j.Logger; +import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.cdi.AutoRegisterWorkspaceListeners; import software.coley.recaf.cdi.WorkspaceScoped; import software.coley.recaf.services.Service; import software.coley.recaf.services.mapping.Mappings; import software.coley.recaf.services.workspace.WorkspaceCloseListener; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.bundle.BasicBundle; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * Manages tracking the state of mappings over time. @@ -22,7 +27,8 @@ @AutoRegisterWorkspaceListeners public class AggregateMappingManager implements Service, WorkspaceCloseListener { public static final String SERVICE_ID = "mapping-aggregator"; - private final List aggregateListeners = new ArrayList<>(); + private static final Logger logger = Logging.get(AggregateMappingManager.class); + private final List aggregateListeners = new CopyOnWriteArrayList<>(); private final AggregatedMappings aggregatedMappings; private final AggregateMappingManagerConfig config; @@ -47,7 +53,8 @@ public void onWorkspaceClosed(@Nonnull Workspace workspace) { */ public void updateAggregateMappings(Mappings newMappings) { aggregatedMappings.update(newMappings); - aggregateListeners.forEach(listener -> listener.onAggregatedMappingsUpdated(getAggregatedMappings())); + CollectionUtil.safeForEach(aggregateListeners, listener -> listener.onAggregatedMappingsUpdated(getAggregatedMappings()), + (listener, t) -> logger.error("Exception thrown when updating aggregate mappings", t)); } /** @@ -55,7 +62,8 @@ public void updateAggregateMappings(Mappings newMappings) { */ private void clearAggregated() { aggregatedMappings.clear(); - aggregateListeners.forEach(listener -> listener.onAggregatedMappingsUpdated(getAggregatedMappings())); + CollectionUtil.safeForEach(aggregateListeners, listener -> listener.onAggregatedMappingsUpdated(getAggregatedMappings()), + (listener, t) -> logger.error("Exception thrown when updating aggregate mappings", t)); } /** diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/MappingFileFormat.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/MappingFileFormat.java index ca823e429..f12c48527 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/MappingFileFormat.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/MappingFileFormat.java @@ -109,7 +109,7 @@ default String exportText(@Nonnull Mappings mappings) throws InvalidMappingExcep */ @Nonnull static IntermediateMappings parse(@Nonnull String mappingText, @Nonnull MappingTreeReader visitor) throws InvalidMappingException { - IntermediateMappings mappings = new IntermediateMappings(); + // Populate the mapping-io model MemoryMappingTree tree = new MemoryMappingTree(); StringReader reader = new StringReader(mappingText); try { @@ -117,27 +117,50 @@ static IntermediateMappings parse(@Nonnull String mappingText, @Nonnull MappingT } catch (IOException ex) { throw new InvalidMappingException(ex); } + + // Create our mapping model. + IntermediateMappings mappings = new IntermediateMappings(); + + // Mapping IO supports multiple namespaces for outputs. + // This is only really used in the 'tiny' format. Generally speaking the input columns look like: + // obfuscated, intermediate, clean + // or: + // intermediate, clean + // We want everything to map to the final column, rather than their notion of the first + // column mapping to one of the following columns. int namespaceCount = tree.getDstNamespaces().size(); int finalNamespace = namespaceCount - 1; for (MappingTree.ClassMapping cm : tree.getClasses()) { String finalClassName = cm.getDstName(finalNamespace); + + // Add the base case: input --> final output name mappings.addClass(cm.getSrcName(), finalClassName); + + // Add destination[n] --> final output name, where n < destinations.length - 1. + // This is how we handle cases like 'intermediate --> clean' despite both of those + // being "output" columns. if (namespaceCount > 1) for (int i = 0; i < finalNamespace; i++) mappings.addClass(cm.getDstName(i), finalClassName); for (MappingTree.FieldMapping fm : cm.getFields()) { String finalFieldName = fm.getDstName(finalNamespace); + + // Base case, like before. mappings.addField(cm.getSrcName(), fm.getSrcDesc(), fm.getSrcName(), finalFieldName); + + // Support extra namespaces, like before. if (namespaceCount > 1) for (int i = 0; i < finalNamespace; i++) - mappings.addField(cm.getSrcName(), fm.getSrcDesc(), fm.getDstName(i), finalFieldName); + mappings.addField(cm.getDstName(i), fm.getDesc(i), fm.getDstName(i), finalFieldName); } for (MappingTree.MethodMapping mm : cm.getMethods()) { String finalMethodName = mm.getDstName(finalNamespace); + + // Same idea as field handling. mappings.addMethod(cm.getSrcName(), mm.getSrcDesc(), mm.getSrcName(), finalMethodName); if (namespaceCount > 1) for (int i = 0; i < finalNamespace; i++) - mappings.addMethod(cm.getSrcName(), mm.getSrcDesc(), mm.getDstName(i), finalMethodName); + mappings.addMethod(cm.getDstName(i), mm.getDstDesc(i), mm.getDstName(i), finalMethodName); } } return mappings; diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/SrgMappings.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/SrgMappings.java index 391ce5b3a..95075a474 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/SrgMappings.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/format/SrgMappings.java @@ -141,6 +141,18 @@ public SrgIntermediateMappings(List> packageMappings) { this.packageMappings = packageMappings; } + @Override + public boolean doesSupportFieldTypeDifferentiation() { + // SRG fields do not include type info. + return false; + } + + @Override + public boolean doesSupportVariableTypeDifferentiation() { + // See above. + return false; + } + @Nullable @Override public ClassMapping getClassMapping(String name) { diff --git a/recaf-core/src/main/java/software/coley/recaf/services/mapping/gen/MappingGenerator.java b/recaf-core/src/main/java/software/coley/recaf/services/mapping/gen/MappingGenerator.java index ff901270f..19ef38917 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/mapping/gen/MappingGenerator.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/mapping/gen/MappingGenerator.java @@ -5,6 +5,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.info.member.ClassMember; import software.coley.recaf.info.member.FieldMember; import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.services.Service; @@ -53,10 +54,10 @@ public MappingGenerator(@Nonnull MappingGeneratorConfig config) { */ @Nonnull public Mappings generate(@Nullable Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull InheritanceGraph inheritanceGraph, - @Nonnull NameGenerator generator, - @Nullable NameGeneratorFilter filter) { + @Nonnull WorkspaceResource resource, + @Nonnull InheritanceGraph inheritanceGraph, + @Nonnull NameGenerator generator, + @Nullable NameGeneratorFilter filter) { // Adapt filter to handle baseline cases. filter = new ExcludeEnumMethodsFilter(filter); @@ -91,15 +92,15 @@ public Mappings generate(@Nullable Workspace workspace, } private void generateFamilyMappings(@Nonnull MappingsAdapter mappings, @Nonnull Set family, - @Nonnull NameGenerator generator, @Nonnull NameGeneratorFilter filter) { + @Nonnull NameGenerator generator, @Nonnull NameGeneratorFilter filter) { // Collect the members in the family that are inheritable, and methods that are library implementations. // We want this information so that for these members we give them a single name throughout the family. // - Methods can be indirectly linked by two interfaces describing the same signature, // and a child type implementing both types. So we have to be strict with naming with cases like this. // - Fields do not have such a concern, but can still be accessed by child type owners. - Set inheritableFields = new HashSet<>(); - Set inheritableMethods = new HashSet<>(); - Set libraryMethods = new HashSet<>(); + Set inheritableFields = new HashSet<>(); + Set inheritableMethods = new HashSet<>(); + Set libraryMethods = new HashSet<>(); family.forEach(vertex -> { // Skip module-info classes if (vertex.isModule()) @@ -109,17 +110,18 @@ private void generateFamilyMappings(@Nonnull MappingsAdapter mappings, @Nonnull for (FieldMember field : vertex.getValue().getFields()) { if (field.hasPrivateModifier()) continue; - inheritableFields.add(field); + inheritableFields.add(MemberKey.of(field)); } for (MethodMember method : vertex.getValue().getMethods()) { if (method.hasPrivateModifier()) continue; - inheritableMethods.add(method); + MemberKey key = MemberKey.of(method); + inheritableMethods.add(key); // Need to track which methods we cannot remap due to them being overrides of libraries // rather than being declared solely in our resource. if (vertex.isLibraryMethod(method.getName(), method.getDescriptor())) - libraryMethods.add(method); + libraryMethods.add(key); } }); // Create mappings for members. @@ -127,6 +129,7 @@ private void generateFamilyMappings(@Nonnull MappingsAdapter mappings, @Nonnull // Skip libraries in the family. if (vertex.isLibraryVertex()) return; + // Skip module-info classes if (vertex.isModule()) return; @@ -146,8 +149,9 @@ private void generateFamilyMappings(@Nonnull MappingsAdapter mappings, @Nonnull continue; // Create mapped name and record into mappings. + MemberKey key = MemberKey.of(field); String mappedFieldName = generator.mapField(owner, field); - if (inheritableFields.contains(field)) { + if (inheritableFields.contains(key)) { // Field is 'inheritable' meaning it needs to have a consistent name // for all children and parents of this vertex. Set targetFamilyMembers = new HashSet<>(); @@ -170,7 +174,7 @@ private void generateFamilyMappings(@Nonnull MappingsAdapter mappings, @Nonnull String methodDesc = method.getDescriptor(); // Skip if reserved method name. - if (methodName.length() > 0 && methodName.charAt(0) == '<') + if (!methodName.isEmpty() && methodName.charAt(0) == '<') continue; // Skip if filtered. @@ -178,7 +182,8 @@ private void generateFamilyMappings(@Nonnull MappingsAdapter mappings, @Nonnull continue; // Skip if method is a library method, or is already mapped. - if (libraryMethods.contains(method) || mappings.getMappedMethodName(ownerName, methodName, methodDesc) != null) + MemberKey key = MemberKey.of(method); + if (libraryMethods.contains(key) || mappings.getMappedMethodName(ownerName, methodName, methodDesc) != null) continue; // Create mapped name and record into mappings. @@ -188,7 +193,7 @@ private void generateFamilyMappings(@Nonnull MappingsAdapter mappings, @Nonnull if (methodName.equals(mappedMethodName)) continue; - if (inheritableMethods.contains(method)) { + if (inheritableMethods.contains(key)) { // Method is 'inheritable' meaning it needs to have a consistent name for the entire family. // But if one of the members of the family is filtered, then we cannot map anything. boolean shouldMapFamily = true; @@ -247,4 +252,23 @@ public String getServiceId() { public MappingGeneratorConfig getServiceConfig() { return config; } + + /** + * Local record to use as set entries that are simpler than {@link ClassMember} implementations. + *

+ * Most importantly the {@link Object#hashCode()} of this type is based only on the name and descriptor. + * This ensures additional data like local variable or generic signature data doesn't interfere with operations + * such as {@link #generateFamilyMappings(MappingsAdapter, Set, NameGenerator, NameGeneratorFilter)}. + * + * @param name + * Field/method name. + * @param descriptor + * Field/method descriptor. + */ + private record MemberKey(@Nonnull String name, @Nonnull String descriptor) { + @Nonnull + static MemberKey of(@Nonnull ClassMember member) { + return new MemberKey(member.getName(), member.getDescriptor()); + } + } } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/search/match/StringPredicateProvider.java b/recaf-core/src/main/java/software/coley/recaf/services/search/match/StringPredicateProvider.java index b2c1f5768..c5f77a1a1 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/search/match/StringPredicateProvider.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/search/match/StringPredicateProvider.java @@ -16,6 +16,14 @@ */ @ApplicationScoped public class StringPredicateProvider { + /** + * Key in {@link #newBiStringPredicate(String, String)} for equality matching. + */ + public static final String KEY_ANYTHING = "anything"; + /** + * Key in {@link #newBiStringPredicate(String, String)} for equality matching. + */ + public static final String KEY_NOTHING = "zilch"; // Use 'zilch' instead of 'nothing' so that the natural key ordering puts it last /** * Key in {@link #newBiStringPredicate(String, String)} for equality matching. */ @@ -56,11 +64,17 @@ public class StringPredicateProvider { * Key in {@link #newBiStringPredicate(String, String)} for full regex matching. */ public static final String KEY_REFEX_FULL = "regex-full"; + private static final BiStringMatcher MATHER_ANYTHING = (a, b) -> true; + private static final BiStringMatcher MATHER_NOTHING = (a, b) -> false; + private static final StringPredicate PREDICATE_ANYTHING = new StringPredicate(KEY_ANYTHING, a -> true); + private static final StringPredicate PREDICATE_NOTHING = new StringPredicate(KEY_NOTHING, a -> false); private final Map biStringMatchers = new ConcurrentHashMap<>(); private final Map multiStringMatchers = new ConcurrentHashMap<>(); @Inject public StringPredicateProvider() { + registerBiMatcher(KEY_ANYTHING, MATHER_ANYTHING); + registerBiMatcher(KEY_NOTHING, MATHER_NOTHING); registerBiMatcher(KEY_EQUALS, String::equals); registerBiMatcher(KEY_EQUALS_IGNORE_CASE, String::equalsIgnoreCase); registerBiMatcher(KEY_CONTAINS, (key, value) -> value.contains(key)); @@ -111,6 +125,22 @@ public boolean registerMultiMatcher(@Nonnull String id, @Nonnull MultiStringMatc return multiStringMatchers.putIfAbsent(id, matcher) == null; } + /** + * @return Predicate that matches anything. + */ + @Nonnull + public StringPredicate newAnythingPredicate() { + return PREDICATE_ANYTHING; + } + + /** + * @return Predicate that matches nothing. + */ + @Nonnull + public StringPredicate newNothingPredicate() { + return PREDICATE_NOTHING; + } + /** * @param key * String to match against, case-sensitive. diff --git a/recaf-core/src/main/java/software/coley/recaf/services/source/AstRangeMapper.java b/recaf-core/src/main/java/software/coley/recaf/services/source/AstRangeMapper.java index 76c397d04..372c83573 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/source/AstRangeMapper.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/source/AstRangeMapper.java @@ -12,6 +12,7 @@ import org.openrewrite.java.tree.Space; import org.openrewrite.marker.Marker; import org.openrewrite.marker.Range; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.DebuggingLogger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.util.StringDiff; @@ -62,7 +63,7 @@ public static SortedMap computeRangeToTreeMapping(@Nonnull Tree tre } return cmp; }); - DiffHelper helper = backingText == null ? null : new DiffHelper(backingText, tree); + DiffHelper helper = backingText == null ? null : Unchecked.getOr(() -> new DiffHelper(backingText, tree), null); PositionPrintOutputCapture ppoc = new PositionPrintOutputCapture(helper); JavaPrinter printer = new JavaPrinter<>() { final JavaPrinter spacePrinter = new JavaPrinter<>(); @@ -80,7 +81,10 @@ public J visit(@Nullable Tree tree, @Nonnull PrintOutputCapturesuper.visit(tree, outputCapture), null); + if (t == null) { + return null; + } Range.Position endPosition = new Range.Position(ppoc.posInBacking, ppoc.line, ppoc.column); Range range = new Range(randomId(), startPosition, endPosition); rangeMap.put(range, t); diff --git a/recaf-core/src/main/java/software/coley/recaf/services/source/AstService.java b/recaf-core/src/main/java/software/coley/recaf/services/source/AstService.java index 06ff6838f..ea2c41fd3 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/source/AstService.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/source/AstService.java @@ -5,11 +5,11 @@ import org.openrewrite.internal.lang.Nullable; import org.openrewrite.java.JavaParser; import org.openrewrite.java.internal.JavaTypeCache; +import software.coley.collections.Unchecked; import software.coley.recaf.cdi.WorkspaceScoped; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.services.Service; import software.coley.recaf.util.ReflectUtil; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import java.util.Map; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/config/TextFormatConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/text/TextFormatConfig.java similarity index 97% rename from recaf-ui/src/main/java/software/coley/recaf/ui/config/TextFormatConfig.java rename to recaf-core/src/main/java/software/coley/recaf/services/text/TextFormatConfig.java index 70e87b9bb..82260a283 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/config/TextFormatConfig.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/text/TextFormatConfig.java @@ -1,4 +1,4 @@ -package software.coley.recaf.ui.config; +package software.coley.recaf.services.text; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; @@ -13,7 +13,7 @@ import software.coley.recaf.util.StringUtil; /** - * Config for text display. + * Config for text formatting. * * @author Matt Coley */ diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/BasicWorkspaceManager.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/BasicWorkspaceManager.java index d96fde1bf..6847d5a02 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/BasicWorkspaceManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/BasicWorkspaceManager.java @@ -7,12 +7,15 @@ import jakarta.inject.Inject; import org.slf4j.Logger; import software.coley.recaf.analytics.logging.Logging; -import software.coley.recaf.workspace.model.WorkspaceModificationListener; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.workspace.model.EmptyWorkspace; import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.WorkspaceModificationListener; +import software.coley.recaf.workspace.model.resource.WorkspaceResource; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * Basic workspace manager implementation. @@ -22,10 +25,10 @@ @ApplicationScoped public class BasicWorkspaceManager implements WorkspaceManager { private static final Logger logger = Logging.get(BasicWorkspaceManager.class); - private final List closeConditions = new ArrayList<>(); - private final List openListeners = new ArrayList<>(); - private final List closeListeners = new ArrayList<>(); - private final List defaultModificationListeners = new ArrayList<>(); + private final List closeConditions = new CopyOnWriteArrayList<>(); + private final List openListeners = new CopyOnWriteArrayList<>(); + private final List closeListeners = new CopyOnWriteArrayList<>(); + private final List defaultModificationListeners = new CopyOnWriteArrayList<>(); private final WorkspaceManagerConfig config; private Workspace current; @@ -45,28 +48,17 @@ public Workspace getCurrent() { @Override public void setCurrentIgnoringConditions(Workspace workspace) { - if (current != null) { - current.close(); - for (WorkspaceCloseListener listener : new ArrayList<>(closeListeners)) { - try { - listener.onWorkspaceClosed(current); - } catch (Throwable t) { - logger.error("Exception thrown by '{}' when closing workspace", - listener.getClass().getName(), t); - } - } + Workspace currentWorkspace = current; + if (currentWorkspace != null) { + currentWorkspace.close(); + CollectionUtil.safeForEach(closeListeners, listener -> listener.onWorkspaceClosed(currentWorkspace), + (listener, t) -> logger.error("Exception thrown when closing workspace", t)); } current = workspace; if (workspace != null) { defaultModificationListeners.forEach(workspace::addWorkspaceModificationListener); - for (WorkspaceOpenListener listener : new ArrayList<>(openListeners)) { - try { - listener.onWorkspaceOpened(workspace); - } catch (Throwable t) { - logger.error("Exception thrown by '{}' when opening workspace", - listener.getClass().getName(), t); - } - } + CollectionUtil.safeForEach(openListeners, listener -> listener.onWorkspaceOpened(workspace), + (listener, t) -> logger.error("Exception thrown by when opening workspace", t)); } } @@ -77,12 +69,12 @@ public List getWorkspaceCloseConditions() { } @Override - public void addWorkspaceCloseCondition(WorkspaceCloseCondition condition) { + public void addWorkspaceCloseCondition(@Nonnull WorkspaceCloseCondition condition) { closeConditions.add(condition); } @Override - public void removeWorkspaceCloseCondition(WorkspaceCloseCondition condition) { + public void removeWorkspaceCloseCondition(@Nonnull WorkspaceCloseCondition condition) { closeConditions.remove(condition); } @@ -93,12 +85,12 @@ public List getWorkspaceOpenListeners() { } @Override - public void addWorkspaceOpenListener(WorkspaceOpenListener listener) { + public void addWorkspaceOpenListener(@Nonnull WorkspaceOpenListener listener) { openListeners.add(listener); } @Override - public void removeWorkspaceOpenListener(WorkspaceOpenListener listener) { + public void removeWorkspaceOpenListener(@Nonnull WorkspaceOpenListener listener) { openListeners.remove(listener); } @@ -109,12 +101,12 @@ public List getWorkspaceCloseListeners() { } @Override - public void addWorkspaceCloseListener(WorkspaceCloseListener listener) { + public void addWorkspaceCloseListener(@Nonnull WorkspaceCloseListener listener) { closeListeners.add(listener); } @Override - public void removeWorkspaceCloseListener(WorkspaceCloseListener listener) { + public void removeWorkspaceCloseListener(@Nonnull WorkspaceCloseListener listener) { closeListeners.remove(listener); } @@ -125,13 +117,25 @@ public List getDefaultWorkspaceModificationListen } @Override - public void addDefaultWorkspaceModificationListeners(WorkspaceModificationListener listener) { + public void addDefaultWorkspaceModificationListeners(@Nonnull WorkspaceModificationListener listener) { defaultModificationListeners.add(listener); } @Override - public void removeDefaultWorkspaceModificationListeners(WorkspaceModificationListener listener) { + public void removeDefaultWorkspaceModificationListeners(@Nonnull WorkspaceModificationListener listener) { defaultModificationListeners.remove(listener); + + addDefaultWorkspaceModificationListeners(new WorkspaceModificationListener() { + @Override + public void onAddLibrary(@Nonnull Workspace workspace, @Nonnull WorkspaceResource library) { + // Supporting library added to workspace + } + + @Override + public void onRemoveLibrary(@Nonnull Workspace workspace, @Nonnull WorkspaceResource library) { + // Supporting library removed from workspace + } + }); } @Nonnull diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/WorkspaceManager.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/WorkspaceManager.java index a60d817f1..82291da43 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/WorkspaceManager.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/WorkspaceManager.java @@ -6,11 +6,9 @@ import jakarta.enterprise.inject.Instance; import jakarta.enterprise.inject.Produces; import software.coley.recaf.services.Service; -import software.coley.recaf.workspace.model.WorkspaceModificationListener; -import software.coley.recaf.services.workspace.io.WorkspaceExportOptions; -import software.coley.recaf.services.workspace.io.WorkspaceExporter; import software.coley.recaf.workspace.model.BasicWorkspace; import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.WorkspaceModificationListener; import software.coley.recaf.workspace.model.resource.WorkspaceResource; import java.util.Collections; @@ -24,17 +22,6 @@ public interface WorkspaceManager extends Service { String SERVICE_ID = "workspace-manager"; - /** - * @param options - * Exporting options, includes details on where to export, how to repackage content, etc. - * - * @return A new exporter configured to match the options. - */ - @Nonnull - default WorkspaceExporter createExporter(@Nonnull WorkspaceExportOptions options) { - return options.create(); - } - /** * Exposes the current workspace directly and through CDI. * Any {@link Instance} in the Recaf application should point to this value. @@ -125,13 +112,13 @@ default Workspace createWorkspace(@Nonnull WorkspaceResource primary, @Nonnull L * @param condition * Condition to add. */ - void addWorkspaceCloseCondition(WorkspaceCloseCondition condition); + void addWorkspaceCloseCondition(@Nonnull WorkspaceCloseCondition condition); /** * @param condition * Condition to remove. */ - void removeWorkspaceCloseCondition(WorkspaceCloseCondition condition); + void removeWorkspaceCloseCondition(@Nonnull WorkspaceCloseCondition condition); /** * @return Listeners for when a new workspace is assigned as the current one. @@ -143,13 +130,13 @@ default Workspace createWorkspace(@Nonnull WorkspaceResource primary, @Nonnull L * @param listener * Listener to add. */ - void addWorkspaceOpenListener(WorkspaceOpenListener listener); + void addWorkspaceOpenListener(@Nonnull WorkspaceOpenListener listener); /** * @param listener * Listener to remove. */ - void removeWorkspaceOpenListener(WorkspaceOpenListener listener); + void removeWorkspaceOpenListener(@Nonnull WorkspaceOpenListener listener); /** * @return Listeners for when the current workspace is removed as being current. @@ -161,13 +148,13 @@ default Workspace createWorkspace(@Nonnull WorkspaceResource primary, @Nonnull L * @param listener * Listener to add. */ - void addWorkspaceCloseListener(WorkspaceCloseListener listener); + void addWorkspaceCloseListener(@Nonnull WorkspaceCloseListener listener); /** * @param listener * Listener to remove. */ - void removeWorkspaceCloseListener(WorkspaceCloseListener listener); + void removeWorkspaceCloseListener(@Nonnull WorkspaceCloseListener listener); /** * @return Listeners to add to any workspace passed to {@link #setCurrent(Workspace)}. @@ -179,11 +166,11 @@ default Workspace createWorkspace(@Nonnull WorkspaceResource primary, @Nonnull L * @param listener * Listener to add. */ - void addDefaultWorkspaceModificationListeners(WorkspaceModificationListener listener); + void addDefaultWorkspaceModificationListeners(@Nonnull WorkspaceModificationListener listener); /** * @param listener * Listener to remove. */ - void removeDefaultWorkspaceModificationListeners(WorkspaceModificationListener listener); + void removeDefaultWorkspaceModificationListeners(@Nonnull WorkspaceModificationListener listener); } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java index 075790f27..0ce2415a7 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/BasicResourceImporter.java @@ -4,6 +4,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.lljzip.format.model.CentralDirectoryFileHeader; import software.coley.lljzip.format.model.ZipArchive; import software.coley.lljzip.util.ExtraFieldTime; @@ -14,6 +15,7 @@ import software.coley.recaf.info.properties.builtin.*; import software.coley.recaf.services.Service; import software.coley.recaf.util.*; +import software.coley.recaf.util.android.DexIOUtil; import software.coley.recaf.util.io.ByteSource; import software.coley.recaf.util.io.ByteSources; import software.coley.recaf.util.io.LocalFileHeaderSource; diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/ResourceImporterConfig.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/ResourceImporterConfig.java index d15883a3f..add51a5b0 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/ResourceImporterConfig.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/ResourceImporterConfig.java @@ -3,6 +3,7 @@ import jakarta.annotation.Nonnull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import software.coley.collections.func.UncheckedFunction; import software.coley.lljzip.ZipIO; import software.coley.lljzip.format.model.CentralDirectoryFileHeader; import software.coley.lljzip.format.model.LocalFileHeader; @@ -14,7 +15,6 @@ import software.coley.recaf.config.BasicConfigValue; import software.coley.recaf.config.ConfigGroups; import software.coley.recaf.services.ServiceConfig; -import software.coley.recaf.util.UncheckedFunction; /** * Config for {@link ResourceImporter}. diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java index 6b87cef1c..a60e97712 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExportOptions.java @@ -1,10 +1,9 @@ package software.coley.recaf.services.workspace.io; import jakarta.annotation.Nonnull; +import software.coley.collections.Unchecked; import software.coley.recaf.info.*; import software.coley.recaf.info.properties.builtin.*; -import software.coley.recaf.services.workspace.WorkspaceManager; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.util.ZipCreationUtils; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; @@ -28,8 +27,7 @@ import static software.coley.lljzip.format.compression.ZipCompressions.STORED; /** - * Options for configuring / preparing a {@link WorkspaceExporter} when calling - * {@link WorkspaceManager#createExporter(WorkspaceExportOptions)}. + * Options for configuring / preparing a {@link WorkspaceExporter}. * * @author Matt Coley */ @@ -46,7 +44,7 @@ public class WorkspaceExportOptions { * @param path * Path to write to. */ - public WorkspaceExportOptions(OutputType outputType, Path path) { + public WorkspaceExportOptions(@Nonnull OutputType outputType, @Nonnull Path path) { this(CompressType.MATCH_ORIGINAL, outputType, path); } @@ -58,7 +56,7 @@ public WorkspaceExportOptions(OutputType outputType, Path path) { * @param path * Path to write to. */ - public WorkspaceExportOptions(CompressType compressType, OutputType outputType, Path path) { + public WorkspaceExportOptions(@Nonnull CompressType compressType, @Nonnull OutputType outputType, @Nonnull Path path) { this.compressType = compressType; this.outputType = outputType; this.path = path; @@ -84,6 +82,7 @@ public void setCreateZipDirEntries(boolean createZipDirEntries) { /** * @return New exporter from current options. */ + @Nonnull public WorkspaceExporter create() { return new WorkspaceExporterImpl(); } diff --git a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExporter.java b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExporter.java index 0ac186413..8ecf032bf 100644 --- a/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExporter.java +++ b/recaf-core/src/main/java/software/coley/recaf/services/workspace/io/WorkspaceExporter.java @@ -11,7 +11,6 @@ * * @author Matt Coley * @see WorkspaceExportOptions - * @see WorkspaceManager#createExporter(WorkspaceExportOptions) */ public interface WorkspaceExporter { /** diff --git a/recaf-core/src/main/java/software/coley/recaf/util/ClassDefiner.java b/recaf-core/src/main/java/software/coley/recaf/util/ClassDefiner.java index 0140d2452..27a219632 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/ClassDefiner.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/ClassDefiner.java @@ -1,5 +1,6 @@ package software.coley.recaf.util; +import jakarta.annotation.Nonnull; import software.coley.collections.Maps; import java.util.Map; @@ -18,7 +19,7 @@ public class ClassDefiner extends ClassLoader { * @param bytecode * Bytecode of class. */ - public ClassDefiner(String name, byte[] bytecode) { + public ClassDefiner(@Nonnull String name, @Nonnull byte[] bytecode) { this(Maps.of(name, bytecode)); } @@ -26,7 +27,7 @@ public ClassDefiner(String name, byte[] bytecode) { * @param classes * Map of classes. */ - public ClassDefiner(Map classes) { + public ClassDefiner(@Nonnull Map classes) { super(ClassLoader.getSystemClassLoader()); this.classes = classes; } diff --git a/recaf-core/src/main/java/software/coley/recaf/util/ClasspathUtil.java b/recaf-core/src/main/java/software/coley/recaf/util/ClasspathUtil.java index 906ec464e..08cc1ddf3 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/ClasspathUtil.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/ClasspathUtil.java @@ -2,6 +2,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import software.coley.collections.Unchecked; import software.coley.collections.tree.SortedTreeImpl; import software.coley.collections.tree.Tree; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/CollectionUtil.java b/recaf-core/src/main/java/software/coley/recaf/util/CollectionUtil.java index 971bd1e25..245d6a443 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/CollectionUtil.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/CollectionUtil.java @@ -1,10 +1,10 @@ package software.coley.recaf.util; import jakarta.annotation.Nonnull; +import org.openrewrite.internal.ThrowingConsumer; -import java.util.AbstractList; -import java.util.Collections; -import java.util.List; +import java.util.*; +import java.util.function.BiConsumer; /** * Various collection utils. @@ -13,6 +13,33 @@ * @author Paul Boddington - Binary search. */ public class CollectionUtil { + /** + * Runs an action via a consumer on each item of the collection. If an error occurs for a given item, + * it is passed along to the error consumer along with the error before moving onto the next item in + * collection. + * + * @param collection + * Collection to iterate over. + * @param consumer + * Action to run on each item. + * @param errorConsumer + * Error handling taking in the item that the consumer failed on, and the error thrown. + * @param + * Item type. + */ + public static void safeForEach(@Nonnull Collection collection, + @Nonnull ThrowingConsumer consumer, + @Nonnull BiConsumer errorConsumer) { + // Iterate over a shallow-copy in case the consumer updates the original collection. + for (T item : new ArrayList<>(collection)) { + try { + consumer.accept(item); + } catch (Throwable t) { + errorConsumer.accept(item, t); + } + } + } + /** * @param list * List to insert into. diff --git a/recaf-core/src/main/java/software/coley/recaf/util/EscapeUtil.java b/recaf-core/src/main/java/software/coley/recaf/util/EscapeUtil.java index 019f8d6e3..621f28978 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/EscapeUtil.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/EscapeUtil.java @@ -1,5 +1,6 @@ package software.coley.recaf.util; +import com.android.tools.r8.utils.TriFunction; import it.unimi.dsi.fastutil.chars.Char2ObjectArrayMap; import it.unimi.dsi.fastutil.chars.Char2ObjectMap; import it.unimi.dsi.fastutil.chars.CharSet; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/JavaVersion.java b/recaf-core/src/main/java/software/coley/recaf/util/JavaVersion.java index 75606743b..90586a225 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/JavaVersion.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/JavaVersion.java @@ -14,11 +14,7 @@ public class JavaVersion { * The offset from which a version and the version constant value is. For example, Java 8 is 52 (44 + 8). */ public static final int VERSION_OFFSET = 44; - private static final String JAVA_CLASS_VERSION = "java.class.version"; - private static final String JAVA_VM_SPEC_VERSION = "java.vm.specification.version"; - private static final int FALLBACK_VERSION = 22; private static final Logger logger = Logging.get(JavaVersion.class); - private static int version = -1; /** * Get the supported Java version of the current JVM. @@ -26,30 +22,7 @@ public class JavaVersion { * @return Version. */ public static int get() { - if (version < 0) { - // Check for class version - String property = System.getProperty(JAVA_CLASS_VERSION, ""); - if (!property.isEmpty()) - return version = (int) (Float.parseFloat(property) - VERSION_OFFSET); - - // Odd, not found. Try the spec version - logger.warn("Property '{}' not found, using '{}' as fallback", - JAVA_CLASS_VERSION, - JAVA_VM_SPEC_VERSION - ); - property = System.getProperty(JAVA_VM_SPEC_VERSION, ""); - if (property.contains(".")) - return version = (int) Float.parseFloat(property.substring(property.indexOf('.') + 1)); - else if (!property.isEmpty()) - return version = Integer.parseInt(property); - logger.warn("Property '{}' not found, using '{}' as fallback", - JAVA_VM_SPEC_VERSION, FALLBACK_VERSION - ); - - // Very odd - return FALLBACK_VERSION; - } - return version; + return Runtime.version().feature(); } /** diff --git a/recaf-core/src/main/java/software/coley/recaf/util/ModulesIOUtil.java b/recaf-core/src/main/java/software/coley/recaf/util/ModulesIOUtil.java index 7a5fdfb35..184aabb53 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/ModulesIOUtil.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/ModulesIOUtil.java @@ -1,5 +1,6 @@ package software.coley.recaf.util; +import software.coley.collections.Unchecked; import software.coley.recaf.util.io.ByteSourceElement; import software.coley.recaf.util.io.ByteSources; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/StringUtil.java b/recaf-core/src/main/java/software/coley/recaf/util/StringUtil.java index a85c9afc8..6e6294400 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/StringUtil.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/StringUtil.java @@ -517,6 +517,70 @@ public static String getCommonPrefix(@Nonnull String a, @Nonnull String b) { return a.substring(0, len); } + /** + * @param text + * Text to compute length of. + * + * @return Length of text, considering tabs. + */ + public static int getTabAdjustedLength(@Nonnull String text) { + return getTabAdjustedLength(text, 4); + } + + /** + * @param text + * Text to compute length of. + * @param tabWidth + * Tab width. + * + * @return Length of text, considering tabs. + */ + public static int getTabAdjustedLength(@Nonnull String text, int tabWidth) { + int tabIndex = text.indexOf('\t'); + while (tabIndex >= 0) { + if (tabIndex == 0) { + text = " ".repeat(tabWidth) + text.substring(1); + } else { + // Assuming tab width is four: Having two spaces then a tab still yields a length of 4 visually + // Thus, we must consider such alignment in our length adjustments. + String pre = text.substring(0, tabIndex); + String post = text.substring(tabIndex + 1); + int alignedLengthExtra = tabWidth - (tabIndex % tabWidth); + text = pre + " ".repeat(alignedLengthExtra) + post; + } + tabIndex = text.indexOf('\t'); + } + return text.length(); + } + + /** + * @param text + * Text to scan prefix of. + * + * @return Number of blank spaces until some non-whitespace char is found. + */ + public static int getWhitespacePrefixLength(@Nonnull String text) { + return getWhitespacePrefixLength(text, 4); + } + + /** + * @param text + * Text to scan prefix of. + * @param tabWidth + * Width of spaces to translate tab characters to. + * + * @return Number of blank spaces until some non-whitespace char is found. + */ + public static int getWhitespacePrefixLength(@Nonnull String text, int tabWidth) { + char[] chars = text.toCharArray(); + int offset = 0; + for (char c : chars) { + if (c == ' ' || c == '\t') offset++; + else break; + } + return getTabAdjustedLength(text.substring(0, offset), tabWidth); + } + /** * @param len * Target string length. @@ -527,6 +591,7 @@ public static String getCommonPrefix(@Nonnull String a, @Nonnull String b) { * * @return String with pattern filling up to the desired length on the left. */ + @Nonnull public static String fillLeft(int len, @Nonnull String pattern, @Nullable String string) { StringBuilder sb = new StringBuilder(string == null ? "" : string); while (sb.length() < len) diff --git a/recaf-core/src/main/java/software/coley/recaf/util/TriFunction.java b/recaf-core/src/main/java/software/coley/recaf/util/TriFunction.java deleted file mode 100644 index 7a75c8713..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/TriFunction.java +++ /dev/null @@ -1,27 +0,0 @@ -package software.coley.recaf.util; - -/** - * Generic function for 3 arguments. - * - * @param - * First parameter type. - * @param - * Second parameter type. - * @param - * Third parameter type. - * @param - * Return type. - */ -interface TriFunction { - /** - * @param a - * First arg. - * @param b - * Second arg. - * @param c - * Third arg. - * - * @return Return value. - */ - R apply(A a, B b, C c); -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/Types.java b/recaf-core/src/main/java/software/coley/recaf/util/Types.java index 96ceaa647..8c72cd440 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/Types.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/Types.java @@ -5,6 +5,7 @@ import org.objectweb.asm.Opcodes; import org.objectweb.asm.Type; import org.objectweb.asm.signature.SignatureReader; +import org.objectweb.asm.signature.SignatureVisitor; import org.objectweb.asm.signature.SignatureWriter; import java.util.Arrays; @@ -325,12 +326,13 @@ public static String pretty(@Nonnull Type type) { * @param signature * Signature text. * @param isTypeSignature - * See {@link org.objectweb.asm.commons.ClassRemapper} for usage. + * See {@link org.objectweb.asm.signature.SignatureReader#accept(SignatureVisitor)} ({@code false}) + * and {@link org.objectweb.asm.signature.SignatureReader#acceptType(SignatureVisitor)} ({@code true}) for usage. * * @return {@code true} for a valid signature. Will be {@code false} otherwise, or for {@code null} values. */ public static boolean isValidSignature(@Nullable String signature, boolean isTypeSignature) { - if (signature == null) + if (signature == null || signature.isEmpty()) return false; try { SignatureReader signatureReader = new SignatureReader(signature); diff --git a/recaf-core/src/main/java/software/coley/recaf/util/Unchecked.java b/recaf-core/src/main/java/software/coley/recaf/util/Unchecked.java deleted file mode 100644 index 4875bee5a..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/Unchecked.java +++ /dev/null @@ -1,204 +0,0 @@ -package software.coley.recaf.util; - -import jakarta.annotation.Nonnull; -import jakarta.annotation.Nullable; - -import java.util.function.*; - -/** - * Convenience calls for the error-able lambda types. - * - * @author Matt Coley - */ -public class Unchecked { - /** - * @param value - * Value to cast. - * @param - * Target type. - * - * @return Value casted. - */ - @SuppressWarnings("unchecked") - public static T cast(@Nullable Object value) { - return (T) value; - } - - /** - * @param runnable - * Runnable. - */ - public static void run(@Nonnull UncheckedRunnable runnable) { - runnable.run(); - } - - /** - * @param supplier - * Supplier. - * @param - * Supplier type. - * - * @return Supplied value. - */ - public static T get(@Nonnull UncheckedSupplier supplier) { - return supplier.get(); - } - - /** - * @param supplier - * Supplier. - * @param fallback - * Value to return if supplier fails. - * @param - * Supplier type. - * - * @return Supplied value, or fallback if supplier failed. - */ - public static T getOr(@Nullable UncheckedSupplier supplier, T fallback) { - if (supplier == null) - return fallback; - try { - return supplier.get(); - } catch (Throwable t) { - return fallback; - } - } - - /** - * @param consumer - * Consumer. - * @param value - * Consumed value. - * @param - * Consumer type. - */ - public static void accept(@Nonnull UncheckedConsumer consumer, T value) { - consumer.accept(value); - } - - /** - * @param consumer - * Consumer. - * @param t - * First value. - * @param u - * Second value. - * @param - * First type. - * @param - * Second type. - */ - public static void baccept(@Nonnull UncheckedBiConsumer consumer, T t, U u) { - consumer.accept(t, u); - } - - /** - * @param fn - * Function. - * @param value - * Function value. - * @param - * Input type. - * @param - * Output type. - */ - public static R map(@Nonnull UncheckedFunction fn, T value) { - return fn.apply(value); - } - - /** - * @param fn - * Function. - * @param t - * First function value. - * @param u - * Second function value. - * @param - * First input type. - * @param - * Second input type. - * @param - * Output type. - */ - public static R bmap(@Nonnull UncheckedBiFunction fn, T t, U u) { - return fn.apply(t, u); - } - - /** - * Helper method to created unchecked runnable. - * - * @param runnable - * Unchecked runnable. - * - * @return Unchecked runnable. - */ - @Nonnull - public static Runnable runnable(@Nonnull UncheckedRunnable runnable) { - return runnable; - } - - /** - * Helper method to created unchecked supplier. - * - * @param supplier - * Unchecked supplier. - * - * @return Unchecked supplier. - */ - @Nonnull - public static Supplier supply(@Nonnull UncheckedSupplier supplier) { - return supplier; - } - - /** - * Helper method to created unchecked consumer. - * - * @param consumer - * Unchecked consumer. - * - * @return Unchecked consumer. - */ - @Nonnull - public static Consumer consumer(@Nonnull UncheckedConsumer consumer) { - return consumer; - } - - /** - * Helper method to created unchecked consumer. - * - * @param consumer - * Unchecked consumer. - * - * @return Unchecked consumer. - */ - @Nonnull - public static BiConsumer bconsumer(@Nonnull UncheckedBiConsumer consumer) { - return consumer; - } - - /** - * Helper method to created unchecked function. - * - * @param fn - * Unchecked function. - * - * @return Unchecked function. - */ - @Nonnull - public static Function function(@Nonnull UncheckedFunction fn) { - return fn; - } - - /** - * Helper method to created unchecked function. - * - * @param fn - * Unchecked function. - * - * @return Unchecked function. - */ - @Nonnull - public static BiFunction bfunction(@Nonnull UncheckedBiFunction fn) { - return fn; - } -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedBiConsumer.java b/recaf-core/src/main/java/software/coley/recaf/util/UncheckedBiConsumer.java deleted file mode 100644 index e956e4619..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedBiConsumer.java +++ /dev/null @@ -1,22 +0,0 @@ -package software.coley.recaf.util; - -import java.util.function.BiConsumer; - -/** - * Its {@link BiConsumer} but can throw an exception. - * - * @author xDark - */ -@FunctionalInterface -public interface UncheckedBiConsumer extends BiConsumer { - @Override - default void accept(T t, U u) { - try { - uncheckedAccept(t, u); - } catch (Throwable th) { - ReflectUtil.propagate(th); - } - } - - void uncheckedAccept(T t, U u) throws Throwable; -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedBiFunction.java b/recaf-core/src/main/java/software/coley/recaf/util/UncheckedBiFunction.java deleted file mode 100644 index f56d698b5..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedBiFunction.java +++ /dev/null @@ -1,34 +0,0 @@ -package software.coley.recaf.util; - -import java.util.function.BiFunction; - -/** - * Its {@link BiFunction} but can throw an exception. - * - * @author xDark - */ -@FunctionalInterface -public interface UncheckedBiFunction extends BiFunction { - @Override - default R apply(T t, U u) { - try { - return uncheckedApply(t, u); - } catch (Throwable th) { - ReflectUtil.propagate(th); - return null; - } - } - - /** - * @param t - * First input. - * @param u - * Second input. - * - * @return The function result. - * - * @throws Throwable - * Whenever. - */ - R uncheckedApply(T t, U u) throws Throwable; -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedConsumer.java b/recaf-core/src/main/java/software/coley/recaf/util/UncheckedConsumer.java deleted file mode 100644 index 3ea7931eb..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedConsumer.java +++ /dev/null @@ -1,29 +0,0 @@ -package software.coley.recaf.util; - -import java.util.function.Consumer; - -/** - * Its {@link Consumer} but can throw an exception. - * - * @author Matt Coley - */ -@FunctionalInterface -public interface UncheckedConsumer extends Consumer { - @Override - default void accept(T t) { - try { - uncheckedAccept(t); - } catch (Throwable th) { - ReflectUtil.propagate(th); - } - } - - /** - * @param input - * Consumer input. - * - * @throws Throwable - * Whenever. - */ - void uncheckedAccept(T input) throws Throwable; -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedFunction.java b/recaf-core/src/main/java/software/coley/recaf/util/UncheckedFunction.java deleted file mode 100644 index 19fe420c5..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedFunction.java +++ /dev/null @@ -1,32 +0,0 @@ -package software.coley.recaf.util; - -import java.util.function.Function; - -/** - * Its {@link Function} but can throw an exception. - * - * @author xDark - */ -@FunctionalInterface -public interface UncheckedFunction extends Function { - @Override - default R apply(T t) { - try { - return uncheckedApply(t); - } catch (Throwable th) { - ReflectUtil.propagate(th); - return null; - } - } - - /** - * @param input - * Function input. - * - * @return The function result. - * - * @throws Throwable - * Whenever. - */ - R uncheckedApply(T input) throws Throwable; -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedRunnable.java b/recaf-core/src/main/java/software/coley/recaf/util/UncheckedRunnable.java deleted file mode 100644 index 2d30c1573..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedRunnable.java +++ /dev/null @@ -1,24 +0,0 @@ -package software.coley.recaf.util; - -/** - * Its {@link Runnable} but can throw an exception. - * - * @author Matt Coley - */ -@FunctionalInterface -public interface UncheckedRunnable extends Runnable { - @Override - default void run() { - try { - uncheckedRun(); - } catch (Throwable t) { - ReflectUtil.propagate(t); - } - } - - /** - * @throws Throwable - * Whenever. - */ - void uncheckedRun() throws Throwable; -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedSupplier.java b/recaf-core/src/main/java/software/coley/recaf/util/UncheckedSupplier.java deleted file mode 100644 index f0bf6df2a..000000000 --- a/recaf-core/src/main/java/software/coley/recaf/util/UncheckedSupplier.java +++ /dev/null @@ -1,30 +0,0 @@ -package software.coley.recaf.util; - -import java.util.function.Supplier; - -/** - * Its {@link Supplier} but can throw an exception. - * - * @author Matt Coley - */ -@FunctionalInterface -public interface UncheckedSupplier extends Supplier { - - @Override - default T get() { - try { - return uncheckedGet(); - } catch (Throwable t) { - ReflectUtil.propagate(t); - return null; - } - } - - /** - * @return Supplier output. - * - * @throws Throwable - * Whenever. - */ - T uncheckedGet() throws Throwable; -} diff --git a/recaf-core/src/main/java/software/coley/recaf/util/ZipCreationUtils.java b/recaf-core/src/main/java/software/coley/recaf/util/ZipCreationUtils.java index 9693caf1f..88d27c378 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/ZipCreationUtils.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/ZipCreationUtils.java @@ -3,6 +3,7 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; import org.slf4j.Logger; +import software.coley.collections.func.UncheckedConsumer; import software.coley.recaf.analytics.logging.Logging; import java.io.ByteArrayOutputStream; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/DexIOUtil.java b/recaf-core/src/main/java/software/coley/recaf/util/android/DexIOUtil.java similarity index 83% rename from recaf-core/src/main/java/software/coley/recaf/util/DexIOUtil.java rename to recaf-core/src/main/java/software/coley/recaf/util/android/DexIOUtil.java index 75bd24872..8e25186f3 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/DexIOUtil.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/android/DexIOUtil.java @@ -1,6 +1,7 @@ -package software.coley.recaf.util; +package software.coley.recaf.util.android; import com.android.tools.r8.graph.DexProgramClass; +import jakarta.annotation.Nonnull; import software.coley.dextranslator.model.ApplicationData; import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.info.builder.AndroidClassInfoBuilder; @@ -25,7 +26,8 @@ public class DexIOUtil { * @throws IOException * When the dex file cannot be read from. */ - public static AndroidClassBundle read(ByteSource source) throws IOException { + @Nonnull + public static AndroidClassBundle read(@Nonnull ByteSource source) throws IOException { return read(source.readAll()); } @@ -38,7 +40,8 @@ public static AndroidClassBundle read(ByteSource source) throws IOException { * @throws IOException * When the dex file cannot be read from. */ - public static AndroidClassBundle read(byte[] dex) throws IOException { + @Nonnull + public static AndroidClassBundle read(@Nonnull byte[] dex) throws IOException { // Read dex file content ApplicationData data = ApplicationData.fromDex(dex); diff --git a/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java b/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java index 35dff8811..3173e8f6d 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/io/MemorySegmentDataSource.java @@ -59,6 +59,7 @@ public MemorySegment mmap() throws IOException { return data; } + // TODO: Replace this class with the one from LL-J-Zip when the next release is made static final class MemorySegmentInputStream extends InputStream { private final MemorySegment data; private long read; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/visitors/FieldAnnotationRemovingVisitor.java b/recaf-core/src/main/java/software/coley/recaf/util/visitors/FieldAnnotationRemovingVisitor.java index 6979a6104..6b6c1149c 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/visitors/FieldAnnotationRemovingVisitor.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/visitors/FieldAnnotationRemovingVisitor.java @@ -9,13 +9,16 @@ import software.coley.recaf.RecafConstants; import software.coley.recaf.info.member.FieldMember; +import java.util.Collection; +import java.util.Collections; + /** * Simple visitor for removing an annotation. * * @author Matt Coley */ public class FieldAnnotationRemovingVisitor extends FieldVisitor { - private final String annotationType; + private final Collection annotationTypes; /** * @param fv @@ -24,9 +27,20 @@ public class FieldAnnotationRemovingVisitor extends FieldVisitor { * Annotation type to remove. */ public FieldAnnotationRemovingVisitor(@Nullable FieldVisitor fv, - @Nonnull String annotationType) { + @Nonnull String annotationType) { + this(fv, Collections.singleton(annotationType)); + } + + /** + * @param fv + * Parent visitor. + * @param annotationTypes + * Annotation types to remove. + */ + public FieldAnnotationRemovingVisitor(@Nullable FieldVisitor fv, + @Nonnull Collection annotationTypes) { super(RecafConstants.getAsmVersion(), fv); - this.annotationType = annotationType; + this.annotationTypes = annotationTypes; } /** @@ -41,12 +55,27 @@ public FieldAnnotationRemovingVisitor(@Nullable FieldVisitor fv, */ @Nonnull public static ClassVisitor forClass(@Nonnull ClassVisitor cv, @Nonnull String annotationType, @Nullable FieldMember field) { + return forClass(cv, Collections.singleton(annotationType), field); + } + + /** + * @param cv + * Visitor of a class. + * @param annotationTypes + * Annotation types to remove on a field. + * @param field + * Field to target, or {@code null} for any field. + * + * @return Visitor that removes field annotations in the requested circumstances. + */ + @Nonnull + public static ClassVisitor forClass(@Nonnull ClassVisitor cv, @Nonnull Collection annotationTypes, @Nullable FieldMember field) { return new ClassVisitor(RecafConstants.getAsmVersion(), cv) { @Override public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { FieldVisitor fv = super.visitField(access, name, descriptor, signature, value); if (field == null || (field.getName().equals(name) && field.getDescriptor().equals(descriptor))) - return new FieldAnnotationRemovingVisitor(fv, annotationType); + return new FieldAnnotationRemovingVisitor(fv, annotationTypes); return fv; } }; @@ -54,14 +83,16 @@ public FieldVisitor visitField(int access, String name, String descriptor, Strin @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationTypes.contains(type)) return null; return super.visitAnnotation(descriptor, visible); } @Override public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationTypes.contains(type)) return null; return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); } diff --git a/recaf-core/src/main/java/software/coley/recaf/util/visitors/IllegalSignatureRemovingVisitor.java b/recaf-core/src/main/java/software/coley/recaf/util/visitors/IllegalSignatureRemovingVisitor.java index 51c670321..bb6d17105 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/visitors/IllegalSignatureRemovingVisitor.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/visitors/IllegalSignatureRemovingVisitor.java @@ -52,10 +52,10 @@ public void visitLocalVariable(String name, String desc, String s, Label start, }; } - private String map(String signature, boolean isOnClassOrMethod) { + private String map(String signature, boolean isTypeSignature) { if (signature == null) return null; - if (Types.isValidSignature(signature, isOnClassOrMethod)) + if (Types.isValidSignature(signature, isTypeSignature)) return signature; detected = true; return null; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/visitors/MemberPredicate.java b/recaf-core/src/main/java/software/coley/recaf/util/visitors/MemberPredicate.java index c2dd06eac..a0de119a9 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/visitors/MemberPredicate.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/visitors/MemberPredicate.java @@ -25,14 +25,14 @@ static MemberPredicate of(@Nonnull ClassMember member) { @Override public boolean matchField(int access, String name, String desc, String sig, Object value) { if (member.isField()) - return name.equals(member.getName()) && desc.equals(member.getName()); + return name.equals(member.getName()) && desc.equals(member.getDescriptor()); return false; } @Override public boolean matchMethod(int access, String name, String desc, String sig, String[] exceptions) { if (member.isMethod()) - return name.equals(member.getName()) && desc.equals(member.getName()); + return name.equals(member.getName()) && desc.equals(member.getDescriptor()); return false; } }; @@ -51,7 +51,7 @@ static MemberPredicate of(@Nonnull Collection members) { public boolean matchField(int access, String name, String desc, String sig, Object value) { for (ClassMember member : members) if (member.isField()) - return name.equals(member.getName()) && desc.equals(member.getName()); + return name.equals(member.getName()) && desc.equals(member.getDescriptor()); return false; } @@ -59,7 +59,7 @@ public boolean matchField(int access, String name, String desc, String sig, Obje public boolean matchMethod(int access, String name, String desc, String sig, String[] exceptions) { for (ClassMember member : members) if (member.isMethod()) - return name.equals(member.getName()) && desc.equals(member.getName()); + return name.equals(member.getName()) && desc.equals(member.getDescriptor()); return false; } }; diff --git a/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodAnnotationRemovingVisitor.java b/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodAnnotationRemovingVisitor.java index 22e73b524..81ddd4941 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodAnnotationRemovingVisitor.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodAnnotationRemovingVisitor.java @@ -4,16 +4,18 @@ import jakarta.annotation.Nullable; import org.objectweb.asm.*; import software.coley.recaf.RecafConstants; -import software.coley.recaf.info.member.FieldMember; import software.coley.recaf.info.member.MethodMember; +import java.util.Collection; +import java.util.Collections; + /** * Simple visitor for removing an annotation. * * @author Matt Coley */ public class MethodAnnotationRemovingVisitor extends MethodVisitor { - private final String annotationType; + private final Collection annotationType; /** * @param mv @@ -22,9 +24,20 @@ public class MethodAnnotationRemovingVisitor extends MethodVisitor { * Annotation type to remove. */ public MethodAnnotationRemovingVisitor(@Nullable MethodVisitor mv, - @Nonnull String annotationType) { + @Nonnull String annotationType) { + this(mv, Collections.singleton(annotationType)); + } + + /** + * @param mv + * Parent visitor. + * @param annotationTypes + * Annotation types to remove. + */ + public MethodAnnotationRemovingVisitor(@Nullable MethodVisitor mv, + @Nonnull Collection annotationTypes) { super(RecafConstants.getAsmVersion(), mv); - this.annotationType = annotationType; + this.annotationType = annotationTypes; } /** @@ -39,12 +52,27 @@ public MethodAnnotationRemovingVisitor(@Nullable MethodVisitor mv, */ @Nonnull public static ClassVisitor forClass(@Nonnull ClassVisitor cv, @Nonnull String annotationType, @Nullable MethodMember method) { + return forClass(cv, Collections.singleton(annotationType), method); + } + + /** + * @param cv + * Visitor of a class. + * @param annotationTypes + * Annotation types to remove on a method. + * @param method + * Method to target, or {@code null} for any method. + * + * @return Visitor that removes method annotations in the requested circumstances. + */ + @Nonnull + public static ClassVisitor forClass(@Nonnull ClassVisitor cv, @Nonnull Collection annotationTypes, @Nullable MethodMember method) { return new ClassVisitor(RecafConstants.getAsmVersion(), cv) { @Override public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { MethodVisitor mv = super.visitMethod(access, name, descriptor, signature, exceptions); if (method == null || (method.getName().equals(name) && method.getDescriptor().equals(descriptor))) - return new MethodAnnotationRemovingVisitor(mv, annotationType); + return new MethodAnnotationRemovingVisitor(mv, annotationTypes); return mv; } }; @@ -52,42 +80,48 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str @Override public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationType.contains(type)) return null; return super.visitAnnotation(descriptor, visible); } @Override public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationType.contains(type)) return null; return super.visitTypeAnnotation(typeRef, typePath, descriptor, visible); } @Override public AnnotationVisitor visitParameterAnnotation(int parameter, String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationType.contains(type)) return null; return super.visitParameterAnnotation(parameter, descriptor, visible); } @Override public AnnotationVisitor visitInsnAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationType.contains(type)) return null; return super.visitInsnAnnotation(typeRef, typePath, descriptor, visible); } @Override public AnnotationVisitor visitTryCatchAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationType.contains(type)) return null; return super.visitTryCatchAnnotation(typeRef, typePath, descriptor, visible); } @Override public AnnotationVisitor visitLocalVariableAnnotation(int typeRef, TypePath typePath, Label[] start, Label[] end, int[] index, String descriptor, boolean visible) { - if (annotationType.equals(descriptor.substring(1, descriptor.length() - 1))) + String type = descriptor.substring(1, descriptor.length() - 1); + if (annotationType.contains(type)) return null; return super.visitLocalVariableAnnotation(typeRef, typePath, start, end, index, descriptor, visible); } diff --git a/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodNoopingVisitor.java b/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodNoopingVisitor.java index 572b6545e..e3b9e14d7 100644 --- a/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodNoopingVisitor.java +++ b/recaf-core/src/main/java/software/coley/recaf/util/visitors/MethodNoopingVisitor.java @@ -50,6 +50,8 @@ public MethodVisitor visitMethod(int access, String name, String descriptor, Str * Method visitor that replaces the contents with a no-op return. */ public static class NoopingMethodVisitor extends MethodVisitor implements Opcodes { + private static final int MAX_LOCALS = 1; + private static final int MAX_STACK = 2; private static final Map> OBJECT_DEFAULTS = new HashMap<>(); private final Type type; @@ -237,7 +239,7 @@ public void visitLineNumber(int line, Label start) { @Override public void visitMaxs(int maxStack, int maxLocals) { - // skip + super.visitMaxs(MAX_STACK, MAX_LOCALS); } private static void register(@Nonnull String name, @Nonnull Consumer consumer) { diff --git a/recaf-core/src/main/java/software/coley/recaf/util/visitors/TypeVisitor.java b/recaf-core/src/main/java/software/coley/recaf/util/visitors/TypeVisitor.java new file mode 100644 index 000000000..f97427137 --- /dev/null +++ b/recaf-core/src/main/java/software/coley/recaf/util/visitors/TypeVisitor.java @@ -0,0 +1,139 @@ +package software.coley.recaf.util.visitors; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import org.objectweb.asm.*; +import org.slf4j.Logger; +import software.coley.recaf.RecafConstants; +import software.coley.recaf.analytics.logging.Logging; + +import java.util.function.Consumer; + +/** + * Visitor to accept top-level types of referenced fields, methods, annotations, and nest mates. + * + * @author Matt Coley + */ +public class TypeVisitor extends ClassVisitor { + private static final Logger logger = Logging.get(TypeVisitor.class); + private final Consumer typeConsumer; + + /** + * @param typeConsumer + * Type consumer to accept seen types. + * The same type may be visited multiple times. + * Method types are also passed in. + */ + public TypeVisitor(@Nonnull Consumer typeConsumer) { + super(RecafConstants.getAsmVersion()); + this.typeConsumer = typeConsumer; + } + + @Override + public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) { + if (interfaces != null) + for (String exception : interfaces) + acceptType(exception); + acceptType(superName); + } + + @Override + public void visitSource(String source, String debug) { + // no-op + } + + @Override + public ModuleVisitor visitModule(String name, int access, String version) { + return null; + } + + @Override + public void visitNestHost(String nestHost) { + // no-op + } + + @Override + public void visitOuterClass(String owner, String name, String descriptor) { + acceptDescriptor(descriptor); + } + + @Override + public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public AnnotationVisitor visitTypeAnnotation(int typeRef, TypePath typePath, String descriptor, boolean visible) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public void visitAttribute(Attribute attribute) { + // no-op + } + + @Override + public void visitNestMember(String nestMember) { + acceptType(nestMember); + } + + @Override + public void visitPermittedSubclass(String permittedSubclass) { + acceptType(permittedSubclass); + } + + @Override + public void visitInnerClass(String name, String outerName, String innerName, int access) { + // no-op + } + + @Override + public RecordComponentVisitor visitRecordComponent(String name, String descriptor, String signature) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public FieldVisitor visitField(int access, String name, String descriptor, String signature, Object value) { + acceptDescriptor(descriptor); + return null; + } + + @Override + public MethodVisitor visitMethod(int access, String name, String descriptor, String signature, String[] exceptions) { + acceptDescriptor(descriptor); + if (exceptions != null) + for (String exception : exceptions) + acceptType(exception); + return null; + } + + private void acceptType(@Nullable String internalName) { + if (internalName == null || internalName.isEmpty()) return; + + try { + Type methodType = Type.getObjectType(internalName); + typeConsumer.accept(methodType); + } catch (Throwable t) { + logger.trace("Ignored invalid internal name: {}", internalName, t); + } + } + + private void acceptDescriptor(@Nullable String descriptor) { + if (descriptor == null || descriptor.isEmpty()) return; + + try { + if (descriptor.charAt(0) == '(') { + Type methodType = Type.getMethodType(descriptor); + typeConsumer.accept(methodType); + } else { + Type type = Type.getType(descriptor); + typeConsumer.accept(type); + } + } catch (Throwable t) { + logger.trace("Ignored invalid type: {}", descriptor, t); + } + } +} diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/BasicWorkspace.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/BasicWorkspace.java index b3da3a805..46825d46f 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/BasicWorkspace.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/BasicWorkspace.java @@ -1,8 +1,11 @@ package software.coley.recaf.workspace.model; import jakarta.annotation.Nonnull; +import org.slf4j.Logger; +import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.behavior.Closing; import software.coley.recaf.services.workspace.WorkspaceManager; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.workspace.model.resource.AndroidApiResource; import software.coley.recaf.workspace.model.resource.RuntimeWorkspaceResource; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -11,6 +14,7 @@ import java.util.Collection; import java.util.Collections; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; /** * Basic workspace implementation. @@ -18,7 +22,8 @@ * @author Matt Coley */ public class BasicWorkspace implements Workspace { - private final List modificationListeners = new ArrayList<>(); + private static final Logger logger = Logging.get(BasicWorkspace.class); + private final List modificationListeners = new CopyOnWriteArrayList<>(); private final WorkspaceResource primary; private final List supporting = new ArrayList<>(); private final List internal; @@ -71,18 +76,16 @@ public List getInternalSupportingResources() { @Override public void addSupportingResource(@Nonnull WorkspaceResource resource) { supporting.add(resource); - for (WorkspaceModificationListener listener : modificationListeners) { - listener.onAddLibrary(this, resource); - } + CollectionUtil.safeForEach(modificationListeners, listener -> listener.onAddLibrary(this, resource), + (listener, t) -> logger.error("Exception thrown when adding supporting resource", t)); } @Override public boolean removeSupportingResource(@Nonnull WorkspaceResource resource) { boolean remove = supporting.remove(resource); if (remove) { - for (WorkspaceModificationListener listener : modificationListeners) { - listener.onRemoveLibrary(this, resource); - } + CollectionUtil.safeForEach(modificationListeners, listener -> listener.onRemoveLibrary(this, resource), + (listener, t) -> logger.error("Exception thrown when removing supporting resource", t)); } return remove; } @@ -94,12 +97,12 @@ public List getWorkspaceModificationListeners() { } @Override - public void addWorkspaceModificationListener(WorkspaceModificationListener listener) { + public void addWorkspaceModificationListener(@Nonnull WorkspaceModificationListener listener) { modificationListeners.add(listener); } @Override - public void removeWorkspaceModificationListener(WorkspaceModificationListener listener) { + public void removeWorkspaceModificationListener(@Nonnull WorkspaceModificationListener listener) { modificationListeners.remove(listener); } diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/Workspace.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/Workspace.java index 19899e33d..ec5d58b96 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/Workspace.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/Workspace.java @@ -2,13 +2,13 @@ import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import software.coley.collections.Unchecked; import software.coley.recaf.behavior.Closing; import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.FileInfo; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.path.*; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.bundle.FileBundle; @@ -103,15 +103,22 @@ default List getAllResources(boolean includeInternal) { * @param listener * Modification listener to add. */ - void addWorkspaceModificationListener(WorkspaceModificationListener listener); + void addWorkspaceModificationListener(@Nonnull WorkspaceModificationListener listener); /** * @param listener * Modification listener to remove. */ - void removeWorkspaceModificationListener(WorkspaceModificationListener listener); + void removeWorkspaceModificationListener(@Nonnull WorkspaceModificationListener listener); /** + * Searches for a class by the given name in the following bundles: + *

    + *
  1. The {@link WorkspaceResource#getJvmClassBundle()}
  2. + *
  3. Each {@link WorkspaceResource#getVersionedJvmClassBundles()}
  4. + *
  5. Each {@link WorkspaceResource#getAndroidClassBundles()}
  6. + *
+ * * @param name * Class name. * @@ -128,6 +135,9 @@ default ClassPathNode findClass(@Nonnull String name) { } /** + * Searches for a class by the given name in the following bundle: + * {@link WorkspaceResource#getJvmClassBundle()} + * * @param name * Class name. * @@ -145,6 +155,9 @@ default ClassPathNode findJvmClass(@Nonnull String name) { } /** + * Searches for a class by the given name in the following bundle: + * {@link WorkspaceResource#getVersionedJvmClassBundles()} + * * @param name * Class name. * @@ -157,6 +170,9 @@ default ClassPathNode findLatestVersionedJvmClass(@Nonnull String name) { } /** + * Searches for a class by the given name in the target version bundle within + * {@link WorkspaceResource#getVersionedJvmClassBundles()}. + * * @param name * Class name. * @param version @@ -182,6 +198,9 @@ default ClassPathNode findVersionedJvmClass(@Nonnull String name, int version) { } /** + * Searches for a class by the given name in the following bundle: + * {@link WorkspaceResource#getAndroidClassBundles()} + * * @param name * Class name. * diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/bundle/BasicBundle.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/bundle/BasicBundle.java index afcd96630..0f0916bb0 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/bundle/BasicBundle.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/bundle/BasicBundle.java @@ -4,6 +4,7 @@ import org.slf4j.Logger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.Info; +import software.coley.recaf.util.CollectionUtil; import java.util.*; import java.util.concurrent.ConcurrentHashMap; @@ -110,6 +111,7 @@ public void decrementHistory(@Nonnull String key) { throw new IllegalStateException("Failed history decrement, no prior history to read from for: " + key); } int size = itemHistory.size(); + // Update map with prior entry I currentItem = get(key); I priorItem; @@ -119,14 +121,10 @@ public void decrementHistory(@Nonnull String key) { priorItem = itemHistory.peek(); } backing.put(key, priorItem); - // Notify listener - for (BundleListener listener : listeners) { - try { - listener.onUpdateItem(key, currentItem, priorItem); - } catch (Throwable t) { - logger.error("Uncaught error in bundle listener (revert)", t); - } - } + + // Notify listeners + CollectionUtil.safeForEach(listeners, listener -> listener.onUpdateItem(key, currentItem, priorItem), + (listener, t) -> logger.error("Exception thrown when decrementing bundle history", t)); } @Override @@ -172,18 +170,15 @@ public I get(@Nonnull Object key) { @Override public I put(@Nonnull String key, @Nonnull I newValue) { I oldValue = backing.put(key, newValue); - // Notify listener - for (BundleListener listener : listeners) { - try { - if (oldValue == null) { - listener.onNewItem(key, newValue); - } else { - listener.onUpdateItem(key, oldValue, newValue); - } - } catch (Throwable t) { - logger.error("Uncaught error in resource listener (put)", t); + // Notify listeners + CollectionUtil.safeForEach(listeners, listener -> { + if (oldValue == null) { + listener.onNewItem(key, newValue); + } else { + listener.onUpdateItem(key, oldValue, newValue); } - } + }, (listener, t) -> logger.error("Exception thrown when putting bundle item", t)); + // Update history if (oldValue == null) { initHistory(newValue); @@ -197,14 +192,10 @@ public I put(@Nonnull String key, @Nonnull I newValue) { public I remove(@Nonnull Object key) { I info = backing.remove(key); if (info != null) { - // Notify listener - for (BundleListener listener : listeners) { - try { - listener.onRemoveItem((String) key, info); - } catch (Throwable t) { - logger.error("Uncaught error in resource listener (remove)", t); - } - } + // Notify listeners + CollectionUtil.safeForEach(listeners, listener -> listener.onRemoveItem((String) key, info), + (listener, t) -> logger.error("Exception thrown when removing bundle item", t)); + // Update history history.remove(key); } diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/AgentServerRemoteVmResource.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/AgentServerRemoteVmResource.java index 382c18f31..c5448495e 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/AgentServerRemoteVmResource.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/AgentServerRemoteVmResource.java @@ -83,6 +83,7 @@ public Map getJvmClassloaderBundles() { return (Map) (Object) remoteBundleMap; } + @Nonnull @Override public Stream jvmClassBundleStream() { return Stream.concat(super.jvmClassBundleStream(), remoteBundleMap.values().stream()); diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/BasicWorkspaceResource.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/BasicWorkspaceResource.java index 0a7cb9e72..84f304fc5 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/BasicWorkspaceResource.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/BasicWorkspaceResource.java @@ -7,6 +7,7 @@ import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.info.FileInfo; import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; import software.coley.recaf.workspace.model.bundle.BundleListener; @@ -14,6 +15,7 @@ import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; /** * Basic workspace resource implementation. @@ -23,9 +25,9 @@ */ public class BasicWorkspaceResource implements WorkspaceResource { private static final Logger logger = Logging.get(BasicWorkspaceResource.class); - private final List jvmClassListeners = new ArrayList<>(); - private final List androidClassListeners = new ArrayList<>(); - private final List fileListeners = new ArrayList<>(); + private final List jvmClassListeners = new CopyOnWriteArrayList<>(); + private final List androidClassListeners = new CopyOnWriteArrayList<>(); + private final List fileListeners = new CopyOnWriteArrayList<>(); private final JvmClassBundle jvmClassBundle; private final NavigableMap versionedJvmClassBundles; private final Map androidClassBundles; @@ -61,11 +63,11 @@ public BasicWorkspaceResource(@Nonnull WorkspaceResourceBuilder builder) { * Parent resource (If we are the JAR within a JAR). */ public BasicWorkspaceResource(JvmClassBundle jvmClassBundle, - FileBundle fileBundle, - NavigableMap versionedJvmClassBundles, - Map androidClassBundles, - Map embeddedResources, - WorkspaceResource containingResource) { + FileBundle fileBundle, + NavigableMap versionedJvmClassBundles, + Map androidClassBundles, + Map embeddedResources, + WorkspaceResource containingResource) { this.jvmClassBundle = jvmClassBundle; this.fileBundle = fileBundle; this.versionedJvmClassBundles = versionedJvmClassBundles; @@ -92,6 +94,13 @@ private void setupListenerDelegation() { jvmClassBundleStream().forEach(bundle -> delegateJvmClassBundle(resource, bundle)); androidClassBundleStream().forEach(bundle -> delegateAndroidClassBundle(resource, bundle)); fileBundleStream().forEach(bundle -> delegateFileBundle(resource, bundle)); + + // Embedded resources will notify listeners of their containing resource when they are updated. + embeddedResources.values().forEach(embeddedResource -> { + embeddedResource.jvmClassBundleStream().forEach(bundle -> delegateJvmClassBundle(embeddedResource, bundle)); + embeddedResource.androidClassBundleStream().forEach(bundle -> delegateAndroidClassBundle(embeddedResource, bundle)); + embeddedResource.fileBundleStream().forEach(bundle -> delegateFileBundle(embeddedResource, bundle)); + }); } /** @@ -106,35 +115,20 @@ protected void delegateJvmClassBundle(@Nonnull WorkspaceResource resource, @Nonn bundle.addBundleListener(new BundleListener<>() { @Override public void onNewItem(@Nonnull String key, @Nonnull JvmClassInfo cls) { - for (ResourceJvmClassListener listener : jvmClassListeners) { - try { - listener.onNewClass(resource, bundle, cls); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (new jvm class)", t); - } - } + CollectionUtil.safeForEach(jvmClassListeners, listener -> listener.onNewClass(resource, bundle, cls), + (listener, t) -> logger.error("Exception thrown when adding class", t)); } @Override public void onUpdateItem(@Nonnull String key, @Nonnull JvmClassInfo oldCls, @Nonnull JvmClassInfo newCls) { - for (ResourceJvmClassListener listener : jvmClassListeners) { - try { - listener.onUpdateClass(resource, bundle, oldCls, newCls); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (update jvm class)", t); - } - } + CollectionUtil.safeForEach(jvmClassListeners, listener -> listener.onUpdateClass(resource, bundle, oldCls, newCls), + (listener, t) -> logger.error("Exception thrown when updating class", t)); } @Override public void onRemoveItem(@Nonnull String key, @Nonnull JvmClassInfo cls) { - for (ResourceJvmClassListener listener : jvmClassListeners) { - try { - listener.onRemoveClass(resource, bundle, cls); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (remove jvm class)", t); - } - } + CollectionUtil.safeForEach(jvmClassListeners, listener -> listener.onRemoveClass(resource, bundle, cls), + (listener, t) -> logger.error("Exception thrown when removing class", t)); } }); } @@ -151,35 +145,20 @@ protected void delegateAndroidClassBundle(@Nonnull WorkspaceResource resource, @ bundle.addBundleListener(new BundleListener<>() { @Override public void onNewItem(@Nonnull String key, @Nonnull AndroidClassInfo cls) { - for (ResourceAndroidClassListener listener : androidClassListeners) { - try { - listener.onNewClass(resource, bundle, cls); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (new android class)", t); - } - } + CollectionUtil.safeForEach(androidClassListeners, listener -> listener.onNewClass(resource, bundle, cls), + (listener, t) -> logger.error("Exception thrown when adding class", t)); } @Override public void onUpdateItem(@Nonnull String key, @Nonnull AndroidClassInfo oldCls, @Nonnull AndroidClassInfo newCls) { - for (ResourceAndroidClassListener listener : androidClassListeners) { - try { - listener.onUpdateClass(resource, bundle, oldCls, newCls); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (update android class)", t); - } - } + CollectionUtil.safeForEach(androidClassListeners, listener -> listener.onUpdateClass(resource, bundle, oldCls, newCls), + (listener, t) -> logger.error("Exception thrown when updating class", t)); } @Override public void onRemoveItem(@Nonnull String key, @Nonnull AndroidClassInfo cls) { - for (ResourceAndroidClassListener listener : androidClassListeners) { - try { - listener.onRemoveClass(resource, bundle, cls); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (remove jvm class)", t); - } - } + CollectionUtil.safeForEach(androidClassListeners, listener -> listener.onRemoveClass(resource, bundle, cls), + (listener, t) -> logger.error("Exception thrown when removing class", t)); } }); } @@ -196,35 +175,20 @@ protected void delegateFileBundle(@Nonnull WorkspaceResource resource, @Nonnull bundle.addBundleListener(new BundleListener<>() { @Override public void onNewItem(@Nonnull String key, @Nonnull FileInfo file) { - for (ResourceFileListener listener : fileListeners) { - try { - listener.onNewFile(resource, bundle, file); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (new file)", t); - } - } + CollectionUtil.safeForEach(fileListeners, listener -> listener.onNewFile(resource, bundle, file), + (listener, t) -> logger.error("Exception thrown when adding file", t)); } @Override public void onUpdateItem(@Nonnull String key, @Nonnull FileInfo oldFile, @Nonnull FileInfo newFile) { - for (ResourceFileListener listener : fileListeners) { - try { - listener.onUpdateFile(resource, bundle, oldFile, newFile); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (update file)", t); - } - } + CollectionUtil.safeForEach(fileListeners, listener -> listener.onUpdateFile(resource, bundle, oldFile, newFile), + (listener, t) -> logger.error("Exception thrown when updating file", t)); } @Override public void onRemoveItem(@Nonnull String key, @Nonnull FileInfo file) { - for (ResourceFileListener listener : fileListeners) { - try { - listener.onRemoveFile(resource, bundle, file); - } catch (Throwable t) { - logger.error("Uncaught error in workspace listener delegation (remove file)", t); - } - } + CollectionUtil.safeForEach(fileListeners, listener -> listener.onRemoveFile(resource, bundle, file), + (listener, t) -> logger.error("Exception thrown when removing file", t)); } }); } diff --git a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/WorkspaceResource.java b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/WorkspaceResource.java index 6a7bb715b..67eb13495 100644 --- a/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/WorkspaceResource.java +++ b/recaf-core/src/main/java/software/coley/recaf/workspace/model/resource/WorkspaceResource.java @@ -102,6 +102,7 @@ default boolean isEmbeddedResource() { /** * @return Stream of all immediate JVM class bundles in the resource. */ + @Nonnull default Stream jvmClassBundleStream() { return of(getJvmClassBundle()); } @@ -109,6 +110,7 @@ default Stream jvmClassBundleStream() { /** * @return Stream of all JVM class bundles in the resource, and in any embedded resources */ + @Nonnull default Stream jvmClassBundleStreamRecursive() { return concat(jvmClassBundleStream(), getEmbeddedResources().values().stream() .flatMap(WorkspaceResource::jvmClassBundleStreamRecursive)); @@ -117,6 +119,7 @@ default Stream jvmClassBundleStreamRecursive() { /** * @return Stream of all versioned JVM class bundles in the resource. */ + @Nonnull default Stream versionedJvmClassBundleStream() { return getVersionedJvmClassBundles().values().stream(); } @@ -124,6 +127,7 @@ default Stream versionedJvmClassBundleStream() { /** * @return Stream of all versioned JVM class bundles in the resource, and in any embedded resources */ + @Nonnull default Stream versionedJvmClassBundleStreamRecursive() { return concat(versionedJvmClassBundleStream(), getEmbeddedResources().values().stream() .flatMap(WorkspaceResource::versionedJvmClassBundleStreamRecursive)); @@ -132,6 +136,7 @@ default Stream versionedJvmClassBundleStreamRecursive() { /** * @return Stream of all immediate Android class bundles in the resource. */ + @Nonnull default Stream androidClassBundleStream() { return getAndroidClassBundles().values().stream(); } @@ -139,6 +144,7 @@ default Stream androidClassBundleStream() { /** * @return Stream of all Android class bundles in the resource, and in any embedded resources. */ + @Nonnull default Stream androidClassBundleStreamRecursive() { return concat(androidClassBundleStream(), getEmbeddedResources().values().stream() .flatMap(WorkspaceResource::androidClassBundleStreamRecursive)); @@ -147,6 +153,7 @@ default Stream androidClassBundleStreamRecursive() { /** * @return Stream of all immediate class bundles in the resource. */ + @Nonnull default Stream> classBundleStream() { return concat(jvmClassBundleStream(), concat(versionedJvmClassBundleStream(), androidClassBundleStream())); } @@ -154,6 +161,7 @@ default Stream> classBundleStream() { /** * @return Stream of all class bundles in the resource, and in any embedded resources. */ + @Nonnull default Stream> classBundleStreamRecursive() { return concat(classBundleStream(), getEmbeddedResources().values().stream() .flatMap(WorkspaceResource::classBundleStreamRecursive)); @@ -162,6 +170,7 @@ default Stream> classBundleStreamRecursive() { /** * @return Stream of all immediate file bundles in the resource. */ + @Nonnull default Stream fileBundleStream() { return of(getFileBundle()); } @@ -169,6 +178,7 @@ default Stream fileBundleStream() { /** * @return Stream of all file bundles in the resource, and in any embedded resources. */ + @Nonnull default Stream fileBundleStreamRecursive() { return concat(fileBundleStream(), getEmbeddedResources().values().stream() .flatMap(WorkspaceResource::fileBundleStreamRecursive)); @@ -177,6 +187,7 @@ default Stream fileBundleStreamRecursive() { /** * @return Stream of all immediate bundles in the resource. */ + @Nonnull @SuppressWarnings("unchecked") default Stream> bundleStream() { // Cast to object is a hack to allow generic usage of this method with . @@ -190,6 +201,7 @@ default Stream> bundleStream() { /** * @return Stream of all bundles in the resource, and in any embedded resources. */ + @Nonnull @SuppressWarnings("unchecked") default Stream> bundleStreamRecursive() { // Cast to object is a hack to allow generic usage of this method with . diff --git a/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java b/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java index 0011fe5a5..75d22710f 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/assembler/ExpressionCompilerTest.java @@ -1,7 +1,6 @@ package software.coley.recaf.services.assembler; import jakarta.annotation.Nonnull; -import org.junit.jupiter.api.AfterEach; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; @@ -30,7 +29,6 @@ * Tests for {@link ExpressionCompiler} */ class ExpressionCompilerTest extends TestBase { - static ExpressionCompiler assembler; static Workspace workspace; static JvmClassInfo targetClass; static JvmClassInfo targetCtorClass; @@ -38,7 +36,6 @@ class ExpressionCompilerTest extends TestBase { @BeforeAll static void setup() throws IOException { - assembler = recaf.get(ExpressionCompiler.class); targetClass = TestClassUtils.fromRuntimeClass(ClassWithFieldsAndMethods.class); targetCtorClass = TestClassUtils.fromRuntimeClass(ClassWithRequiredConstructor.class); targetEnum = TestClassUtils.fromRuntimeClass(DummyEnum.class); @@ -46,14 +43,10 @@ static void setup() throws IOException { workspaceManager.setCurrent(workspace); } - @AfterEach - void cleanup() { - assembler.clearContext(); - } - @Test void importSupport() { - ExpressionResult result = compile(""" + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); + ExpressionResult result = compile(assembler, """ import java.util.Random; try { @@ -70,8 +63,9 @@ void importSupport() { @Test void classContext() { + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(targetClass); - ExpressionResult result = compile(""" + ExpressionResult result = compile(assembler, """ int localConst = CONST_INT; int localField = finalInt; int localMethod = plusTwo(); @@ -82,15 +76,17 @@ void classContext() { @Test void classContextWithRequiredCtor() { + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(targetCtorClass); - ExpressionResult result = compile(""); + ExpressionResult result = compile(assembler, ""); assertSuccess(result); } @Test void enumContext() { + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(targetEnum); - ExpressionResult result = compile(""" + ExpressionResult result = compile(assembler, """ int i1 = ONE.ordinal(); int i2 = TWO.ordinal(); int i3 = THREE.ordinal(); @@ -101,9 +97,10 @@ void enumContext() { @Test void classAndMethodContextForParameters() { + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(targetClass); assembler.setMethodContext(targetClass.getFirstDeclaredMethodByName("methodWithParameters")); - ExpressionResult result = compile(""" + ExpressionResult result = compile(assembler, """ System.out.println(foo + ": " + Long.toHexString(wide) + "/" + @@ -116,9 +113,10 @@ void classAndMethodContextForParameters() { @Test void classAndMethodContextForLocals() { // Tests that local variables are accessible to the expression compiler + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(targetClass); assembler.setMethodContext(targetClass.getFirstDeclaredMethodByName("methodWithLocalVariables")); - ExpressionResult result = compile(""" + ExpressionResult result = compile(assembler, """ out.println(message.contains("0") ? "Has zero" : "No zero found"); """); assertSuccess(result); @@ -127,18 +125,20 @@ void classAndMethodContextForLocals() { @Test void classAndMethodContextForConstructor() { // Tests that the assembler works for constructor method contexts + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(targetClass); assembler.setMethodContext(targetClass.getFirstDeclaredMethodByName("")); - ExpressionResult result = compile(""); + ExpressionResult result = compile(assembler, ""); assertSuccess(result); } @Test void classAndMethodContextForStaticInitializer() { // Tests that the assembler works for static initializer method contexts + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(targetEnum); assembler.setMethodContext(targetEnum.getFirstDeclaredMethodByName("")); - ExpressionResult result = compile(""); + ExpressionResult result = compile(assembler, ""); assertSuccess(result); } @@ -154,9 +154,10 @@ void ignoreIllegalFieldName(String illegalFieldName) { JvmClassInfo classInfo = new JvmClassInfoBuilder(cw.toByteArray()).build(); // The expression compiler should skip the field since it has an illegal name. + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(classInfo); assembler.setMethodContext(classInfo.getFirstDeclaredMethodByName("methodName")); - ExpressionResult result = compile(""); + ExpressionResult result = compile(assembler, ""); assertSuccess(result); } @@ -170,9 +171,10 @@ void ignoreIllegalMethodName(String illegalMethodName) { JvmClassInfo classInfo = new JvmClassInfoBuilder(cw.toByteArray()).build(); // The expression compiler should skip the method since it has an illegal name. + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(classInfo); assembler.setMethodContext(classInfo.getFirstDeclaredMethodByName("methodName")); - ExpressionResult result = compile(""); + ExpressionResult result = compile(assembler, ""); assertSuccess(result); } @@ -199,9 +201,10 @@ void ignoreIllegalMethodContextName(String illegalMethodName) { // The expression compiler should rename the obfuscated method specified as the context. // Variables passed in (that are not illegally named) and such should still be accessible. + ExpressionCompiler assembler = recaf.get(ExpressionCompiler.class); assembler.setClassContext(classInfo); assembler.setMethodContext(classInfo.getFirstDeclaredMethodByName(illegalMethodName)); - ExpressionResult result = compile("int result = one + two + three;"); + ExpressionResult result = compile(assembler, "int result = one + two + three;"); assertSuccess(result); } } @@ -213,7 +216,7 @@ private static void assertSuccess(@Nonnull ExpressionResult result) { } @Nonnull - private static ExpressionResult compile(@Nonnull String expressionResult) { + private static ExpressionResult compile(@Nonnull ExpressionCompiler assembler, @Nonnull String expressionResult) { ExpressionResult result = assembler.compile(expressionResult); List diagnostics = result.getDiagnostics(); diagnostics.forEach(System.out::println); diff --git a/recaf-core/src/test/java/software/coley/recaf/services/decompile/DecompileManagerTest.java b/recaf-core/src/test/java/software/coley/recaf/services/decompile/DecompileManagerTest.java index 61e78040a..7a16c570b 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/decompile/DecompileManagerTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/decompile/DecompileManagerTest.java @@ -6,6 +6,7 @@ import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.services.decompile.cfr.CfrDecompiler; +import software.coley.recaf.services.decompile.fallback.FallbackDecompiler; import software.coley.recaf.services.decompile.filter.JvmBytecodeFilter; import software.coley.recaf.services.decompile.filter.OutputTextFilter; import software.coley.recaf.services.decompile.procyon.ProcyonDecompiler; @@ -62,6 +63,13 @@ void testVineflower() { runJvmDecompilation(decompiler); } + @Test + void testFallback() { + JvmDecompiler decompiler = decompilerManager.getJvmDecompiler(FallbackDecompiler.NAME); + assertNotNull(decompiler, "Fallback decompiler was never registered with manager"); + runJvmDecompilation(decompiler); + } + @Test void testFiltersUsed() { JvmDecompiler decompiler = decompilerManager.getTargetJvmDecompiler(); diff --git a/recaf-core/src/test/java/software/coley/recaf/services/inheritance/InheritanceAndRenamingTest.java b/recaf-core/src/test/java/software/coley/recaf/services/inheritance/InheritanceAndRenamingTest.java new file mode 100644 index 000000000..2e32fce29 --- /dev/null +++ b/recaf-core/src/test/java/software/coley/recaf/services/inheritance/InheritanceAndRenamingTest.java @@ -0,0 +1,94 @@ +package software.coley.recaf.services.inheritance; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.objectweb.asm.ClassWriter; +import software.coley.recaf.info.JvmClassInfo; +import software.coley.recaf.info.builder.JvmClassInfoBuilder; +import software.coley.recaf.services.mapping.IntermediateMappings; +import software.coley.recaf.services.mapping.MappingApplier; +import software.coley.recaf.services.mapping.MappingResults; +import software.coley.recaf.test.TestBase; +import software.coley.recaf.test.TestClassUtils; +import software.coley.recaf.workspace.model.Workspace; +import software.coley.recaf.workspace.model.bundle.BasicJvmClassBundle; + +import java.util.Set; +import java.util.stream.IntStream; + +import static org.junit.jupiter.api.Assertions.*; +import static org.objectweb.asm.Opcodes.ACC_INTERFACE; +import static org.objectweb.asm.Opcodes.V1_8; + +/** + * Tests for {@link InheritanceGraph} and interactions with {@link MappingApplier}. + */ +class InheritanceAndRenamingTest extends TestBase { + static Workspace workspace; + static InheritanceGraph graph; + static MappingApplier mappingApplier; + static JvmClassInfo[] generatedClasses; + + + @BeforeAll + static void setup() { + generatedClasses = IntStream.rangeClosed(1, 5).mapToObj(i -> { + String[] interfaces = i == 1 ? null : new String[]{"I" + (i - 1)}; + ClassWriter cw = new ClassWriter(0); + cw.visit(V1_8, ACC_INTERFACE, "I" + i, null, "java/lang/Object", interfaces); + return new JvmClassInfoBuilder(cw.toByteArray()).build(); + }).toList().toArray(JvmClassInfo[]::new); + + // Create workspace with the inheritance classes + BasicJvmClassBundle classes = TestClassUtils.fromClasses(generatedClasses); + workspace = TestClassUtils.fromBundle(classes); + workspaceManager.setCurrent(workspace); + + // Get graph + graph = recaf.get(InheritanceGraph.class); + graph.toString(); // Force immediate init. + + // Get mapping applier + mappingApplier = recaf.get(MappingApplier.class); + } + + @Test + void test() { + // Verify initial state + for (int i = 1; i <= 5; i++) { + String name = "I" + i; + InheritanceVertex vertex = graph.getVertex(name); + assertNotNull(vertex, "Graph missing '" + name + "'"); + } + + // Remap classes + IntermediateMappings mappings = new IntermediateMappings(); + for (int i = 1; i <= 5; i++) + mappings.addClass("I" + i, "R" + i); + MappingResults results = mappingApplier.applyToPrimaryResource(mappings); + results.apply(); + + // Very old classes are removed from the graph + for (int i = 1; i <= 5; i++) { + String name = "I" + i; + InheritanceVertex vertex = graph.getVertex(name); + assertNull(vertex, "Graph contains pre-mapped '" + name + "'"); + } + + // Verify the new classes are added to the graph + InheritanceVertex objectVertex = graph.getVertex("java/lang/Object"); + for (int i = 1; i <= 5; i++) { + String name = "R" + i; + InheritanceVertex vertex = graph.getVertex(name); + assertNotNull(vertex, "Graph missing post-mapped '" + name + "'"); + if (i > 1) { + Set parents = vertex.getParents(); + Set allParents = vertex.getAllParents(); + int directParentCount = parents.size(); + int allParentCount = allParents.size(); + assertEquals(2, directParentCount, "Vertex R" + i + " should have 2 direct parents (extended interface R" + (i - 1) + " + java/lang/Object)"); + assertEquals(i, allParentCount, "Vertex R" + i + " should have " + i + " total (direct + transitive) parents"); + } + } + } +} \ No newline at end of file diff --git a/recaf-core/src/test/java/software/coley/recaf/services/mapping/format/MappingImplementationTests.java b/recaf-core/src/test/java/software/coley/recaf/services/mapping/format/MappingImplementationTests.java index 4d4bb25b5..f9cb8b0ce 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/mapping/format/MappingImplementationTests.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/mapping/format/MappingImplementationTests.java @@ -33,6 +33,11 @@ void testTinyV1WithTwoOutputs() { MappingFileFormat format = new TinyV1Mappings(); IntermediateMappings mappings = assertDoesNotThrow(() -> format.parse(mappingsText)); assertInheritMap(mappings); + + // Extra asserts for the intermediate 'obfuscated' column + assertEquals("rename/Hello", mappings.getMappedClassName("a")); + assertEquals("newField", mappings.getMappedFieldName("a", "b", "Ljava/lang/String;")); + assertEquals("speak", mappings.getMappedMethodName("a", "c", "()V")); } @Test diff --git a/recaf-core/src/test/java/software/coley/recaf/services/source/AstServiceTest.java b/recaf-core/src/test/java/software/coley/recaf/services/source/AstServiceTest.java index 115763f9f..59a0a6b0e 100644 --- a/recaf-core/src/test/java/software/coley/recaf/services/source/AstServiceTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/services/source/AstServiceTest.java @@ -8,6 +8,7 @@ import org.openrewrite.InMemoryExecutionContext; import org.openrewrite.java.JavaParser; import org.openrewrite.java.tree.J; +import software.coley.collections.Unchecked; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.member.ClassMember; @@ -19,7 +20,6 @@ import software.coley.recaf.test.TestClassUtils; import software.coley.recaf.test.dummy.*; import software.coley.recaf.util.Types; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.BasicJvmClassBundle; diff --git a/recaf-core/src/test/java/software/coley/recaf/util/StringUtilTest.java b/recaf-core/src/test/java/software/coley/recaf/util/StringUtilTest.java index 80966a22e..286105e19 100644 --- a/recaf-core/src/test/java/software/coley/recaf/util/StringUtilTest.java +++ b/recaf-core/src/test/java/software/coley/recaf/util/StringUtilTest.java @@ -32,6 +32,34 @@ void testFastSplit() { assertEquals(List.of("", "", "", ""), StringUtil.fastSplit("///", true, '/')); } + @Test + void testGetTabAdjustedLength() { + assertEquals(4, StringUtil.getTabAdjustedLength("\t", 4)); + assertEquals(4, StringUtil.getTabAdjustedLength(" \t", 4)); + assertEquals(4, StringUtil.getTabAdjustedLength(" \t", 4)); + assertEquals(4, StringUtil.getTabAdjustedLength(" \t", 4)); + assertEquals(4, StringUtil.getTabAdjustedLength(" ", 4)); + assertEquals(10, StringUtil.getTabAdjustedLength("\t\t", 5)); + assertEquals(10, StringUtil.getTabAdjustedLength(" \t\t", 5)); + assertEquals(10, StringUtil.getTabAdjustedLength(" \t\t", 5)); + assertEquals(10, StringUtil.getTabAdjustedLength(" \t\t", 5)); + assertEquals(10, StringUtil.getTabAdjustedLength(" \t", 5)); + } + + @Test + void testGetWhitespacePrefixLength() { + assertEquals(4, StringUtil.getWhitespacePrefixLength(" text", 4)); + assertEquals(4, StringUtil.getWhitespacePrefixLength(" \ttext", 4)); + assertEquals(4, StringUtil.getWhitespacePrefixLength(" \ttext", 4)); + assertEquals(4, StringUtil.getWhitespacePrefixLength(" \ttext", 4)); + assertEquals(4, StringUtil.getWhitespacePrefixLength("\ttext", 4)); + assertEquals(8, StringUtil.getWhitespacePrefixLength(" \ttext", 4)); + assertEquals(8, StringUtil.getWhitespacePrefixLength(" \t \ttext", 4)); + assertEquals(8, StringUtil.getWhitespacePrefixLength(" \t \ttext", 4)); + assertEquals(8, StringUtil.getWhitespacePrefixLength("\t \ttext", 4)); + assertEquals(8, StringUtil.getWhitespacePrefixLength(" \t \ttext", 4)); + } + @Test void testSplitNewline() { assertEquals(1, StringUtil.splitNewline("").length); diff --git a/recaf-core/src/test/java/software/coley/recaf/util/UncheckedTest.java b/recaf-core/src/test/java/software/coley/recaf/util/UncheckedTest.java deleted file mode 100644 index 5f00997d2..000000000 --- a/recaf-core/src/test/java/software/coley/recaf/util/UncheckedTest.java +++ /dev/null @@ -1,106 +0,0 @@ -package software.coley.recaf.util; - -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; -import java.util.Set; - -import static org.junit.jupiter.api.Assertions.*; - -/** - * Tests for {@link Unchecked}. - */ -public class UncheckedTest { - @Test - void testBoxing() { - UncheckedRunnable r = () -> {}; - assertSame(r, Unchecked.runnable(r)); - - UncheckedSupplier s = () -> null; - assertSame(s, Unchecked.supply(s)); - - UncheckedConsumer c = (p) -> {}; - assertSame(c, Unchecked.consumer(c)); - - UncheckedBiConsumer bc = (p1, p2) -> {}; - assertSame(bc, Unchecked.bconsumer(bc)); - - UncheckedFunction f = (p) -> null; - assertSame(f, Unchecked.function(f)); - - UncheckedBiFunction bf = (p1, p2) -> null; - assertSame(bf, Unchecked.bfunction(bf)); - } - - @Test - void testValidCast() { - List list = new ArrayList<>(); - list.add("foo"); - ArrayList castList = Unchecked.cast(list); - assertSame(list, castList); - } - - @Test - void testInvalidCast() { - List list = new ArrayList<>(); - list.add("foo"); - assertThrows(ClassCastException.class, () -> { - Set set = Unchecked.cast(list); - fail("Shouldn't be castable: " + set); - }); - } - - @Test - void testRun() { - int zero = 0; - assertThrows(ArithmeticException.class, () -> - Unchecked.run(() -> System.out.println(10 / zero)) - ); - } - - @Test - void testGet() { - int zero = 0; - assertThrows(ArithmeticException.class, () -> - Unchecked.get(() -> (10 / zero)) - ); - } - - @Test - void testGetOr() { - int zero = 0; - int value = assertDoesNotThrow(() -> - Unchecked.getOr(() -> (10 / zero), 7) - ); - assertEquals(7, value); - } - - @Test - void testAccept() { - assertThrows(ArithmeticException.class, () -> - Unchecked.accept(v -> System.out.println(10 / v), 0) - ); - } - - @Test - void testBAccept() { - assertThrows(ArithmeticException.class, () -> - Unchecked.baccept((a, b) -> System.out.println(a / b), 10, 0) - ); - } - - @Test - void testMap() { - assertThrows(ArithmeticException.class, () -> - Unchecked.map(v -> 10 / v, 0) - ); - } - - @Test - void testBMap() { - assertThrows(ArithmeticException.class, () -> - Unchecked.bmap((a, b) -> a / b, 10, 0) - ); - } -} diff --git a/recaf-core/src/testFixtures/java/software/coley/recaf/test/dummy/DummyEmptyMap.java b/recaf-core/src/testFixtures/java/software/coley/recaf/test/dummy/DummyEmptyMap.java new file mode 100644 index 000000000..3827b2a55 --- /dev/null +++ b/recaf-core/src/testFixtures/java/software/coley/recaf/test/dummy/DummyEmptyMap.java @@ -0,0 +1,69 @@ +package software.coley.recaf.test.dummy; + +import java.util.Collection; +import java.util.Collections; +import java.util.Map; +import java.util.Set; + +public class DummyEmptyMap implements Map { + + @Override + public int size() { + return 0; + } + + @Override + public boolean isEmpty() { + return true; + } + + @Override + public boolean containsKey(Object key) { + return false; + } + + @Override + public boolean containsValue(Object value) { + return false; + } + + @Override + public V get(Object key) { + return null; + } + + @Override + public V put(K key, V value) { + return null; + } + + @Override + public V remove(Object key) { + return null; + } + + @Override + public void putAll(Map m) { + // no-op + } + + @Override + public void clear() { + // no-op + } + + @Override + public Set keySet() { + return Collections.emptySet(); + } + + @Override + public Collection values() { + return Collections.emptySet(); + } + + @Override + public Set> entrySet() { + return Collections.emptySet(); + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java index 19783acd7..a9f855d01 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/CellConfigurationService.java @@ -38,6 +38,7 @@ import software.coley.recaf.ui.control.FontIconView; import software.coley.recaf.ui.control.tree.TreeItems; import software.coley.recaf.ui.control.tree.WorkspaceTreeCell; +import software.coley.recaf.util.Lang; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.*; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -74,10 +75,10 @@ public class CellConfigurationService implements Service { */ @Inject public CellConfigurationService(@Nonnull CellConfigurationServiceConfig config, - @Nonnull TextProviderService textService, - @Nonnull IconProviderService iconService, - @Nonnull ContextMenuProviderService contextMenuService, - @Nonnull Actions actions) { + @Nonnull TextProviderService textService, + @Nonnull IconProviderService iconService, + @Nonnull ContextMenuProviderService contextMenuService, + @Nonnull Actions actions) { this.config = config; this.textService = textService; this.iconService = iconService; @@ -102,7 +103,7 @@ public void reset(@Nonnull Cell cell) { cell.setText(null); cell.setGraphic(null); cell.setContextMenu(null); - cell.setOnMousePressed(null); + cell.setOnMouseClicked(null); } /** @@ -357,6 +358,8 @@ public String textOf(@Nonnull PathNode item) { return textService.getCatchTextProvider(workspace, resource, bundle, declaringClass, declaringMethod, catchPath.getValue()).makeText(); + } else if (item instanceof EmbeddedResourceContainerPathNode) { + return Lang.get("tree.embedded-resources"); } // No text @@ -553,6 +556,8 @@ public Node graphicOf(@Nonnull PathNode item) { String caught = catchPath.getValue(); return iconService.getCatchIconProvider(workspace, resource, bundle, classInfo, method, caught).makeIcon(); + } else if (item instanceof EmbeddedResourceContainerPathNode) { + return new FontIconView(CarbonIcons.CATEGORIES); } // No graphic diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAnnotationContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAnnotationContextMenuProviderFactory.java index e24cdc9f3..406e83fc0 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAnnotationContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAnnotationContextMenuProviderFactory.java @@ -4,18 +4,12 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import javafx.scene.control.ContextMenu; -import org.objectweb.asm.ClassWriter; import org.objectweb.asm.Type; import org.slf4j.Logger; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; -import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.annotation.Annotated; import software.coley.recaf.info.annotation.AnnotationInfo; -import software.coley.recaf.info.builder.JvmClassInfoBuilder; -import software.coley.recaf.info.member.ClassMember; -import software.coley.recaf.info.member.FieldMember; -import software.coley.recaf.info.member.MethodMember; import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.services.cell.icon.IconProvider; import software.coley.recaf.services.cell.icon.IconProviderService; @@ -23,17 +17,13 @@ import software.coley.recaf.services.cell.text.TextProviderService; import software.coley.recaf.services.navigation.Actions; import software.coley.recaf.ui.contextmenu.ContextMenuBuilder; -import software.coley.recaf.util.visitors.ClassAnnotationRemovingVisitor; -import software.coley.recaf.util.visitors.FieldAnnotationRemovingVisitor; -import software.coley.recaf.util.visitors.MethodAnnotationRemovingVisitor; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; import static org.kordamp.ikonli.carbonicons.CarbonIcons.ARROW_RIGHT; import static org.kordamp.ikonli.carbonicons.CarbonIcons.TRASH_CAN; -import static software.coley.recaf.util.Unchecked.cast; -import static software.coley.recaf.util.Unchecked.runnable; +import static software.coley.collections.Unchecked.runnable; /** * Basic implementation for {@link AnnotationContextMenuProviderFactory}. @@ -47,19 +37,19 @@ public class BasicAnnotationContextMenuProviderFactory extends AbstractContextMe @Inject public BasicAnnotationContextMenuProviderFactory(@Nonnull TextProviderService textService, - @Nonnull IconProviderService iconService, - @Nonnull Actions actions) { + @Nonnull IconProviderService iconService, + @Nonnull Actions actions) { super(textService, iconService, actions); } @Nonnull @Override public ContextMenuProvider getAnnotationContextMenuProvider(@Nonnull ContextSource source, - @Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull Annotated annotated, - @Nonnull AnnotationInfo annotation) { + @Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull Annotated annotated, + @Nonnull AnnotationInfo annotation) { return () -> { TextProvider nameProvider = textService.getAnnotationTextProvider(workspace, resource, bundle, annotated, annotation); IconProvider iconProvider = iconService.getAnnotationIconProvider(workspace, resource, bundle, annotated, annotation); @@ -73,31 +63,11 @@ public ContextMenuProvider getAnnotationContextMenuProvider(@Nonnull ContextSour if (annotationDecPath != null) builder.item("menu.goto.class", ARROW_RIGHT, runnable(() -> actions.gotoDeclaration(annotationDecPath))); - builder.item("menu.edit.remove.annotation", TRASH_CAN, () -> { - try { - if (annotated instanceof JvmClassInfo target) { - ClassWriter writer = new ClassWriter(0); - target.getClassReader().accept(new ClassAnnotationRemovingVisitor(writer, annotationType), 0); - JvmClassInfo updatedClass = new JvmClassInfoBuilder(writer.toByteArray()).build(); - bundle.put(cast(updatedClass)); - } else if (annotated instanceof ClassMember member && member.getDeclaringClass() instanceof JvmClassInfo target) { - ClassWriter writer = new ClassWriter(0); - if (member.isField()) { - FieldMember field = (FieldMember) member; - target.getClassReader().accept(FieldAnnotationRemovingVisitor.forClass(writer, annotationType, field), 0); - } else { - MethodMember method = (MethodMember) member; - target.getClassReader().accept(MethodAnnotationRemovingVisitor.forClass(writer, annotationType, method), 0); - } - JvmClassInfo updatedClass = new JvmClassInfoBuilder(writer.toByteArray()).build(); - bundle.put(cast(updatedClass)); - } - } catch (Throwable t) { - logger.error("Failed removing annotation", t); - } - }); + builder.item("menu.edit.remove.annotation", TRASH_CAN, () -> actions.immediateDeleteAnnotations(bundle, annotated, annotationType)); return menu; }; } + + } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAssemblerContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAssemblerContextMenuProviderFactory.java index f82f36286..4286a2acd 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAssemblerContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicAssemblerContextMenuProviderFactory.java @@ -10,6 +10,7 @@ import me.darknet.assembler.util.Range; import org.fxmisc.richtext.CodeArea; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.path.AssemblerPathData; @@ -21,7 +22,6 @@ import software.coley.recaf.ui.control.ActionMenuItem; import software.coley.recaf.ui.control.richtext.Editor; import software.coley.recaf.ui.pane.editing.assembler.resolve.*; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java index 8b8741faa..d17cf8396 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicClassContextMenuProviderFactory.java @@ -8,6 +8,7 @@ import javafx.scene.control.MenuItem; import org.kordamp.ikonli.carbonicons.CarbonIcons; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.info.ClassInfo; @@ -23,7 +24,6 @@ import software.coley.recaf.ui.pane.search.ClassReferenceSearchPane; import software.coley.recaf.ui.pane.search.MemberReferenceSearchPane; import software.coley.recaf.util.ClipboardUtil; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; import software.coley.recaf.workspace.model.bundle.ClassBundle; @@ -143,22 +143,18 @@ private void populateJvmMenu(@Nonnull ContextMenu menu, if (source.isReference()) { builder.infoItem("menu.goto.class", ARROW_RIGHT, actions::gotoDeclaration); } else if (source.isDeclaration()) { - builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(info)); - // Edit menu var edit = builder.submenu("menu.edit", EDIT); edit.item("menu.edit.assemble.class", EDIT, Unchecked.runnable(() -> actions.openAssembler(PathNodes.classPath(workspace, resource, bundle, info)) )); - // TODO: Open an dialog which allows the user to create a member, and then open the relevant assembler edit.infoItem("menu.edit.add.field", ADD_ALT, actions::addClassField); edit.infoItem("menu.edit.add.method", ADD_ALT, actions::addClassMethod); - edit.item("menu.edit.add.annotation", ADD_ALT, () -> {}).disableWhen(true); edit.infoItem("menu.edit.remove.field", CLOSE, actions::deleteClassFields).disableWhen(info.getFields().isEmpty()); edit.infoItem("menu.edit.remove.method", CLOSE, actions::deleteClassMethods).disableWhen(info.getMethods().isEmpty()); edit.infoItem("menu.edit.remove.annotation", CLOSE, actions::deleteClassAnnotations).disableWhen(info.getAnnotations().isEmpty()); - builder.infoItem("menu.edit.copy", COPY_FILE, actions::copyClass); - builder.infoItem("menu.edit.delete", COPY_FILE, actions::deleteClass); + edit.infoItem("menu.edit.copy", COPY_FILE, actions::copyClass); + edit.infoItem("menu.edit.delete", COPY_FILE, actions::deleteClass); } // Search actions @@ -174,14 +170,17 @@ private void populateJvmMenu(@Nonnull ContextMenu menu, pane.typeValueProperty().setValue(info.getName()); }); - // Documentation actions - builder.infoItem("menu.analysis.comment", ADD_COMMENT, actions::openCommentEditing); - // Refactor actions var refactor = builder.submenu("menu.refactor", PAINT_BRUSH); refactor.infoItem("menu.refactor.rename", TAG_EDIT, actions::renameClass); refactor.infoItem("menu.refactor.move", STACKED_MOVE, actions::moveClass); + // Copy path + builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(info)); + + // Documentation actions + builder.infoItem("menu.analysis.comment", ADD_COMMENT, actions::openCommentEditing); + // Export actions builder.infoItem("menu.export.class", EXPORT, actions::exportClass); diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicDirectoryContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicDirectoryContextMenuProviderFactory.java index ac73dfb73..06ca933a5 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicDirectoryContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicDirectoryContextMenuProviderFactory.java @@ -48,7 +48,6 @@ public ContextMenuProvider getDirectoryContextMenuProvider(@Nonnull ContextSourc var builder = new ContextMenuBuilder(menu, source).forDirectory(workspace, resource, bundle, directoryName); if (source.isDeclaration()) { - builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(directoryName)); builder.directoryItem("menu.edit.copy", COPY_FILE, actions::copyDirectory); builder.directoryItem("menu.edit.delete", TRASH_CAN, actions::deleteDirectory); @@ -59,6 +58,10 @@ public ContextMenuProvider getDirectoryContextMenuProvider(@Nonnull ContextSourc // TODO: implement operations // - Search references } + + // Copy path + builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(directoryName)); + return menu; }; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFieldContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFieldContextMenuProviderFactory.java index edff44493..d5cefbcec 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFieldContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFieldContextMenuProviderFactory.java @@ -5,6 +5,7 @@ import jakarta.inject.Inject; import javafx.scene.control.ContextMenu; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.JvmClassInfo; @@ -21,7 +22,6 @@ import software.coley.recaf.ui.contextmenu.ContextMenuBuilder; import software.coley.recaf.ui.pane.search.MemberReferenceSearchPane; import software.coley.recaf.util.ClipboardUtil; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; @@ -43,19 +43,19 @@ public class BasicFieldContextMenuProviderFactory extends AbstractContextMenuPro @Inject public BasicFieldContextMenuProviderFactory(@Nonnull TextProviderService textService, - @Nonnull IconProviderService iconService, - @Nonnull Actions actions) { + @Nonnull IconProviderService iconService, + @Nonnull Actions actions) { super(textService, iconService, actions); } @Nonnull @Override public ContextMenuProvider getFieldContextMenuProvider(@Nonnull ContextSource source, - @Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull ClassInfo declaringClass, - @Nonnull FieldMember field) { + @Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull ClassInfo declaringClass, + @Nonnull FieldMember field) { return () -> { TextProvider nameProvider = textService.getFieldMemberTextProvider(workspace, resource, bundle, declaringClass, field); IconProvider iconProvider = iconService.getClassMemberIconProvider(workspace, resource, bundle, declaringClass, field); @@ -74,27 +74,26 @@ public ContextMenuProvider getFieldContextMenuProvider(@Nonnull ContextSource so } }); } else { - builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(declaringClass, field)); - builder.item("menu.edit.assemble.field", EDIT, () -> Unchecked.runnable(() -> - actions.openAssembler(PathNodes.memberPath(workspace, resource, bundle, declaringClass, field)) - ).run()); - + // Edit menu + var edit = builder.submenu("menu.edit", EDIT); + edit.item("menu.edit.assemble.field", EDIT, () -> Unchecked.runnable(() -> actions.openAssembler(PathNodes.memberPath(workspace, resource, bundle, declaringClass, field))).run()); if (declaringClass.isJvmClass()) { JvmClassBundle jvmBundle = (JvmClassBundle) bundle; JvmClassInfo declaringJvmClass = declaringClass.asJvmClass(); - builder.item("menu.edit.copy", COPY_FILE, () -> actions.copyMember(workspace, resource, jvmBundle, declaringJvmClass, field)); - builder.item("menu.edit.delete", TRASH_CAN, () -> actions.deleteClassFields(workspace, resource, jvmBundle, declaringJvmClass, List.of(field))); + edit.item("menu.edit.copy", COPY_FILE, () -> actions.copyMember(workspace, resource, jvmBundle, declaringJvmClass, field)); + edit.item("menu.edit.delete", TRASH_CAN, () -> actions.deleteClassFields(workspace, resource, jvmBundle, declaringJvmClass, List.of(field))); + edit.item("menu.edit.remove.annotation", CLOSE, () -> actions.deleteMemberAnnotations(workspace, resource, jvmBundle, declaringJvmClass, field)) + .disableWhen(field.getAnnotations().isEmpty()); } // TODO: implement operations // - Edit // - Add annotation - // - Remove annotations } // Search actions - builder.item("menu.search.field-references", CODE, () -> { + builder.item("menu.search.field-references", CODE_REFERENCE, () -> { MemberReferenceSearchPane pane = actions.openNewMemberReferenceSearch(); pane.ownerPredicateIdProperty().setValue(StringPredicateProvider.KEY_EQUALS); pane.namePredicateIdProperty().setValue(StringPredicateProvider.KEY_EQUALS); @@ -104,6 +103,9 @@ public ContextMenuProvider getFieldContextMenuProvider(@Nonnull ContextSource so pane.descValueProperty().setValue(field.getDescriptor()); }); + // Copy path + builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(declaringClass, field)); + // Documentation actions builder.memberItem("menu.analysis.comment", ADD_COMMENT, actions::openCommentEditing); diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFileContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFileContextMenuProviderFactory.java index 802cee98a..1dd5f20f2 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFileContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicFileContextMenuProviderFactory.java @@ -4,6 +4,7 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; import javafx.scene.control.ContextMenu; +import software.coley.collections.Unchecked; import software.coley.recaf.info.FileInfo; import software.coley.recaf.path.FilePathNode; import software.coley.recaf.path.PathNodes; @@ -19,7 +20,6 @@ import software.coley.recaf.workspace.model.resource.WorkspaceResource; import static org.kordamp.ikonli.carbonicons.CarbonIcons.*; -import static software.coley.recaf.util.Unchecked.runnable; /** * Basic implementation for {@link FileContextMenuProviderFactory}. @@ -52,7 +52,7 @@ public ContextMenuProvider getFileInfoContextMenuProvider(@Nonnull ContextSource FilePathNode filePath = PathNodes.filePath(workspace, resource, bundle, info); if (source.isReference()) { - builder.item("menu.goto.file", ARROW_RIGHT, runnable(() -> actions.gotoDeclaration(filePath))); + builder.item("menu.goto.file", ARROW_RIGHT, Unchecked.runnable(() -> actions.gotoDeclaration(filePath))); } else if (source.isDeclaration()) { builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(info)); builder.infoItem("menu.edit.copy", COPY_FILE, actions::copyFile); diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java index 77a3c2973..9af2989a6 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicMethodContextMenuProviderFactory.java @@ -8,6 +8,7 @@ import static org.kordamp.ikonli.carbonicons.CarbonIcons.*; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.JvmClassInfo; @@ -24,7 +25,6 @@ import software.coley.recaf.ui.contextmenu.ContextMenuBuilder; import software.coley.recaf.ui.pane.search.MemberReferenceSearchPane; import software.coley.recaf.util.ClipboardUtil; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; @@ -32,8 +32,6 @@ import java.util.List; -import static software.coley.recaf.util.Menus.action; - /** * Basic implementation for {@link MethodContextMenuProviderFactory}. * @@ -76,28 +74,45 @@ public ContextMenuProvider getMethodContextMenuProvider(@Nonnull ContextSource s } }); } else { - builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(declaringClass, method)); - builder.item("menu.edit.assemble.method", EDIT, Unchecked.runnable(() -> - actions.openAssembler(PathNodes.memberPath(workspace, resource, bundle, declaringClass, method)) - )); - + // Edit menu + var edit = builder.submenu("menu.edit", EDIT); + edit.item("menu.edit.assemble.method", EDIT, Unchecked.runnable(() -> actions.openAssembler(PathNodes.memberPath(workspace, resource, bundle, declaringClass, method)))); if (declaringClass.isJvmClass()) { JvmClassBundle jvmBundle = (JvmClassBundle) bundle; JvmClassInfo declaringJvmClass = declaringClass.asJvmClass(); - builder.item("menu.edit.copy", COPY_FILE, () -> actions.copyMember(workspace, resource, jvmBundle,declaringJvmClass, method)); - builder.item("menu.edit.noop", CIRCLE_DASH, () -> actions.makeMethodsNoop(workspace, resource, jvmBundle, declaringJvmClass, List.of(method))); - builder.item("menu.edit.delete", TRASH_CAN, () -> actions.deleteClassMethods(workspace, resource, jvmBundle, declaringJvmClass, List.of(method))); + edit.item("menu.edit.copy", COPY_FILE, () -> actions.copyMember(workspace, resource, jvmBundle,declaringJvmClass, method)); + edit.item("menu.edit.noop", CIRCLE_DASH, () -> actions.makeMethodsNoop(workspace, resource, jvmBundle, declaringJvmClass, List.of(method))); + edit.item("menu.edit.delete", TRASH_CAN, () -> actions.deleteClassMethods(workspace, resource, jvmBundle, declaringJvmClass, List.of(method))); + edit.item("menu.edit.remove.annotation", CLOSE, () -> actions.deleteMemberAnnotations(workspace, resource, jvmBundle, declaringJvmClass, method)) + .disableWhen(method.getAnnotations().isEmpty()); } // TODO: implement additional operations // - Edit // - Add annotation - // - Remove annotations } + // TODO: implement additional operations + // - View + // - Control flow graph + // - Application flow graph + var view = builder.submenu("menu.view", VIEW); + if (declaringClass.isJvmClass()) { + JvmClassBundle jvmBundle = (JvmClassBundle) bundle; + JvmClassInfo declaringJvmClass = declaringClass.asJvmClass(); + view.item("menu.view.methodcallgraph", FLOW, () -> actions.openMethodCallGraph(workspace, resource, jvmBundle,declaringJvmClass, method)); + } + + // TODO: implement additional operations + // - Deobfuscate + // - Regenerate variable names + // - Optimize with pattern matchers + // - Optimize with SSVM + // - Simulate with SSVM (Virtualize > Run) + // Search actions - builder.item("menu.search.method-references", CODE, () -> { + builder.item("menu.search.method-references", CODE_REFERENCE, () -> { MemberReferenceSearchPane pane = actions.openNewMemberReferenceSearch(); pane.ownerPredicateIdProperty().setValue(StringPredicateProvider.KEY_EQUALS); pane.namePredicateIdProperty().setValue(StringPredicateProvider.KEY_EQUALS); @@ -107,21 +122,15 @@ public ContextMenuProvider getMethodContextMenuProvider(@Nonnull ContextSource s pane.descValueProperty().setValue(method.getDescriptor()); }); + // Copy path + builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(declaringClass, method)); + // Documentation actions builder.memberItem("menu.analysis.comment", ADD_COMMENT, actions::openCommentEditing); // Refactor actions - builder.memberItem("menu.refactor.rename", TAG_EDIT, actions::renameMethod); + builder.memberItem("menu.refactor.rename", TAG_EDIT, actions::renameMethod); // TODO: Hide when a library method (like System.exit) - // TODO: implement additional operations - // - View - // - Control flow graph - // - Application flow graph - // - Deobfuscate - // - Regenerate variable names - // - Optimize with pattern matchers - // - Optimize with SSVM - // - Simulate with SSVM (Virtualize > Run) return menu; }; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicPackageContextMenuProviderFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicPackageContextMenuProviderFactory.java index 322d6a067..73464763b 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicPackageContextMenuProviderFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/context/BasicPackageContextMenuProviderFactory.java @@ -54,7 +54,6 @@ public ContextMenuProvider getPackageContextMenuProvider(@Nonnull ContextSource var builder = new ContextMenuBuilder(menu, source).forDirectory(workspace, resource, bundle, packageName); if (source.isDeclaration()) { - builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(packageName)); if (bundle instanceof JvmClassBundle) { var jvmBuilder = builder.cast(JvmClassBundle.class); jvmBuilder.directoryItem("menu.edit.copy", COPY_FILE, actions::copyPackage); @@ -64,21 +63,24 @@ public ContextMenuProvider getPackageContextMenuProvider(@Nonnull ContextSource refactor.directoryItem("menu.refactor.move", STACKED_MOVE, actions::movePackage); refactor.directoryItem("menu.refactor.rename", TAG_EDIT, actions::renamePackage); } - - // Search actions - var search = builder.submenu("menu.search", SEARCH); - search.item("menu.search.class.member-references", CODE_REFERENCE, () -> { - MemberReferenceSearchPane pane = actions.openNewMemberReferenceSearch(); - pane.ownerPredicateIdProperty().setValue(StringPredicateProvider.KEY_STARTS_WITH); - pane.ownerValueProperty().setValue(packageName + "/"); - }); - search.item("menu.search.class.type-references", CODE_REFERENCE, () -> { - ClassReferenceSearchPane pane = actions.openNewClassReferenceSearch(); - pane.typePredicateIdProperty().setValue(StringPredicateProvider.KEY_STARTS_WITH); - pane.typeValueProperty().setValue(packageName + "/"); - }); } + // Search actions + var search = builder.submenu("menu.search", SEARCH); + search.item("menu.search.class.member-references", CODE_REFERENCE, () -> { + MemberReferenceSearchPane pane = actions.openNewMemberReferenceSearch(); + pane.ownerPredicateIdProperty().setValue(StringPredicateProvider.KEY_STARTS_WITH); + pane.ownerValueProperty().setValue(packageName + "/"); + }); + search.item("menu.search.class.type-references", CODE_REFERENCE, () -> { + ClassReferenceSearchPane pane = actions.openNewClassReferenceSearch(); + pane.typePredicateIdProperty().setValue(StringPredicateProvider.KEY_STARTS_WITH); + pane.typeValueProperty().setValue(packageName + "/"); + }); + + // Copy path + builder.item("menu.tab.copypath", COPY_LINK, () -> ClipboardUtil.copyString(packageName)); + return menu; }; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java b/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java index 9529bba9d..0852458ee 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/cell/text/TextProviderService.java @@ -17,7 +17,7 @@ import software.coley.recaf.services.Service; import software.coley.recaf.services.phantom.GeneratedPhantomWorkspaceResource; import software.coley.recaf.ui.config.MemberDisplayFormatConfig; -import software.coley.recaf.ui.config.TextFormatConfig; +import software.coley.recaf.services.text.TextFormatConfig; import software.coley.recaf.ui.control.tree.WorkspaceTreeCell; import software.coley.recaf.util.BlwUtil; import software.coley.recaf.util.Lang; diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java index ec1ac7517..f0141def7 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/info/summary/builtin/AntiDecompilationSummarizer.java @@ -23,10 +23,10 @@ import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.services.info.summary.ResourceSummarizer; import software.coley.recaf.services.info.summary.SummaryConsumer; -import software.coley.recaf.services.mapping.gen.filter.NameGeneratorFilter; import software.coley.recaf.services.mapping.gen.filter.IncludeKeywordNameFilter; import software.coley.recaf.services.mapping.gen.filter.IncludeNonAsciiNameFilter; import software.coley.recaf.services.mapping.gen.filter.IncludeWhitespaceNameFilter; +import software.coley.recaf.services.mapping.gen.filter.NameGeneratorFilter; import software.coley.recaf.services.window.WindowFactory; import software.coley.recaf.ui.control.ActionButton; import software.coley.recaf.ui.control.BoundLabel; @@ -71,7 +71,7 @@ public class AntiDecompilationSummarizer implements ResourceSummarizer { @Inject public AntiDecompilationSummarizer(@Nonnull Instance generatorPaneProvider, - @Nonnull WindowFactory windowFactory) { + @Nonnull WindowFactory windowFactory) { this.generatorPaneProvider = generatorPaneProvider; this.windowFactory = windowFactory; } @@ -79,8 +79,8 @@ public AntiDecompilationSummarizer(@Nonnull Instance gener @Override @SuppressWarnings("unchecked") public boolean summarize(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull SummaryConsumer consumer) { + @Nonnull WorkspaceResource resource, + @Nonnull SummaryConsumer consumer) { Set classesWithInvalidSignatures = Collections.newSetFromMap(new IdentityHashMap<>()); Set classesWithDuplicateAnnotations = Collections.newSetFromMap(new IdentityHashMap<>()); Set classesWithIllegalNames = Collections.newSetFromMap(new IdentityHashMap<>()); @@ -89,25 +89,8 @@ public boolean summarize(@Nonnull Workspace workspace, resource.jvmClassBundleStream().forEach(bundle -> { bundle.forEach(cls -> { // Check for invalid signatures in the class. - signature: - { - if (isInvalidSignature(cls.getSignature(), false)) { - classesWithInvalidSignatures.add(cls); - break signature; - } - for (FieldMember field : cls.getFields()) { - if (isInvalidSignature(field.getSignature(), true)) { - classesWithInvalidSignatures.add(cls); - break signature; - } - } - for (MethodMember method : cls.getMethods()) { - if (isInvalidSignature(method.getSignature(), false)) { - classesWithInvalidSignatures.add(cls); - break signature; - } - } - } + if (!cls.hasValidSignatures()) + classesWithInvalidSignatures.add(cls); // Check for duplicate annotations, which is not allowed at source level. // Commonly paired with bogus long annotation names. @@ -258,6 +241,7 @@ public boolean summarize(@Nonnull Workspace workspace, // Option to remove cycles if (cycleCount > 0) { + BoundLabel label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-remove", cycleCount)); Button action = new ActionButton(CarbonIcons.TRASH_CAN, Lang.getBinding("service.analysis.anti-decompile.cyclic"), () -> { CompletableFuture.supplyAsync(() -> { int patched = 0; @@ -272,19 +256,21 @@ public boolean summarize(@Nonnull Workspace workspace, } } return patched; - }, service).whenComplete((count, error) -> { - if (error == null) + }, service).whenCompleteAsync((count, error) -> { + if (error == null) { + label.rebind(Lang.format("service.analysis.anti-decompile.label-remove", cycleCount - count)); logger.info("Removed {} illegal cyclic classes", count); - else + } else { logger.error("Failed removing cyclic classes", error); - }); + } + }, FxThreadUtil.executor()); }).once().width(BUTTON_WIDTH); - Label label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-remove", cycleCount)); consumer.appendSummary(box(action, label)); } // Option to remove invalid signatures if (invalidSigCount > 0) { + BoundLabel label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", invalidSigCount)); Button action = new ActionButton(CarbonIcons.CLEAN, Lang.getBinding("service.analysis.anti-decompile.illegal-sig"), () -> { CompletableFuture.supplyAsync(() -> { int patched = 0; @@ -305,20 +291,22 @@ public boolean summarize(@Nonnull Workspace workspace, } } return patched; - }, service).whenComplete((count, error) -> { - if (error == null) + }, service).whenCompleteAsync((count, error) -> { + if (error == null) { + label.rebind(Lang.format("service.analysis.anti-decompile.label-patch", invalidSigCount - count)); logger.info("Patched {} classes with illegal signatures", count); - else + } else { logger.error("Failed patching illegal signatures", error); - }); + } + }, FxThreadUtil.executor()); }).once().width(BUTTON_WIDTH); - Label label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", invalidSigCount)); consumer.appendSummary(box(action, label)); } // Option to remove duplicate annotations if (dupAnnoCount > 0) { + BoundLabel label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", dupAnnoCount)); Button action = new ActionButton(CarbonIcons.CLEAN, Lang.getBinding("service.analysis.anti-decompile.duplicate-annos"), () -> { CompletableFuture.supplyAsync(() -> { int patched = 0; @@ -339,19 +327,21 @@ public boolean summarize(@Nonnull Workspace workspace, } } return patched; - }, service).whenComplete((count, error) -> { - if (error == null) + }, service).whenCompleteAsync((count, error) -> { + if (error == null) { + label.rebind(Lang.format("service.analysis.anti-decompile.label-patch", dupAnnoCount - count)); logger.info("Patched {} classes with duplicate annotations", count); - else + } else { logger.error("Failed patching classes with duplicate annotations", error); - }); + } + }, FxThreadUtil.executor()); }).once().width(BUTTON_WIDTH); - Label label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", dupAnnoCount)); consumer.appendSummary(box(action, label)); } // Option to remove long named annotations if (longAnnoCount > 0) { + BoundLabel label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", longAnnoCount)); Button action = new ActionButton(CarbonIcons.CLEAN, Lang.getBinding("service.analysis.anti-decompile.long-annos"), () -> { CompletableFuture.supplyAsync(() -> { int patched = 0; @@ -372,19 +362,21 @@ public boolean summarize(@Nonnull Workspace workspace, } } return patched; - }, service).whenComplete((count, error) -> { - if (error == null) + }, service).whenCompleteAsync((count, error) -> { + if (error == null) { + label.rebind(Lang.format("service.analysis.anti-decompile.label-patch", longAnnoCount - count)); logger.info("Patched {} classes with long annotations", count); - else + } else { logger.error("Failed patching classes with long annotations", error); - }); + } + }, FxThreadUtil.executor()); }).once().width(BUTTON_WIDTH); - Label label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", longAnnoCount)); consumer.appendSummary(box(action, label)); } // Option to remove empty named annotations if (illegalAnnoCount > 0) { + BoundLabel label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", illegalAnnoCount)); Button action = new ActionButton(CarbonIcons.CLEAN, Lang.getBinding("service.analysis.anti-decompile.illegal-annos"), () -> { CompletableFuture.supplyAsync(() -> { int patched = 0; @@ -405,14 +397,15 @@ public boolean summarize(@Nonnull Workspace workspace, } } return patched; - }, service).whenComplete((count, error) -> { - if (error == null) + }, service).whenCompleteAsync((count, error) -> { + if (error == null) { + label.rebind(Lang.format("service.analysis.anti-decompile.label-patch", illegalAnnoCount - count)); logger.info("Patched {} classes with illegal annotations", count); - else + } else { logger.error("Failed patching classes with illegal annotations", error); - }); + } + }, FxThreadUtil.executor()); }).once().width(BUTTON_WIDTH); - Label label = new BoundLabel(Lang.format("service.analysis.anti-decompile.label-patch", illegalAnnoCount)); consumer.appendSummary(box(action, label)); } @@ -430,6 +423,12 @@ public boolean summarize(@Nonnull Workspace workspace, Stage window = windowFactory.createAnonymousStage(scene, getBinding("mapgen"), 800, 400); window.show(); window.requestFocus(); + + // Because our service is application scoped, the injected mapping generator panes won't + // be automatically destroyed until all of Recaf is closed. Thus, for optimal GC usage we + // need to manually invoke the destruction of our injected mapping generator panes. + // We can do this when the stage is closed. + window.setOnHidden(e -> generatorPaneProvider.destroy(mappingGeneratorPane)); }); }, service).exceptionally(t -> { logger.error("Failed to open mapping viewer", t); @@ -454,15 +453,7 @@ private static Node box(@Nonnull Node left, @Nonnull Node right) { return box; } - private static boolean isInvalidSignature(@Nullable String signature, boolean isType) { - if (signature == null) - return false; - try { - return !Types.isValidSignature(signature, isType); - } catch (Throwable t) { - return true; - } - } + /** * Simple class hierarchy graph for detecting cycles. @@ -478,7 +469,7 @@ public Graph(@Nonnull Workspace workspace) { // Initiate graph vertices. vertices = workspace.getPrimaryResource().jvmClassBundleStream() .flatMap(Bundle::stream) - .collect(Collectors.toMap(Function.identity(), ClassVertex::new)); + .collect(Collectors.toMap(Function.identity(), ClassVertex::new, (a, b) -> a, IdentityHashMap::new)); // Link edges together. vertices.forEach((key, vertex) -> { diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java index 2b3b10fed..e91d13dd1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Actions.java @@ -16,8 +16,10 @@ import org.kordamp.ikonli.carbonicons.CarbonIcons; import org.objectweb.asm.ClassWriter; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.*; +import software.coley.recaf.info.annotation.Annotated; import software.coley.recaf.info.annotation.AnnotationInfo; import software.coley.recaf.info.builder.JvmClassInfoBuilder; import software.coley.recaf.info.member.ClassMember; @@ -32,6 +34,7 @@ import software.coley.recaf.services.mapping.MappingResults; import software.coley.recaf.services.window.WindowFactory; import software.coley.recaf.ui.control.FontIconView; +import software.coley.recaf.ui.control.graph.MethodCallGraphsPane; import software.coley.recaf.ui.control.popup.AddMemberPopup; import software.coley.recaf.ui.control.popup.ItemListSelectionPopup; import software.coley.recaf.ui.control.popup.ItemTreeSelectionPopup; @@ -59,14 +62,12 @@ import software.coley.recaf.workspace.model.bundle.*; import software.coley.recaf.workspace.model.resource.WorkspaceResource; -import java.util.ArrayList; -import java.util.Collection; -import java.util.List; -import java.util.Objects; +import java.util.*; import java.util.function.Consumer; import java.util.function.Supplier; import java.util.stream.Collectors; +import static software.coley.collections.Unchecked.cast; import static software.coley.recaf.util.Lang.getBinding; import static software.coley.recaf.util.Menus.*; import static software.coley.recaf.util.StringUtil.*; @@ -96,34 +97,36 @@ public class Actions implements Service { private final Instance videoPaneProvider; private final Instance assemblerPaneProvider; private final Instance documentationPaneProvider; - private final ActionsConfig config; + private final Instance callGraphsPaneProvider; private final Instance stringSearchPaneProvider; private final Instance numberSearchPaneProvider; private final Instance classReferenceSearchPaneProvider; private final Instance memberReferenceSearchPaneProvider; + private final ActionsConfig config; @Inject public Actions(@Nonnull ActionsConfig config, - @Nonnull NavigationManager navigationManager, - @Nonnull DockingManager dockingManager, - @Nonnull WindowFactory windowFactory, - @Nonnull TextProviderService textService, - @Nonnull IconProviderService iconService, - @Nonnull PathExportingManager pathExportingManager, - @Nonnull Instance applierProvider, - @Nonnull Instance jvmPaneProvider, - @Nonnull Instance androidPaneProvider, - @Nonnull Instance binaryXmlPaneProvider, - @Nonnull Instance textPaneProvider, - @Nonnull Instance imagePaneProvider, - @Nonnull Instance audioPaneProvider, - @Nonnull Instance videoPaneProvider, - @Nonnull Instance assemblerPaneProvider, - @Nonnull Instance documentationPaneProvider, - @Nonnull Instance stringSearchPaneProvider, - @Nonnull Instance numberSearchPaneProvider, - @Nonnull Instance classReferenceSearchPaneProvider, - @Nonnull Instance memberReferenceSearchPaneProvider) { + @Nonnull NavigationManager navigationManager, + @Nonnull DockingManager dockingManager, + @Nonnull WindowFactory windowFactory, + @Nonnull TextProviderService textService, + @Nonnull IconProviderService iconService, + @Nonnull PathExportingManager pathExportingManager, + @Nonnull Instance applierProvider, + @Nonnull Instance jvmPaneProvider, + @Nonnull Instance androidPaneProvider, + @Nonnull Instance binaryXmlPaneProvider, + @Nonnull Instance textPaneProvider, + @Nonnull Instance imagePaneProvider, + @Nonnull Instance audioPaneProvider, + @Nonnull Instance videoPaneProvider, + @Nonnull Instance assemblerPaneProvider, + @Nonnull Instance documentationPaneProvider, + @Nonnull Instance stringSearchPaneProvider, + @Nonnull Instance numberSearchPaneProvider, + @Nonnull Instance callGraphsPaneProvider, + @Nonnull Instance classReferenceSearchPaneProvider, + @Nonnull Instance memberReferenceSearchPaneProvider) { this.config = config; this.navigationManager = navigationManager; this.dockingManager = dockingManager; @@ -143,6 +146,7 @@ public Actions(@Nonnull ActionsConfig config, this.documentationPaneProvider = documentationPaneProvider; this.stringSearchPaneProvider = stringSearchPaneProvider; this.numberSearchPaneProvider = numberSearchPaneProvider; + this.callGraphsPaneProvider = callGraphsPaneProvider; this.classReferenceSearchPaneProvider = classReferenceSearchPaneProvider; this.memberReferenceSearchPaneProvider = memberReferenceSearchPaneProvider; } @@ -240,10 +244,10 @@ public ClassNavigable gotoDeclaration(@Nonnull ClassPathNode path) throws Incomp */ @Nonnull public ClassNavigable gotoDeclaration(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { - ClassPathNode path = buildPath(workspace, resource, bundle, info); + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { + ClassPathNode path = PathNodes.classPath(workspace, resource, bundle, info); return (ClassNavigable) getOrCreatePathContent(path, () -> { // Create text/graphic for the tab to create. String title = textService.getJvmClassInfoTextProvider(workspace, resource, bundle, info).makeText(); @@ -302,10 +306,10 @@ public ClassNavigable gotoDeclaration(@Nonnull Workspace workspace, */ @Nonnull public ClassNavigable gotoDeclaration(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull AndroidClassBundle bundle, - @Nonnull AndroidClassInfo info) { - ClassPathNode path = buildPath(workspace, resource, bundle, info); + @Nonnull WorkspaceResource resource, + @Nonnull AndroidClassBundle bundle, + @Nonnull AndroidClassInfo info) { + ClassPathNode path = PathNodes.classPath(workspace, resource, bundle, info); return (ClassNavigable) getOrCreatePathContent(path, () -> { // Create text/graphic for the tab to create. String title = textService.getAndroidClassInfoTextProvider(workspace, resource, bundle, info).makeText(); @@ -399,10 +403,10 @@ public FileNavigable gotoDeclaration(@Nonnull FilePathNode path) throws Incomple */ @Nonnull public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull BinaryXmlFileInfo info) { - FilePathNode path = buildPath(workspace, resource, bundle, info); + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull BinaryXmlFileInfo info) { + FilePathNode path = PathNodes.filePath(workspace, resource, bundle, info); return (FileNavigable) getOrCreatePathContent(path, () -> { // Create text/graphic for the tab to create. String title = textService.getFileInfoTextProvider(workspace, resource, bundle, info).makeText(); @@ -438,10 +442,10 @@ public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, */ @Nonnull public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull TextFileInfo info) { - FilePathNode path = buildPath(workspace, resource, bundle, info); + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull TextFileInfo info) { + FilePathNode path = PathNodes.filePath(workspace, resource, bundle, info); return (FileNavigable) getOrCreatePathContent(path, () -> { // Create text/graphic for the tab to create. String title = textService.getFileInfoTextProvider(workspace, resource, bundle, info).makeText(); @@ -477,10 +481,10 @@ public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, */ @Nonnull public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull ImageFileInfo info) { - FilePathNode path = buildPath(workspace, resource, bundle, info); + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull ImageFileInfo info) { + FilePathNode path = PathNodes.filePath(workspace, resource, bundle, info); return (FileNavigable) getOrCreatePathContent(path, () -> { // Create text/graphic for the tab to create. String title = textService.getFileInfoTextProvider(workspace, resource, bundle, info).makeText(); @@ -516,10 +520,10 @@ public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, */ @Nonnull public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull AudioFileInfo info) { - FilePathNode path = buildPath(workspace, resource, bundle, info); + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull AudioFileInfo info) { + FilePathNode path = PathNodes.filePath(workspace, resource, bundle, info); return (FileNavigable) getOrCreatePathContent(path, () -> { // Create text/graphic for the tab to create. String title = textService.getFileInfoTextProvider(workspace, resource, bundle, info).makeText(); @@ -555,10 +559,10 @@ public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, */ @Nonnull public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull VideoFileInfo info) { - FilePathNode path = buildPath(workspace, resource, bundle, info); + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull VideoFileInfo info) { + FilePathNode path = PathNodes.filePath(workspace, resource, bundle, info); return (FileNavigable) getOrCreatePathContent(path, () -> { // Create text/graphic for the tab to create. String title = textService.getFileInfoTextProvider(workspace, resource, bundle, info).makeText(); @@ -590,9 +594,9 @@ public FileNavigable gotoDeclaration(@Nonnull Workspace workspace, * Class to document. */ public void openCommentEditing(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull ClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull ClassInfo info) { createContent(() -> { ClassPathNode path = PathNodes.classPath(workspace, resource, bundle, info); @@ -619,10 +623,10 @@ public void openCommentEditing(@Nonnull Workspace workspace, * Member to document. */ public void openCommentEditing(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull ClassInfo declaringClass, - @Nonnull ClassMember member) { + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull ClassInfo declaringClass, + @Nonnull ClassMember member) { createContent(() -> { ClassMemberPathNode path = PathNodes.memberPath(workspace, resource, bundle, declaringClass, member); @@ -637,7 +641,7 @@ public void openCommentEditing(@Nonnull Workspace workspace, @Nonnull private DockingTab createCommentEditTab(@Nonnull PathNode path, @Nonnull String title, - @Nonnull Node graphic, @Nonnull ClassInfo classInfo) { + @Nonnull Node graphic, @Nonnull ClassInfo classInfo) { // Create content for the tab. CommentEditPane content = documentationPaneProvider.get(); content.onUpdatePath(path); @@ -667,9 +671,9 @@ private DockingTab createCommentEditTab(@Nonnull PathNode path, @Nonnull Stri * Class to move into a different package. */ public void moveClass(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { boolean isRootDirectory = isNullOrEmpty(info.getPackageName()); ItemTreeSelectionPopup.forPackageNames(bundle, packages -> { // We only allow a single package, so the list should contain just one item. @@ -712,9 +716,9 @@ public void moveClass(@Nonnull Workspace workspace, * File to move into a different directory. */ public void moveFile(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull FileInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull FileInfo info) { boolean isRootDirectory = isNullOrEmpty(info.getDirectoryName()); ItemTreeSelectionPopup.forDirectoryNames(bundle, chosenDirectories -> { // We only allow a single directory, so the list should contain just one item. @@ -747,9 +751,9 @@ public void moveFile(@Nonnull Workspace workspace, * Package to go move into another package as a sub-package. */ public void movePackage(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull String packageName) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull String packageName) { boolean isRootDirectory = packageName.isEmpty(); ItemTreeSelectionPopup.forPackageNames(bundle, chosenPackages -> { if (chosenPackages.isEmpty()) return; @@ -805,9 +809,9 @@ public void movePackage(@Nonnull Workspace workspace, * Directory to go move into another directory as a sub-directory. */ public void moveDirectory(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull String directoryName) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull String directoryName) { boolean isRootDirectory = directoryName.isEmpty(); String localDirectoryName = shortenPath(directoryName); ItemTreeSelectionPopup.forDirectoryNames(bundle, chosenDirectories -> { @@ -912,9 +916,9 @@ public void renameClass(@Nonnull ClassPathNode path) throws IncompletePathExcept * Class to rename. */ public void renameClass(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull ClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull ClassInfo info) { String originalName = info.getName(); Consumer renameTask = newName -> { // Create mapping for the class and any inner classes. @@ -986,10 +990,10 @@ public void renameField(@Nonnull ClassMemberPathNode path) throws IncompletePath * Field to rename. */ public void renameField(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull ClassInfo declaringClass, - @Nonnull FieldMember field) { + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull ClassInfo declaringClass, + @Nonnull FieldMember field) { String originalName = field.getName(); Consumer renameTask = newName -> { IntermediateMappings mappings = new IntermediateMappings(); @@ -1055,10 +1059,10 @@ public void renameMethod(@Nonnull ClassMemberPathNode path) throws IncompletePat * Method to rename. */ public void renameMethod(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull ClassInfo declaringClass, - @Nonnull MethodMember method) { + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull ClassInfo declaringClass, + @Nonnull MethodMember method) { String originalName = method.getName(); Consumer renameTask = newName -> { IntermediateMappings mappings = new IntermediateMappings(); @@ -1117,9 +1121,9 @@ public void renameFile(@Nonnull FilePathNode path) throws IncompletePathExceptio * File to rename. */ public void renameFile(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull FileInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull FileInfo info) { String name = info.getName(); new NamePopup(newFileName -> { if (name.equals(newFileName)) return; @@ -1177,9 +1181,9 @@ else if (bundle instanceof JvmClassBundle classBundle) * Name of directory to rename. */ public void renameDirectory(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull String directoryName) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull String directoryName) { boolean isRootDirectory = directoryName.isEmpty(); new NamePopup(newDirectoryName -> { if (directoryName.equals(newDirectoryName)) return; @@ -1230,9 +1234,9 @@ public void renameDirectory(@Nonnull Workspace workspace, * Name of directory to copy. */ public void renamePackage(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull String packageName) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull String packageName) { boolean isRootDirectory = packageName.isEmpty(); new NamePopup(newPackageName -> { // Create mappings. @@ -1275,9 +1279,9 @@ public void renamePackage(@Nonnull Workspace workspace, * Class to copy. */ public void copyClass(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { String originalName = info.getName(); Consumer copyTask = newName -> { // Create mappings. @@ -1328,10 +1332,10 @@ public void copyClass(@Nonnull Workspace workspace, * member to copy. */ public void copyMember(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo declaringClass, - @Nonnull ClassMember member) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo declaringClass, + @Nonnull ClassMember member) { String originalName = member.getName(); Consumer copyTask = newName -> { ClassWriter cw = new ClassWriter(0); @@ -1358,9 +1362,9 @@ public void copyMember(@Nonnull Workspace workspace, * File to copy. */ public void copyFile(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull FileInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull FileInfo info) { new NamePopup(newName -> { if (info.getName().equals(newName)) return; bundle.put(info.toFileBuilder().withName(newName).build()); @@ -1382,9 +1386,9 @@ public void copyFile(@Nonnull Workspace workspace, * Name of directory to copy. */ public void copyDirectory(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull String directoryName) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull String directoryName) { boolean isRootDirectory = directoryName.isEmpty(); new NamePopup(newDirectoryName -> { if (directoryName.equals(newDirectoryName)) return; @@ -1420,9 +1424,9 @@ public void copyDirectory(@Nonnull Workspace workspace, * Name of directory to copy. */ public void copyPackage(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull String packageName) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull String packageName) { boolean isRootDirectory = packageName.isEmpty(); new NamePopup(newPackageName -> { // Create mappings. @@ -1512,6 +1516,44 @@ else if (path instanceof ClassMemberPathNode classMemberPathNode) }); } + + /** + * Exports a class, prompting the user to select a location to save the class to. + * + * @param workspace + * Containing workspace. + * @param resource + * Containing resource. + * @param bundle + * Containing bundle. + * @param declaringClass + * Class declaring the method + * @param method + * Method to show the incoming/outgoing calls of. + * + * @return Navigable reference to the call graph pane. + */ + @Nonnull + public Navigable openMethodCallGraph(@Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo declaringClass, + @Nonnull MethodMember method) { + return createContent(() -> { + // Create text/graphic for the tab to create. + String title = Lang.get("menu.view.methodcallgraph") + ": " + method.getName(); + Node graphic = new FontIconView(CarbonIcons.FLOW); + + // Create content for the tab. + MethodCallGraphsPane content = callGraphsPaneProvider.get(); + content.onUpdatePath(PathNodes.memberPath(workspace, resource, bundle, declaringClass, method)); + + // Build the tab. + return createTab(dockingManager.getPrimaryRegion(), title, graphic, content); + }); + } + + /** * Exports a class, prompting the user to select a location to save the class to. * @@ -1525,9 +1567,9 @@ else if (path instanceof ClassMemberPathNode classMemberPathNode) * Class to delete. */ public void exportClass(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { pathExportingManager.export(info); } @@ -1544,9 +1586,9 @@ public void exportClass(@Nonnull Workspace workspace, * Class to delete. */ public void deleteClass(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { // TODO: Ask user if they are sure // - Use config to check if "are you sure" prompts should be bypassed bundle.remove(info.getName()); @@ -1565,9 +1607,9 @@ public void deleteClass(@Nonnull Workspace workspace, * File to delete. */ public void deleteFile(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull FileInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull FileInfo info) { // TODO: Ask user if they are sure // - Use config to check if "are you sure" prompts should be bypassed bundle.remove(info.getName()); @@ -1586,9 +1628,9 @@ public void deleteFile(@Nonnull Workspace workspace, * Name of package to delete. */ public void deletePackage(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull String packageName) { + @Nonnull WorkspaceResource resource, + @Nonnull ClassBundle bundle, + @Nonnull String packageName) { // TODO: Ask user if they are sure // - Use config to check if "are you sure" prompts should be bypassed boolean isRootDirectory = packageName.isEmpty(); @@ -1622,9 +1664,9 @@ public void deletePackage(@Nonnull Workspace workspace, * Name of directory to delete. */ public void deleteDirectory(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull String directoryName) { + @Nonnull WorkspaceResource resource, + @Nonnull FileBundle bundle, + @Nonnull String directoryName) { // TODO: Ask user if they are sure // - Use config to check if "are you sure" prompts should be bypassed boolean isRootDirectory = directoryName.isEmpty(); @@ -1658,9 +1700,9 @@ public void deleteDirectory(@Nonnull Workspace workspace, * Class to update. */ public void deleteClassFields(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { ItemListSelectionPopup.forFields(info, fields -> deleteClassFields(workspace, resource, bundle, info, fields)) .withMultipleSelection() .withTitle(Lang.getBinding("menu.edit.remove.field")) @@ -1684,10 +1726,10 @@ public void deleteClassFields(@Nonnull Workspace workspace, * Fields to delete. */ public void deleteClassFields(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo declaringClass, - @Nonnull Collection fields) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo declaringClass, + @Nonnull Collection fields) { ClassWriter writer = new ClassWriter(0); MemberRemovingVisitor visitor = new MemberRemovingVisitor(writer, FieldPredicate.of(fields)); declaringClass.getClassReader().accept(visitor, 0); @@ -1709,9 +1751,9 @@ public void deleteClassFields(@Nonnull Workspace workspace, * Class to update. */ public void deleteClassMethods(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { ItemListSelectionPopup.forMethods(info, methods -> deleteClassMethods(workspace, resource, bundle, info, methods)) .withMultipleSelection() .withTitle(Lang.getBinding("menu.edit.remove.method")) @@ -1735,10 +1777,10 @@ public void deleteClassMethods(@Nonnull Workspace workspace, * Methods to delete. */ public void deleteClassMethods(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo declaringClass, - @Nonnull Collection methods) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo declaringClass, + @Nonnull Collection methods) { ClassWriter writer = new ClassWriter(0); MemberRemovingVisitor visitor = new MemberRemovingVisitor(writer, MethodPredicate.of(methods)); declaringClass.getClassReader().accept(visitor, 0); @@ -1760,20 +1802,48 @@ public void deleteClassMethods(@Nonnull Workspace workspace, * Class to update. */ public void deleteClassAnnotations(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { ItemListSelectionPopup.forAnnotationRemoval(info, annotations -> { List names = annotations.stream() .map(AnnotationInfo::getDescriptor) .map(desc -> desc.substring(1, desc.length() - 1)) .collect(Collectors.toList()); - ClassWriter writer = new ClassWriter(0); - ClassAnnotationRemovingVisitor visitor = new ClassAnnotationRemovingVisitor(writer, names); - info.getClassReader().accept(visitor, 0); - bundle.put(info.toJvmClassBuilder() - .adaptFrom(writer.toByteArray()) - .build()); + immediateDeleteAnnotations(bundle, info, names); + }) + .withMultipleSelection() + .withTitle(Lang.getBinding("menu.edit.remove.annotation")) + .withTextMapping(anno -> textService.getAnnotationTextProvider(workspace, resource, bundle, info, anno).makeText()) + .withGraphicMapping(anno -> iconService.getAnnotationIconProvider(workspace, resource, bundle, info, anno).makeIcon()) + .show(); + } + + /** + * Prompts the user to select annotations on the field or method to remove. + * + * @param workspace + * Containing workspace. + * @param resource + * Containing resource. + * @param bundle + * Containing bundle. + * @param info + * Class to update. + * @param member + * Field or method to remove annotations from. + */ + public void deleteMemberAnnotations(@Nonnull Workspace workspace, + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info, + @Nonnull ClassMember member) { + ItemListSelectionPopup.forAnnotationRemoval(member, annotations -> { + List names = annotations.stream() + .map(AnnotationInfo::getDescriptor) + .map(desc -> desc.substring(1, desc.length() - 1)) + .collect(Collectors.toList()); + immediateDeleteAnnotations(bundle, member, names); }) .withMultipleSelection() .withTitle(Lang.getBinding("menu.edit.remove.annotation")) @@ -1795,9 +1865,9 @@ public void deleteClassAnnotations(@Nonnull Workspace workspace, * Class to update. */ public void addClassField(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { new AddMemberPopup(member -> { ClassWriter writer = new ClassWriter(0); info.getClassReader().accept(new MemberStubAddingVisitor(writer, member), 0); @@ -1828,9 +1898,9 @@ public void addClassField(@Nonnull Workspace workspace, * Class to update. */ public void addClassMethod(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo info) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo info) { new AddMemberPopup(member -> { ClassWriter writer = new ClassWriter(0); info.getClassReader().accept(new MemberStubAddingVisitor(writer, member), 0); @@ -1863,10 +1933,10 @@ public void addClassMethod(@Nonnull Workspace workspace, * Methods to noop. */ public void makeMethodsNoop(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull JvmClassBundle bundle, - @Nonnull JvmClassInfo declaringClass, - @Nonnull Collection methods) { + @Nonnull WorkspaceResource resource, + @Nonnull JvmClassBundle bundle, + @Nonnull JvmClassInfo declaringClass, + @Nonnull Collection methods) { ClassWriter writer = new ClassWriter(0); MethodNoopingVisitor visitor = new MethodNoopingVisitor(writer, MethodPredicate.of(methods)); declaringClass.getClassReader().accept(visitor, 0); @@ -1875,6 +1945,56 @@ public void makeMethodsNoop(@Nonnull Workspace workspace, .build()); } + /** + * @param bundle + * Containing bundle. + * @param annotated + * Annotated class or member. + * @param annotationType + * Annotation type to remove. + */ + public void immediateDeleteAnnotations(@Nonnull ClassBundle bundle, + @Nonnull Annotated annotated, + @Nonnull String annotationType) { + immediateDeleteAnnotations(bundle, annotated, Collections.singleton(annotationType)); + } + + /** + * @param bundle + * Containing bundle. + * @param annotated + * Annotated class or member. + * @param annotationTypes + * Annotation types to remove. + */ + public void immediateDeleteAnnotations(@Nonnull ClassBundle bundle, + @Nonnull Annotated annotated, + @Nonnull Collection annotationTypes) { + try { + if (annotated instanceof JvmClassInfo target) { + ClassWriter writer = new ClassWriter(0); + target.getClassReader().accept(new ClassAnnotationRemovingVisitor(writer, annotationTypes), 0); + JvmClassInfo updatedClass = new JvmClassInfoBuilder(writer.toByteArray()).build(); + bundle.put(cast(updatedClass)); + } else if (annotated instanceof ClassMember member && member.getDeclaringClass() instanceof JvmClassInfo target) { + ClassWriter writer = new ClassWriter(0); + if (member.isField()) { + FieldMember field = (FieldMember) member; + target.getClassReader().accept(FieldAnnotationRemovingVisitor.forClass(writer, annotationTypes, field), 0); + } else { + MethodMember method = (MethodMember) member; + target.getClassReader().accept(MethodAnnotationRemovingVisitor.forClass(writer, annotationTypes, method), 0); + } + JvmClassInfo updatedClass = new JvmClassInfoBuilder(writer.toByteArray()).build(); + bundle.put(cast(updatedClass)); + } else { + logger.warn("Cannot remove annotations on unsupported annotated type: {}", annotated.getClass().getSimpleName()); + } + } catch (Throwable t) { + logger.error("Failed removing annotation", t); + } + } + /** * @return New string search pane, opened in a new docking tab. */ @@ -1922,7 +2042,7 @@ private T openSearchPane(@Nonnull String titleId, region = dockingManager.newRegion(); DockingTab tab = region.createTab(getBinding(titleId), content); tab.setGraphic(new FontIconView(icon)); - RecafScene scene = new RecafScene((region)); + RecafScene scene = new RecafScene(region); Stage window = windowFactory.createAnonymousStage(scene, getBinding("menu.search"), 800, 400); window.show(); window.requestFocus(); @@ -2004,10 +2124,11 @@ private static void selectTab(Navigable navigable) { * * @return Created tab. */ + @Nonnull private static DockingTab createTab(@Nonnull DockingRegion region, - @Nonnull String title, - @Nonnull Node graphic, - @Nonnull Node content) { + @Nonnull String title, + @Nonnull Node graphic, + @Nonnull Node content) { DockingTab tab = region.createTab(title, content); tab.setGraphic(graphic); return tab; @@ -2027,10 +2148,11 @@ private static DockingTab createTab(@Nonnull DockingRegion region, * * @return Created tab. */ + @Nonnull private static DockingTab createTab(@Nonnull DockingRegion region, - @Nonnull ObservableValue title, - @Nonnull Node graphic, - @Nonnull Node content) { + @Nonnull ObservableValue title, + @Nonnull Node graphic, + @Nonnull Node content) { DockingTab tab = region.createTab(title, content); tab.setGraphic(graphic); return tab; @@ -2065,46 +2187,6 @@ private static void addCloseActions(@Nonnull ContextMenu menu, @Nonnull DockingT ); } - /** - * @param workspace - * Containing workspace. - * @param resource - * Containing resource. - * @param bundle - * Containing bundle. - * @param info - * Class item to end path with. - * - * @return Class path node. - */ - @Nonnull - private static ClassPathNode buildPath(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull ClassBundle bundle, - @Nonnull ClassInfo info) { - return PathNodes.classPath(workspace, resource, bundle, info); - } - - /** - * @param workspace - * Containing workspace. - * @param resource - * Containing resource. - * @param bundle - * Containing bundle. - * @param info - * File item to end path with. - * - * @return File path node. - */ - @Nonnull - private static FilePathNode buildPath(@Nonnull Workspace workspace, - @Nonnull WorkspaceResource resource, - @Nonnull FileBundle bundle, - @Nonnull FileInfo info) { - return PathNodes.filePath(workspace, resource, bundle, info); - } - @Nonnull @Override public String getServiceId() { diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Navigable.java b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Navigable.java index d9bf0e610..6e774882d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Navigable.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/Navigable.java @@ -1,6 +1,7 @@ package software.coley.recaf.services.navigation; import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; import jakarta.enterprise.context.Dependent; import javafx.scene.layout.Pane; import software.coley.recaf.info.ClassInfo; @@ -23,9 +24,10 @@ public interface Navigable { /** * @return The path layout pointing to the content (Such as a {@link ClassInfo}, {@link ClassMember}, etc) - * that this {@link Navigable} class is representing. + * that this {@link Navigable} class is representing. Can be {@code null} if this content is populated dynamically + * (Common for {@link UpdatableNavigable}). */ - @Nonnull + @Nullable PathNode getPath(); /** @@ -70,12 +72,15 @@ public interface Navigable { @Nonnull default List getNavigableChildrenByPath(@Nonnull PathNode path) { PathNode value = getPath(); - if (path.equals(value)) + if (value == null || path.equals(value)) return Collections.singletonList(this); List list = null; - for (Navigable child : getNavigableChildren()) - if (path.isDescendantOf(child.getPath())) { + for (Navigable child : getNavigableChildren()) { + PathNode childPath = child.getPath(); + if (childPath == null) continue; + + if (path.isDescendantOf(childPath)) { List childM = child.getNavigableChildrenByPath(path); if (!childM.isEmpty()) { if (list == null) @@ -84,6 +89,7 @@ default List getNavigableChildrenByPath(@Nonnull PathNode path) { list.addAll(childM); } } + } return list == null ? Collections.emptyList() : list; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java index b1f7f5ed7..c914264e5 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/navigation/NavigationManager.java @@ -22,6 +22,7 @@ import software.coley.recaf.ui.docking.DockingManager; import software.coley.recaf.ui.docking.DockingTab; import software.coley.recaf.services.workspace.WorkspaceManager; +import software.coley.recaf.util.FxThreadUtil; import software.coley.recaf.workspace.model.WorkspaceModificationListener; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; @@ -117,7 +118,7 @@ public NavigationManager(@Nonnull NavigationManagerConfig config, // Force close any remaining tabs that hold navigable content. for (DockingTab tab : dockingManager.getDockTabs()) { if (tab.getContent() instanceof Navigable navigable) { - navigable.disable(); + FxThreadUtil.run(navigable::disable); tab.setClosable(true); tab.close(); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java index d3c94704a..102d08b8a 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowFactory.java @@ -8,7 +8,6 @@ import javafx.stage.Stage; import software.coley.recaf.services.Service; import software.coley.recaf.ui.window.RecafStage; -import software.coley.recaf.util.Icons; /** * Creates new windows. diff --git a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java index bbc458c34..82322ee2d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/services/window/WindowManager.java @@ -5,7 +5,6 @@ import jakarta.enterprise.context.ApplicationScoped; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; -import javafx.event.EventHandler; import javafx.stage.Stage; import javafx.stage.WindowEvent; import org.slf4j.Logger; @@ -27,6 +26,7 @@ public class WindowManager implements Service { private static final Logger logger = Logging.get(WindowManager.class); public static final String SERVICE_ID = "window-manager"; + private static final String ANON_PREFIX = "anon-"; // Built-in window keys public static final String WIN_MAIN = "main"; public static final String WIN_REMOTE_VMS = "remote-vms"; @@ -41,7 +41,7 @@ public class WindowManager implements Service { private final Map windowMappings = new HashMap<>(); @Inject - public WindowManager(WindowManagerConfig config, Instance stages) { + public WindowManager(@Nonnull WindowManagerConfig config, @Nonnull Instance stages) { this.config = config; // Register identifiable stages. @@ -58,7 +58,7 @@ public WindowManager(WindowManagerConfig config, Instance sta * Stage to register. */ public void registerAnonymous(@Nonnull Stage stage) { - register(UUID.randomUUID().toString(), stage); + register(ANON_PREFIX + UUID.randomUUID(), stage); } /** @@ -84,24 +84,24 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { if (windowMappings.containsKey(id)) throw new IllegalStateException("The stage ID was already registered: " + id); - EventHandler baseOnShown = stage.getOnShown(); - EventHandler baseOnHidden = stage.getOnHidden(); - - // Wrap original handlers to keep existing behavior. // Record when windows are 'active' based on visibility. - EventHandler onShown = e -> { + // We're using event filters so users can still do things like 'stage.setOnShown(...)' and not interfere with + // our window tracking logic in here + stage.addEventFilter(WindowEvent.WINDOW_SHOWN, e -> { logger.trace("Stage showing: {}", id); activeWindows.add(stage); - if (baseOnShown != null) baseOnShown.handle(e); - - }; - EventHandler onHidden = e -> { + }); + stage.addEventFilter(WindowEvent.WINDOW_HIDDEN, e -> { logger.trace("Stage hiding: {}", id); activeWindows.remove(stage); - if (baseOnHidden != null) baseOnHidden.handle(e); - }; - stage.setOnShown(onShown); - stage.setOnHidden(onHidden); + + // Anonymous stages can be pruned from the id->stage map. + // They are not meant to be persistent. But, we register them anyway for our duplicate register check above. + if (id.startsWith(ANON_PREFIX)) { + logger.trace("Stage pruned: {} ({})", id, stage.getTitle()); + windowMappings.remove(id); + } + }); // If state is already visible, add it right away. if (stage.isShowing()) activeWindows.add(stage); @@ -119,7 +119,7 @@ public void register(@Nonnull String id, @Nonnull Stage stage) { * @return Active windows. */ @Nonnull - public ObservableList getActiveWindows() { + public Collection getActiveWindows() { return activeWindows; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/contextmenu/AnnotationMenuBuilder.java b/recaf-ui/src/main/java/software/coley/recaf/ui/contextmenu/AnnotationMenuBuilder.java index 8226ab367..45ee406bd 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/contextmenu/AnnotationMenuBuilder.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/contextmenu/AnnotationMenuBuilder.java @@ -4,12 +4,12 @@ import jakarta.annotation.Nullable; import javafx.scene.control.MenuItem; import org.kordamp.ikonli.Ikon; +import software.coley.collections.Unchecked; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.annotation.Annotated; import software.coley.recaf.info.annotation.AnnotationInfo; import software.coley.recaf.ui.contextmenu.actions.*; import software.coley.recaf.util.Lang; -import software.coley.recaf.util.Unchecked; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/BoundLabel.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/BoundLabel.java index d4adec9f8..51f1249b1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/BoundLabel.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/BoundLabel.java @@ -1,5 +1,6 @@ package software.coley.recaf.ui.control; +import jakarta.annotation.Nonnull; import javafx.beans.binding.StringBinding; import javafx.beans.value.ObservableValue; import javafx.scene.control.Label; @@ -14,7 +15,19 @@ public class BoundLabel extends Label implements Tooltipable { * @param binding * Text binding. */ - public BoundLabel(ObservableValue binding) { + public BoundLabel(@Nonnull ObservableValue binding) { textProperty().bind(binding); } + + /** + * Unbinds the old text property and rebinds to the given value. + * + * @param binding + * New value to bind to. + */ + public void rebind(@Nonnull ObservableValue binding) { + var property = textProperty(); + property.unbind(); + property.bind(binding); + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/ClosableActionMenuItem.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/ClosableActionMenuItem.java index e93be186b..fb8153e8e 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/ClosableActionMenuItem.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/ClosableActionMenuItem.java @@ -1,6 +1,7 @@ package software.coley.recaf.ui.control; import atlantafx.base.theme.Styles; +import javafx.geometry.Insets; import javafx.geometry.Pos; import javafx.scene.Node; import javafx.scene.control.Button; @@ -8,9 +9,10 @@ import javafx.scene.control.Label; import javafx.scene.control.Menu; import javafx.scene.layout.HBox; -import javafx.scene.layout.Pane; -import javafx.scene.layout.Priority; +import javafx.scene.layout.Region; import org.kordamp.ikonli.carbonicons.CarbonIcons; +import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.util.NodeEvents; import software.coley.recaf.util.threading.ThreadUtil; /** @@ -30,13 +32,8 @@ public class ClosableActionMenuItem extends CustomMenuItem { * Action to run to remove the item from the parent menu. */ public ClosableActionMenuItem(String text, Node graphic, Runnable action, Runnable onClose) { - HBox item = new HBox(); - Pane spacer = new Pane(); - HBox.setHgrow(spacer, Priority.ALWAYS); - item.setAlignment(Pos.CENTER); - item.setSpacing(6); - Label label = new Label(text); + label.setPadding(new Insets(10, 5, 10, 0)); Button closeButton = new GraphicActionButton(new FontIconView(CarbonIcons.CLOSE), () -> { Menu parent = getParentMenu(); if (parent != null) @@ -47,15 +44,44 @@ public ClosableActionMenuItem(String text, Node graphic, Runnable action, Runnab // We can't instantly refresh the menu, so this is as good as we can do. setDisable(true); }); - closeButton.getStyleClass().addAll(Styles.ROUNDED, Styles.BUTTON_OUTLINED); + closeButton.getStyleClass().addAll(Styles.RIGHT_PILL); closeButton.prefWidthProperty().bind(closeButton.heightProperty()); + getStyleClass().add("closable-menu-item"); // Layout - item.getChildren().addAll(closeButton, graphic, label); - setContent(item); + HBox box = new HBox(); + box.setSpacing(10); + box.setAlignment(Pos.CENTER_LEFT); + box.getChildren().addAll(closeButton, graphic, label); + setContent(box); + + // Hack to make the box fill the menu width. + // - When we are added to a menu... + // - And the menu is shown... + // - Initially show the precomputed size for items... + // - But then use those sizes of all items to figure the max width and set that for this (all) boxes + NodeEvents.runOnceOnChange(parentMenuProperty(), parent -> { + NodeEvents.dispatchAndRemoveIf(parent.showingProperty(), showing -> { + if (showing) { + box.setPrefWidth(Region.USE_COMPUTED_SIZE); + FxThreadUtil.delayedRun(1, () -> { + double size = parent.getItems().stream() + .filter(i -> i instanceof CustomMenuItem) + .map(i -> ((CustomMenuItem) i).getContent()) + .mapToDouble(n -> n.getBoundsInParent().getWidth()) + .max().orElse(100); + double max = Math.max(100, size); + box.setPrefWidth(max); + }); + } + return false; + }); + }); // With 'setOnAction(...)' the action is run on the JFX thread. // We want the actions to be run on background threads so the UI does not hang on long-running tasks. setOnAction(e -> ThreadUtil.run(action)); } + + } \ No newline at end of file diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/PathNodeTree.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/PathNodeTree.java index 2d4c255f2..4bf9d6177 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/PathNodeTree.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/PathNodeTree.java @@ -3,6 +3,8 @@ import atlantafx.base.theme.Styles; import atlantafx.base.theme.Tweaks; import jakarta.annotation.Nonnull; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; import javafx.scene.input.KeyCode; @@ -25,6 +27,8 @@ * @author Matt Coley */ public class PathNodeTree extends TreeView> { + protected final ObjectProperty contextSourceObjectProperty = new SimpleObjectProperty<>(ContextSource.REFERENCE); + /** * @param configurationService * Cell service to configure tree cell rendering and population. @@ -33,7 +37,7 @@ public class PathNodeTree extends TreeView> { */ public PathNodeTree(@Nonnull CellConfigurationService configurationService, @Nonnull Actions actions) { setShowRoot(false); - setCellFactory(param -> new WorkspaceTreeCell(ContextSource.REFERENCE, configurationService)); + setCellFactory(param -> new WorkspaceTreeCell(contextSourceObjectProperty.get(), configurationService)); getStyleClass().addAll(Tweaks.EDGE_TO_EDGE, Styles.DENSE); setOnKeyPressed(e -> { KeyCode code = e.getCode(); @@ -57,4 +61,12 @@ public PathNodeTree(@Nonnull CellConfigurationService configurationService, @Non } }); } + + /** + * @return Property of the {@link ContextSource} passed to newly created {@link WorkspaceTreeCell} instances. + */ + @Nonnull + public ObjectProperty contextSourceObjectPropertyProperty() { + return contextSourceObjectProperty; + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/VirtualizedScrollPaneWrapper.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/VirtualizedScrollPaneWrapper.java new file mode 100644 index 000000000..823cf7144 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/VirtualizedScrollPaneWrapper.java @@ -0,0 +1,52 @@ +package software.coley.recaf.ui.control; + +import jakarta.annotation.Nonnull; +import javafx.beans.property.SimpleDoubleProperty; +import javafx.scene.layout.Region; +import org.fxmisc.flowless.Virtualized; +import org.fxmisc.flowless.VirtualizedScrollPane; +import org.reactfx.value.Var; + +/** + * Wrapper for {@link VirtualizedScrollPane} to properly expose properties with JavaFX's property types instead of + * {@link Var} which cannot be used in a number of scenarios. + * + * @param + * Node type. + * + * @author Matt Coley + */ +public class VirtualizedScrollPaneWrapper extends VirtualizedScrollPane { + private final SimpleDoubleProperty xScroll = new SimpleDoubleProperty(0); + private final SimpleDoubleProperty yScroll = new SimpleDoubleProperty(0); + + /** + * @param content + * Virtualized content. + */ + public VirtualizedScrollPaneWrapper(V content) { + super(content); + setup(); + } + + private void setup() { + xScroll.bind(estimatedScrollXProperty()); + yScroll.bind(estimatedScrollYProperty()); + } + + /** + * @return Horizontal scroll property. + */ + @Nonnull + public SimpleDoubleProperty horizontalScrollProperty() { + return xScroll; + } + + /** + * @return Vertical scroll property. + */ + @Nonnull + public SimpleDoubleProperty verticalScrollProperty() { + return yScroll; + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/graph/MethodCallGraphPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/graph/MethodCallGraphPane.java new file mode 100644 index 000000000..2f4552e65 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/graph/MethodCallGraphPane.java @@ -0,0 +1,310 @@ +package software.coley.recaf.ui.control.graph; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.event.EventHandler; +import javafx.scene.control.*; +import javafx.scene.input.MouseButton; +import javafx.scene.input.MouseEvent; +import javafx.scene.layout.BorderPane; +import javafx.scene.layout.HBox; +import javafx.scene.paint.Color; +import javafx.scene.text.Text; +import javafx.scene.text.TextFlow; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; +import org.kordamp.ikonli.carbonicons.CarbonIcons; +import software.coley.collections.Unchecked; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.info.member.ClassMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.path.ClassMemberPathNode; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.IncompletePathException; +import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.callgraph.CallGraph; +import software.coley.recaf.services.callgraph.MethodVertex; +import software.coley.recaf.services.cell.CellConfigurationService; +import software.coley.recaf.services.cell.context.ContextSource; +import software.coley.recaf.services.navigation.Actions; +import software.coley.recaf.services.navigation.ClassNavigable; +import software.coley.recaf.services.navigation.Navigable; +import software.coley.recaf.services.navigation.UpdatableNavigable; +import software.coley.recaf.services.text.TextFormatConfig; +import software.coley.recaf.ui.control.FontIconView; +import software.coley.recaf.util.CollectionUtil; +import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.util.Lang; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.*; +import java.util.concurrent.CompletableFuture; +import java.util.function.Function; +import java.util.stream.Collectors; + +/** + * Tree display of method calls. + * + * @author Amejonah + */ +public class MethodCallGraphPane extends BorderPane implements ClassNavigable, UpdatableNavigable { + public static final int MAX_TREE_DEPTH = 20; + private final ObjectProperty currentMethod = new SimpleObjectProperty<>(); + private final CallGraphTreeView graphTreeView = new CallGraphTreeView(); + private final CellConfigurationService configurationService; + private final TextFormatConfig format; + private final CallGraph callGraph; + private final CallGraphMode mode; + private final Workspace workspace; + private final Actions actions; + private ClassPathNode path; + + public MethodCallGraphPane(@Nonnull Workspace workspace, @Nonnull CallGraph callGraph, @Nonnull CellConfigurationService configurationService, + @Nonnull TextFormatConfig format, @Nonnull Actions actions, @Nonnull CallGraphMode mode, + @Nullable ObjectProperty methodInfoObservable) { + this.configurationService = configurationService; + this.workspace = workspace; + this.callGraph = callGraph; + this.actions = actions; + this.format = format; + this.mode = mode; + + currentMethod.addListener((ob, old, cur) -> graphTreeView.onUpdate()); + graphTreeView.onUpdate(); + + setCenter(graphTreeView); + + if (methodInfoObservable != null) currentMethod.bindBidirectional(methodInfoObservable); + } + + @Nullable + @Override + public PathNode getPath() { + return getClassPath(); + } + + @Nonnull + @Override + public ClassPathNode getClassPath() { + return path; + } + + @Override + public void onUpdatePath(@Nonnull PathNode path) { + if (path instanceof ClassPathNode classPath) + this.path = classPath; + } + + @Nonnull + @Override + public Collection getNavigableChildren() { + return Collections.emptyList(); + } + + @Override + public void disable() { + graphTreeView.setRoot(null); + } + + @Override + public void requestFocus(@Nonnull ClassMember member) { + // no-op + } + + public enum CallGraphMode { + CALLS(MethodVertex::getCalls), + CALLERS(MethodVertex::getCallers); + + private final Function> childrenGetter; + + CallGraphMode(@Nonnull Function> getCallers) { + childrenGetter = getCallers; + } + } + + /** + * Item of a class in the hierarchy. + */ + private static class CallGraphItem extends TreeItem implements Comparable { + private static final Comparator comparator = CaseInsensitiveSimpleNaturalComparator.getInstance(); + boolean recursive; + + private CallGraphItem(@Nonnull MethodMember method, boolean recursive) { + super(method); + this.recursive = recursive; + } + + @Nullable + private ClassInfo getDeclaringClass() { + return getValue().getDeclaringClass(); + } + + @Override + public int compareTo(CallGraphItem o) { + // We want the tree display to have items in sorted order by + // package > class > method-name > method-args + int cmp = 0; + MethodMember method = getValue(); + MethodMember otherMethod = o.getValue(); + ClassInfo declaringClass = getDeclaringClass(); + ClassInfo otherDeclaringClass = o.getDeclaringClass(); + if (declaringClass != null) + cmp = comparator.compare(declaringClass.getName(), otherDeclaringClass.getName()); + if (cmp == 0) + cmp = comparator.compare(method.getName(), otherMethod.getName()); + if (cmp == 0) + cmp = comparator.compare(method.getDescriptor(), otherMethod.getDescriptor()); + return cmp; + } + } + + /** + * Cell of a class in the hierarchy. + */ + class CallGraphCell extends TreeCell { + private EventHandler onClickFilter; + + private CallGraphCell() { + getStyleClass().addAll("code-area", "transparent-cell"); + } + + @Override + protected void updateItem(MethodMember method, boolean empty) { + super.updateItem(method, empty); + if (empty || method == null) { + setText(null); + setGraphic(null); + setOnMouseClicked(null); + setContextMenu(null); + if (onClickFilter != null) + removeEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); + setOpacity(1); + } else { + onClickFilter = null; + + ClassInfo declaringClass = method.getDeclaringClass(); + if (declaringClass == null) + return; + + ClassPathNode ownerPath = workspace.findClass(declaringClass.getName()); + if (ownerPath == null) + return; + + ClassMemberPathNode methodPath = ownerPath.child(method); + + String methodOwnerName = declaringClass.getName(); + Text classText = new Text(format.filter(methodOwnerName, false, true, true)); + classText.setFill(Color.CADETBLUE); + + Text methodText = new Text(method.getName()); + if (method.hasStaticModifier()) methodText.setFill(Color.LIGHTGREEN); + else methodText.setFill(Color.YELLOW); + + // Layout + TextFlow textFlow = new TextFlow(classText, new Label("#"), methodText, new Label(method.getDescriptor())); + HBox box = new HBox(configurationService.graphicOf(methodPath), textFlow); + box.setSpacing(5); + if (getTreeItem() instanceof CallGraphItem i && i.recursive) { + box.getChildren().add(new FontIconView(CarbonIcons.CODE_REFERENCE)); + box.setOpacity(0.4); + } + setGraphic(box); + + // Context menu support + ContextMenu contextMenu = configurationService.contextMenuOf(ContextSource.REFERENCE, methodPath); + MenuItem focusItem = new MenuItem(); + focusItem.setGraphic(new FontIconView(CarbonIcons.CI_3D_CURSOR_ALT)); + focusItem.textProperty().bind(Lang.getBinding("menu.view.methodcallgraph.focus")); + focusItem.setOnAction(e -> currentMethod.set(method)); + contextMenu.getItems().add(1, focusItem); + setContextMenu(contextMenu); + + // Override the double click behavior to open the class. Doesn't work using the "setOn..." methods. + onClickFilter = e -> { + if (e.getButton().equals(MouseButton.PRIMARY) && e.getClickCount() >= 2) { + e.consume(); + try { + actions.gotoDeclaration(ownerPath).requestFocus(method); + } catch (IncompletePathException ex) { + // TODO: Log error + } + } + }; + addEventFilter(MouseEvent.MOUSE_PRESSED, onClickFilter); + } + } + + public MethodMember getCurrentMethod() { + return currentMethod.get(); + } + + public ObjectProperty currentMethodProperty() { + return currentMethod; + } + } + + private class CallGraphTreeView extends TreeView { + public CallGraphTreeView() { + getStyleClass().add("transparent-tree"); + setCellFactory(param -> new CallGraphCell()); + } + + public void onUpdate() { + final MethodMember methodInfo = currentMethod.get(); + if (methodInfo == null) { + setRoot(null); + } else { + CompletableFuture.supplyAsync(() -> { + while (!callGraph.isReady().getValue()) Unchecked.run(() -> Thread.sleep(100)); + return buildCallGraph(methodInfo, mode.childrenGetter); + }).thenAcceptAsync(root -> { + root.setExpanded(true); + setRoot(root); + }, FxThreadUtil.executor()); + } + } + + @Nonnull + private CallGraphItem buildCallGraph(@Nonnull MethodMember rootMethod, @Nonnull Function> childrenGetter) { + ArrayDeque visitedMethods = new ArrayDeque<>(); + ArrayDeque> workingStack = new ArrayDeque<>(); + CallGraphItem root = new CallGraphItem(rootMethod, false); + workingStack.push(new ArrayList<>(Set.of(root))); + int depth = 0; + while (!workingStack.isEmpty()) { + List todo = workingStack.peek(); + if (!todo.isEmpty()) { + final CallGraphItem item = todo.removeLast(); + if (item.recursive) + continue; + visitedMethods.push(item.getValue()); + depth++; + final MethodVertex vertex = callGraph.getVertex(item.getValue()); + if (vertex != null) { + final List newTodo = childrenGetter.apply(vertex).stream() + .filter(c -> c.getResolvedMethod() != null) + .map(c -> { + MethodMember cm = c.getResolvedMethod(); + return new CallGraphItem(cm, visitedMethods.contains(cm)); + }) + .filter(i -> { + if (i.getValue() == null) return false; + int insert = CollectionUtil.sortedInsertIndex(Unchecked.cast(item.getChildren()), i); + item.getChildren().add(insert, i); + return !i.recursive; + }).collect(Collectors.toList()); + if (!newTodo.isEmpty() && depth < MAX_TREE_DEPTH) { + workingStack.push(newTodo); + } else visitedMethods.pop(); + } + continue; + } + workingStack.pop(); + if (!visitedMethods.isEmpty()) visitedMethods.pop(); + depth--; + } + return root; + } + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/graph/MethodCallGraphsPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/graph/MethodCallGraphsPane.java new file mode 100644 index 000000000..702af8552 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/graph/MethodCallGraphsPane.java @@ -0,0 +1,111 @@ +package software.coley.recaf.ui.control.graph; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import javafx.beans.property.ObjectProperty; +import javafx.beans.property.SimpleObjectProperty; +import javafx.scene.control.Tab; +import javafx.scene.control.TabPane; +import software.coley.recaf.info.member.ClassMember; +import software.coley.recaf.info.member.MethodMember; +import software.coley.recaf.path.ClassMemberPathNode; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.callgraph.CallGraph; +import software.coley.recaf.services.cell.CellConfigurationService; +import software.coley.recaf.services.navigation.Actions; +import software.coley.recaf.services.navigation.ClassNavigable; +import software.coley.recaf.services.navigation.Navigable; +import software.coley.recaf.services.navigation.UpdatableNavigable; +import software.coley.recaf.services.text.TextFormatConfig; +import software.coley.recaf.util.Lang; +import software.coley.recaf.workspace.model.Workspace; + +import java.util.Collection; +import java.util.Collections; + +/** + * Container pane for two {@link MethodCallGraphPane} for inbound and outbound calls. + * + * @author Amejonah + */ +@Dependent +public class MethodCallGraphsPane extends TabPane implements ClassNavigable, UpdatableNavigable { + private final ObjectProperty currentMethodInfo; + private ClassPathNode path; + + @Inject + public MethodCallGraphsPane(@Nonnull Workspace workspace, @Nonnull CallGraph callGraph, + @Nonnull TextFormatConfig format, @Nonnull Actions actions, + @Nonnull CellConfigurationService configurationService) { + currentMethodInfo = new SimpleObjectProperty<>(); + + getTabs().add(creatTab(workspace, callGraph, configurationService, format, actions, MethodCallGraphPane.CallGraphMode.CALLS, currentMethodInfo)); + getTabs().add(creatTab(workspace, callGraph, configurationService, format, actions, MethodCallGraphPane.CallGraphMode.CALLERS, currentMethodInfo)); + + // Remove the standard tab-pane border. + getStyleClass().addAll("borderless"); + } + + @Nonnull + private Tab creatTab(@Nonnull Workspace workspace, @Nonnull CallGraph callGraph, @Nonnull CellConfigurationService configurationService, + @Nonnull TextFormatConfig format, @Nonnull Actions actions, @Nonnull MethodCallGraphPane.CallGraphMode mode, + @Nullable ObjectProperty methodInfoObservable) { + Tab tab = new Tab(); + tab.setContent(new MethodCallGraphPane(workspace, callGraph, configurationService, format, actions, mode, methodInfoObservable)); + tab.textProperty().bind(Lang.getBinding("menu.view.methodcallgraph." + mode.name().toLowerCase())); + tab.setClosable(false); + return tab; + } + + @Nonnull + public ObjectProperty currentMethodInfoProperty() { + return currentMethodInfo; + } + + @Override + public void onUpdatePath(@Nonnull PathNode path) { + if (path instanceof ClassMemberPathNode memberPathNode) { + this.path = memberPathNode.getParent(); + ClassMember member = memberPathNode.getValue(); + if (member instanceof MethodMember method) + currentMethodInfo.setValue(method); + } + } + + @Nonnull + @Override + public ClassPathNode getClassPath() { + return path; + } + + @Nullable + @Override + public PathNode getPath() { + return getClassPath(); + } + + @Override + public boolean isTrackable() { + // Disabling tracking allows other panels with the same path-node to be opened. + return false; + } + + @Nonnull + @Override + public Collection getNavigableChildren() { + return Collections.emptyList(); + } + + @Override + public void disable() { + getTabs().clear(); + } + + @Override + public void requestFocus(@Nonnull ClassMember member) { + // no-op + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/DecompileAllPopup.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/DecompileAllPopup.java index 1c4d08ea9..9666d373f 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/DecompileAllPopup.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/DecompileAllPopup.java @@ -28,10 +28,7 @@ import software.coley.recaf.ui.pane.editing.jvm.DecompilerPaneConfig; import software.coley.recaf.ui.window.RecafScene; import software.coley.recaf.ui.window.RecafStage; -import software.coley.recaf.util.FxThreadUtil; -import software.coley.recaf.util.Lang; -import software.coley.recaf.util.StringUtil; -import software.coley.recaf.util.ZipCreationUtils; +import software.coley.recaf.util.*; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceFileResource; @@ -61,9 +58,9 @@ public class DecompileAllPopup extends RecafStage { @Inject public DecompileAllPopup(@Nonnull DecompilerManager decompilerManager, - @Nonnull RecentFilesConfig recentFilesConfig, - @Nonnull DecompilerPaneConfig decompilerPaneConfig, - @Nonnull Workspace workspace) { + @Nonnull RecentFilesConfig recentFilesConfig, + @Nonnull DecompilerPaneConfig decompilerPaneConfig, + @Nonnull Workspace workspace) { String defaultName = buildName(workspace); targetBundle = workspace.getPrimaryResource().getJvmClassBundle(); @@ -75,11 +72,12 @@ public DecompileAllPopup(@Nonnull DecompilerManager decompilerManager, ObservableComboBox decompilerCombo = new ObservableComboBox<>(decompilerProperty, decompilerManager.getJvmDecompilers()); ProgressBar progress = new ProgressBar(0); Button pathButton = new ActionButton(CarbonIcons.EDIT, pathProperty.map(Path::toString), () -> { - FileChooser chooser = new FileChooser(); - chooser.setInitialFileName(defaultName); - chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter("Archives", "zip", "jar")); - chooser.setInitialDirectory(recentFilesConfig.getLastWorkspaceExportDirectory().unboxingMap(File::new)); - chooser.setTitle(Lang.get("dialog.file.open")); + FileChooser chooser = new FileChooserBuilder() + .setInitialFileName(defaultName) + .setInitialDirectory(recentFilesConfig.getLastWorkspaceExportDirectory()) + .setFileExtensionFilter("Archives", "*.zip", "*.jar") + .setTitle(Lang.get("dialog.file.open")) + .build(); File file = chooser.showSaveDialog(getScene().getWindow()); if (file != null) { String parent = file.getParent(); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java index beec8ffdd..c6c3825de 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/ItemListSelectionPopup.java @@ -63,6 +63,10 @@ protected void updateItem(T item, boolean empty) { } } }); + + // Initial selection if there is only one item. + // Allows the user to jump straight to accept/cancel buttons. + if (items.size() == 1) list.getSelectionModel().selectFirst(); } @Nonnull diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/NamePopup.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/NamePopup.java index 5e3d8e0d2..27968aa42 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/NamePopup.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/popup/NamePopup.java @@ -91,7 +91,7 @@ public NamePopup(@Nonnull Consumer nameConsumer) { */ private void accept(@Nonnull Consumer nameConsumer) { // Do nothing if conflict detected - if (nameConflict.get()) { + if (nameConflict.get() || isIllegalValue.get()) { Toolkit.getDefaultToolkit().beep(); return; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java index e67589898..9002c5803 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/Editor.java @@ -4,12 +4,17 @@ import jakarta.annotation.Nullable; import javafx.beans.value.ChangeListener; import javafx.beans.value.ObservableValue; +import javafx.collections.ObservableList; +import javafx.scene.Node; import javafx.scene.control.IndexRange; import javafx.scene.control.ScrollBar; import javafx.scene.input.KeyCode; import javafx.scene.input.KeyEvent; import javafx.scene.layout.BorderPane; +import javafx.scene.layout.Region; import javafx.scene.layout.StackPane; +import javafx.scene.text.Text; +import org.fxmisc.flowless.Cell; import org.fxmisc.flowless.VirtualFlow; import org.fxmisc.flowless.VirtualizedScrollPane; import org.fxmisc.richtext.CodeArea; @@ -18,7 +23,10 @@ import org.reactfx.Change; import org.reactfx.EventStream; import org.reactfx.EventStreams; +import org.reactfx.collection.MemoizationList; import software.coley.collections.Lists; +import software.coley.collections.Unchecked; +import software.coley.recaf.ui.control.VirtualizedScrollPaneWrapper; import software.coley.recaf.ui.control.richtext.bracket.SelectedBracketTracking; import software.coley.recaf.ui.control.richtext.linegraphics.RootLineGraphicFactory; import software.coley.recaf.ui.control.richtext.problem.ProblemTracking; @@ -33,6 +41,7 @@ import java.time.Duration; import java.util.Collection; import java.util.Collections; +import java.util.List; import java.util.Objects; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; @@ -58,6 +67,7 @@ public class Editor extends BorderPane { private final ScrollBar horizontalScrollbar; private final ScrollBar verticalScrollbar; private final VirtualFlow virtualFlow; + private final MemoizationList> virtualCellList; private final ExecutorService syntaxPool = ThreadPoolFactory.newSingleThreadExecutor("syntax-highlight"); private final RootLineGraphicFactory rootLineGraphicFactory = new RootLineGraphicFactory(this); private final EventStream> caretPosEventStream; @@ -74,10 +84,12 @@ public class Editor extends BorderPane { public Editor() { // Get the reflection hacks out of the way first. // - Want to have access to scrollbars & the internal 'virtualFlow' - VirtualizedScrollPane scrollPane = new VirtualizedScrollPane<>(codeArea); + VirtualizedScrollPaneWrapper scrollPane = new VirtualizedScrollPaneWrapper<>(codeArea); horizontalScrollbar = Unchecked.get(() -> ReflectUtil.quietGet(scrollPane, VirtualizedScrollPane.class.getDeclaredField("hbar"))); verticalScrollbar = Unchecked.get(() -> ReflectUtil.quietGet(scrollPane, VirtualizedScrollPane.class.getDeclaredField("vbar"))); virtualFlow = Unchecked.get(() -> ReflectUtil.quietGet(codeArea, GenericStyledArea.class.getDeclaredField("virtualFlow"))); + Object virtualCellManager = Unchecked.get(() -> ReflectUtil.quietGet(virtualFlow, VirtualFlow.class.getDeclaredField("cellListManager"))); + virtualCellList = ReflectUtil.quietInvoke(virtualCellManager.getClass(), virtualCellManager, "getLazyCellList", new Class[0], new Object[0]); // Initial layout / style. getStylesheets().add("/style/code-editor.css"); @@ -185,6 +197,7 @@ public CompletableFuture restyleAtPosition(int position, int length) { * * @return The {@link StackPane} present in the {@link #getCenter() center} of the editor. */ + @Nonnull public StackPane getPrimaryStack() { return stackPane; } @@ -204,6 +217,7 @@ public void redrawParagraphGraphics() { /** * @return Current style spans for the entire document. */ + @Nonnull public StyleSpans> getStyleSpans() { return codeArea.getStyleSpans(0, getTextLength()); } @@ -446,6 +460,101 @@ public CodeArea getCodeArea() { return codeArea; } + /** + * @return Virtual flow backing the {@link #getCodeArea() code area}. + */ + @Nonnull + public VirtualFlow getVirtualFlow() { + return virtualFlow; + } + + /** + * @return Virtualized cell list within the {@link #getVirtualFlow() virtual flow}. + */ + @Nonnull + public MemoizationList> getVirtualCellList() { + return virtualCellList; + } + + + /** + * Be very aware of when you call this. You may encounter unexpected values if invoked during early + * layout of your node / scene. + *

+ * This method is why we have to do the {@link FxThreadUtil#delayedRun(long, Runnable)} call above. + * Normally when you use {@code virtualFlow.getCellIfVisible(paragraph)} it lays out the nodes for + * you so that you don't run into this problem. The problem is it lays out the whole {@code ParagraphBox} + * class, which includes the graphic factory we're currently populating the content of. + * This means using that method will cause a {@link StackOverflowError}. + * Thus, we have the hacky delayed run instead. + * + * @param paragraph + * Paragraph index to compute empty space (in pixels) to the first non-whitespace character. + * + * @return Pixels to first non-whitespace character. + */ + public double computeWhitespacePrefixWidth(int paragraph) { + // Get the cell from the given paragraph. It should exist since we're + // initializing a paragraph graphic for it. + Cell cell = virtualCellList.get(paragraph); + if (cell == null) return 0; + + // ParagraphBox is private in RichTextFX, but we just need to get the children so + // casting to region suffices. + Region paragraphBox = (Region) cell.getNode(); + ObservableList paragraphBoxChildren = paragraphBox.getChildrenUnmodifiable(); + if (!paragraphBoxChildren.isEmpty()) { + // The text flow is always the first child of the box. + Region textFlow = (Region) paragraphBoxChildren.getFirst(); + + // In the text flow, we want the first 'Text' child. This should be the first one with empty spaces. + ObservableList flowChildren = textFlow.getChildrenUnmodifiable(); + List textNodes = Unchecked.cast(flowChildren.stream() + .filter(c -> c instanceof Text) + .toList()); + + // If we found the node, and it is only whitespace (blank) then we can use its width. + double width = 0; + for (Text textNode : textNodes) { + String text = textNode.getText(); + double boundWidth = textNode.getBoundsInLocal().getWidth(); + if (text.isBlank()) { + // Texts that are blank are all whitespace, add it up. + width += boundWidth; + } else { + // Some texts have leading whitespace that we want to consider. + int whitespacePrefix = StringUtil.getWhitespacePrefixLength(text); + if (whitespacePrefix > 0) { + double charWidth = boundWidth / StringUtil.getTabAdjustedLength(text); + width += charWidth * whitespacePrefix; + } + break; + } + } + return width; + } + + return 0; + } + + /** + * @param line + * Paragraph index, 0-based. + * + * @return {@code true} when the paragraph is visible. + */ + public boolean isParagraphVisible(int line) { + // TODO: If we ever add paragraph folding back, we need to check those cases here and return false + + // We use the internal virtual flow because the provided methods call 'layout()' unnecessarily + // - firstVisibleParToAllParIndex() + // - lastVisibleParToAllParIndex() + // It is very likely by the time of calling this that our text is already populated and laid out. + // This gets called rather frequently so the constant layout requests contribute a massive waste of time. + // If we use these methods from the internal 'VirtualFlow' we skip all that and the result is almost instant. + return line >= virtualFlow.getFirstVisibleIndex() && line <= virtualFlow.getLastVisibleIndex(); + } + /** * @return {@link #getCodeArea() Code area's} horizontal scrollbar. */ @@ -476,7 +585,7 @@ public ScrollBar getVerticalScrollbar() { */ @Nonnull public CompletableFuture schedule(@Nonnull ExecutorService supplierService, - @Nonnull Supplier supplier, @Nonnull Consumer consumer) { + @Nonnull Supplier supplier, @Nonnull Consumer consumer) { return CompletableFuture.supplyAsync(supplier, supplierService) .thenAcceptAsync(consumer, FxThreadUtil.executor()); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/SafeCodeArea.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/SafeCodeArea.java index cd1c21e26..9b8470f4f 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/SafeCodeArea.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/SafeCodeArea.java @@ -44,4 +44,17 @@ public void moveTo(int paragraphIndex, int columnIndex, SelectionPolicy selectio if (paragraphIndex >= 0 && paragraphIndex < getParagraphs().size()) super.moveTo(paragraphIndex, columnIndex, selectionPolicy); } + + @Override + public void selectRange(int anchor, int caretPosition) { + if (anchor >= 0 && anchor <= getLength() && caretPosition >= 0 && caretPosition <= getLength()) + super.selectRange(anchor, caretPosition); + } + + @Override + public void selectRange(int anchorParagraph, int anchorColumn, int caretPositionParagraph, int caretPositionColumn) { + if (anchorParagraph >= 0 && anchorParagraph < getParagraphs().size() && + caretPositionParagraph >= 0 && caretPositionParagraph < getParagraphs().size()) + super.selectRange(anchorParagraph, anchorColumn, caretPositionParagraph, caretPositionColumn); + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/LineNumberFactory.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/LineNumberFactory.java index 6012cb6c7..7b39683d4 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/LineNumberFactory.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/linegraphics/LineNumberFactory.java @@ -39,6 +39,7 @@ public void apply(@Nonnull LineContainer container, int paragraph) { if (codeArea == null) return; Label label = new Label(format(paragraph + 1, computeDigits(codeArea.getParagraphs().size()))); + label.getStyleClass().add("bg"); HBox.setHgrow(label, Priority.ALWAYS); container.addHorizontal(label); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java index 765370bbb..3f02b02dc 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/problem/ProblemTracking.java @@ -10,8 +10,10 @@ import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.ui.control.richtext.Editor; import software.coley.recaf.ui.control.richtext.EditorComponent; +import software.coley.recaf.util.CollectionUtil; import java.util.*; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; import java.util.function.Predicate; @@ -22,7 +24,7 @@ */ public class ProblemTracking implements EditorComponent, Consumer { private static final DebuggingLogger logger = Logging.get(ProblemTracking.class); - private final List listeners = new ArrayList<>(); + private final List listeners = new CopyOnWriteArrayList<>(); private final NavigableMap problems = new TreeMap<>(); private Editor editor; @@ -43,7 +45,8 @@ public Problem getProblem(int line) { */ public void add(@Nonnull Problem problem) { problems.put(problem.getLine(), problem); - listeners.forEach(ProblemInvalidationListener::onProblemInvalidation); + CollectionUtil.safeForEach(listeners, ProblemInvalidationListener::onProblemInvalidation, + (listener, t) -> logger.error("Exception thrown when adding problem to tracking", t)); } /** @@ -56,7 +59,8 @@ public void add(@Nonnull Problem problem) { public boolean removeByInstance(@Nonnull Problem problem) { boolean updated = problems.entrySet().removeIf(p -> p.getValue() == problem); if (updated) - listeners.forEach(ProblemInvalidationListener::onProblemInvalidation); + CollectionUtil.safeForEach(listeners, ProblemInvalidationListener::onProblemInvalidation, + (listener, t) -> logger.error("Exception thrown when removing problem from tracking", t)); return updated; } @@ -70,7 +74,8 @@ public boolean removeByInstance(@Nonnull Problem problem) { public boolean removeByLine(int line) { boolean updated = problems.remove(line) != null; if (updated) - listeners.forEach(ProblemInvalidationListener::onProblemInvalidation); + CollectionUtil.safeForEach(listeners, ProblemInvalidationListener::onProblemInvalidation, + (listener, t) -> logger.error("Exception thrown when removing problem from tracking", t)); return updated; } @@ -83,7 +88,8 @@ public boolean removeByLine(int line) { public boolean removeByPhase(@Nonnull ProblemPhase phase) { boolean updated = problems.entrySet().removeIf(e -> e.getValue().getPhase() == phase); if (updated) - listeners.forEach(ProblemInvalidationListener::onProblemInvalidation); + CollectionUtil.safeForEach(listeners, ProblemInvalidationListener::onProblemInvalidation, + (listener, t) -> logger.error("Exception thrown when removing problem from tracking", t)); return updated; } @@ -92,7 +98,8 @@ public boolean removeByPhase(@Nonnull ProblemPhase phase) { */ public void clear() { problems.clear(); - listeners.forEach(ProblemInvalidationListener::onProblemInvalidation); + CollectionUtil.safeForEach(listeners, ProblemInvalidationListener::onProblemInvalidation, + (listener, t) -> logger.error("Exception thrown when clearing problems from tracking", t)); } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java index d4a8bc615..5ffbd171e 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/source/JavaContextActionSupport.java @@ -50,12 +50,12 @@ import software.coley.recaf.util.Lang; import software.coley.recaf.util.StringUtil; import software.coley.recaf.util.threading.ThreadPoolFactory; +import software.coley.recaf.util.threading.ThreadUtil; import software.coley.recaf.workspace.model.Workspace; import java.time.Duration; import java.util.*; import java.util.concurrent.ExecutorService; -import java.util.concurrent.Executors; import java.util.concurrent.Future; /** @@ -88,8 +88,8 @@ public class JavaContextActionSupport implements EditorComponent, UpdatableNavig @Inject public JavaContextActionSupport(@Nonnull CellConfigurationService cellConfigurationService, - @Nonnull AstService astService, - @Nonnull Workspace workspace) { + @Nonnull AstService astService, + @Nonnull Workspace workspace) { this.cellConfigurationService = cellConfigurationService; this.astService = astService; contextHelper = new AstContextHelper(workspace); @@ -179,40 +179,23 @@ public void select(@Nonnull ClassMember member) { queuedSelectionTask = () -> select(member); } else { queuedSelectionTask = null; - SortedMap map = AstRangeMapper.computeRangeToTreeMapping(unit, editor.getText()); - for (Map.Entry entry : map.entrySet()) { - Tree tree = entry.getValue(); - Range range = entry.getKey(); - - // Check against method and variable (field) declarations. - if (member.isMethod() && tree instanceof J.MethodDeclaration method) { - JavaType.Method methodType = method.getMethodType(); - - // Extract method info. - String name = method.getSimpleName(); - String desc = methodType == null ? null : AstUtils.toDesc(methodType); - if (method.isConstructor()) { - name = ""; - if (desc != null) desc = StringUtil.cutOffAtFirst(desc, ")") + ")V"; - } - - // Compare to passed member. - if (member.getName().equals(name) && (desc == null || member.getDescriptor().equals(desc))) { - // Select it in the editor. - selectRange(range); - return; - } - } else if (member.isField() && tree instanceof J.VariableDeclarations variableDeclarations) { - for (J.VariableDeclarations.NamedVariable variable : variableDeclarations.getVariables()) { - JavaType.Variable variableType = variable.getVariableType(); - - // Skip variable declarations that are not fields. - if (variableType != null && !(variableType.getOwner() instanceof JavaType.FullyQualified)) - continue; - - // Extract variable info. - String name = variable.getSimpleName(); - String desc = variableType == null ? null : AstUtils.toDesc(variableType); + try { + SortedMap map = AstRangeMapper.computeRangeToTreeMapping(unit, editor.getText()); + for (Map.Entry entry : map.entrySet()) { + Tree tree = entry.getValue(); + Range range = entry.getKey(); + + // Check against method and variable (field) declarations. + if (member.isMethod() && tree instanceof J.MethodDeclaration method) { + JavaType.Method methodType = method.getMethodType(); + + // Extract method info. + String name = method.getSimpleName(); + String desc = methodType == null ? null : AstUtils.toDesc(methodType); + if (method.isConstructor()) { + name = ""; + if (desc != null) desc = StringUtil.cutOffAtFirst(desc, ")") + ")V"; + } // Compare to passed member. if (member.getName().equals(name) && (desc == null || member.getDescriptor().equals(desc))) { @@ -220,12 +203,33 @@ public void select(@Nonnull ClassMember member) { selectRange(range); return; } + } else if (member.isField() && tree instanceof J.VariableDeclarations variableDeclarations) { + for (J.VariableDeclarations.NamedVariable variable : variableDeclarations.getVariables()) { + JavaType.Variable variableType = variable.getVariableType(); + + // Skip variable declarations that are not fields. + if (variableType != null && !(variableType.getOwner() instanceof JavaType.FullyQualified)) + continue; + + // Extract variable info. + String name = variable.getSimpleName(); + String desc = variableType == null ? null : AstUtils.toDesc(variableType); + + // Compare to passed member. + if (member.getName().equals(name) && (desc == null || member.getDescriptor().equals(desc))) { + // Select it in the editor. + selectRange(range); + return; + } + } + } else if (member.getName().equals("") && tree instanceof J.Block block && block.isStatic()) { + // Select it in the editor. + selectRange(range); + return; } - } else if (member.getName().equals("") && tree instanceof J.Block block && block.isStatic()) { - // Select it in the editor. - selectRange(range); - return; } + } catch (Throwable t) { + logger.error("Unhandled exception in Java context support - select '{}'", member.getName(), t); } } } @@ -273,9 +277,13 @@ private AstResolveResult resolvePosition(int pos, boolean doOffset) { * Text changed. */ private void handleShortDurationChange(@Nonnull PlainTextChange change) { - int position = change.getPosition(); - int offset = change.getNetLength(); - offsetMap.merge(position, offset, Integer::sum); + try { + int position = change.getPosition(); + int offset = change.getNetLength(); + offsetMap.merge(position, offset, Integer::sum); + } catch (Throwable t) { + logger.error("Unhandled exception merging offset-maps with new text-change", t); + } } /** @@ -291,7 +299,7 @@ private void handleLongDurationChange() { lastFuture.cancel(true); // Do parsing on BG thread, it can be slower on complex inputs. - lastFuture = parseThreadPool.submit(() -> { + lastFuture = parseThreadPool.submit(ThreadUtil.wrap(() -> { String text = editor.getText(); // Skip if the source hasn't changed since the last time. @@ -342,7 +350,7 @@ private void handleLongDurationChange() { // Wipe offset map now that we have a new AST offsetMap.clear(); - }); + })); } /** @@ -456,7 +464,7 @@ public void onUpdatePath(@Nonnull PathNode path) { // This addresses situations where changes to the class introduce new type dependencies. // If we used the existing parser, the newly added types would be unresolvable. ClassInfo classInfo = classPath.getValue(); - Executors.newSingleThreadExecutor().submit(() -> initialize(classInfo)); + ThreadUtil.run(() -> initialize(classInfo)); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/AbstractSyntaxHighlighter.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/AbstractSyntaxHighlighter.java new file mode 100644 index 000000000..bccae3add --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/AbstractSyntaxHighlighter.java @@ -0,0 +1,98 @@ +package software.coley.recaf.ui.control.richtext.syntax; + +import jakarta.annotation.Nonnull; +import org.fxmisc.richtext.model.StyleSpan; +import org.fxmisc.richtext.model.StyleSpans; +import org.slf4j.Logger; +import software.coley.recaf.analytics.logging.Logging; + +import java.util.Collection; +import java.util.Collections; + +/** + * Common base for syntax highlighter implementations. + * + * @author Matt Coley + */ +public abstract class AbstractSyntaxHighlighter implements SyntaxHighlighter { + private static final Logger logger = Logging.get(AbstractSyntaxHighlighter.class); + private static final int DEFAULT_MAX_SPANS = 400_000; + private static final int DEFAULT_MAX_SPANS_PER_LINE = 25_000; + private final int maxSpans; + private final int maxSpansPerLine; + + /** + * New base highlighter with default span limits. + */ + protected AbstractSyntaxHighlighter() { + this(DEFAULT_MAX_SPANS, DEFAULT_MAX_SPANS_PER_LINE); + } + + /** + * New base highlighter with the given span limits. + * + * @param maxSpans + * Max span count to allow in output. + * @param maxSpansPerLine + * Max span count per line to allow in output. + */ + protected AbstractSyntaxHighlighter(int maxSpans, int maxSpansPerLine) { + this.maxSpans = maxSpans; + this.maxSpansPerLine = maxSpansPerLine; + } + + @Nonnull + @Override + @SuppressWarnings("UnnecessaryLocalVariable") // Used for optimization + public final StyleSpans> createStyleSpans(@Nonnull String text, int start, int end) { + StyleSpans> spans = createStyleSpansImpl(text, start, end); + + // Prevent bogus inputs from hanging the application by emitting stupidly complex span collections. + // This counts the number of global spans across all lines. + int spanCount = spans.getSpanCount(); + if (spanCount > maxSpans) { + logger.warn("Skipping syntax computation, input of {} spans, max is {}", spanCount, maxSpans); + return StyleSpans.singleton(Collections.emptyList(), start - end); + } + + // The more problematic "lag" comes when a single line holds many spans. + // This is because if 100_000 spans are separated across 100_000 lines, rendering 1 span per line is trivial + // when considering the lines are virtualized vertically. However, because text is not horizontally virtualized + // 100_000 spans on one line would cause problems. + int maxSpansPerLineLocal = maxSpansPerLine; + int currentLine = 0; + int currentOffset = 0; + int nextLineOffset = text.indexOf('\n'); + spanCount = 0; + for (StyleSpan> span : spans) { + int spanLength = span.getLength(); + int spanBegin = currentOffset; + int spanEnd = spanBegin + spanLength; + spanCount++; + + // When the span goes beyond the next line offset, we scan forward for the next line that contains the span end. + // In each scan step, we increment the line and clear the span count since we've moved onto a new line. + while (spanEnd > nextLineOffset) { + spanCount = 1; + currentLine++; + nextLineOffset = text.indexOf('\n', nextLineOffset + 1); + if (nextLineOffset == -1) nextLineOffset = end; + } + + // If this line has more spans than allowed, do not yield the results, as that would cause UI slowdowns. + if (spanCount > maxSpansPerLineLocal) { + logger.warn("Skipping syntax computation, input of {} spans on line {}, max-per-line is {}", spanCount, currentLine + 1, maxSpans); + return StyleSpans.singleton(Collections.emptyList(), start - end); + } + + // Move current offset forward by the size of the span. + // Areas without syntax are still spans, but with an empty list of style classes. + currentOffset += spanLength; + } + + return spans; + } + + @Nonnull + protected abstract StyleSpans> createStyleSpansImpl(@Nonnull String text, int start, int end); +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java index 74391225d..b5447223e 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/richtext/syntax/RegexSyntaxHighlighter.java @@ -20,7 +20,7 @@ * @author Matt Coley * @see RegexLanguages Predefined languages to pass to {@link RegexSyntaxHighlighter#RegexSyntaxHighlighter(RegexRule)}. */ -public class RegexSyntaxHighlighter implements SyntaxHighlighter { +public class RegexSyntaxHighlighter extends AbstractSyntaxHighlighter { private static final Logger logger = Logging.get(RegexSyntaxHighlighter.class); private static final Map, Pattern> patternCache = new ConcurrentHashMap<>(); private final RegexRule rootRule; @@ -35,11 +35,12 @@ public RegexSyntaxHighlighter(@Nonnull RegexRule rootRule) { @Nonnull @Override - public StyleSpans> createStyleSpans(@Nonnull String text, int start, int end) { + protected StyleSpans> createStyleSpansImpl(@Nonnull String text, int start, int end) { try { - StyleSpansBuilder> builder = new StyleSpansBuilder<>(); Region region = new Region(text, null, rootRule, start, end); region.split(rootRule.subRules()); + + StyleSpansBuilder> builder = new StyleSpansBuilder<>(); region.visitBuilder(builder); return builder.create(); } catch (RuntimeException ex) { diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java index f01875701..bb224d667 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/FilterableTreeItem.java @@ -10,9 +10,9 @@ import javafx.collections.transformation.FilteredList; import javafx.scene.control.TreeItem; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.util.ReflectUtil; -import software.coley.recaf.util.Unchecked; import java.lang.reflect.Field; import java.util.Collections; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java index ac4b23b8b..7b789412a 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/TreeItems.java @@ -5,7 +5,7 @@ import javafx.scene.control.MultipleSelectionModel; import javafx.scene.control.TreeItem; import javafx.scene.control.TreeView; -import software.coley.recaf.util.Unchecked; +import software.coley.collections.Unchecked; /** * Utilities for {@link TreeItem} types. diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java index 10370a052..35cf98cce 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTree.java @@ -4,6 +4,7 @@ import jakarta.annotation.Nullable; import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import software.coley.recaf.info.*; import software.coley.recaf.path.*; import software.coley.recaf.services.cell.CellConfigurationService; @@ -16,12 +17,10 @@ import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.WorkspaceModificationListener; import software.coley.recaf.workspace.model.bundle.AndroidClassBundle; +import software.coley.recaf.workspace.model.bundle.ClassBundle; import software.coley.recaf.workspace.model.bundle.FileBundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; -import software.coley.recaf.workspace.model.resource.ResourceAndroidClassListener; -import software.coley.recaf.workspace.model.resource.ResourceFileListener; -import software.coley.recaf.workspace.model.resource.ResourceJvmClassListener; -import software.coley.recaf.workspace.model.resource.WorkspaceResource; +import software.coley.recaf.workspace.model.resource.*; import java.util.HashMap; import java.util.List; @@ -50,7 +49,7 @@ public class WorkspaceTree extends PathNodeTree implements */ @Inject public WorkspaceTree(@Nonnull CellConfigurationService configurationService, @Nonnull Actions actions, - @Nonnull WorkspaceExplorerConfig explorerConfig) { + @Nonnull WorkspaceExplorerConfig explorerConfig) { super(configurationService, actions); this.explorerConfig = explorerConfig; @@ -98,40 +97,72 @@ public void createWorkspaceRoot(@Nullable Workspace workspace) { * @param resource * Resource to add to the tree. */ - private void createResourceSubTree(WorkspaceResource resource) { + private void createResourceSubTree(@Nonnull WorkspaceResource resource) { ResourcePathNode resourcePath = rootPath.child(resource); - resource.classBundleStream().forEach(bundle -> { - Map directories = new HashMap<>(); - BundlePathNode bundlePath = resourcePath.child(bundle); - - // Pre-sort classes to skip tree-building comparisons/synchronizations. - TreeSet sortedClasses = new TreeSet<>(Named.NAME_PATH_COMPARATOR); - sortedClasses.addAll(bundle.values()); - - // Add each class in sorted order. - for (ClassInfo classInfo : sortedClasses) { - String packageName = interceptDirectoryName(classInfo.getPackageName()); - DirectoryPathNode packagePath = directories.computeIfAbsent(packageName, bundlePath::child); - ClassPathNode classPath = packagePath.child(classInfo); - WorkspaceTreeNode.getOrInsertIntoTree(root, classPath, true); - } - }); - resource.fileBundleStream().forEach(bundle -> { - Map directories = new HashMap<>(); - BundlePathNode bundlePath = resourcePath.child(bundle); - - // Pre-sort classes to skip tree-building comparisons/synchronizations. - TreeSet sortedFiles = new TreeSet<>(Named.NAME_PATH_COMPARATOR); - sortedFiles.addAll(bundle.values()); - - // Add each class in sorted order. - for (FileInfo fileInfo : sortedFiles) { - String directoryName = interceptDirectoryName(fileInfo.getDirectoryName()); - DirectoryPathNode directoryPath = directories.computeIfAbsent(directoryName, bundlePath::child); - FilePathNode filePath = directoryPath.child(fileInfo); - WorkspaceTreeNode.getOrInsertIntoTree(root, filePath, true); - } - }); + resource.classBundleStream().forEach(bundle -> insertClasses(resourcePath, bundle)); + resource.fileBundleStream().forEach(bundle -> insertFiles(resourcePath, bundle)); + + // Create sub-trees for embedded resources + Map embeddedResources = resource.getEmbeddedResources(); + if (!embeddedResources.isEmpty()) { + EmbeddedResourceContainerPathNode containerPath = resourcePath.embeddedChildContainer(); + embeddedResources.entrySet().stream() // Insert in sorted order of path name + .sorted((o1, o2) -> CaseInsensitiveSimpleNaturalComparator.getInstance().compare(o1.getKey(), o2.getKey())) + .map(Map.Entry::getValue) + .forEach(embeddedResource -> { + ResourcePathNode resourcePathEmbedded = containerPath.child(embeddedResource); + embeddedResource.classBundleStream().forEach(bundle -> insertClasses(resourcePathEmbedded, bundle)); + embeddedResource.fileBundleStream().forEach(bundle -> insertFiles(resourcePathEmbedded, bundle)); + }); + } + } + + /** + * @param containingResourcePath + * Path to resource holding classes. + * @param bundle + * Bundle of classes to insert. + */ + private void insertClasses(@Nonnull ResourcePathNode containingResourcePath, + @Nonnull ClassBundle bundle) { + Map directories = new HashMap<>(); + BundlePathNode bundlePath = containingResourcePath.child(bundle); + + // Pre-sort classes to skip tree-building comparisons/synchronizations. + TreeSet sortedClasses = new TreeSet<>(Named.NAME_PATH_COMPARATOR); + sortedClasses.addAll(bundle.values()); + + // Add each class in sorted order. + for (ClassInfo classInfo : sortedClasses) { + String packageName = interceptDirectoryName(classInfo.getPackageName()); + DirectoryPathNode packagePath = directories.computeIfAbsent(packageName, bundlePath::child); + ClassPathNode classPath = packagePath.child(classInfo); + WorkspaceTreeNode.getOrInsertIntoTree(root, classPath, true); + } + } + + /** + * @param containingResourcePath + * Path to resource holding files. + * @param bundle + * Bundle of files to insert. + */ + private void insertFiles(@Nonnull ResourcePathNode containingResourcePath, + @Nonnull FileBundle bundle) { + Map directories = new HashMap<>(); + BundlePathNode bundlePath = containingResourcePath.child(bundle); + + // Pre-sort classes to skip tree-building comparisons/synchronizations. + TreeSet sortedFiles = new TreeSet<>(Named.NAME_PATH_COMPARATOR); + sortedFiles.addAll(bundle.values()); + + // Add each file in sorted order. + for (FileInfo fileInfo : sortedFiles) { + String directoryName = interceptDirectoryName(fileInfo.getDirectoryName()); + DirectoryPathNode directoryPath = directories.computeIfAbsent(directoryName, bundlePath::child); + FilePathNode filePath = directoryPath.child(fileInfo); + WorkspaceTreeNode.getOrInsertIntoTree(root, filePath, true); + } } /** @@ -140,7 +171,7 @@ private void createResourceSubTree(WorkspaceResource resource) { * * @return {@code true} when it matches our current {@link #workspace}. */ - private boolean isTargetWorkspace(Workspace workspace) { + private boolean isTargetWorkspace(@Nonnull Workspace workspace) { return this.workspace == workspace; } @@ -150,7 +181,7 @@ private boolean isTargetWorkspace(Workspace workspace) { * * @return {@code true} when it belongs to the target workspace. */ - private boolean isTargetResource(WorkspaceResource resource) { + private boolean isTargetResource(@Nonnull WorkspaceResource resource) { if (workspace.getPrimaryResource() == resource) return true; for (WorkspaceResource supportingResource : workspace.getSupportingResources()) { @@ -185,96 +216,181 @@ public void onRemoveLibrary(@Nonnull Workspace workspace, @Nonnull WorkspaceReso @Override public void onNewClass(@Nonnull WorkspaceResource resource, @Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo cls) { - if (isTargetResource(resource)) - root.getOrCreateNodeByPath(rootPath.child(resource) - .child(bundle) - .child(interceptDirectoryName(cls.getPackageName())) - .child(cls)); + newClass(resource, bundle, cls); } @Override public void onUpdateClass(@Nonnull WorkspaceResource resource, @Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo oldCls, @Nonnull JvmClassInfo newCls) { - if (isTargetResource(resource)) { - WorkspaceTreeNode node = root.getOrCreateNodeByPath(rootPath - .child(resource) - .child(bundle) - .child(interceptDirectoryName(oldCls.getPackageName())) - .child(oldCls)); - node.setValue(rootPath.child(resource).child(bundle).child(newCls.getPackageName()).child(newCls)); - } + updateClass(resource, bundle, oldCls, newCls); } @Override public void onRemoveClass(@Nonnull WorkspaceResource resource, @Nonnull JvmClassBundle bundle, @Nonnull JvmClassInfo cls) { - if (isTargetResource(resource)) - root.removeNodeByPath(rootPath.child(resource) - .child(bundle) - .child(interceptDirectoryName(cls.getPackageName())) - .child(cls)); + removeClass(resource, bundle, cls); } @Override public void onNewClass(@Nonnull WorkspaceResource resource, @Nonnull AndroidClassBundle bundle, @Nonnull AndroidClassInfo cls) { - if (isTargetResource(resource)) - root.getOrCreateNodeByPath(rootPath.child(resource) - .child(bundle) - .child(interceptDirectoryName(cls.getPackageName())) - .child(cls)); + newClass(resource, bundle, cls); } @Override public void onUpdateClass(@Nonnull WorkspaceResource resource, @Nonnull AndroidClassBundle bundle, @Nonnull AndroidClassInfo oldCls, @Nonnull AndroidClassInfo newCls) { - if (isTargetResource(resource)) { - WorkspaceTreeNode node = root.getOrCreateNodeByPath(rootPath.child(resource) - .child(bundle) - .child(interceptDirectoryName(oldCls.getPackageName())) - .child(oldCls)); - node.setValue(rootPath.child(resource) - .child(bundle) - .child(interceptDirectoryName(newCls.getPackageName())) - .child(newCls)); - } + updateClass(resource, bundle, oldCls, newCls); } @Override public void onRemoveClass(@Nonnull WorkspaceResource resource, @Nonnull AndroidClassBundle bundle, @Nonnull AndroidClassInfo cls) { - if (isTargetResource(resource)) - root.removeNodeByPath(rootPath.child(resource) - .child(bundle) - .child(interceptDirectoryName(cls.getPackageName())) - .child(cls)); + removeClass(resource, bundle, cls); } @Override public void onNewFile(@Nonnull WorkspaceResource resource, @Nonnull FileBundle bundle, @Nonnull FileInfo file) { if (isTargetResource(resource)) - root.getOrCreateNodeByPath(rootPath.child(resource) + root.getOrCreateNodeByPath(rootPath + .child(resource) .child(bundle) .child(interceptDirectoryName(file.getDirectoryName())) .child(file)); + else { + WorkspaceResource containingResource = resource.getContainingResource(); + if (containingResource != null && isTargetResource(containingResource)) { + root.getOrCreateNodeByPath(rootPath + .child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(file.getDirectoryName())) + .child(file)); + } + } } @Override public void onUpdateFile(@Nonnull WorkspaceResource resource, @Nonnull FileBundle bundle, @Nonnull FileInfo oldFile, @Nonnull FileInfo newFile) { if (isTargetResource(resource)) { - WorkspaceTreeNode node = root.getOrCreateNodeByPath(rootPath.child(resource) + WorkspaceTreeNode node = root.getOrCreateNodeByPath(rootPath + .child(resource) .child(bundle) .child(interceptDirectoryName(oldFile.getDirectoryName())) .child(oldFile)); - node.setValue(rootPath.child(resource) + node.setValue(rootPath + .child(resource) .child(bundle) - .child(interceptDirectoryName(newFile.getDirectoryName())) + .child(newFile.getDirectoryName()) .child(newFile)); + } else { + WorkspaceResource containingResource = resource.getContainingResource(); + if (containingResource != null && isTargetResource(containingResource)) { + WorkspaceTreeNode node = root.getOrCreateNodeByPath(rootPath.child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(oldFile.getDirectoryName())) + .child(oldFile)); + node.setValue(rootPath.child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(newFile.getDirectoryName())) + .child(newFile)); + } } } @Override public void onRemoveFile(@Nonnull WorkspaceResource resource, @Nonnull FileBundle bundle, @Nonnull FileInfo file) { if (isTargetResource(resource)) - root.removeNodeByPath(rootPath.child(resource) + root.removeNodeByPath(rootPath + .child(resource) .child(bundle) .child(interceptDirectoryName(file.getDirectoryName())) .child(file)); + else { + WorkspaceResource containingResource = resource.getContainingResource(); + if (containingResource != null && isTargetResource(containingResource)) { + root.removeNodeByPath(rootPath + .child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(file.getDirectoryName())) + .child(file)); + } + } + } + + private void newClass(@Nonnull WorkspaceResource resource, @Nonnull ClassBundle bundle, @Nonnull ClassInfo cls) { + if (isTargetResource(resource)) + root.getOrCreateNodeByPath(rootPath + .child(resource) + .child(bundle) + .child(interceptDirectoryName(cls.getPackageName())) + .child(cls)); + else { + WorkspaceResource containingResource = resource.getContainingResource(); + if (containingResource != null && isTargetResource(containingResource)) { + root.getOrCreateNodeByPath(rootPath + .child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(cls.getPackageName())) + .child(cls)); + } + } + } + + private void updateClass(@Nonnull WorkspaceResource resource, @Nonnull ClassBundle bundle, @Nonnull ClassInfo oldCls, @Nonnull ClassInfo newCls) { + if (isTargetResource(resource)) { + WorkspaceTreeNode node = root.getOrCreateNodeByPath(rootPath + .child(resource) + .child(bundle) + .child(interceptDirectoryName(oldCls.getPackageName())) + .child(oldCls)); + node.setValue(rootPath + .child(resource) + .child(bundle) + .child(newCls.getPackageName()) + .child(newCls)); + } else { + WorkspaceResource containingResource = resource.getContainingResource(); + if (containingResource != null && isTargetResource(containingResource)) { + WorkspaceTreeNode node = root.getOrCreateNodeByPath(rootPath.child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(oldCls.getPackageName())) + .child(oldCls)); + node.setValue(rootPath.child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(newCls.getPackageName())) + .child(newCls)); + } + } + } + + private void removeClass(@Nonnull WorkspaceResource resource, @Nonnull ClassBundle bundle, @Nonnull ClassInfo cls) { + if (isTargetResource(resource)) + root.removeNodeByPath(rootPath + .child(resource) + .child(bundle) + .child(interceptDirectoryName(cls.getPackageName())) + .child(cls)); + else { + WorkspaceResource containingResource = resource.getContainingResource(); + if (containingResource != null && isTargetResource(containingResource)) { + root.removeNodeByPath(rootPath + .child(containingResource) + .embeddedChildContainer() + .child(resource) + .child(bundle) + .child(interceptDirectoryName(cls.getPackageName())) + .child(cls)); + } + } } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeCell.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeCell.java index dbceb3ebc..6c2ec9966 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeCell.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeCell.java @@ -2,9 +2,9 @@ import jakarta.annotation.Nonnull; import javafx.scene.control.TreeCell; +import software.coley.recaf.path.PathNode; import software.coley.recaf.services.cell.CellConfigurationService; import software.coley.recaf.services.cell.context.ContextSource; -import software.coley.recaf.path.PathNode; import java.util.function.Function; @@ -24,7 +24,7 @@ public class WorkspaceTreeCell extends TreeCell> { * Service to configure cell content. */ public WorkspaceTreeCell(@Nonnull ContextSource source, - @Nonnull CellConfigurationService configurationService) { + @Nonnull CellConfigurationService configurationService) { this(path -> source, configurationService); } @@ -35,7 +35,7 @@ public WorkspaceTreeCell(@Nonnull ContextSource source, * Service to configure cell content. */ public WorkspaceTreeCell(@Nonnull Function, ContextSource> sourceFunc, - @Nonnull CellConfigurationService configurationService) { + @Nonnull CellConfigurationService configurationService) { this.sourceFunc = sourceFunc; this.configurationService = configurationService; } @@ -43,10 +43,11 @@ public WorkspaceTreeCell(@Nonnull Function, ContextSource> sourceFun @Override protected void updateItem(PathNode item, boolean empty) { super.updateItem(item, empty); - if (empty || item == null) { - configurationService.reset(this); - } else { - configurationService.configure(this, item, sourceFunc.apply(item)); - } + + // Always reset the cell between item updates. + configurationService.reset(this); + + // Apply new cell properties if the item is valid. + if (!empty && item != null) configurationService.configure(this, item, sourceFunc.apply(item)); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNode.java b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNode.java index dd45b806d..c8d2bb1a8 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNode.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNode.java @@ -97,6 +97,17 @@ public WorkspaceTreeNode getNodeByPath(@Nonnull PathNode path) { return null; } + /** + * @return First child tree node. {@code null} if no child is found. + */ + @Nullable + @SuppressWarnings("deprecation") + public WorkspaceTreeNode getFirstChild() { + return getChildren().isEmpty() + ? null : getChildren().getFirst() instanceof WorkspaceTreeNode node + ? node : null; + } + /** * @param path * Path to check against. diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java index 80b137f74..43924341d 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingManager.java @@ -3,10 +3,14 @@ import jakarta.annotation.Nonnull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.slf4j.Logger; +import software.coley.recaf.analytics.logging.Logging; +import software.coley.recaf.ui.dnd.DragAndDrop; import software.coley.recaf.ui.docking.listener.TabClosureListener; import software.coley.recaf.ui.docking.listener.TabCreationListener; import software.coley.recaf.ui.docking.listener.TabMoveListener; import software.coley.recaf.ui.docking.listener.TabSelectionListener; +import software.coley.recaf.util.CollectionUtil; import java.util.ArrayList; import java.util.List; @@ -20,13 +24,14 @@ */ @ApplicationScoped public class DockingManager { + private static final Logger logger = Logging.get(DockingManager.class); private final DockingRegionFactory factory = new DockingRegionFactory(this); private final DockingRegion primaryRegion; private final List regions = new CopyOnWriteArrayList<>(); - private final List tabSelectionListeners = new ArrayList<>(); - private final List tabCreationListeners = new ArrayList<>(); - private final List tabClosureListeners = new ArrayList<>(); - private final List tabMoveListeners = new ArrayList<>(); + private final List tabSelectionListeners = new CopyOnWriteArrayList <>(); + private final List tabCreationListeners = new CopyOnWriteArrayList <>(); + private final List tabClosureListeners = new CopyOnWriteArrayList <>(); + private final List tabMoveListeners = new CopyOnWriteArrayList <>(); @Inject public DockingManager() { @@ -127,8 +132,8 @@ boolean onRegionClose(@Nonnull DockingRegion region) { * Tab created. */ void onTabCreate(@Nonnull DockingRegion parent, @Nonnull DockingTab tab) { - for (TabCreationListener listener : tabCreationListeners) - listener.onCreate(parent, tab); + CollectionUtil.safeForEach(tabCreationListeners, listener -> listener.onCreate(parent, tab), + (listener, t) -> logger.error("Exception thrown when opening tab '{}'", tab.getText(), t)); } /** @@ -140,8 +145,8 @@ void onTabCreate(@Nonnull DockingRegion parent, @Nonnull DockingTab tab) { * Tab created. */ void onTabClose(@Nonnull DockingRegion parent, @Nonnull DockingTab tab) { - for (TabClosureListener listener : tabClosureListeners) - listener.onClose(parent, tab); + CollectionUtil.safeForEach(tabClosureListeners, listener -> listener.onClose(parent, tab), + (listener, t) -> logger.error("Exception thrown when closing tab '{}'", tab.getText(), t)); } /** @@ -156,8 +161,8 @@ void onTabClose(@Nonnull DockingRegion parent, @Nonnull DockingTab tab) { * Tab created. */ void onTabMove(@Nonnull DockingRegion oldRegion, @Nonnull DockingRegion newRegion, @Nonnull DockingTab tab) { - for (TabMoveListener listener : tabMoveListeners) - listener.onMove(oldRegion, newRegion, tab); + CollectionUtil.safeForEach(tabMoveListeners, listener -> listener.onMove(oldRegion, newRegion, tab), + (listener, t) -> logger.error("Exception thrown when moving tab '{}'", tab.getText(), t)); } /** @@ -169,8 +174,8 @@ void onTabMove(@Nonnull DockingRegion oldRegion, @Nonnull DockingRegion newRegio * Tab created. */ void onTabSelection(@Nonnull DockingRegion parent, @Nonnull DockingTab tab) { - for (TabSelectionListener listener : tabSelectionListeners) - listener.onSelection(parent, tab); + CollectionUtil.safeForEach(tabSelectionListeners, listener -> listener.onSelection(parent, tab), + (listener, t) -> logger.error("Exception thrown when selecting tab '{}'", tab.getText(), t)); } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java index a489b9fc1..8dab29da7 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/docking/DockingRegion.java @@ -14,9 +14,9 @@ import javafx.scene.control.TabPane; import javafx.scene.shape.*; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.util.Icons; -import software.coley.recaf.util.Unchecked; import java.util.List; import java.util.function.Supplier; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/MappingMenu.java b/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/MappingMenu.java index beb12cf6a..a20426601 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/MappingMenu.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/menubar/MappingMenu.java @@ -24,6 +24,7 @@ import software.coley.recaf.ui.control.FontIconView; import software.coley.recaf.ui.pane.MappingGeneratorPane; import software.coley.recaf.ui.window.RecafScene; +import software.coley.recaf.util.FileChooserBuilder; import software.coley.recaf.util.Lang; import software.coley.recaf.util.threading.ThreadPoolFactory; import software.coley.recaf.services.workspace.WorkspaceManager; @@ -70,9 +71,10 @@ public MappingMenu(@Nonnull WindowManager windowManager, // Use a shared file-chooser for mapping menu actions. // That way there is some continuity when working with mappings. - FileChooser chooser = new FileChooser(); - chooser.setInitialDirectory(recentFiles.getLastWorkspaceOpenDirectory().unboxingMap(File::new)); - chooser.setTitle(Lang.get("dialog.file.open")); + FileChooser chooser = new FileChooserBuilder() + .setInitialDirectory(recentFiles.getLastWorkspaceOpenDirectory()) + .setTitle(Lang.get("dialog.file.open")) + .build(); for (String formatName : formatManager.getMappingFileFormats()) { apply.getItems().add(actionLiteral(formatName, CarbonIcons.LICENSE, () -> { @@ -88,7 +90,7 @@ public MappingMenu(@Nonnull WindowManager windowManager, MappingResults results = mappingApplier.applyToPrimaryResource(parsedMappings); results.apply(); - logger.info("Applied mappings from {}", file.getName()); + logger.info("Applied mappings from {} - Updated {} classes", file.getName(), results.getPostMappingPaths().size()); } catch (Exception ex) { logger.error("Failed to read mappings from {}", file.getName(), ex); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/CommentListPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/CommentListPane.java index 4b15aabb1..51e80c5f1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/CommentListPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/CommentListPane.java @@ -10,7 +10,6 @@ import javafx.scene.Cursor; import javafx.scene.Node; import javafx.scene.control.Label; -import javafx.scene.control.Labeled; import javafx.scene.control.ScrollPane; import javafx.scene.control.TextField; import javafx.scene.layout.*; @@ -25,16 +24,15 @@ import software.coley.recaf.services.comment.*; import software.coley.recaf.services.navigation.Actions; import software.coley.recaf.services.navigation.Navigable; +import software.coley.recaf.services.workspace.WorkspaceManager; import software.coley.recaf.util.FxThreadUtil; import software.coley.recaf.util.Lang; -import software.coley.recaf.services.workspace.WorkspaceManager; import java.util.Collection; import java.util.Collections; import java.util.Map; import java.util.Objects; import java.util.concurrent.ConcurrentHashMap; -import java.util.stream.Collectors; /** * Pane for listing comments made on classes and their members in the current workspace. @@ -54,9 +52,9 @@ public class CommentListPane extends BorderPane implements Navigable, Documentat @Inject public CommentListPane(@Nonnull CommentManager commentManager, - @Nonnull CellConfigurationService cellConfigurationService, - @Nonnull WorkspaceManager workspaceManager, - @Nonnull Actions actions) { + @Nonnull CellConfigurationService cellConfigurationService, + @Nonnull WorkspaceManager workspaceManager, + @Nonnull Actions actions) { this.cellConfigurationService = cellConfigurationService; this.commentManager = commentManager; this.actions = actions; @@ -184,6 +182,7 @@ private class ClassCommentPane extends BorderPane implements CommentUpdateListen private final Map memberComments = new ConcurrentHashMap<>(); private final DelegatingClassComments comments; private final ClassPathNode classPath; + private String fullTextCached; private ClassCommentPane(@Nonnull ClassComments comments) { if (comments instanceof DelegatingClassComments delegatingComments) { @@ -284,8 +283,10 @@ private void addMemberComment(@Nullable String comment, @Nonnull ClassMemberPath @Override public void onClassCommentUpdated(@Nonnull ClassPathNode path, @Nullable String comment) { - if (isApplicableClass(path)) + if (isApplicableClass(path)) { + fullTextCached = null; FxThreadUtil.run(() -> classComment.setText(comment)); + } } @Override @@ -300,6 +301,7 @@ public void onMethodCommentUpdated(@Nonnull ClassMemberPathNode path, @Nullable private void onMemberCommentUpdated(@Nonnull ClassMemberPathNode path, @Nullable String comment) { if (isApplicableClass(path.getParent())) { + fullTextCached = null; ClassMember member = path.getValue(); Label memberComment = memberComments.get(memberKey(member)); if (comment != null && memberComment != null) @@ -343,8 +345,8 @@ private String filterComment(@Nullable String comment) { if (comment == null) return ""; comment = comment.replace('\n', ' '); - if (comment.length() > 300) - comment = comment.substring(0, 300) + "..."; + if (comment.length() > 60) + comment = comment.substring(0, 60) + "..."; return comment; } @@ -353,7 +355,23 @@ private String filterComment(@Nullable String comment) { */ @Nonnull private String buildFullText() { - return classComment.getText() + memberComments.values().stream().map(Labeled::getText).collect(Collectors.joining()); + if (fullTextCached == null) { + StringBuilder sb = new StringBuilder(); + String comment = comments.getClassComment(); + if (comment != null) sb.append(comment); + + ClassInfo classInfo = classPath.getValue(); + for (FieldMember field : classInfo.getFields()) { + comment = comments.getFieldComment(field); + if (comment != null) sb.append(comment); + } + for (MethodMember method : classInfo.getMethods()) { + comment = comments.getMethodComment(method); + if (comment != null) sb.append(comment); + } + fullTextCached = sb.toString(); + } + return fullTextCached; } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java index 3b9b99bcc..84c7b986b 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/MappingGeneratorPane.java @@ -6,6 +6,7 @@ import atlantafx.base.theme.Styles; import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import jakarta.annotation.PreDestroy; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; @@ -85,18 +86,18 @@ public class MappingGeneratorPane extends StackPane { private final InheritanceGraph graph; private final ModalPane modal = new ModalPane(); private final MappingApplier mappingApplier; - private final Node previewGroup; + private final Pane previewGroup; @Inject public MappingGeneratorPane(@Nonnull Workspace workspace, - @Nonnull NameGeneratorProviders nameGeneratorProviders, - @Nonnull StringPredicateProvider stringPredicateProvider, - @Nonnull MappingGenerator mappingGenerator, - @Nonnull ConfigComponentManager componentManager, - @Nonnull InheritanceGraph graph, - @Nonnull AggregateMappingManager aggregateMappingManager, - @Nonnull MappingApplier mappingApplier, - @Nonnull Instance searchBarProvider) { + @Nonnull NameGeneratorProviders nameGeneratorProviders, + @Nonnull StringPredicateProvider stringPredicateProvider, + @Nonnull MappingGenerator mappingGenerator, + @Nonnull ConfigComponentManager componentManager, + @Nonnull InheritanceGraph graph, + @Nonnull AggregateMappingManager aggregateMappingManager, + @Nonnull MappingApplier mappingApplier, + @Nonnull Instance searchBarProvider) { this.workspace = workspace; this.nameGeneratorProviders = nameGeneratorProviders; @@ -126,6 +127,18 @@ public MappingGeneratorPane(@Nonnull Workspace workspace, getChildren().addAll(modal, horizontalWrapper); } + @PreDestroy + private void destroy() { + // We want to clear out a number of things here to assist in proper GC cleanup. + // - Mappings (which can hold a workspace/graph reference) + // - UI components (which can hold large virtualized text models for the mapping text representation) + FxThreadUtil.run(() -> { + mappingsToApply.setValue(null); + previewGroup.getChildren().clear(); + getChildren().clear(); + }); + } + public void addConfiguredFilter(@Nonnull FilterWithConfigNode filterConfig) { filters.getItems().add(filterConfig); } @@ -212,6 +225,9 @@ private void apply() { if (mappings != null) { MappingResults results = mappingApplier.applyToPrimaryResource(mappings); results.apply(); + + // Clear property now that the mappings have been applied + mappingsToApply.set(null); } } @@ -231,11 +247,13 @@ private Node createPreviewDisplay(@Nonnull Instance searchBarProvider Label stats = new Label(); stats.textProperty().bind(Lang.getBinding("mapgen.preview.empty")); mappingsToApply.addListener((ob, old, cur) -> { - stats.textProperty().unbind(); + if (stats.textProperty().isBound()) stats.textProperty().unbind(); if (cur == null) { // Shouldn't happen, but here just in case - stats.textProperty().bind(Lang.getBinding("mapgen.preview.empty")); - FxThreadUtil.run(() -> editor.setText("# Nothing")); + FxThreadUtil.run(() -> { + stats.textProperty().bind(Lang.getBinding("mapgen.preview.empty")); + editor.setText("# Nothing"); + }); } else { // Update stats IntermediateMappings mappings = cur.exportIntermediate(); @@ -293,18 +311,7 @@ public String fromString(String s) { @Nonnull private Node createFilterDisplay(@Nonnull AggregateMappingManager aggregateMappingManager) { // List to house current filters. - filters.setCellFactory(param -> new ListCell<>() { - @Override - protected void updateItem(FilterWithConfigNode item, boolean empty) { - super.updateItem(item, empty); - if (empty || item == null) { - textProperty().unbind(); - setText(null); - } else { - textProperty().bind(item.display().map(display -> (getIndex() + 1) + ". " + display)); - } - } - }); + filters.setCellFactory(param -> new ConfiguredFilterCell()); filters.getItems().add(new ExcludeExistingMapped(aggregateMappingManager.getAggregatedMappings())); ReadOnlyObjectProperty> selectedItem = filters.getSelectionModel().selectedItemProperty(); BooleanBinding hasItemSelection = selectedItem.isNull(); @@ -370,9 +377,9 @@ protected void updateItem(FilterWithConfigNode item, boolean empty) { @Nonnull private ActionMenuItem typeSetAction(@Nonnull ObjectProperty>> nodeSupplier, - @Nonnull StringProperty parentText, - @Nonnull String translationKey, - @Nonnull Supplier> supplier) { + @Nonnull StringProperty parentText, + @Nonnull String translationKey, + @Nonnull Supplier> supplier) { StringBinding nameBinding = Lang.getBinding(translationKey); return new ActionMenuItem(nameBinding, () -> { @@ -440,16 +447,49 @@ public class ExcludeName extends FilterWithConfigNode { @Nonnull @Override public ObservableValue display() { - return Bindings.concat(Lang.getBinding("mapgen.filter.excludename"), ": ", className); + return classPredicateId.isNotNull().flatMap(hasClass -> { + if (hasClass) + return Bindings.concat(Lang.getBinding("mapgen.filter.excludename"), ": ", classPredicateId.flatMap(key -> { + if (StringPredicateProvider.KEY_ANYTHING.equals(key)) + return Lang.getBinding("string.match.anything"); + else if (StringPredicateProvider.KEY_NOTHING.equals(key)) + return Lang.getBinding("string.match.zilch"); + else + return className; + })); + return fieldPredicateId.isNotNull().flatMap(hasField -> { + if (hasField) + return Bindings.concat(Lang.getBinding("mapgen.filter.excludename"), ": ", fieldPredicateId.flatMap(key -> { + if (StringPredicateProvider.KEY_ANYTHING.equals(key)) + return Lang.getBinding("string.match.anything"); + else if (StringPredicateProvider.KEY_NOTHING.equals(key)) + return Lang.getBinding("string.match.zilch"); + else + return fieldName; + })); + return methodPredicateId.isNotNull().flatMap(hasMethod -> { + if (hasMethod) + return Bindings.concat(Lang.getBinding("mapgen.filter.excludename"), ": ", methodPredicateId.flatMap(key -> { + if (StringPredicateProvider.KEY_ANYTHING.equals(key)) + return Lang.getBinding("string.match.anything"); + else if (StringPredicateProvider.KEY_NOTHING.equals(key)) + return Lang.getBinding("string.match.zilch"); + else + return methodName; + })); + return Lang.getBinding("misc.ignored"); + }); + }); + }); } @Nonnull @Override protected Function makeProvider() { return next -> new ExcludeNameFilter(next, - classPredicateId.isNull().get() ? stringPredicateProvider.newBiStringPredicate(classPredicateId.get(), className.get()) : null, - fieldPredicateId.isNull().get() ? stringPredicateProvider.newBiStringPredicate(fieldPredicateId.get(), fieldName.get()) : null, - methodPredicateId.isNull().get() ? stringPredicateProvider.newBiStringPredicate(methodPredicateId.get(), methodName.get()) : null + classPredicateId.isNotNull().get() ? stringPredicateProvider.newBiStringPredicate(classPredicateId.get(), className.get()) : null, + fieldPredicateId.isNotNull().get() ? stringPredicateProvider.newBiStringPredicate(fieldPredicateId.get(), fieldName.get()) : null, + methodPredicateId.isNotNull().get() ? stringPredicateProvider.newBiStringPredicate(methodPredicateId.get(), methodName.get()) : null ); } @@ -458,9 +498,15 @@ protected void fillConfigurator(@Nonnull BiConsumer sink) { BoundTextField txtClass = new BoundTextField(className); BoundTextField txtField = new BoundTextField(fieldName); BoundTextField txtMethod = new BoundTextField(methodName); - txtClass.disableProperty().bind(classPredicateId.isNull()); - txtField.disableProperty().bind(fieldPredicateId.isNull()); - txtMethod.disableProperty().bind(methodPredicateId.isNull()); + txtClass.disableProperty().bind(classPredicateId.isNull() + .or(classPredicateId.isEqualTo(StringPredicateProvider.KEY_ANYTHING)) + .or(classPredicateId.isEqualTo(StringPredicateProvider.KEY_NOTHING))); + txtField.disableProperty().bind(fieldPredicateId.isNull() + .or(fieldPredicateId.isEqualTo(StringPredicateProvider.KEY_ANYTHING)) + .or(fieldPredicateId.isEqualTo(StringPredicateProvider.KEY_NOTHING))); + txtMethod.disableProperty().bind(methodPredicateId.isNull() + .or(methodPredicateId.isEqualTo(StringPredicateProvider.KEY_ANYTHING)) + .or(methodPredicateId.isEqualTo(StringPredicateProvider.KEY_NOTHING))); GridPane grid = new GridPane(); grid.setVgap(5); @@ -479,7 +525,7 @@ protected void fillConfigurator(@Nonnull BiConsumer sink) { * Config node for {@link ExcludeClassesFilter}. */ public class ExcludeClasses extends FilterWithConfigNode { - private final StringProperty classPredicateId = new SimpleStringProperty(); + private final StringProperty classPredicateId = new SimpleStringProperty(StringPredicateProvider.KEY_CONTAINS); private final StringProperty className = new SimpleStringProperty("com/example/Foo"); @Nonnull @@ -492,7 +538,13 @@ public ObservableValue display() { @Override @SuppressWarnings("DataFlowIssue") protected Function makeProvider() { - return next -> new ExcludeClassesFilter(next, stringPredicateProvider.newBiStringPredicate(classPredicateId.get(), className.get())); + return next -> { + String id = classPredicateId.get(); + StringPredicate predicate; + if (id == null) predicate = stringPredicateProvider.newNothingPredicate(); + else predicate = stringPredicateProvider.newBiStringPredicate(id, className.get()); + return new ExcludeClassesFilter(next, predicate); + }; } @Override @@ -585,16 +637,49 @@ public class IncludeName extends FilterWithConfigNode { @Nonnull @Override public ObservableValue display() { - return Bindings.concat(Lang.getBinding("mapgen.filter.includename"), ": ", className); + return classPredicateId.isNotNull().flatMap(hasClass -> { + if (hasClass) + return Bindings.concat(Lang.getBinding("mapgen.filter.includename"), ": ", classPredicateId.flatMap(key -> { + if (StringPredicateProvider.KEY_ANYTHING.equals(key)) + return Lang.getBinding("string.match.anything"); + else if (StringPredicateProvider.KEY_NOTHING.equals(key)) + return Lang.getBinding("string.match.zilch"); + else + return className; + })); + return fieldPredicateId.isNotNull().flatMap(hasField -> { + if (hasField) + return Bindings.concat(Lang.getBinding("mapgen.filter.includename"), ": ", fieldPredicateId.flatMap(key -> { + if (StringPredicateProvider.KEY_ANYTHING.equals(key)) + return Lang.getBinding("string.match.anything"); + else if (StringPredicateProvider.KEY_NOTHING.equals(key)) + return Lang.getBinding("string.match.zilch"); + else + return fieldName; + })); + return methodPredicateId.isNotNull().flatMap(hasMethod -> { + if (hasMethod) + return Bindings.concat(Lang.getBinding("mapgen.filter.includename"), ": ", methodPredicateId.flatMap(key -> { + if (StringPredicateProvider.KEY_ANYTHING.equals(key)) + return Lang.getBinding("string.match.anything"); + else if (StringPredicateProvider.KEY_NOTHING.equals(key)) + return Lang.getBinding("string.match.zilch"); + else + return methodName; + })); + return Lang.getBinding("misc.ignored"); + }); + }); + }); } @Nonnull @Override protected Function makeProvider() { return next -> new IncludeNameFilter(next, - classPredicateId.isNull().get() ? stringPredicateProvider.newBiStringPredicate(classPredicateId.get(), className.get()) : null, - fieldPredicateId.isNull().get() ? stringPredicateProvider.newBiStringPredicate(fieldPredicateId.get(), fieldName.get()) : null, - methodPredicateId.isNull().get() ? stringPredicateProvider.newBiStringPredicate(methodPredicateId.get(), methodName.get()) : null + classPredicateId.isNotNull().get() ? stringPredicateProvider.newBiStringPredicate(classPredicateId.get(), className.get()) : null, + fieldPredicateId.isNotNull().get() ? stringPredicateProvider.newBiStringPredicate(fieldPredicateId.get(), fieldName.get()) : null, + methodPredicateId.isNotNull().get() ? stringPredicateProvider.newBiStringPredicate(methodPredicateId.get(), methodName.get()) : null ); } @@ -603,9 +688,15 @@ protected void fillConfigurator(@Nonnull BiConsumer sink) { BoundTextField txtClass = new BoundTextField(className); BoundTextField txtField = new BoundTextField(fieldName); BoundTextField txtMethod = new BoundTextField(methodName); - txtClass.disableProperty().bind(classPredicateId.isNull()); - txtField.disableProperty().bind(fieldPredicateId.isNull()); - txtMethod.disableProperty().bind(methodPredicateId.isNull()); + txtClass.disableProperty().bind(classPredicateId.isNull() + .or(classPredicateId.isEqualTo(StringPredicateProvider.KEY_ANYTHING)) + .or(classPredicateId.isEqualTo(StringPredicateProvider.KEY_NOTHING))); + txtField.disableProperty().bind(fieldPredicateId.isNull() + .or(fieldPredicateId.isEqualTo(StringPredicateProvider.KEY_ANYTHING)) + .or(fieldPredicateId.isEqualTo(StringPredicateProvider.KEY_NOTHING))); + txtMethod.disableProperty().bind(methodPredicateId.isNull() + .or(methodPredicateId.isEqualTo(StringPredicateProvider.KEY_ANYTHING)) + .or(methodPredicateId.isEqualTo(StringPredicateProvider.KEY_NOTHING))); GridPane grid = new GridPane(); grid.setVgap(5); grid.setHgap(5); @@ -623,7 +714,7 @@ protected void fillConfigurator(@Nonnull BiConsumer sink) { * Config node for {@link IncludeClassesFilter}. */ public class IncludeClasses extends FilterWithConfigNode { - private final StringProperty classPredicateId = new SimpleStringProperty(); + private final StringProperty classPredicateId = new SimpleStringProperty(StringPredicateProvider.KEY_CONTAINS); private final StringProperty className = new SimpleStringProperty("com/example/Foo"); @Nonnull @@ -636,7 +727,13 @@ public ObservableValue display() { @Override @SuppressWarnings("DataFlowIssue") protected Function makeProvider() { - return next -> new IncludeClassesFilter(next, stringPredicateProvider.newBiStringPredicate(classPredicateId.get(), className.get())); + return next -> { + String id = classPredicateId.get(); + StringPredicate predicate; + if (id == null) predicate = stringPredicateProvider.newNothingPredicate(); + else predicate = stringPredicateProvider.newBiStringPredicate(id, className.get()); + return new IncludeClassesFilter(next, predicate); + }; } @Override @@ -781,6 +878,23 @@ protected void fillConfigurator(@Nonnull BiConsumer sink) { } } + /** + * List cell to render {@link FilterWithConfigNode}. + */ + private static class ConfiguredFilterCell extends ListCell> { + @Override + protected void updateItem(FilterWithConfigNode item, boolean empty) { + super.updateItem(item, empty); + StringProperty property = textProperty(); + if (empty || item == null) { + property.unbind(); + setText(null); + } else { + property.bind(item.display().map(display -> (getIndex() + 1) + ". " + display)); + } + } + } + /** * Base to create a filter with configuration node. * diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/PathPromptPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/PathPromptPane.java index 5d29b12e9..a126bcfa9 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/PathPromptPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/PathPromptPane.java @@ -21,6 +21,8 @@ import software.coley.recaf.ui.config.RecentFilesConfig; import software.coley.recaf.ui.control.ActionButton; import software.coley.recaf.ui.control.FontIconView; +import software.coley.recaf.util.DirectoryChooserBuilder; +import software.coley.recaf.util.FileChooserBuilder; import software.coley.recaf.util.Lang; import java.io.File; @@ -61,9 +63,10 @@ public PathPromptPane(@Nonnull RecentFilesConfig recentFilesConfig) { Button openButton = new ActionButton(openText, () -> { File recentOpenDir = recentFilesConfig.getLastWorkspaceOpenDirectory().unboxingMap(File::new); if (isFile.get()) { - FileChooser chooser = new FileChooser(); - chooser.setInitialDirectory(recentOpenDir); - chooser.setTitle(Lang.get("dialog.file.open")); + FileChooser chooser = new FileChooserBuilder() + .setInitialDirectory(recentOpenDir) + .setTitle(Lang.get("dialog.file.open")) + .build(); // Show the prompt, update the path when complete File file = chooser.showOpenDialog(getScene().getWindow()); @@ -73,9 +76,10 @@ public PathPromptPane(@Nonnull RecentFilesConfig recentFilesConfig) { path.set(file.toPath()); } } else { - DirectoryChooser chooser = new DirectoryChooser(); - chooser.setInitialDirectory(recentOpenDir); - chooser.setTitle(Lang.get("dialog.file.open")); + DirectoryChooser chooser = new DirectoryChooserBuilder() + .setInitialDirectory(recentOpenDir) + .setTitle(Lang.get("dialog.file.open")) + .build(); // Show the prompt, update the path when complete File file = chooser.showDialog(getScene().getWindow()); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/RemoteVirtualMachinesPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/RemoteVirtualMachinesPane.java index 1033f29e6..df02b9165 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/RemoteVirtualMachinesPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/RemoteVirtualMachinesPane.java @@ -20,6 +20,7 @@ import org.kordamp.ikonli.Ikon; import org.kordamp.ikonli.carbonicons.CarbonIcons; import org.slf4j.Logger; +import software.coley.collections.func.UncheckedSupplier; import software.coley.observables.ObservableObject; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.services.attach.AttachManager; @@ -31,7 +32,6 @@ import software.coley.recaf.ui.window.RemoteVirtualMachinesWindow; import software.coley.recaf.util.ErrorDialogs; import software.coley.recaf.util.FxThreadUtil; -import software.coley.recaf.util.UncheckedSupplier; import software.coley.recaf.util.threading.ThreadUtil; import software.coley.recaf.services.workspace.WorkspaceCloseListener; import software.coley.recaf.services.workspace.WorkspaceManager; @@ -341,6 +341,10 @@ Tab tab() { // JMX tiles JmxBeanServerConnection jmxConnection = attachManager.getJmxServerConnection(descriptor); + if (jmxConnection == null) { + logger.warn("Failed to get JMX connection for descriptor: {}", descriptor); + return; + } List beanSuppliers = List.of( new JmxWrapper(CarbonIcons.OBJECT_STORAGE, "attach.tab.classloading", jmxConnection::getClassloadingBeanInfo), new JmxWrapper(CarbonIcons.QUERY_QUEUE, "attach.tab.compilation", jmxConnection::getCompilationBeanInfo), diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/ScriptManagerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/ScriptManagerPane.java index fa73224b4..1ee96b7ad 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/ScriptManagerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/ScriptManagerPane.java @@ -248,9 +248,11 @@ private void save() { // Write to disk regardless, if path is given. If not given, prompt user for it. if (scriptPath == null) { - FileChooser chooser = new FileChooser(); - chooser.setInitialDirectory(directories.getScriptsDirectory().toFile()); - chooser.setTitle(Lang.get("dialog.file.save")); + FileChooser chooser = new FileChooserBuilder() + .setInitialDirectory(directories.getScriptsDirectory()) + .setTitle(Lang.get("dialog.file.save")) + .build(); + File selected = chooser.showSaveDialog(getScene().getWindow()); if (selected != null) scriptPath = selected.toPath(); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceExplorerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceExplorerPane.java index 04bd4bbff..a71395afe 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceExplorerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/WorkspaceExplorerPane.java @@ -5,6 +5,7 @@ import jakarta.enterprise.context.Dependent; import jakarta.inject.Inject; import javafx.scene.layout.BorderPane; +import software.coley.recaf.services.cell.context.ContextSource; import software.coley.recaf.ui.control.tree.TreeFiltering; import software.coley.recaf.ui.control.tree.WorkspaceTree; import software.coley.recaf.ui.control.tree.WorkspaceTreeFilterPane; @@ -36,6 +37,9 @@ public WorkspaceExplorerPane(@Nonnull WorkspaceLoadingDropListener listener, @Nullable Workspace workspace) { this.workspaceTree = workspaceTree; + // As we are the explorer pane, these items should be treated as declarations and not references. + workspaceTree.contextSourceObjectPropertyProperty().setValue(ContextSource.DECLARATION); + // Add filter pane, and hook up key-events so the user can easily // navigate between the tree and the filter. WorkspaceTreeFilterPane workspaceTreeFilterPane = new WorkspaceTreeFilterPane(workspaceTree); diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractContentPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractContentPane.java index 19e9a2a7f..5dba502be 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractContentPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractContentPane.java @@ -15,6 +15,7 @@ import java.util.ArrayList; import java.util.Collection; import java.util.List; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.function.Consumer; /** @@ -27,7 +28,7 @@ * @see ClassPane For {@link ClassInfo} */ public abstract class AbstractContentPane

> extends BorderPane implements UpdatableNavigable { - protected final List> pathUpdateListeners = new ArrayList<>(); + protected final List> pathUpdateListeners = new CopyOnWriteArrayList<>(); protected final List children = new ArrayList<>(); protected SideTabs sideTabs; protected P path; @@ -95,8 +96,11 @@ protected void refreshDisplay() { generateDisplay(); // Refresh UI with path - if (getCenter() instanceof UpdatableNavigable updatable) - updatable.onUpdatePath(getPath()); + if (getCenter() instanceof UpdatableNavigable updatable) { + PathNode currentPath = getPath(); + if (currentPath != null) + updatable.onUpdatePath(currentPath); + } } /** @@ -109,7 +113,7 @@ protected void refreshDisplay() { * @param tab * Tab to add to the side panel. */ - protected void addSideTab(Tab tab) { + public void addSideTab(@Nonnull Tab tab) { // Lazily create/add side-tabs to UI. if (sideTabs == null) { sideTabs = new SideTabs(Orientation.VERTICAL); @@ -125,10 +129,18 @@ protected void addSideTab(Tab tab) { * @param listener * Listener to add. */ - public void addPathUpdateListener(Consumer

listener) { + public void addPathUpdateListener(@Nonnull Consumer

listener) { pathUpdateListeners.add(listener); } + /** + * @param listener + * Listener to remove. + */ + public void removePathUpdateListener(@Nonnull Consumer

listener) { + pathUpdateListeners.remove(listener); + } + @Nonnull @Override public Collection getNavigableChildren() { diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractDecompilePane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractDecompilePane.java index 92ef61dcd..eef9b6da8 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractDecompilePane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/AbstractDecompilePane.java @@ -370,7 +370,7 @@ private DecompileProgressOverlay() { }, FxThreadUtil.executor()); } - private class BytecodeTransition extends Transition { + private static class BytecodeTransition extends Transition { private final Labeled labeled; private byte[] bytecode; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ClassPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ClassPane.java index 820160d98..88c91eb3a 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ClassPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/ClassPane.java @@ -2,6 +2,8 @@ import jakarta.annotation.Nonnull; import org.kordamp.ikonli.carbonicons.CarbonIcons; +import software.coley.recaf.analytics.logging.DebuggingLogger; +import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.JvmClassInfo; @@ -16,6 +18,7 @@ import software.coley.recaf.ui.pane.editing.jvm.JvmClassPane; import software.coley.recaf.ui.pane.editing.tabs.FieldsAndMethodsPane; import software.coley.recaf.ui.pane.editing.tabs.InheritancePane; +import software.coley.recaf.util.CollectionUtil; import software.coley.recaf.util.Icons; import software.coley.recaf.util.Lang; @@ -27,29 +30,7 @@ * @see AndroidClassPane For {@link AndroidClassInfo}. */ public abstract class ClassPane extends AbstractContentPane implements ClassNavigable { - /** - * Configures common side-tab content of child types. - * - * @param fieldsAndMethodsPane - * Tab content to show fields/methods of a class. - * @param inheritancePane - * Tab content to show the inheritance hierarchy of a class. - */ - protected void configureCommonSideTabs(@Nonnull FieldsAndMethodsPane fieldsAndMethodsPane, - @Nonnull InheritancePane inheritancePane) { - // Setup so clicking on items in fields-and-methods pane will synchronize with content in our class pane. - fieldsAndMethodsPane.setupSelectionNavigationListener(this); - - // Setup side-tabs - addSideTab(new BoundTab(Lang.getBinding("fieldsandmethods.title"), - Icons.getIconView(Icons.FIELD_N_METHOD), - fieldsAndMethodsPane - )); - addSideTab(new BoundTab(Lang.getBinding("hierarchy.title"), - CarbonIcons.FLOW, - inheritancePane - )); - } + private static final DebuggingLogger logger = Logging.get(ClassPane.class); @Override public void requestFocus(@Nonnull ClassMember member) { @@ -71,7 +52,8 @@ public void onUpdatePath(@Nonnull PathNode path) { // Update if class has changed. if (path instanceof ClassPathNode classPath) { this.path = classPath; - pathUpdateListeners.forEach(listener -> listener.accept(classPath)); + CollectionUtil.safeForEach(pathUpdateListeners, listener -> listener.accept(classPath), + (listener, t) -> logger.error("Exception thrown when handling class-pane path update callback", t)); // Initialize UI if it has not been done yet. if (getCenter() == null) diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/SideTabsInjector.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/SideTabsInjector.java new file mode 100644 index 000000000..ace81b8cb --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/SideTabsInjector.java @@ -0,0 +1,119 @@ +package software.coley.recaf.ui.pane.editing; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.Dependent; +import jakarta.enterprise.inject.Instance; +import jakarta.inject.Inject; +import org.kordamp.ikonli.carbonicons.CarbonIcons; +import org.slf4j.Logger; +import software.coley.collections.Unchecked; +import software.coley.recaf.analytics.logging.Logging; +import software.coley.recaf.info.ClassInfo; +import software.coley.recaf.path.ClassPathNode; +import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.navigation.ClassNavigable; +import software.coley.recaf.ui.control.BoundTab; +import software.coley.recaf.ui.pane.editing.tabs.FieldsAndMethodsPane; +import software.coley.recaf.ui.pane.editing.tabs.InheritancePane; +import software.coley.recaf.util.Icons; +import software.coley.recaf.util.Lang; + +import java.util.function.Consumer; + +/** + * Injector for adding common {@link SideTabs} content to {@link AbstractContentPane} instances. + * + * @author Matt Coley + */ +@Dependent +public class SideTabsInjector { + private static final Logger logger = Logging.get(SideTabsInjector.class); + private final Instance fieldsAndMethodsPaneProvider; + private final Instance inheritancePaneProvider; + + @Inject + public SideTabsInjector(@Nonnull Instance fieldsAndMethodsPaneProvider, + @Nonnull Instance inheritancePaneProvider) { + this.fieldsAndMethodsPaneProvider = fieldsAndMethodsPaneProvider; + this.inheritancePaneProvider = inheritancePaneProvider; + } + + /** + * Registers a path update listener within the pane. Once a {@link AbstractContentPane#getPath() path} is + * assigned to the pane the side-tabs appropriate for the given content will be added. + *

+ * NOTE: Because path update listeners are handled before paths are passed off to + * {@link AbstractContentPane#getNavigableChildren()} we can add the side-tabs in the listener action + * and still have them be notified of the path update handled in {@link AbstractContentPane#onUpdatePath(PathNode)}. + * + * @param pane + * Pane to inject into. + */ + public void injectLater(@Nonnull AbstractContentPane pane) { + pane.addPathUpdateListener(Unchecked.cast(new TabAdder(pane))); + } + + /** + * Adds the appropriate side-tabs for the pane's current assigned {@link AbstractContentPane#getPath() path}. + * + * @param pane + * Pane to inject into. + */ + public void injectNow(@Nonnull AbstractContentPane pane) { + PathNode path = pane.getPath(); + if (path != null) injectInto(pane, path); + else logger.warn("Attempted to inject side-tabs into content pane with no path registered yet"); + } + + private void injectInto(@Nonnull AbstractContentPane pane, @Nonnull PathNode path) { + if (path instanceof ClassPathNode classPath) + injectClassTabs(pane); + else + logger.warn("Attempted to inject side-tabs into content pane with unsupported path type: {}", path.getClass().getSimpleName()); + } + + /** + * Adds class-specific content to the given pane. + * + * @param pane + * Pane to inject tabs into for {@link ClassInfo} content. + */ + private void injectClassTabs(@Nonnull AbstractContentPane pane) { + if (pane instanceof ClassNavigable classNavigable) { + FieldsAndMethodsPane fieldsAndMethodsPane = fieldsAndMethodsPaneProvider.get(); + InheritancePane inheritancePane = inheritancePaneProvider.get(); + + // Setup so clicking on items in fields-and-methods pane will synchronize with content in our class pane. + fieldsAndMethodsPane.setupSelectionNavigationListener(classNavigable); + + // Setup side-tabs + pane.addSideTab(new BoundTab(Lang.getBinding("fieldsandmethods.title"), + Icons.getIconView(Icons.FIELD_N_METHOD), + fieldsAndMethodsPane + )); + pane.addSideTab(new BoundTab(Lang.getBinding("hierarchy.title"), + CarbonIcons.FLOW, + inheritancePane + )); + } else { + logger.warn("Called 'injectClassTabs' for non-class navigable content"); + } + } + + /** + * Listener that waits for a non-null input. Once found, the listener removes itself. + */ + private class TabAdder implements Consumer> { + private final AbstractContentPane pane; + + private TabAdder(@Nonnull AbstractContentPane pane) {this.pane = pane;} + + @Override + public void accept(PathNode path) { + if (path != null) { + injectNow(pane); + pane.removePathUpdateListener(Unchecked.cast(this)); + } + } + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/android/AndroidClassPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/android/AndroidClassPane.java index 57954dfa3..2f4313f18 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/android/AndroidClassPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/android/AndroidClassPane.java @@ -5,16 +5,10 @@ import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import javafx.scene.control.Label; -import org.kordamp.ikonli.carbonicons.CarbonIcons; import software.coley.recaf.info.AndroidClassInfo; import software.coley.recaf.ui.config.ClassEditingConfig; -import software.coley.recaf.ui.control.BoundTab; -import software.coley.recaf.ui.control.IconView; import software.coley.recaf.ui.pane.editing.ClassPane; -import software.coley.recaf.ui.pane.editing.tabs.FieldsAndMethodsPane; -import software.coley.recaf.ui.pane.editing.tabs.InheritancePane; -import software.coley.recaf.util.Icons; -import software.coley.recaf.util.Lang; +import software.coley.recaf.ui.pane.editing.SideTabsInjector; /** * Displays {@link AndroidClassInfo} in a configurable manner. @@ -28,12 +22,11 @@ public class AndroidClassPane extends ClassPane { @Inject public AndroidClassPane(@Nonnull ClassEditingConfig config, - @Nonnull FieldsAndMethodsPane fieldsAndMethodsPane, - @Nonnull InheritancePane inheritancePane, + @Nonnull SideTabsInjector sideTabsInjector, @Nonnull Instance decompilerProvider) { + sideTabsInjector.injectLater(this); editorType = config.getDefaultAndroidEditor().getValue(); this.decompilerProvider = decompilerProvider; - configureCommonSideTabs(fieldsAndMethodsPane, inheritancePane); } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java index 4c2c46cc1..3c95f42c1 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerPane.java @@ -4,6 +4,7 @@ import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import javafx.scene.Node; import me.darknet.assembler.ast.ASTElement; import me.darknet.assembler.ast.specific.ASTClass; import me.darknet.assembler.ast.specific.ASTField; @@ -17,6 +18,7 @@ import me.darknet.assembler.util.Location; import org.fxmisc.richtext.CodeArea; import org.slf4j.Logger; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.member.ClassMember; @@ -39,6 +41,8 @@ import software.coley.recaf.ui.control.richtext.syntax.RegexLanguages; import software.coley.recaf.ui.control.richtext.syntax.RegexSyntaxHighlighter; import software.coley.recaf.ui.pane.editing.AbstractContentPane; +import software.coley.recaf.ui.pane.editing.SideTabs; +import software.coley.recaf.ui.pane.editing.SideTabsInjector; import software.coley.recaf.ui.pane.editing.tabs.FieldsAndMethodsPane; import software.coley.recaf.util.*; import software.coley.recaf.workspace.model.bundle.Bundle; @@ -62,10 +66,10 @@ public class AssemblerPane extends AbstractContentPane> implements U private final AssemblerPipelineManager pipelineManager; private final AssemblerToolTabs assemblerToolTabs; + private final SideTabsInjector sideTabsInjector; private final ProblemTracking problemTracking = new ProblemTracking(); private final Editor editor = new Editor(); private final AtomicBoolean updateLock = new AtomicBoolean(); - private final Instance fieldsAndMethodsPaneProvider; private AssemblerPipeline pipeline; private ClassResult lastResult; private ClassRepresentation lastAssembledClassRepresentation; @@ -81,10 +85,10 @@ public AssemblerPane(@Nonnull AssemblerPipelineManager pipelineManager, @Nonnull AssemblerContextActionSupport contextActionSupport, @Nonnull SearchBar searchBar, @Nonnull KeybindingConfig keys, - @Nonnull Instance fieldsAndMethodsPaneProvider) { + @Nonnull SideTabsInjector sideTabsInjector) { this.pipelineManager = pipelineManager; this.assemblerToolTabs = assemblerToolTabs; - this.fieldsAndMethodsPaneProvider = fieldsAndMethodsPaneProvider; + this.sideTabsInjector = sideTabsInjector; int timeToWait = pipelineManager.getServiceConfig().getDisassemblyAstParseDelay().getValue(); @@ -133,15 +137,6 @@ private void lateInit() { * The given path. */ private void lateInitForClass(@Nonnull ClassPathNode classPathNode) { - // Show declared fields/methods - FieldsAndMethodsPane fieldsAndMethodsPane = fieldsAndMethodsPaneProvider.get(); - fieldsAndMethodsPane.setupSelectionNavigationListener(this); - addSideTab(new BoundTab(Lang.getBinding("fieldsandmethods.title"), - Icons.getIconView(Icons.FIELD_N_METHOD), - fieldsAndMethodsPane - )); - fieldsAndMethodsPane.onUpdatePath(classPathNode); - // Since the content displayed is for a whole class, and the tool tabs are scoped to a method, we need to // update them when a method is selected. We do so by tracking the caret position for being within the // range of one of the methods in the last AST model. @@ -184,6 +179,13 @@ private void lateInitForClass(@Nonnull ClassPathNode classPathNode) { lateInit(); } + @Override + public void requestFocus() { + // The editor is not the first thing focused when added to the scene, so when it is added to the scene + // we'll want to manually focus it so that you can immediately use keybinds and navigate around. + SceneUtils.whenAddedToSceneConsume(editor.getCodeArea(), Node::requestFocus); + } + /** * Called by {@link #onUpdatePath(PathNode)} once before the {@link #path} is set for the first time. * @@ -264,7 +266,8 @@ else if (path instanceof ClassMemberPathNode memberPathNode) // Update the path and call any path listeners. this.path = path; - pathUpdateListeners.forEach(listener -> listener.accept(path)); + CollectionUtil.safeForEach(pathUpdateListeners, listener -> listener.accept(path), + (listener, t) -> logger.error("Exception thrown when handling assembler-pane path update callback", t)); // Update UI state. if (!updateLock.get()) { diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerToolTabs.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerToolTabs.java index 33c511762..67c50b785 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerToolTabs.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AssemblerToolTabs.java @@ -8,7 +8,6 @@ import javafx.geometry.Orientation; import javafx.scene.control.Tab; import me.darknet.assembler.ast.ASTElement; -import me.darknet.assembler.compiler.ClassRepresentation; import me.darknet.assembler.compiler.ClassResult; import org.kordamp.ikonli.carbonicons.CarbonIcons; import software.coley.recaf.info.ClassInfo; @@ -39,6 +38,7 @@ public class AssemblerToolTabs implements AssemblerAstConsumer, AssemblerBuildCo private final Instance jvmStackAnalysisPaneProvider; private final Instance jvmVariablesPaneProvider; private final Instance jvmExpressionCompilerPaneProvider; + private final Instance controlFlowLineProvider; private final List children = new CopyOnWriteArrayList<>(); private final SideTabs tabs = new SideTabs(Orientation.HORIZONTAL); private PathNode path; @@ -46,10 +46,12 @@ public class AssemblerToolTabs implements AssemblerAstConsumer, AssemblerBuildCo @Inject public AssemblerToolTabs(@Nonnull Instance jvmStackAnalysisPaneProvider, @Nonnull Instance jvmVariablesPaneProvider, - @Nonnull Instance jvmExpressionCompilerPaneProvider) { + @Nonnull Instance jvmExpressionCompilerPaneProvider, + @Nonnull Instance controlFlowLineProvider) { this.jvmStackAnalysisPaneProvider = jvmStackAnalysisPaneProvider; this.jvmVariablesPaneProvider = jvmVariablesPaneProvider; this.jvmExpressionCompilerPaneProvider = jvmExpressionCompilerPaneProvider; + this.controlFlowLineProvider = controlFlowLineProvider; // Without an initial size, the first frame of a method has nothing in it. So the auto-size to fit content // has nothing to fit to, which leads to only table headers being visible. Looks really dumb so giving it @@ -73,13 +75,15 @@ private void createChildren(@Nonnull ClassInfo classInPath) { JvmStackAnalysisPane stackAnalysisPane = jvmStackAnalysisPaneProvider.get(); JvmVariablesPane variablesPane = jvmVariablesPaneProvider.get(); JvmExpressionCompilerPane expressionPane = jvmExpressionCompilerPaneProvider.get(); - children.addAll(Arrays.asList(stackAnalysisPane, variablesPane, expressionPane)); + ControlFlowLines controlFlowLines = controlFlowLineProvider.get(); + children.addAll(Arrays.asList(stackAnalysisPane, variablesPane, expressionPane, controlFlowLines)); FxThreadUtil.run(() -> { ObservableList tabs = this.tabs.getTabs(); tabs.clear(); tabs.add(new BoundTab(Lang.getBinding("assembler.analysis.title"), CarbonIcons.VIEW_NEXT, stackAnalysisPane)); tabs.add(new BoundTab(Lang.getBinding("assembler.variables.title"), CarbonIcons.LIST_BOXES, variablesPane)); tabs.add(new BoundTab(Lang.getBinding("assembler.playground.title"), CarbonIcons.CODE, expressionPane)); + // Note: There is intentionally no tab for the jump arrow pane at the moment tabs.forEach(t -> t.setClosable(false)); }); } else if (classInPath.isAndroidClass()) { diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AstUsages.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AstUsages.java new file mode 100644 index 000000000..7f747bf79 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/AstUsages.java @@ -0,0 +1,58 @@ +package software.coley.recaf.ui.pane.editing.assembler; + +import jakarta.annotation.Nonnull; +import me.darknet.assembler.ast.ASTElement; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; +import java.util.stream.Stream; + +/** + * Models AST usage for things that can be read/written to (literally or in an abstract sense). + * + * @param readers + * Elements that read from the target item. + * @param writers + * Elements that write to the target item. + */ +public record AstUsages(@Nonnull List readers, @Nonnull List writers) { + /** + * Empty usage. + */ + public static final AstUsages EMPTY_USAGE = new AstUsages(Collections.emptyList(), Collections.emptyList()); + + /** + * @return Stream of both readers and writers. + */ + @Nonnull + public Stream readersAndWriters() { + return Stream.concat(readers.stream(), writers.stream()); + } + + /** + * @param element + * Element to add as a reader. + * + * @return Copy with added element. + */ + @Nonnull + public AstUsages withNewRead(@Nonnull ASTElement element) { + List newReaders = new ArrayList<>(readers); + newReaders.add(element); + return new AstUsages(newReaders, writers); + } + + /** + * @param element + * Element to add as a writer. + * + * @return Copy with added element. + */ + @Nonnull + public AstUsages withNewWrite(@Nonnull ASTElement element) { + List newWriters = new ArrayList<>(writers); + newWriters.add(element); + return new AstUsages(readers, newWriters); + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/ControlFlowLines.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/ControlFlowLines.java new file mode 100644 index 000000000..f8744f785 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/ControlFlowLines.java @@ -0,0 +1,555 @@ +package software.coley.recaf.ui.pane.editing.assembler; + +import atlantafx.base.controls.Spacer; +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.Dependent; +import jakarta.inject.Inject; +import javafx.animation.Interpolator; +import javafx.animation.Transition; +import javafx.beans.binding.Bindings; +import javafx.beans.value.ObservableValue; +import javafx.geometry.Pos; +import javafx.scene.canvas.Canvas; +import javafx.scene.canvas.GraphicsContext; +import javafx.scene.effect.*; +import javafx.scene.layout.StackPane; +import javafx.scene.paint.Color; +import javafx.util.Duration; +import me.darknet.assembler.ast.ASTElement; +import me.darknet.assembler.ast.primitive.ASTArray; +import me.darknet.assembler.ast.primitive.ASTInstruction; +import me.darknet.assembler.ast.primitive.ASTLabel; +import me.darknet.assembler.ast.primitive.ASTObject; +import me.darknet.assembler.ast.specific.ASTClass; +import me.darknet.assembler.ast.specific.ASTMethod; +import me.darknet.assembler.util.Location; +import org.reactfx.Change; +import software.coley.collections.box.Box; +import software.coley.collections.box.IntBox; +import software.coley.observables.ObservableBoolean; +import software.coley.observables.ObservableObject; +import software.coley.recaf.ui.control.VirtualizedScrollPaneWrapper; +import software.coley.recaf.ui.control.richtext.Editor; +import software.coley.recaf.ui.control.richtext.linegraphics.AbstractLineGraphicFactory; +import software.coley.recaf.ui.control.richtext.linegraphics.LineContainer; +import software.coley.recaf.util.FxThreadUtil; +import software.coley.recaf.util.SceneUtils; + +import java.util.*; +import java.util.function.BiConsumer; +import java.util.function.Consumer; + +/** + * Controller for displaying control flow jump lines. + * + * @author Matt Coley + */ +@Dependent +public class ControlFlowLines extends AstBuildConsumerComponent { + private static final Set INSN_SET = Set.of("goto", "ifnull", "ifnonnull", "ifeq", "ifne", "ifle", "ifge", "iflt", "ifgt", + "if_acmpeq", "if_acmpne", "if_icmpeq", "if_icmpge", "if_icmpgt", "if_icmple", "if_icmplt", "if_icmpne"); + private static final Set SWITCH_INSNS = Set.of("tableswitch", "lookupswitch"); + private final Consumer> onCaretMove = this::onCaretMove; + private final ObservableObject currentInstructionSelection = new ObservableObject<>(null); + private final ObservableBoolean drawLines = new ObservableBoolean(false); + private final ControlFlowLineFactory arrowFactory = new ControlFlowLineFactory(); + private final ControlFlowLinesConfig config; + private List model = Collections.emptyList(); + + + @Inject + public ControlFlowLines(@Nonnull ControlFlowLinesConfig config) { + this.config = config; + + Runnable redraw = () -> {if (editor != null) editor.redrawParagraphGraphics();}; + drawLines.addChangeListener((ob, old, cur) -> redraw.run()); + currentInstructionSelection.addChangeListener((ob, old, cur) -> redraw.run()); + config.getConnectionMode().addChangeListener((ob, old, cur) -> redraw.run()); + config.getRenderMode().addChangeListener((ob, old, cur) -> redraw.run()); + } + + @Override + public void install(@Nonnull Editor editor) { + super.install(editor); + + editor.getRootLineGraphicFactory().addLineGraphicFactory(arrowFactory); + editor.getCaretPosEventStream().addObserver(onCaretMove); + } + + @Override + public void uninstall(@Nonnull Editor editor) { + super.uninstall(editor); + + editor.getRootLineGraphicFactory().removeLineGraphicFactory(arrowFactory); + editor.getCaretPosEventStream().removeObserver(onCaretMove); + } + + @Override + protected void onClassSelected() { + clearData(); + } + + @Override + protected void onMethodSelected() { + updateModel(); + } + + @Override + protected void onFieldSelected() { + clearData(); + } + + @Override + protected void onPipelineOutputUpdate() { + // Keep a reference to the old model. + List oldModel = model; + + // Update the model. + updateModel(); + + // If the model has changed, refresh the visible paragraph graphics. + // This can mean a new label as added, new reference to one, etc. + // This could result in new line shapes, so redrawing them all is wise. + List newModel = model; + if (!Objects.equals(oldModel, newModel)) + FxThreadUtil.run(() -> editor.redrawParagraphGraphics()); + } + + /** + * Handles updating the {@link ControlFlowLineFactory}. + *

+ * This logic is shoe-horned into here (for now) because + * the variable tracking logic is internal to this class only. + * + * @param caretChange + * Caret pos change. + */ + private void onCaretMove(Change caretChange) { + int pos = editor.getCodeArea().getCaretPosition(); + int line = editor.getCodeArea().getCurrentParagraph() + 1; + + // Find selected instruction (can be null) + Box selected = new Box<>(); + for (ASTElement element : astElements) { + if (element.range().within(pos)) { + element.walk(ast -> { + if (ast instanceof ASTInstruction instruction) { + Location location = ast.location(); + if (location != null && location.line() == line) + selected.set(instruction); + else { + String identifier = instruction.identifier().content(); + if (("tableswitch".equals(identifier) || "lookupswitch".equals(identifier)) && ast.range().within(pos)) { + selected.set(instruction); + } + } + } + return !selected.isSet(); + }); + } + } + + // Check if the selection was a label or supported instruction. + ASTInstruction current = selected.get(); + boolean hasSelection = false; + if (current instanceof ASTLabel) { + hasSelection = true; + } else if (current != null) { + String insnName = current.identifier().content(); + List arguments = current.arguments(); + if (!arguments.isEmpty() && INSN_SET.contains(insnName) || SWITCH_INSNS.contains(insnName)) { + hasSelection = true; + } + } + currentInstructionSelection.setValue(current); + drawLines.setValue(hasSelection); + } + + private void updateModel() { + // Collect all label usage information from the AST. + AstUsages emptyUsage = AstUsages.EMPTY_USAGE; + Map labelUsages = new HashMap<>(); + BiConsumer readUpdater = (name, element) -> { + AstUsages existing = labelUsages.getOrDefault(name, emptyUsage); + labelUsages.put(name, existing.withNewRead(element)); + }; + BiConsumer writeUpdater = (name, element) -> { + AstUsages existing = labelUsages.getOrDefault(name, emptyUsage); + labelUsages.put(name, existing.withNewWrite(element)); + }; + if (astElements != null) { + Consumer methodConsumer = astMethod -> { + if (currentMethod != null && !Objects.equals(currentMethod.getName(), astMethod.getName().literal())) + return; + for (ASTInstruction instruction : astMethod.code().instructions()) { + if (instruction instanceof ASTLabel label) { + readUpdater.accept(label.identifier().content(), label); + } else { + String insnName = instruction.identifier().content(); + List arguments = instruction.arguments(); + if (!arguments.isEmpty()) { + if (INSN_SET.contains(insnName)) { + writeUpdater.accept(arguments.getLast().content(), instruction); + } else if ("tableswitch".equals(insnName)) { + if (!instruction.arguments().isEmpty() && instruction.arguments().getFirst() instanceof ASTObject switchObj) { + ASTArray cases = switchObj.value("cases"); + ASTElement defaultCase = switchObj.value("default"); + cases.values().forEach(caseAst -> writeUpdater.accept(caseAst.content(), caseAst)); + writeUpdater.accept(defaultCase.content(), defaultCase); + } + } else if ("lookupswitch".equals(insnName)) { + if (!instruction.arguments().isEmpty() && instruction.arguments().getFirst() instanceof ASTObject switchObj) { + ASTElement defaultCase = switchObj.value("default"); + writeUpdater.accept(defaultCase.content(), defaultCase); + switchObj.values().pairs().forEach(pair -> { + writeUpdater.accept(pair.second().content(), pair.first()); + }); + } + } + } + } + } + }; + for (ASTElement astElement : astElements) { + if (astElement instanceof ASTMethod astMethod) { + methodConsumer.accept(astMethod); + } else if (astElement instanceof ASTClass astClass) { + for (ASTElement child : astClass.children()) { + if (child instanceof ASTMethod astMethod) { + methodConsumer.accept(astMethod); + } + } + } + } + } + model = labelUsages.entrySet().stream() + .filter(e -> !e.getValue().readers().isEmpty()) // Must have a label declaration + .map(e -> new LabelData(e.getKey(), e.getValue(), new IntBox(-1), new Box<>())) + .sorted(Comparator.comparing(LabelData::name)) + .toList(); + model.forEach(data -> { + List overlapping = data.computeOverlapping(model); + IntBox slot = data.lineSlot(); + int slotIndex = 0; + while (true) { + incr: + { + for (LabelData d : overlapping) { + if (slotIndex == d.lineSlot().get()) { + slotIndex++; + break incr; + } + } + break; + } + } + slot.set(slotIndex); + }); + } + + private void clearData() { + model = Collections.emptyList(); + currentMethod = null; + } + + /** + * Highlighter which shows read and write access of a {@link LabelData}. + */ + private class ControlFlowLineFactory extends AbstractLineGraphicFactory { + private static final int MASK_NORTH = 0; + private static final int MASK_SOUTH = 1; + private static final int MASK_EAST = 2; + private final int containerHeight = 16; // Each line graphic region is only 16px tall + private final int containerWidth = 16; + private final int[] offsets = new int[containerWidth]; + private final long rainbowHueRotationDurationMillis = 3000; + + private ControlFlowLineFactory() { + super(AbstractLineGraphicFactory.P_BRACKET_MATCH - 1); + + // Populate offsets + int j = 0; + for (int i = 0; i < offsets.length; i++) { + offsets[i] = 1 + (i * 3); + } + } + + @Override + public void install(@Nonnull Editor editor) { + // no-op, outer class has all the data we need + } + + @Override + public void uninstall(@Nonnull Editor editor) { + // no-op + } + + @Override + public void apply(@Nonnull LineContainer container, int paragraph) { + List localModel = model; + + if (!drawLines.getValue() || localModel.isEmpty()) { + container.addHorizontal(new Spacer(0)); + return; + } + + // To keep the ordering of the line graphic factory priority we need to add the stack pane now + // since the rest of the work is done async below. We want this to have zero width so that it doesn't + // shit the editor around when the content becomes active/inactive. + StackPane stack = new StackPane(); + stack.setManaged(false); + stack.setPrefWidth(0); + stack.setMouseTransparent(true); + container.addHorizontal(stack); + + // This looks stupid because it is, however it is necessary. + // + // When we're computing how long the lines need to be to connect to the text that references labels + // we need the cell layout to be up-to-date. But because the line graphic can be initialized at the same + // time as the paragraph in some cases it won't be always up-to-date, leading to the wrong length of lines. + // + // We could make this faster AND less complex if we assumed the font family and font size NEVER change + // and is always 'JetBrains Mono 12px' but if we did that and changed things we'd 100% forget about the hack + // and wonder why the thing broke. The magic 'width' per space char in such case is '7.2'. + FxThreadUtil.delayedRun(0, () -> { + stack.setPrefSize(containerHeight, containerHeight); + stack.setAlignment(Pos.CENTER_LEFT); + SceneUtils.getParentOfTypeLater(container, VirtualizedScrollPaneWrapper.class).whenComplete((parentScroll, error) -> { + ObservableValue translateX; + if (parentScroll != null) { + translateX = Bindings.add(container.widthProperty().subtract(containerHeight), parentScroll.horizontalScrollProperty().negate()); + } else { + // Should never happen since the 'VirtualizedScrollPaneWrapper' is mandated internally by 'Editor'. + translateX = container.widthProperty().subtract(containerHeight); + } + stack.translateXProperty().bind(translateX); + }); + + + double indent = editor.computeWhitespacePrefixWidth(paragraph) - 3 /* padding so lines aren't right up against text */; + double width = containerWidth + indent; + double height = containerHeight + 2; + Canvas canvas = new Canvas(width, height); + canvas.setManaged(false); + canvas.setMouseTransparent(true); + canvas.setTranslateY(-1); + + // Setup canvas styling for the render mode. + var renderMode = config.getRenderMode().getValue(); + Blend blend = new Blend(BlendMode.HARD_LIGHT); + Effect effect = switch (renderMode) { + case FLAT -> blend; + case RAINBOW_GLOWING, FLAT_GLOWING -> { + Bloom bloom = new Bloom(0.2); + Glow glow = new Glow(0.7); + bloom.setInput(blend); + glow.setInput(bloom); + yield glow; + } + }; + canvas.setEffect(effect); + if (renderMode == ControlFlowLinesConfig.LineRenderMode.RAINBOW_GLOWING) { + setupRainbowAnimation(effect, canvas).play(); + } + + GraphicsContext gc = canvas.getGraphicsContext2D(); + gc.setLineWidth(1); + stack.getChildren().add(canvas); + + for (LabelData labelData : localModel) { + // Skip if there are no references to the current label. + List labelReferrers = labelData.usage().writers(); + if (labelReferrers.isEmpty()) continue; + + // Skip if line is not inside a jump range. + if (!labelData.isInRange(paragraph + 1)) continue; + + // Handle skipping over cases if we only want to draw lines for what is currently selected. + if (config.getConnectionMode().getValue() == ControlFlowLinesConfig.ConnectionMode.CURRENT_CONNECTION) { + ASTInstruction value = currentInstructionSelection.getValue(); + if (value == null) { + // No current selection? We can skip everything. Just return. + return; + } else if (SWITCH_INSNS.contains(value.identifier().literal())) { + // If the selected item is a switch we want to draw all the lines to all destinations. + // + // The label data writers will be targeting the label identifier children in the AST + // so if we walk the switch instruction's children we can see if the current label data + // references one of those elements. + List elements = new ArrayList<>(); + value.walk(e -> { + elements.add(e); + return true; + }); + if (labelData.usage().readersAndWriters().noneMatch(elements::contains)) + continue; + } else if (labelData.usage().readersAndWriters().noneMatch(m -> m.equals(value))) { + // Anything else like a label declaration or a jump instruction mentioning a label + // can be handled with a basic equality check against all the usage readers/writers. + continue; + } + } + + // There is always one 'reader' AKA the label itself. + // We will use this to figure out which direction to draw lines in below. + ASTElement labelTarget = labelData.labelDeclaration(); + int declarationLine = labelTarget.location().line() - 1; + int nameHashBase = labelData.name().repeat(15).hashCode(); + + int parallelLines = Math.max(1, labelData.computeOverlapping(model).size()); + int lineSlot = labelData.lineSlot().get(); + int offsetIndex = lineSlot % offsets.length; + int horizontalOffset = offsets[offsetIndex]; + double hue = 360.0 / parallelLines * lineSlot; + Color color = createColor(hue); + + // Mask for tracking which portions of the jump lines have been drawn. + BitSet shapeMask = new BitSet(3); + + // Iterate over AST elements that refer to the label. + // We will use their position and the label declaration position to determine what shape to draw. + for (ASTElement referrer : labelReferrers) { + // Sanity check the AST element has location data. + Location referenceLoc = referrer.location(); + if (referenceLoc == null) continue; + + int referenceLine = referenceLoc.line() - 1; + boolean isBackReference = referenceLine > declarationLine; + + gc.setStroke(color); + gc.beginPath(); + boolean multiLine = labelData.countRefsOnLine(referenceLine) > 0; + double targetY = multiLine ? horizontalOffset : height / 2; + if (referenceLine == paragraph) { + // The Y coordinates in these lines is the midpoint because as references + // there should only be one line coming out of them. We don't need to fit + // multiple lines. + if (isBackReference) { + // Shape: └ + if (!shapeMask.get(MASK_NORTH)) { + // Top section + gc.moveTo(horizontalOffset, 0); + gc.lineTo(horizontalOffset, targetY); + shapeMask.set(MASK_NORTH); + } + if (!shapeMask.get(MASK_EAST)) { + // Right section + gc.moveTo(horizontalOffset, targetY); + gc.lineTo(width, targetY); + shapeMask.set(MASK_EAST); + } + } else { + // Shape: ┌ + if (!shapeMask.get(MASK_SOUTH)) { + // Bottom section + gc.moveTo(horizontalOffset, height); + gc.lineTo(horizontalOffset, targetY); + shapeMask.set(MASK_SOUTH); + } + if (!shapeMask.get(MASK_EAST)) { + // Right section + gc.moveTo(horizontalOffset, targetY); + gc.lineTo(width, targetY); + shapeMask.set(MASK_EAST); + } + } + gc.stroke(); + } else if (paragraph == declarationLine) { + if (isBackReference) { + // Shape: ┌ + if (!shapeMask.get(MASK_SOUTH)) { + // Bottom section + gc.moveTo(horizontalOffset, height); + gc.lineTo(horizontalOffset, targetY); + shapeMask.set(MASK_SOUTH); + } + if (!shapeMask.get(MASK_EAST)) { + // Right section + gc.moveTo(horizontalOffset, targetY); + gc.lineTo(width, targetY); + shapeMask.set(MASK_EAST); + } + } else { + // Shape: └ + if (!shapeMask.get(MASK_NORTH)) { + // Top section + gc.moveTo(horizontalOffset, 0); + gc.lineTo(horizontalOffset, targetY); + shapeMask.set(MASK_NORTH); + } + if (!shapeMask.get(MASK_EAST)) { + // Right section + gc.moveTo(horizontalOffset, targetY); + gc.lineTo(width, targetY); + shapeMask.set(MASK_EAST); + } + } + gc.stroke(); + } else if ((isBackReference && (paragraph > declarationLine && paragraph < referenceLine)) || + (!isBackReference && (paragraph < declarationLine && paragraph > referenceLine))) { + if (!shapeMask.get(MASK_NORTH)) { + // Top section + gc.moveTo(horizontalOffset, 0); + gc.lineTo(horizontalOffset, height / 2); + shapeMask.set(MASK_NORTH); + } + if (!shapeMask.get(MASK_SOUTH)) { + // Bottom section + gc.moveTo(horizontalOffset, height / 2); + gc.lineTo(horizontalOffset, height); + shapeMask.set(MASK_SOUTH); + } + gc.stroke(); + } + gc.closePath(); + } + } + }); + } + + @Nonnull + private static Color createColor(double hue) { + Color color = Color.hsb(hue, 1.0, 1.0); + + // Ensure the color is actually bright enough. + // In cases like pure blue, we have to lower the saturation incrementally to allow the brightness + // boosting math to have any effect. The brightness constants should approximate perceived brightness. + int i = 0; + while (i < 30) { + double red = color.getRed(); + double green = color.getGreen(); + double blue = color.getBlue(); + double brightness = 0.2126 * red + 0.7152 * green + 0.0722 * blue; + if (brightness > 0.4) + break; + color = color.deriveColor(0, 0.97, 1.2, 1); + i++; + } + + return color; + } + + @Nonnull + private Transition setupRainbowAnimation(@Nonnull Effect effect, @Nonnull Canvas canvas) { + return new Transition() { + { + setInterpolator(Interpolator.LINEAR); + setCycleDuration(Duration.millis(rainbowHueRotationDurationMillis)); + setCycleCount(Integer.MAX_VALUE); + } + + @Override + protected void interpolate(double frac) { + long now = System.currentTimeMillis(); + float diff = now % rainbowHueRotationDurationMillis; + + float halfMillis = (float) rainbowHueRotationDurationMillis / 2; + float hue = Math.abs((4 * diff / rainbowHueRotationDurationMillis) - 2) - 1; + ColorAdjust adjust = new ColorAdjust(hue, 0.0, 0.0, 0.0); + adjust.setInput(effect); + canvas.setEffect(adjust); + } + }; + } + } +} \ No newline at end of file diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/ControlFlowLinesConfig.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/ControlFlowLinesConfig.java new file mode 100644 index 000000000..c17b4ef64 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/ControlFlowLinesConfig.java @@ -0,0 +1,75 @@ +package software.coley.recaf.ui.pane.editing.assembler; + +import jakarta.annotation.Nonnull; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Inject; +import software.coley.observables.ObservableObject; +import software.coley.recaf.config.BasicConfigContainer; +import software.coley.recaf.config.BasicConfigValue; +import software.coley.recaf.config.ConfigGroups; + +/** + * Config for {@link ControlFlowLines}. + * + * @author Matt Coley + */ +@ApplicationScoped +public class ControlFlowLinesConfig extends BasicConfigContainer { + private final ObservableObject connectionMode = new ObservableObject<>(ConnectionMode.ALL_CONNECTIONS); + private final ObservableObject renderMode = new ObservableObject<>(LineRenderMode.FLAT); + + @Inject + public ControlFlowLinesConfig() { + super(ConfigGroups.SERVICE_ASSEMBLER, "flow-lines" + CONFIG_SUFFIX); + addValue(new BasicConfigValue<>("connection-mode", ConnectionMode.class, connectionMode)); + addValue(new BasicConfigValue<>("render-mode", LineRenderMode.class, renderMode)); + } + + /** + * @return Current line connection mode. + */ + @Nonnull + public ObservableObject getConnectionMode() { + return connectionMode; + } + + /** + * @return Current line render mode. + */ + @Nonnull + public ObservableObject getRenderMode() { + return renderMode; + } + + /** + * Modes for how to render lines. + */ + public enum LineRenderMode { + /** + * Simple flat lines. + */ + FLAT, + /** + * Simple flat lines with some glowing. + */ + FLAT_GLOWING, + /** + * Party time! + */ + RAINBOW_GLOWING + } + + /** + * Modes for where to draw lines. + */ + public enum ConnectionMode { + /** + * Show control flow connections for all flow edges. + */ + ALL_CONNECTIONS, + /** + * Show control flow connections for only the current item. + */ + CURRENT_CONNECTION + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmExpressionCompilerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmExpressionCompilerPane.java index 9cd0c180d..c6d7c56bd 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmExpressionCompilerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmExpressionCompilerPane.java @@ -1,13 +1,18 @@ package software.coley.recaf.ui.pane.editing.assembler; +import dev.xdark.blw.type.ClassType; +import dev.xdark.blw.type.ObjectType; +import dev.xdark.blw.type.PrimitiveType; +import dev.xdark.blw.type.Types; import jakarta.annotation.Nonnull; import jakarta.enterprise.context.Dependent; import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.scene.control.SplitPane; import software.coley.collections.Lists; import software.coley.recaf.info.member.FieldMember; -import software.coley.recaf.info.member.LocalVariable; import software.coley.recaf.services.assembler.ExpressionCompileException; import software.coley.recaf.services.assembler.ExpressionCompiler; import software.coley.recaf.services.assembler.ExpressionResult; @@ -43,11 +48,12 @@ public class JvmExpressionCompilerPane extends AstBuildConsumerComponent { private final ExpressionCompiler expressionCompiler; private final Editor javaEditor = new Editor(); private final Editor jasmEditor = new Editor(); + private boolean isDirty; @Inject public JvmExpressionCompilerPane(@Nonnull ExpressionCompiler expressionCompiler, - @Nonnull FileTypeAssociationService languageAssociation, - @Nonnull Instance searchBarProvider) { + @Nonnull FileTypeAssociationService languageAssociation, + @Nonnull Instance searchBarProvider) { this.expressionCompiler = expressionCompiler; languageAssociation.configureEditorSyntax("java", javaEditor); @@ -57,7 +63,6 @@ public JvmExpressionCompilerPane(@Nonnull ExpressionCompiler expressionCompiler, new BracketMatchGraphicFactory(), new ProblemGraphicFactory() ); - javaEditor.setText(Lang.get("assembler.playground.comment").replace("\\n", "\n")); // TODO: The comment should reflect what contexts are allowed jasmEditor.getCodeArea().getStylesheets().add(LanguageStylesheets.getJasmStylesheet()); jasmEditor.setSelectedBracketTracking(new SelectedBracketTracking()); jasmEditor.setSyntaxHighlighter(new RegexSyntaxHighlighter(RegexLanguages.getJasmLanguage())); @@ -80,6 +85,7 @@ protected void onClassSelected() { expressionCompiler.clearContext(); if (canAssignClassContext()) expressionCompiler.setClassContext(currentClass.asJvmClass()); + init(ContextType.CLASS); scheduleCompile(); } @@ -92,6 +98,7 @@ protected void onMethodSelected() { expressionCompiler.setMethodContext(currentMethod); } } + init(ContextType.METHOD); scheduleCompile(); } @@ -100,6 +107,7 @@ protected void onFieldSelected() { expressionCompiler.clearContext(); if (canAssignClassContext()) expressionCompiler.setClassContext(currentClass.asJvmClass()); + init(ContextType.FIELD); scheduleCompile(); } @@ -108,6 +116,70 @@ protected void onPipelineOutputUpdate() { // no-op } + /** + * Populates the initial text of the expression compiler pane. + * + * @param type + * Content type in the {@link AssemblerPane}. + */ + private void init(@Nonnull ContextType type) { + if (!javaEditor.getCodeArea().getText().isBlank()) return; + + // TODO: The comment should reflect what contexts are active + // - Should query expression compiler for this info + String text = Lang.get("assembler.playground.comment").replace("\\n", "\n") + '\n'; + + switch (type) { + case CLASS, FIELD -> text += "return;"; + case METHOD -> { + ClassType returnType = Types.methodType(currentMethod.getDescriptor()).returnType(); + if (returnType instanceof ObjectType ot) { + text += "return null;"; + } else if (returnType instanceof PrimitiveType pt) { + switch (pt.descriptor().charAt(0)) { + case 'V': + text += "return;"; + break; + case 'J': + text += "return 0L;"; + break; + case 'D': + text += "return 0.0;"; + break; + case 'F': + text += "return 0F;"; + break; + case 'I': + text += "return 0;"; + break; + case 'C': + text += "return 'a';"; + break; + case 'S': + text += "return (short) 0;"; + break; + case 'B': + text += "return (byte) 0;"; + break; + case 'Z': + text += "return false;"; + break; + } + } + } + } + javaEditor.setText(text); + + // Mark dirty when a user makes a change. + javaEditor.textProperty().addListener(new ChangeListener<>() { + @Override + public void changed(ObservableValue ob, String old, String cur) { + isDirty = true; + javaEditor.textProperty().removeListener(this); + } + }); + } + /** * Checks for things in the {@link #currentClass} which would prevent its use in the expression compiler as context. * @@ -134,7 +206,7 @@ private boolean canAssignMethodContext() { } private void scheduleCompile() { - compilePool.submit(this::compile); + if (isDirty) compilePool.submit(this::compile); } private void compile() { @@ -166,4 +238,11 @@ private void compile() { jasmEditor.setText(Objects.requireNonNullElse(assembly, "")); }); } + + /** + * Type of content in the containing {@link AssemblerPane}. + */ + private enum ContextType { + CLASS, FIELD, METHOD + } } \ No newline at end of file diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmStackAnalysisPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmStackAnalysisPane.java index 423b1abfd..ffb2264fc 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmStackAnalysisPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmStackAnalysisPane.java @@ -28,7 +28,7 @@ import org.reactfx.EventStreams; import software.coley.collections.Lists; import software.coley.recaf.services.cell.CellConfigurationService; -import software.coley.recaf.ui.config.TextFormatConfig; +import software.coley.recaf.services.text.TextFormatConfig; import software.coley.recaf.ui.control.richtext.Editor; import software.coley.recaf.util.FxThreadUtil; import software.coley.recaf.util.Lang; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmVariablesPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmVariablesPane.java index 1e02d56f9..5fc7a36f3 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmVariablesPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/JvmVariablesPane.java @@ -22,7 +22,6 @@ import me.darknet.assembler.ast.specific.ASTClass; import me.darknet.assembler.ast.specific.ASTMethod; import me.darknet.assembler.compile.analysis.AnalysisResults; -import me.darknet.assembler.compile.analysis.Local; import me.darknet.assembler.compile.analysis.frame.Frame; import me.darknet.assembler.util.Location; import me.darknet.assembler.util.Range; @@ -31,7 +30,7 @@ import org.reactfx.EventStreams; import software.coley.collections.Lists; import software.coley.recaf.services.cell.CellConfigurationService; -import software.coley.recaf.ui.config.TextFormatConfig; +import software.coley.recaf.services.text.TextFormatConfig; import software.coley.recaf.ui.control.richtext.Editor; import software.coley.recaf.ui.control.richtext.linegraphics.AbstractLineGraphicFactory; import software.coley.recaf.ui.control.richtext.linegraphics.LineContainer; @@ -46,7 +45,6 @@ import java.util.*; import java.util.function.BiConsumer; import java.util.function.Consumer; -import java.util.stream.Stream; /** * Component panel for the assembler which shows the variables of the currently selected method. @@ -67,21 +65,21 @@ public JvmVariablesPane(@Nonnull CellConfigurationService cellConfigurationServi @Nonnull Workspace workspace) { TableColumn columnName = new TableColumn<>(Lang.get("assembler.variables.name")); TableColumn columnType = new TableColumn<>(Lang.get("assembler.variables.type")); - TableColumn columnUsage = new TableColumn<>(Lang.get("assembler.variables.usage")); - columnName.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().name)); - columnType.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().type)); - columnUsage.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().usage)); + TableColumn columnUsage = new TableColumn<>(Lang.get("assembler.variables.usage")); + columnName.setCellValueFactory(param -> new SimpleStringProperty(param.getValue().name())); + columnType.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().type())); + columnUsage.setCellValueFactory(param -> new SimpleObjectProperty<>(param.getValue().usage())); columnType.setCellFactory(param -> new TypeTableCell<>(cellConfigurationService, formatConfig, workspace)); columnUsage.setCellFactory(param -> new TableCell<>() { @Override - protected void updateItem(VariableUsages usages, boolean empty) { + protected void updateItem(AstUsages usages, boolean empty) { super.updateItem(usages, empty); if (empty || usages == null) { setText(null); setGraphic(null); setOnMousePressed(null); } else { - String usageFmt = "%d reads, %d writes".formatted(usages.readers.size(), usages.writers.size()); + String usageFmt = "%d reads, %d writes".formatted(usages.readers().size(), usages.writers().size()); setText(usageFmt); } } @@ -172,7 +170,7 @@ private void onCaretMove(Change caretChange) { // Determine which variable is at the caret position VariableData currentVarSelection = null; for (VariableData item : table.getItems()) { - VariableUsages usage = item.usage(); + AstUsages usage = item.usage(); ASTElement matchedAst = usage.readersAndWriters() .filter(e -> e.range().within(pos)) .findFirst().orElse(null); @@ -196,14 +194,14 @@ private void updateTable() { items.clear(); // Collect all variable usage information from the AST. - VariableUsages emptyUsage = VariableUsages.EMPTY_USAGE; - Map variableUsages = new HashMap<>(); + AstUsages emptyUsage = AstUsages.EMPTY_USAGE; + Map variableUsages = new HashMap<>(); BiConsumer readUpdater = (name, element) -> { - VariableUsages existing = variableUsages.getOrDefault(name, emptyUsage); + AstUsages existing = variableUsages.getOrDefault(name, emptyUsage); variableUsages.put(name, existing.withNewRead(element)); }; BiConsumer writeUpdater = (name, element) -> { - VariableUsages existing = variableUsages.getOrDefault(name, emptyUsage); + AstUsages existing = variableUsages.getOrDefault(name, emptyUsage); variableUsages.put(name, existing.withNewWrite(element)); }; if (astElements != null) { @@ -342,89 +340,4 @@ public void apply(@Nonnull LineContainer container, int paragraph) { container.addHorizontal(graphic); } } - - /** - * Models a variable. - * - * @param name - * Name of variable. - * @param type - * Type of variable. - * @param usage - * Usages of the variable in the AST. - */ - public record VariableData(@Nonnull String name, @Nonnull ClassType type, @Nonnull VariableUsages usage) { - /** - * @param local - * blw variable declaration. - * @param usage - * AST usage. - * - * @return Data from a blw variable, plus AST usage. - */ - @Nonnull - public static VariableData adaptFrom(@Nonnull Local local, @Nonnull VariableUsages usage) { - return new VariableData(local.name(), local.type(), usage); - } - - /** - * @param other - * Other variable data to check against. - * - * @return {@code true} if the variable held by this data is the same as the other. - */ - public boolean matchesNameType(@Nullable VariableData other) { - if (other == null) return false; - return name.equals(other.name) && type.equals(other.type); - } - } - - /** - * Models variable usage. - * - * @param readers - * Elements that read from the variable. - * @param writers - * Elements that write to the variable. - */ - public record VariableUsages(@Nonnull List readers, @Nonnull List writers) { - /** - * Empty variable usage. - */ - private static final VariableUsages EMPTY_USAGE = new VariableUsages(Collections.emptyList(), Collections.emptyList()); - - /** - * @return Stream of both readers and writers. - */ - @Nonnull - public Stream readersAndWriters() { - return Stream.concat(readers.stream(), writers.stream()); - } - - /** - * @param element - * Element to add as a reader. - * - * @return Copy with added element. - */ - @Nonnull - public VariableUsages withNewRead(@Nonnull ASTElement element) { - List newReaders = new ArrayList<>(readers); - newReaders.add(element); - return new VariableUsages(newReaders, writers); - } - - /** - * @param element - * Element to add as a writer. - * - * @return Copy with added element. - */ - @Nonnull - public VariableUsages withNewWrite(@Nonnull ASTElement element) { - List newWriters = new ArrayList<>(writers); - newWriters.add(element); - return new VariableUsages(readers, newWriters); - } - } } \ No newline at end of file diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/LabelData.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/LabelData.java new file mode 100644 index 000000000..cee74ad29 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/LabelData.java @@ -0,0 +1,107 @@ +package software.coley.recaf.ui.pane.editing.assembler; + +import jakarta.annotation.Nonnull; +import me.darknet.assembler.ast.ASTElement; +import me.darknet.assembler.ast.primitive.ASTLabel; +import me.darknet.assembler.util.Location; +import me.darknet.assembler.util.Range; +import software.coley.collections.box.Box; +import software.coley.collections.box.IntBox; + +import java.util.*; + +/** + * Models a variable. + * + * @param name + * Name of label. + * @param usage + * Usages of the loabel in the AST. + */ +public record LabelData(@Nonnull String name, @Nonnull AstUsages usage, + @Nonnull IntBox lineSlot, @Nonnull Box> overlapping) { + @Nonnull + public Range range() { + IntSummaryStatistics summary = usage.readersAndWriters() + .mapToInt(e -> Objects.requireNonNull(e.location()).line()) + .summaryStatistics(); + return new Range(summary.getMin(), summary.getMax()); + } + + @Nonnull + public ASTLabel labelDeclaration() { + return (ASTLabel) usage.readers().getFirst(); + } + + + public long countRefsOnLine(int line) { + return usage.readersAndWriters() + .filter(u -> u.location().line() == line) + .count(); + } + + public List computeOverlapping(@Nonnull Collection labelDatum) { + return overlapping.computeIfAbsent(() -> { + Range range = range(); + List overlap = new ArrayList<>(); + for (LabelData data : labelDatum) { + // Skip self + if (name.equals(data.name)) continue; + + // Skip labels that don't have references + if (data.usage().writers().isEmpty()) continue; + + Range otherRange = data.range(); + if (Math.max(range.start(), otherRange.start()) <= Math.min(range.end(), otherRange.end())) + overlap.add(data); + } + return overlap; + + }); + } + + public boolean isInRange(int line) { + ASTLabel declaration = labelDeclaration(); + Location declarationLoc = declaration.location(); + if (declarationLoc == null) return false; + int declarationLine = declarationLoc.line(); + + // Base case, range included declaration line. + if (declarationLine == line) return true; + + // Check if inside range: + // goto X + // .. <---- line somewhere in here + // X: + for (ASTElement referrer : usage.writers()) { + Location referrerLoc = referrer.location(); + if (referrerLoc == null) continue; + int referrerLine = referrerLoc.line(); + + // Base case, range included referrer's line. + if (referrerLine == line) return true; + + // Otherwise check if the line is between the range of the reference and the declaration. + // The range bounds are swapped based on if the reference is forwards or backwards. + if ((declarationLine > referrerLine) ? + (line > referrerLine && line < declarationLine) : + (line > declarationLine && line < referrerLine)) return true; + } + return false; + } + + @Override + public boolean equals(Object o) { + if (this == o) return true; + if (o == null || getClass() != o.getClass()) return false; + + LabelData labelData = (LabelData) o; + + return name.equals(labelData.name); + } + + @Override + public int hashCode() { + return name.hashCode(); + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/TypeTableCell.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/TypeTableCell.java index 552392044..4cebc5fc8 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/TypeTableCell.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/TypeTableCell.java @@ -9,7 +9,7 @@ import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.services.cell.CellConfigurationService; import software.coley.recaf.services.cell.context.ContextMenuProvider; -import software.coley.recaf.ui.config.TextFormatConfig; +import software.coley.recaf.services.text.TextFormatConfig; import software.coley.recaf.util.Icons; import software.coley.recaf.workspace.model.Workspace; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/VariableData.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/VariableData.java new file mode 100644 index 000000000..8ebbc25a9 --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/assembler/VariableData.java @@ -0,0 +1,42 @@ +package software.coley.recaf.ui.pane.editing.assembler; + +import dev.xdark.blw.type.ClassType; +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import me.darknet.assembler.compile.analysis.Local; + +/** + * Models a variable. + * + * @param name + * Name of variable. + * @param type + * Type of variable. + * @param usage + * Usages of the variable in the AST. + */ +public record VariableData(@Nonnull String name, @Nonnull ClassType type, @Nonnull AstUsages usage) { + /** + * @param local + * blw variable declaration. + * @param usage + * AST usage. + * + * @return Data from a blw variable, plus AST usage. + */ + @Nonnull + public static VariableData adaptFrom(@Nonnull Local local, @Nonnull AstUsages usage) { + return new VariableData(local.name(), local.type(), usage); + } + + /** + * @param other + * Other variable data to check against. + * + * @return {@code true} if the variable held by this data is the same as the other. + */ + public boolean matchesNameType(@Nullable VariableData other) { + if (other == null) return false; + return name.equals(other.name) && type.equals(other.type); + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmClassPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmClassPane.java index 7154d8f15..2207fddf8 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmClassPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmClassPane.java @@ -5,16 +5,10 @@ import jakarta.enterprise.inject.Instance; import jakarta.inject.Inject; import javafx.scene.control.Label; -import org.kordamp.ikonli.carbonicons.CarbonIcons; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.ui.config.ClassEditingConfig; -import software.coley.recaf.ui.control.BoundTab; -import software.coley.recaf.ui.control.IconView; import software.coley.recaf.ui.pane.editing.ClassPane; -import software.coley.recaf.ui.pane.editing.tabs.FieldsAndMethodsPane; -import software.coley.recaf.ui.pane.editing.tabs.InheritancePane; -import software.coley.recaf.util.Icons; -import software.coley.recaf.util.Lang; +import software.coley.recaf.ui.pane.editing.SideTabsInjector; /** * Displays {@link JvmClassInfo} in a configurable manner. @@ -28,12 +22,11 @@ public class JvmClassPane extends ClassPane { @Inject public JvmClassPane(@Nonnull ClassEditingConfig config, - @Nonnull FieldsAndMethodsPane fieldsAndMethodsPane, - @Nonnull InheritancePane inheritancePane, + @Nonnull SideTabsInjector sideTabsInjector, @Nonnull Instance decompilerProvider) { + sideTabsInjector.injectLater(this); editorType = config.getDefaultJvmEditor().getValue(); this.decompilerProvider = decompilerProvider; - configureCommonSideTabs(fieldsAndMethodsPane, inheritancePane); } /** diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java index cd45e6ffc..e9fda7512 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPane.java @@ -22,16 +22,17 @@ import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.builder.JvmClassInfoBuilder; import software.coley.recaf.info.properties.builtin.CachedDecompileProperty; +import software.coley.recaf.path.ClassPathNode; import software.coley.recaf.services.compile.*; import software.coley.recaf.services.decompile.DecompileResult; import software.coley.recaf.services.decompile.DecompilerManager; import software.coley.recaf.services.decompile.JvmDecompiler; +import software.coley.recaf.services.info.association.FileTypeAssociationService; import software.coley.recaf.services.navigation.Actions; import software.coley.recaf.services.phantom.GeneratedPhantomWorkspaceResource; import software.coley.recaf.services.phantom.PhantomGenerationException; import software.coley.recaf.services.phantom.PhantomGenerator; import software.coley.recaf.services.source.AstResolveResult; -import software.coley.recaf.services.info.association.FileTypeAssociationService; import software.coley.recaf.ui.config.KeybindingConfig; import software.coley.recaf.ui.control.BoundLabel; import software.coley.recaf.ui.control.FontIconView; @@ -54,10 +55,7 @@ import software.coley.recaf.workspace.model.bundle.JvmClassBundle; import software.coley.recaf.workspace.model.resource.WorkspaceResource; -import java.util.Collection; -import java.util.Collections; -import java.util.List; -import java.util.Map; +import java.util.*; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; @@ -83,16 +81,16 @@ public class JvmDecompilerPane extends AbstractDecompilePane { @Inject public JvmDecompilerPane(@Nonnull DecompilerPaneConfig config, - @Nonnull KeybindingConfig keys, - @Nonnull SearchBar searchBar, - @Nonnull ToolsContainerComponent toolsContainer, - @Nonnull JavaContextActionSupport contextActionSupport, - @Nonnull FileTypeAssociationService languageAssociation, - @Nonnull DecompilerManager decompilerManager, - @Nonnull JavacCompiler javac, - @Nonnull JavacCompilerConfig javacConfig, - @Nonnull PhantomGenerator phantomGenerator, - @Nonnull Actions actions) { + @Nonnull KeybindingConfig keys, + @Nonnull SearchBar searchBar, + @Nonnull ToolsContainerComponent toolsContainer, + @Nonnull JavaContextActionSupport contextActionSupport, + @Nonnull FileTypeAssociationService languageAssociation, + @Nonnull DecompilerManager decompilerManager, + @Nonnull JavacCompiler javac, + @Nonnull JavacCompilerConfig javacConfig, + @Nonnull PhantomGenerator phantomGenerator, + @Nonnull Actions actions) { super(config, searchBar, contextActionSupport, languageAssociation, decompilerManager); this.phantomGenerator = phantomGenerator; this.javacDebug = new ObservableBoolean(javacConfig.getDefaultEmitDebug().getValue()); @@ -208,12 +206,35 @@ private void save() { // Check if any non-external-reference inner class entry no longer exists. // - Removal/updating/insertion is OK, renaming is not. + // - Because inners may have other inners we need to recursively collect inner classes Map realInners = info.getInnerClasses().stream() .filter(inner -> !inner.isExternalReference()) .collect(Collectors.toMap(InnerClassInfo::getInnerClassName, Function.identity())); + Set names = new HashSet<>(); + boolean recurseAddInners; + do { + // Reset the recurse flag each iteration. We'll enable it only if we need to. + recurseAddInners = false; + for (String type : new HashSet<>(realInners.keySet())) { + // Skip if we already checked this type for further inner classes. + if (!names.add(type)) continue; + + // Lookup inner class in workspace and add its inner classes to the map. + ClassPathNode typePath = workspace.findClass(type); + if (typePath != null) { + List innerClasses = typePath.getValue().getInnerClasses(); + for (InnerClassInfo inner : innerClasses) { + if (inner.isExternalReference()) continue; + + // Enable another recursive pass if a new inner class was found. + recurseAddInners |= realInners.putIfAbsent(inner.getInnerClassName(), inner) == null; + } + } + } + } while (recurseAddInners); // Ensure all names in the compilation exist in the previous inner classes info - if (!realInners.isEmpty() && compilations.keySet().stream().anyMatch(name -> !realInners.containsKey(name))) { + if (!realInners.isEmpty() && compilations.keySet().stream().anyMatch(name -> !name.equals(infoName) && !realInners.containsKey(name))) { logger.warn("Please only rename inner classes via mapping operations."); Animations.animateWarn(this, 1000); return; diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPaneConfigurator.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPaneConfigurator.java index 30c3c58d5..63566d6f0 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPaneConfigurator.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/jvm/JvmDecompilerPaneConfigurator.java @@ -102,6 +102,11 @@ private JavacVersionComboBox() { return String.valueOf(v); })); + // Hack to prevent odd resize-based deadlock: #798 + int w = 200; + setMaxWidth(w); + setPrefWidth(w); + // Update property. valueProperty().addListener((ob, old, cur) -> javacTarget.setValue(cur)); } @@ -123,6 +128,11 @@ private JavacDownsampleVersionComboBox() { return String.valueOf(v); })); + // Hack to prevent odd resize-based deadlock: #798 + int w = 200; + setMaxWidth(w); + setPrefWidth(w); + // Update property. valueProperty().addListener((ob, old, cur) -> javacDownsampleTarget.setValue(cur)); } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/FieldsAndMethodsPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/FieldsAndMethodsPane.java index b7db226e4..4ce340667 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/FieldsAndMethodsPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/tabs/FieldsAndMethodsPane.java @@ -18,6 +18,7 @@ import javafx.scene.layout.HBox; import javafx.scene.layout.Priority; import javafx.scene.layout.VBox; +import net.greypanther.natsort.CaseInsensitiveSimpleNaturalComparator; import org.kordamp.ikonli.carbonicons.CarbonIcons; import software.coley.recaf.info.Accessed; import software.coley.recaf.info.ClassInfo; @@ -175,7 +176,7 @@ private void refreshTreeSort() { // Then by alphabetic order. if (result == 0 && sortAlphabetically.get()) { if (valueA instanceof Named namedA && valueB instanceof Named namedB) { - result = String.CASE_INSENSITIVE_ORDER.compare(namedA.getName(), namedB.getName()); + result = CaseInsensitiveSimpleNaturalComparator.getInstance().compare(namedA.getName(), namedB.getName()); } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/text/TextPane.java b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/text/TextPane.java index ea075bc52..22ee20be5 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/text/TextPane.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/pane/editing/text/TextPane.java @@ -8,10 +8,10 @@ import software.coley.recaf.info.TextFileInfo; import software.coley.recaf.path.FilePathNode; import software.coley.recaf.path.PathNode; +import software.coley.recaf.services.info.association.FileTypeAssociationService; import software.coley.recaf.services.navigation.FileNavigable; import software.coley.recaf.services.navigation.Navigable; import software.coley.recaf.services.navigation.UpdatableNavigable; -import software.coley.recaf.services.info.association.FileTypeAssociationService; import software.coley.recaf.ui.config.KeybindingConfig; import software.coley.recaf.ui.control.richtext.Editor; import software.coley.recaf.ui.control.richtext.bracket.BracketMatchGraphicFactory; @@ -42,8 +42,8 @@ public class TextPane extends BorderPane implements FileNavigable, UpdatableNavi @Inject public TextPane(@Nonnull FileTypeAssociationService languageAssociation, - @Nonnull KeybindingConfig keys, - @Nonnull SearchBar searchBar) { + @Nonnull KeybindingConfig keys, + @Nonnull SearchBar searchBar) { this.languageAssociation = languageAssociation; // Configure the editor @@ -125,7 +125,12 @@ public void onUpdatePath(@Nonnull PathNode path) { languageAssociation.configureEditorSyntax(info, editor); // Update the text. - FxThreadUtil.run(() -> editor.setText(textInfo.getText())); + FxThreadUtil.run(() -> { + editor.setText(textInfo.getText()); + + // Prevent undo from reverting to empty state. + editor.getCodeArea().getUndoManager().forgetHistory(); + }); } } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/ui/window/QuickNavWindow.java b/recaf-ui/src/main/java/software/coley/recaf/ui/window/QuickNavWindow.java index 9ff25a138..21f7bf180 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/ui/window/QuickNavWindow.java +++ b/recaf-ui/src/main/java/software/coley/recaf/ui/window/QuickNavWindow.java @@ -29,6 +29,7 @@ import org.slf4j.Logger; import regexodus.Matcher; import regexodus.Pattern; +import software.coley.collections.Unchecked; import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.info.ClassInfo; import software.coley.recaf.info.TextFileInfo; @@ -45,7 +46,7 @@ import software.coley.recaf.services.window.WindowManager; import software.coley.recaf.services.workspace.WorkspaceCloseListener; import software.coley.recaf.services.workspace.WorkspaceManager; -import software.coley.recaf.ui.config.TextFormatConfig; +import software.coley.recaf.services.text.TextFormatConfig; import software.coley.recaf.ui.control.AbstractSearchBar; import software.coley.recaf.ui.control.BoundTab; import software.coley.recaf.ui.control.FontIconView; diff --git a/recaf-ui/src/main/java/software/coley/recaf/util/DirectoryChooserBuilder.java b/recaf-ui/src/main/java/software/coley/recaf/util/DirectoryChooserBuilder.java new file mode 100644 index 000000000..6ad223bcd --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/util/DirectoryChooserBuilder.java @@ -0,0 +1,91 @@ +package software.coley.recaf.util; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import javafx.stage.DirectoryChooser; +import javafx.stage.Window; +import software.coley.observables.ObservableString; + +import java.io.File; +import java.nio.file.Path; + +/** + * Builder for {@link DirectoryChooser} which filters out invalid inputs. + * + * @author Matt Coley + */ +public class DirectoryChooserBuilder { + private String title; + private File initialDirectory; + + /** + * @param title + * Dialog title. + * + * @return Self. + */ + @Nonnull + public DirectoryChooserBuilder setTitle(@Nonnull String title) { + this.title = title; + return this; + } + + /** + * @param initialDirectory + * Dialog's initial directory to open within and have selected. + * + * @return Self. + */ + @Nonnull + public DirectoryChooserBuilder setInitialDirectory(@Nonnull ObservableString initialDirectory) { + File file = initialDirectory.unboxingMap(File::new); + return setInitialDirectory(file); + } + + /** + * @param initialDirectory + * Dialog's initial directory to open within and have selected. + * + * @return Self. + */ + @Nonnull + public DirectoryChooserBuilder setInitialDirectory(@Nonnull Path initialDirectory) { + return setInitialDirectory(initialDirectory.toFile()); + } + + /** + * @param initialDirectory + * Dialog's initial directory to open within and have selected. + * + * @return Self. + */ + @Nonnull + public DirectoryChooserBuilder setInitialDirectory(@Nonnull File initialDirectory) { + this.initialDirectory = initialDirectory; + return this; + } + + /** + * @return New directory chooser from current settings. + */ + @Nonnull + public DirectoryChooser build() { + DirectoryChooser chooser = new DirectoryChooser(); + if (initialDirectory != null && initialDirectory.isDirectory()) + chooser.setInitialDirectory(initialDirectory); + if (title != null) + chooser.setTitle(title); + return chooser; + } + + /** + * @param owner + * The owner window of the displayed directory dialog. + * + * @return Selected directory path, otherwise {@code null} if cancelled. + */ + @Nullable + public File pick(@Nullable Window owner) { + return build().showDialog(owner); + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/util/FileChooserBuilder.java b/recaf-ui/src/main/java/software/coley/recaf/util/FileChooserBuilder.java new file mode 100644 index 000000000..be79a1c9d --- /dev/null +++ b/recaf-ui/src/main/java/software/coley/recaf/util/FileChooserBuilder.java @@ -0,0 +1,196 @@ +package software.coley.recaf.util; + +import jakarta.annotation.Nonnull; +import jakarta.annotation.Nullable; +import javafx.stage.FileChooser; +import javafx.stage.Window; +import software.coley.observables.ObservableString; + +import java.io.File; +import java.nio.file.Path; +import java.util.List; + +/** + * Builder for {@link FileChooser} which filters out invalid inputs. + * + * @author Matt Coley + */ +public class FileChooserBuilder { + private String title; + private String initialFileName; + private String fileExtensionFilterName; + private String[] fileExtensions; + private File initialDirectory; + + /** + * @param title + * Dialog title. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setTitle(@Nonnull String title) { + this.title = title; + return this; + } + + /** + * @param initialFileName + * Dialog's initial file name. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setInitialFileName(@Nonnull String initialFileName) { + this.initialFileName = initialFileName; + return this; + } + + /** + * @param initialDirectory + * Dialog's initial directory to open within. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setInitialDirectory(@Nonnull ObservableString initialDirectory) { + File file = initialDirectory.unboxingMap(File::new); + return setInitialDirectory(file); + } + + /** + * @param initialDirectory + * Dialog's initial directory to open within. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setInitialDirectory(@Nonnull Path initialDirectory) { + return setInitialDirectory(initialDirectory.toFile()); + } + + /** + * @param initialDirectory + * Dialog's initial directory to open within. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setInitialDirectory(@Nonnull File initialDirectory) { + this.initialDirectory = initialDirectory; + return this; + } + + /** + * @param fileExtensionFilterName + * Name of file extension filter. + * @param fileExtensions + * File extensions to show. Example: {@code "*.jar", "*.zip"}. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setFileExtensionFilter(@Nonnull String fileExtensionFilterName, @Nonnull List fileExtensions) { + return setFileExtensionFilterName(fileExtensionFilterName) + .setFileExtensions(fileExtensions); + } + + /** + * @param fileExtensionFilterName + * Name of file extension filter. + * @param fileExtensions + * File extensions to show. Example: {@code "*.jar", "*.zip"}. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setFileExtensionFilter(@Nonnull String fileExtensionFilterName, @Nonnull String... fileExtensions) { + return setFileExtensionFilterName(fileExtensionFilterName) + .setFileExtensions(fileExtensions); + } + + /** + * @param fileExtensionFilterName + * Name of file extension filter. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setFileExtensionFilterName(@Nonnull String fileExtensionFilterName) { + this.fileExtensionFilterName = fileExtensionFilterName; + return this; + } + + /** + * @param fileExtensions + * File extensions to show. Example: {@code "*.jar", "*.zip"}. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setFileExtensions(@Nonnull List fileExtensions) { + return setFileExtensions(fileExtensions.toArray(new String[0])); + } + + /** + * @param fileExtensions + * File extensions to show. Example: {@code "*.jar", "*.zip"}. + * + * @return Self. + */ + @Nonnull + public FileChooserBuilder setFileExtensions(@Nonnull String... fileExtensions) { + this.fileExtensions = fileExtensions; + return this; + } + + /** + * @return New filer chooser from current settings. + */ + @Nonnull + public FileChooser build() { + FileChooser chooser = new FileChooser(); + if (initialFileName != null) + chooser.setInitialFileName(initialFileName); + if (fileExtensionFilterName != null && fileExtensions != null) + chooser.setSelectedExtensionFilter(new FileChooser.ExtensionFilter(fileExtensionFilterName, fileExtensions)); + if (initialDirectory != null && initialDirectory.isDirectory()) + chooser.setInitialDirectory(initialDirectory); + if (title != null) + chooser.setTitle(title); + return chooser; + } + + /** + * @param owner + * The owner window of the displayed file dialog. + * + * @return Selected file path, otherwise {@code null} if cancelled. + */ + @Nullable + public File save(@Nullable Window owner) { + return build().showSaveDialog(owner); + } + + /** + * @param owner + * The owner window of the displayed file dialog. + * + * @return Selected file path, otherwise {@code null} if cancelled. + */ + @Nullable + public File open(@Nullable Window owner) { + return build().showOpenDialog(owner); + } + + /** + * @param owner + * The owner window of the displayed file dialog. + * + * @return Selected file paths, otherwise {@code null} if cancelled. + */ + @Nullable + public List openMultiple(@Nullable Window owner) { + return build().showOpenMultipleDialog(owner); + } +} diff --git a/recaf-ui/src/main/java/software/coley/recaf/util/Lang.java b/recaf-ui/src/main/java/software/coley/recaf/util/Lang.java index f479d3f54..79c849d8f 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/util/Lang.java +++ b/recaf-ui/src/main/java/software/coley/recaf/util/Lang.java @@ -1,5 +1,6 @@ package software.coley.recaf.util; +import jakarta.annotation.Nonnull; import javafx.beans.binding.StringBinding; import javafx.beans.property.StringProperty; import javafx.beans.value.ObservableValue; @@ -31,6 +32,7 @@ public class Lang { /** * @return Provided translations, also keys for {@link #getTranslations()}. */ + @Nonnull public static List getTranslationKeys() { return translationKeys; } @@ -38,6 +40,7 @@ public static List getTranslationKeys() { /** * @return Default translations, English, also key for {@link #getTranslations()}. */ + @Nonnull public static String getDefaultTranslations() { return DEFAULT_TRANSLATIONS; } @@ -84,6 +87,7 @@ public static void setSystemLanguage(String translations) { /** * @return System language, or {@link #getDefaultTranslations()} if not set. */ + @Nonnull public static String getSystemLanguage() { return SYSTEM_LANGUAGE == null ? getDefaultTranslations() : SYSTEM_LANGUAGE; } @@ -91,6 +95,7 @@ public static String getSystemLanguage() { /** * @return Map of supported translations and their key entries. */ + @Nonnull public static Map> getTranslations() { return translations; } @@ -101,7 +106,8 @@ public static Map> getTranslations() { * * @return JavaFX string binding for specific translation key. */ - public static synchronized StringBinding getBinding(String translationKey) { + @Nonnull + public static synchronized StringBinding getBinding(@Nonnull String translationKey) { return translationBindings.computeIfAbsent(translationKey, k -> { StringProperty currentTranslation = Lang.currentTranslation; return new SynchronizedStringBinding() { @@ -112,8 +118,7 @@ public static synchronized StringBinding getBinding(String translationKey) { @Override protected synchronized String computeValue() { String translated = Lang.get(currentTranslation.get(), translationKey); - if (translated != null) - translated = translated.replace("\\n", "\n"); + translated = translated.replace("\\n", "\n"); return translated; } }; @@ -128,7 +133,8 @@ protected synchronized String computeValue() { * * @return JavaFX string binding for specific translation key with arguments. */ - public static StringBinding formatBy(String format, ObservableValue... args) { + @Nonnull + public static StringBinding formatBy(@Nonnull String format, ObservableValue... args) { return new SynchronizedStringBinding() { { bind(args); @@ -150,7 +156,8 @@ protected synchronized String computeValue() { * * @return JavaFX string binding for specific translation key with arguments. */ - public static StringBinding format(String translationKey, ObservableValue... args) { + @Nonnull + public static StringBinding format(@Nonnull String translationKey, ObservableValue... args) { StringBinding root = getBinding(translationKey); return new SynchronizedStringBinding() { { @@ -174,7 +181,8 @@ protected synchronized String computeValue() { * * @return JavaFX string binding for specific translation key with arguments. */ - public static StringBinding formatLiterals(String translationKey, Object... args) { + @Nonnull + public static StringBinding formatLiterals(@Nonnull String translationKey, Object... args) { StringBinding root = getBinding(translationKey); return new SynchronizedStringBinding() { { @@ -196,7 +204,8 @@ protected synchronized String computeValue() { * * @return JavaFX string binding for specific translation key with arguments. */ - public static StringBinding format(String translationKey, Object... args) { + @Nonnull + public static StringBinding format(@Nonnull String translationKey, Object... args) { StringBinding root = getBinding(translationKey); return new SynchronizedStringBinding() { { @@ -218,7 +227,8 @@ protected synchronized String computeValue() { * * @return JavaFX string binding for specific translation key with arguments. */ - public static StringBinding concat(ObservableValue translation, String... args) { + @Nonnull + public static StringBinding concat(@Nonnull ObservableValue translation, String... args) { return new SynchronizedStringBinding() { { bind(translation); @@ -239,7 +249,8 @@ protected synchronized String computeValue() { * * @return JavaFX string binding for specific translation key with arguments. */ - public static StringBinding concat(String translationKey, String... args) { + @Nonnull + public static StringBinding concat(@Nonnull String translationKey, String... args) { StringBinding root = getBinding(translationKey); return new SynchronizedStringBinding() { { @@ -256,6 +267,7 @@ protected synchronized String computeValue() { /** * @return Translations property. */ + @Nonnull public static StringProperty translationsProperty() { return currentTranslation; } @@ -278,7 +290,8 @@ public static String get(String translationKey) { * * @return Translated value, based on {@link #getCurrentTranslations() current loaded mappings}. */ - public static String get(String translations, String translationKey) { + @Nonnull + public static String get(@Nonnull String translations, @Nonnull String translationKey) { Map map = Lang.translations.getOrDefault(translations, currentTranslationMap); String value = map.get(translationKey); if (value == null) { @@ -331,7 +344,7 @@ public static void initialize() { SelfReferenceUtil.initializeFromContext(Lang.class); SelfReferenceUtil selfReferenceUtil = SelfReferenceUtil.getInstance(); List translations = selfReferenceUtil.getTranslations(); - if (translations.size() > 0) + if (!translations.isEmpty()) logger.debug("Found {} translations", translations.size()); else logger.error("Translations could not be loaded! CodeSource: {}", diff --git a/recaf-ui/src/main/java/software/coley/recaf/util/NodeEvents.java b/recaf-ui/src/main/java/software/coley/recaf/util/NodeEvents.java index 42226763a..04cc0bf2e 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/util/NodeEvents.java +++ b/recaf-ui/src/main/java/software/coley/recaf/util/NodeEvents.java @@ -8,6 +8,8 @@ import javafx.event.EventHandler; import javafx.scene.Node; import javafx.scene.input.KeyEvent; +import javafx.scene.input.MouseEvent; +import software.coley.collections.Unchecked; import java.util.function.BiConsumer; import java.util.function.Consumer; @@ -24,6 +26,39 @@ public class NodeEvents { private NodeEvents() { } + /** + * @param node + * Node to add to. + * @param handler + * Handler to add. + */ + public static void addMousePressHandler(@Nonnull Node node, @Nonnull EventHandler handler) { + Function> original = Node::getOnMousePressed; + addHandler(node, handler, Unchecked.cast(original), Node::setOnMousePressed); + } + + /** + * @param node + * Node to add to. + * @param handler + * Handler to add. + */ + public static void addMouseReleaseHandler(@Nonnull Node node, @Nonnull EventHandler handler) { + Function> original = Node::getOnMouseReleased; + addHandler(node, handler, Unchecked.cast(original), Node::setOnMouseReleased); + } + + /** + * @param node + * Node to add to. + * @param handler + * Handler to add. + */ + public static void addMouseClickHandler(@Nonnull Node node, @Nonnull EventHandler handler) { + Function> original = Node::getOnMouseClicked; + addHandler(node, handler, Unchecked.cast(original), Node::setOnMouseClicked); + } + /** * @param node * Node to add to. @@ -91,15 +126,15 @@ public static void removeKeyTypedHandler(@Nonnull Node node, @Nonnull EventHandl } private static void addHandler(@Nonnull Node node, @Nonnull EventHandler handler, - @Nonnull Function> handlerGetter, - @Nonnull BiConsumer> handlerSetter) { + @Nonnull Function> handlerGetter, + @Nonnull BiConsumer> handlerSetter) { EventHandler oldHandler = handlerGetter.apply(node); handlerSetter.accept(node, new SplittingHandler<>(handler, oldHandler)); } private static void removeHandler(@Nonnull Node node, @Nonnull EventHandler handler, - @Nonnull Function> handlerGetter, - @Nonnull BiConsumer> handlerSetter) { + @Nonnull Function> handlerGetter, + @Nonnull BiConsumer> handlerSetter) { EventHandler currentHandler = Unchecked.cast(handlerGetter.apply(node)); if (currentHandler instanceof SplittingHandler splittingHandler) { if (splittingHandler.primary == handler) @@ -206,7 +241,7 @@ private static class SplittingHandler implements EventHandler primary, - @Nullable EventHandler secondary) { + @Nullable EventHandler secondary) { this.primary = primary; this.secondary = secondary; } diff --git a/recaf-ui/src/main/java/software/coley/recaf/util/SceneUtils.java b/recaf-ui/src/main/java/software/coley/recaf/util/SceneUtils.java index 6a9cc4199..0057518fa 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/util/SceneUtils.java +++ b/recaf-ui/src/main/java/software/coley/recaf/util/SceneUtils.java @@ -1,6 +1,9 @@ package software.coley.recaf.util; +import jakarta.annotation.Nonnull; import jakarta.annotation.Nullable; +import javafx.beans.value.ChangeListener; +import javafx.beans.value.ObservableValue; import javafx.scene.Node; import javafx.scene.Parent; import javafx.scene.Scene; @@ -9,6 +12,10 @@ import software.coley.recaf.ui.docking.DockingRegion; import software.coley.recaf.ui.docking.DockingTab; +import java.util.concurrent.CompletableFuture; +import java.util.function.Consumer; +import java.util.function.Function; + /** * Scene utilities. * @@ -53,6 +60,8 @@ public static void focus(@Nullable Scene scene) { return; Window window = scene.getWindow(); + if (window.isFocused()) return; + if (window instanceof Stage stage) { // If minified, unminify it. stage.setIconified(false); @@ -66,4 +75,95 @@ public static void focus(@Nullable Scene scene) { window.requestFocus(); } + /** + * @param node + * Node to search hierarchy of. + * @param parentType + * Parent type to check for. + * @param + * Parent type. + * + * @return Matching parent node in hierarchy, or {@code null} if nothing matched. + */ + @Nullable + @SuppressWarnings("unchecked") + public static T getParentOfType(@Nonnull Node node, @Nonnull Class parentType) { + Parent parent = node.getParent(); + while (parent != null) { + if (parent.getClass().isAssignableFrom(parentType)) return (T) parent; + parent = parent.getParent(); + } + return null; + } + + /** + * @param node + * Node to search hierarchy of. + * @param parentType + * Parent type to check for. + * @param + * Parent type. + * + * @return Future containing the matching parent node in hierarchy, or {@code null} if nothing matched. + */ + @Nonnull + public static CompletableFuture getParentOfTypeLater(@Nonnull Node node, @Nonnull Class parentType) { + return whenAddedToSceneMap(node, (n) -> getParentOfType(n, parentType)); + } + + /** + * @param node + * Node to initiate query with. + * @param function + * Function taking in the node and yielding some value. To be run when the node has a {@link Scene} associated with it. + * @param + * Function return type. + * @param + * Node type. + * + * @return Future of function lookup. + */ + @Nonnull + public static CompletableFuture whenAddedToSceneMap(@Nonnull N node, @Nonnull Function function) { + // If added to the UI, immediately look up value. + if (node.getScene() != null) return CompletableFuture.completedFuture(function.apply(node)); + + // When there is no scene it is not added to the UI yet. + // We want to wait for it to be added before calling the function. + CompletableFuture future = new CompletableFuture<>(); + node.sceneProperty().addListener(new ChangeListener<>() { + @Override + public void changed(ObservableValue observable, Scene prior, Scene current) { + node.sceneProperty().removeListener(this); + future.complete(function.apply(node)); + } + }); + return future; + } + + /** + * @param node + * Node to initiate query with. + * @param consumer + * Consumer taking in the node. To be run when the node has a {@link Scene} associated with it. + * @param + * Node type. + */ + public static void whenAddedToSceneConsume(@Nonnull N node, @Nonnull Consumer consumer) { + // If added to the UI, immediately call the consumer. + if (node.getScene() != null) { + consumer.accept(node); + return; + } + + // When there is no scene it is not added to the UI yet. + // We want to wait for it to be added before calling the consumer. + node.sceneProperty().addListener(new ChangeListener<>() { + @Override + public void changed(ObservableValue observable, Scene prior, Scene current) { + node.sceneProperty().removeListener(this); + consumer.accept(node); + } + }); + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java index 812786fce..5bd0139b8 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathExportingManager.java @@ -8,20 +8,18 @@ import javafx.stage.DirectoryChooser; import javafx.stage.FileChooser; import javafx.stage.Stage; -import org.kordamp.ikonli.carbonicons.CarbonIcons; import org.slf4j.Logger; import software.coley.observables.ObservableString; import software.coley.recaf.analytics.logging.Logging; +import software.coley.recaf.info.FileInfo; +import software.coley.recaf.info.Info; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.services.workspace.WorkspaceManager; -import software.coley.recaf.ui.config.ExportConfig; -import software.coley.recaf.ui.config.RecentFilesConfig; -import software.coley.recaf.ui.control.FontIconView; -import software.coley.recaf.util.ErrorDialogs; -import software.coley.recaf.util.Icons; -import software.coley.recaf.util.Lang; import software.coley.recaf.services.workspace.io.WorkspaceExportOptions; import software.coley.recaf.services.workspace.io.WorkspaceExporter; +import software.coley.recaf.ui.config.ExportConfig; +import software.coley.recaf.ui.config.RecentFilesConfig; +import software.coley.recaf.util.*; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.resource.WorkspaceDirectoryResource; import software.coley.recaf.workspace.model.resource.WorkspaceFileResource; @@ -33,7 +31,7 @@ import java.nio.file.Path; /** - * Manager module handle exporting {@link Workspace} instances to {@link Path}s. + * Manager module handle exporting {@link Info} and {@link Workspace} instances to {@link Path}s. * * @author Matt Coley */ @@ -46,8 +44,8 @@ public class PathExportingManager { @Inject public PathExportingManager(WorkspaceManager workspaceManager, - ExportConfig exportConfig, - RecentFilesConfig recentFilesConfig) { + ExportConfig exportConfig, + RecentFilesConfig recentFilesConfig) { this.workspaceManager = workspaceManager; this.exportConfig = exportConfig; this.recentFilesConfig = recentFilesConfig; @@ -75,7 +73,7 @@ public void export(@Nonnull Workspace workspace) { // Check if the user hasn't made any changes. Plenty of people have not understood that their changes weren't // saved for one reason or another (the amount of people seeing a red flash thinking that is fine is crazy) WorkspaceResource primaryResource = workspace.getPrimaryResource(); - boolean noChangesFound = exportConfig.getWarnNoChanges().getValue() && primaryResource.bundleStream() + boolean noChangesFound = exportConfig.getWarnNoChanges().getValue() && primaryResource.bundleStreamRecursive() .allMatch(b -> b.getDirtyKeys().isEmpty()); if (noChangesFound) { Alert alert = new Alert(Alert.AlertType.CONFIRMATION, Lang.get("dialog.file.nochanges"), ButtonType.YES, ButtonType.NO); @@ -91,14 +89,16 @@ public void export(@Nonnull Workspace workspace) { File lastExportDir = lastWorkspaceExportDir.unboxingMap(File::new); File selectedPath; if (primaryResource instanceof WorkspaceDirectoryResource) { - DirectoryChooser chooser = new DirectoryChooser(); - chooser.setInitialDirectory(lastExportDir); - chooser.setTitle(Lang.get("dialog.file.export")); + DirectoryChooser chooser = new DirectoryChooserBuilder() + .setInitialDirectory(lastExportDir) + .setTitle(Lang.get("dialog.file.export")) + .build(); selectedPath = chooser.showDialog(null); } else { - FileChooser chooser = new FileChooser(); - chooser.setInitialDirectory(lastExportDir); - chooser.setTitle(Lang.get("dialog.file.export")); + FileChooser chooser = new FileChooserBuilder() + .setInitialDirectory(lastExportDir) + .setTitle(Lang.get("dialog.file.export")) + .build(); selectedPath = chooser.showSaveDialog(null); } @@ -114,21 +114,8 @@ public void export(@Nonnull Workspace workspace) { return; } - // Create export options from the resource type. - WorkspaceExportOptions.CompressType compression = exportConfig.getCompression().getValue(); - WorkspaceExportOptions options; - if (primaryResource instanceof WorkspaceDirectoryResource) { - options = new WorkspaceExportOptions(WorkspaceExportOptions.OutputType.DIRECTORY, exportPath); - } else if (primaryResource instanceof WorkspaceFileResource) { - options = new WorkspaceExportOptions(compression, WorkspaceExportOptions.OutputType.FILE, exportPath); - } else { - options = new WorkspaceExportOptions(compression, WorkspaceExportOptions.OutputType.FILE, exportPath); - } - options.setBundleSupporting(exportConfig.getBundleSupportingResources().getValue()); - options.setCreateZipDirEntries(exportConfig.getCreateZipDirEntries().getValue()); - - // Export the workspace to the selected path. - WorkspaceExporter exporter = workspaceManager.createExporter(options); + // Create export options from the resource type and export the workspace to the selected path. + WorkspaceExporter exporter = createResourceExporter(primaryResource, exportPath); try { exporter.export(workspace); logger.info("Exported workspace to path '{}'", exportPath); @@ -147,16 +134,18 @@ public void export(@Nonnull Workspace workspace) { * Export the given class. * * @param classInfo - * Workspace to export. + * Class to export. */ public void export(@Nonnull JvmClassInfo classInfo) { // Prompt a path for the user to write to. ObservableString lastClassExportDir = recentFilesConfig.getLastClassExportDirectory(); File lastExportDir = lastClassExportDir.unboxingMap(File::new); - FileChooser chooser = new FileChooser(); - chooser.setInitialDirectory(lastExportDir); - chooser.setTitle(Lang.get("dialog.file.export")); - chooser.getExtensionFilters().add(new FileChooser.ExtensionFilter("Java Class", "*.class")); + FileChooser chooser = new FileChooserBuilder() + .setInitialFileName(StringUtil.shortenPath(classInfo.getName()) + ".class") + .setInitialDirectory(lastExportDir) + .setFileExtensionFilter("Java Class", "*.class") + .setTitle(Lang.get("dialog.file.export")) + .build(); File selectedPath = chooser.showSaveDialog(null); // Selected path is null, meaning user closed out of file chooser. @@ -164,14 +153,12 @@ public void export(@Nonnull JvmClassInfo classInfo) { if (selectedPath == null) return; - // Ensure path ends with '.class' - Path exportPath = selectedPath.toPath(); - // Update last export dir for classes. lastClassExportDir.setValue(selectedPath.getParent()); // Write to path. try { + Path exportPath = selectedPath.toPath(); Files.write(exportPath, classInfo.getBytecode()); } catch (IOException ex) { logger.error("Failed to export class to path '{}'", selectedPath, ex); @@ -183,4 +170,57 @@ public void export(@Nonnull JvmClassInfo classInfo) { ); } } + + /** + * Export the given file. + * + * @param fileInfo + * File to export. + */ + public void export(@Nonnull FileInfo fileInfo) { + // Prompt a path for the user to write to. + ObservableString lastClassExportDir = recentFilesConfig.getLastClassExportDirectory(); + File lastExportDir = lastClassExportDir.unboxingMap(File::new); + FileChooser chooser = new FileChooserBuilder() + .setInitialFileName(StringUtil.shortenPath(fileInfo.getName())) + .setInitialDirectory(lastExportDir) + .setTitle(Lang.get("dialog.file.export")) + .build(); + File selectedPath = chooser.showSaveDialog(null); + + // Selected path is null, meaning user closed out of file chooser. + // Cancel export. + if (selectedPath == null) + return; + + // Write to path. + try { + Path exportPath = selectedPath.toPath(); + Files.write(exportPath, fileInfo.getRawContent()); + } catch (IOException ex) { + logger.error("Failed to export file to path '{}'", selectedPath, ex); + ErrorDialogs.show( + Lang.getBinding("dialog.error.exportfile.title"), + Lang.getBinding("dialog.error.exportfile.header"), + Lang.getBinding("dialog.error.exportfile.content"), + ex + ); + } + } + + @Nonnull + private WorkspaceExporter createResourceExporter(@Nonnull WorkspaceResource resource, @Nonnull Path path) { + WorkspaceExportOptions.CompressType compression = exportConfig.getCompression().getValue(); + WorkspaceExportOptions options; + if (resource instanceof WorkspaceDirectoryResource) { + options = new WorkspaceExportOptions(WorkspaceExportOptions.OutputType.DIRECTORY, path); + } else if (resource instanceof WorkspaceFileResource) { + options = new WorkspaceExportOptions(compression, WorkspaceExportOptions.OutputType.FILE, path); + } else { + options = new WorkspaceExportOptions(compression, WorkspaceExportOptions.OutputType.FILE, path); + } + options.setBundleSupporting(exportConfig.getBundleSupportingResources().getValue()); + options.setCreateZipDirEntries(exportConfig.getCreateZipDirEntries().getValue()); + return options.create(); + } } diff --git a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java index 6a7d9a115..883410e58 100644 --- a/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java +++ b/recaf-ui/src/main/java/software/coley/recaf/workspace/PathLoadingManager.java @@ -3,9 +3,12 @@ import jakarta.annotation.Nonnull; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import org.slf4j.Logger; +import software.coley.recaf.analytics.logging.Logging; import software.coley.recaf.services.workspace.WorkspaceManager; -import software.coley.recaf.util.threading.ThreadPoolFactory; import software.coley.recaf.services.workspace.io.ResourceImporter; +import software.coley.recaf.util.CollectionUtil; +import software.coley.recaf.util.threading.ThreadPoolFactory; import software.coley.recaf.workspace.model.BasicWorkspace; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.resource.WorkspaceResource; @@ -14,6 +17,8 @@ import java.nio.file.Path; import java.util.ArrayList; import java.util.List; +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.CopyOnWriteArrayList; import java.util.concurrent.ExecutorService; import java.util.function.Consumer; @@ -25,8 +30,9 @@ */ @ApplicationScoped public class PathLoadingManager { + private static final Logger logger = Logging.get(PathLoadingManager.class); private final ExecutorService loadPool = ThreadPoolFactory.newSingleThreadExecutor("path-loader"); - private final List preLoadListeners = new ArrayList<>(); + private final List preLoadListeners = new CopyOnWriteArrayList<>(); private final WorkspaceManager workspaceManager; private final ResourceImporter resourceImporter; @@ -59,14 +65,19 @@ public void removePreLoadListener(@Nonnull WorkspacePreLoadListener listener) { * Paths to the supporting resource files. * @param errorHandling * Error handling for invalid input. + * + * @return Future of the created workspace. */ - public void asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List supportingPaths, - @Nonnull Consumer errorHandling) { + @Nonnull + public CompletableFuture asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List supportingPaths, + @Nonnull Consumer errorHandling) { // Invoke listeners, new content is being loaded. - for (WorkspacePreLoadListener listener : preLoadListeners) - listener.onPreLoad(primaryPath, supportingPaths); + CollectionUtil.safeForEach(preLoadListeners, + listener -> listener.onPreLoad(primaryPath, supportingPaths), + (listener, t) -> logger.error("Exception thrown opening workspace from '{}'", primaryPath, t)); // Load resources from paths. + CompletableFuture future = new CompletableFuture<>(); loadPool.submit(() -> { try { List supportingResources = new ArrayList<>(); @@ -78,11 +89,14 @@ public void asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List sup // Wrap into workspace and assign it Workspace workspace = new BasicWorkspace(primaryResource, supportingResources); + future.complete(workspace); workspaceManager.setCurrent(workspace); } catch (Throwable t) { + future.completeExceptionally(t); errorHandling.accept(t); } }); + return future; } /** @@ -92,20 +106,29 @@ public void asyncNewWorkspace(@Nonnull Path primaryPath, @Nonnull List sup * Paths to the supporting resource files. * @param errorHandling * Error handling for invalid input. + * + * @return Future of added supporting resources. */ - public void asyncAddSupportingResourcesToWorkspace(@Nonnull Workspace workspace, - @Nonnull List supportingPaths, - @Nonnull Consumer errorHandling) { + @Nonnull + public CompletableFuture> asyncAddSupportingResourcesToWorkspace(@Nonnull Workspace workspace, + @Nonnull List supportingPaths, + @Nonnull Consumer errorHandling) { // Load resources from paths. + CompletableFuture> future = new CompletableFuture<>(); loadPool.submit(() -> { try { + List loadedResources = new ArrayList<>(supportingPaths.size()); for (Path supportingPath : supportingPaths) { WorkspaceResource supportResource = resourceImporter.importResource(supportingPath); + loadedResources.add(supportResource); workspace.addSupportingResource(supportResource); } + future.complete(loadedResources); } catch (IOException ex) { + future.completeExceptionally(ex); errorHandling.accept(ex); } }); + return future; } } diff --git a/recaf-ui/src/main/resources/style/code-editor.css b/recaf-ui/src/main/resources/style/code-editor.css index b4d08d83d..1aacc29c9 100644 --- a/recaf-ui/src/main/resources/style/code-editor.css +++ b/recaf-ui/src/main/resources/style/code-editor.css @@ -74,6 +74,12 @@ Style for line numbers added next to paragraphs. Simply shows what lines text is -fx-background-color: -color-bg-overlay; -fx-max-height: 1000px; } +.code-area .lineno .bg { + /* Used by the line number text label. Giving it a background allows hiding other line number graphics + under the background if they are rendered before the line number graphic. */ + -fx-background-color: -color-bg-overlay; + -fx-background-insets: -2px -2px -2px -10px +} .code-area .lineno .text { -fx-fill: -color-fg-muted; } diff --git a/recaf-ui/src/main/resources/style/tweaks.css b/recaf-ui/src/main/resources/style/tweaks.css index b4c50bcf3..aa0e259a5 100644 --- a/recaf-ui/src/main/resources/style/tweaks.css +++ b/recaf-ui/src/main/resources/style/tweaks.css @@ -3,6 +3,7 @@ -fx-padding: 5px; } + /* Fix for TiwulFX relying on Modena values while we use AtlantaFX */ .adjacent-drop { -fx-background-color: -color-accent-2; @@ -13,6 +14,11 @@ -fx-background-color: -color-bg-inset; } +/* For the file menu items with nested nodes. We don't want any padding so the whole menu is clickable */ +.closable-menu-item { + -fx-padding: 0; +} + /* AtlantaFX tweaks to split-pane default behavior/style to fit the docking UI style more. */ .split-pane { -fx-background-color: transparent; @@ -152,4 +158,21 @@ } .analysis-value-changed { -fx-background-color: -color-accent-muted; -} \ No newline at end of file +} + +.transparent-tree { + -fx-background-color: transparent; + -fx-border-width: 0 0 1 0; +} +.transparent-cell { + -fx-background-color: transparent; +} +.transparent-cell:selected { + -fx-background-color: rgb(54, 58, 65); +} + +/* Removes borders from controls */ +.borderless { + -fx-border-width: 0px; + -fx-border-insets: 0px; + } diff --git a/recaf-ui/src/main/resources/translations/en_US.lang b/recaf-ui/src/main/resources/translations/en_US.lang index 1982a5340..6c1cbb510 100644 --- a/recaf-ui/src/main/resources/translations/en_US.lang +++ b/recaf-ui/src/main/resources/translations/en_US.lang @@ -92,6 +92,10 @@ menu.view.hierarchy=Class hierarchy menu.view.hierarchy.children=Children menu.view.hierarchy.parents=Parents menu.view.methodcfg=Control flow graph +menu.view.methodcallgraph=Call Graph +menu.view.methodcallgraph.calls=Calls +menu.view.methodcallgraph.callers=Callers +menu.view.methodcallgraph.focus=Focus on Method menu.tab.close=Close menu.tab.closeothers=Close others menu.tab.closeall=Close all @@ -274,6 +278,9 @@ dialog.quicknav.tab.commented=Comments dialog.error.exportclass.title=Failed to export class dialog.error.exportclass.header=An error occurred when writing to the destination dialog.error.exportclass.content=The error was: +dialog.error.exportfile.title=Failed to export file +dialog.error.exportfile.header=An error occurred when writing to the destination +dialog.error.exportfile.content=The error was: dialog.error.exportworkspace.title=Failed to export workspace dialog.error.exportworkspace.header=An error occurred when writing to the destination dialog.error.exportworkspace.content=The error was: @@ -468,6 +475,7 @@ tree.defaultdirectory=(Root directory) tree.prompt=Drag your files here tree.hidelibs=Hide libraries tree.phantoms=Generated phantoms +tree.embedded-resources=Embedded ##### Services service=All services @@ -502,6 +510,9 @@ service.assembler.dalvik-assembler-config.simulate-jvm-calls=Simulate common JVM service.assembler.jvm-assembler-config=JVM service.assembler.jvm-assembler-config.value-analysis=Enable value analysis service.assembler.jvm-assembler-config.simulate-jvm-calls=Simulate common JVM calls +service.assembler.flow-lines-config=Control flow lines +service.assembler.flow-lines-config.connection-mode=Line mode +service.assembler.flow-lines-config.render-mode=Render mode service.compile=Compilation service.compile.java-compiler-config=Javac service.compile.java-compiler-config.generate-phantoms=Generate missing classes @@ -631,67 +642,67 @@ service.decompile.impl.decompiler-procyon-config.simplifyMemberReferences=Simpli service.decompile.impl.decompiler-procyon-config.textBlockLineMinimum=Text block minimum lines service.decompile.impl.decompiler-vineflower-config=Vineflower service.decompile.impl.decompiler-vineflower-config.logging-level=Logging level -service.decompile.impl.decompiler-vineflower-config.rbr=Remove bridge methods -service.decompile.impl.decompiler-vineflower-config.rsy=Remove synthetic methods and fields -service.decompile.impl.decompiler-vineflower-config.din=Decompile inner classes -service.decompile.impl.decompiler-vineflower-config.dc4=Decompile Java 4 class references -service.decompile.impl.decompiler-vineflower-config.das=Decompile assertions -service.decompile.impl.decompiler-vineflower-config.hes=Hide empty super() -service.decompile.impl.decompiler-vineflower-config.hdc=Hide default constructor -service.decompile.impl.decompiler-vineflower-config.dgs=Decompile generics -service.decompile.impl.decompiler-vineflower-config.ner=No exceptions in return -service.decompile.impl.decompiler-vineflower-config.esm=Ensure synchronized ranges are complete -service.decompile.impl.decompiler-vineflower-config.den=Decompile enums -service.decompile.impl.decompiler-vineflower-config.dpr=Decompile preview features -service.decompile.impl.decompiler-vineflower-config.rgn=Remove reference getClass() -service.decompile.impl.decompiler-vineflower-config.lit=Keep literals as is -service.decompile.impl.decompiler-vineflower-config.bto=Represent boolean as 0/1 -service.decompile.impl.decompiler-vineflower-config.asc=ASCII string characters -service.decompile.impl.decompiler-vineflower-config.nns=Synthetic not set -service.decompile.impl.decompiler-vineflower-config.uto=Treat undefined param type as object -service.decompile.impl.decompiler-vineflower-config.udv=Use LVT names -service.decompile.impl.decompiler-vineflower-config.ump=Use method parameters -service.decompile.impl.decompiler-vineflower-config.rer=Remove empty try-catch blocks -service.decompile.impl.decompiler-vineflower-config.fdi=Decompile finally blocks -service.decompile.impl.decompiler-vineflower-config.inn=Resugar IntelliJ IDEA @NotNull -service.decompile.impl.decompiler-vineflower-config.lac=Decompile lambdas as anonymous classes -service.decompile.impl.decompiler-vineflower-config.bsm=Bytecode to source mapping -service.decompile.impl.decompiler-vineflower-config.dcl=Dump code lines -service.decompile.impl.decompiler-vineflower-config.iib=Ignore invalid bytecode -service.decompile.impl.decompiler-vineflower-config.vac=Verify anonymous classes -service.decompile.impl.decompiler-vineflower-config.tcs=Ternary constant simplification -service.decompile.impl.decompiler-vineflower-config.pam=Pattern matching -service.decompile.impl.decompiler-vineflower-config.tlf=Try-loop fix -service.decompile.impl.decompiler-vineflower-config.tco=Ternary in if conditions (experimental) -service.decompile.impl.decompiler-vineflower-config.swe=Decompile switch expressions -service.decompile.impl.decompiler-vineflower-config.shs=Show hidden statements (debug) -service.decompile.impl.decompiler-vineflower-config.ovr=Override annotation -service.decompile.impl.decompiler-vineflower-config.ssp=Second pass stack simplification -service.decompile.impl.decompiler-vineflower-config.vvm=Verify variable merges (experimental) -service.decompile.impl.decompiler-vineflower-config.iec=Include entire classpath -service.decompile.impl.decompiler-vineflower-config.jrt=Include Java runtime -service.decompile.impl.decompiler-vineflower-config.ega=Explicit generic arguments -service.decompile.impl.decompiler-vineflower-config.isl=Inline simple lambdas -service.decompile.impl.decompiler-vineflower-config.log=Logging level -service.decompile.impl.decompiler-vineflower-config.mpm=Max processing method -service.decompile.impl.decompiler-vineflower-config.ren=Rename entities / members -service.decompile.impl.decompiler-vineflower-config.urc=User renamer class -service.decompile.impl.decompiler-vineflower-config.nls=New line separator -service.decompile.impl.decompiler-vineflower-config.ind=Indent string -service.decompile.impl.decompiler-vineflower-config.pll=Preferred line length -service.decompile.impl.decompiler-vineflower-config.ban=User renamer class -service.decompile.impl.decompiler-vineflower-config.erm=Error message -service.decompile.impl.decompiler-vineflower-config.thr=Thread count -service.decompile.impl.decompiler-vineflower-config.jvn=Use JAD style variable naming -service.decompile.impl.decompiler-vineflower-config.jpr=Use JAD style parameter naming -service.decompile.impl.decompiler-vineflower-config.sef=Skip extra files -service.decompile.impl.decompiler-vineflower-config.win=Warn about inconsistent inner attributes -service.decompile.impl.decompiler-vineflower-config.dbe=Dump bytecode on error -service.decompile.impl.decompiler-vineflower-config.dee=Dump exceptions on error -service.decompile.impl.decompiler-vineflower-config.dec=Decompiler comments -service.decompile.impl.decompiler-vineflower-config.sfc=Source file comments -service.decompile.impl.decompiler-vineflower-config.dcc=Decompile complex constant-dynamic expressions -service.decompile.impl.decompiler-vineflower-config.fji=Force JSR inline +service.decompile.impl.decompiler-vineflower-config.remove-bridge=Remove Bridge Methods +service.decompile.impl.decompiler-vineflower-config.remove-synthetic=Remove Synthetic Methods And Fields +service.decompile.impl.decompiler-vineflower-config.decompile-inner=Decompile Inner Classes +service.decompile.impl.decompiler-vineflower-config.decompile-java4=Decompile Java 4 class references +service.decompile.impl.decompiler-vineflower-config.decompile-assert=Decompile Assertions +service.decompile.impl.decompiler-vineflower-config.hide-empty-super=Hide Empty super() +service.decompile.impl.decompiler-vineflower-config.hide-default-constructor=Hide Default Constructor +service.decompile.impl.decompiler-vineflower-config.decompile-generics=Decompile Generics +service.decompile.impl.decompiler-vineflower-config.incorporate-returns=Incorporate returns in try-catch blocks +service.decompile.impl.decompiler-vineflower-config.ensure-synchronized-monitors=Ensure synchronized ranges are complete +service.decompile.impl.decompiler-vineflower-config.decompile-enums=Decompile Enums +service.decompile.impl.decompiler-vineflower-config.decompile-preview=Decompile Preview Features +service.decompile.impl.decompiler-vineflower-config.remove-getclass=Remove reference getClass() +service.decompile.impl.decompiler-vineflower-config.keep-literals=Keep Literals As Is +service.decompile.impl.decompiler-vineflower-config.boolean-as-int=Represent boolean as 0/1 +service.decompile.impl.decompiler-vineflower-config.ascii-strings=ASCII String Characters +service.decompile.impl.decompiler-vineflower-config.synthetic-not-set=Synthetic Not Set +service.decompile.impl.decompiler-vineflower-config.undefined-as-object=Treat Undefined Param Type As Object +service.decompile.impl.decompiler-vineflower-config.use-lvt-names=Use LVT Names +service.decompile.impl.decompiler-vineflower-config.use-method-parameters=Use Method Parameters +service.decompile.impl.decompiler-vineflower-config.remove-empty-try-catch=Remove Empty try-catch blocks +service.decompile.impl.decompiler-vineflower-config.decompile-finally=Decompile Finally +service.decompile.impl.decompiler-vineflower-config.lambda-to-anonymous-class=Decompile Lambdas as Anonymous Classes +service.decompile.impl.decompiler-vineflower-config.bytecode-source-mapping=Bytecode to Source Mapping +service.decompile.impl.decompiler-vineflower-config.dump-code-lines=Dump Code Lines +service.decompile.impl.decompiler-vineflower-config.ignore-invalid-bytecode=Ignore Invalid Bytecode +service.decompile.impl.decompiler-vineflower-config.verify-anonymous-classes=Verify Anonymous Classes +service.decompile.impl.decompiler-vineflower-config.ternary-constant-simplification=Ternary Constant Simplification +service.decompile.impl.decompiler-vineflower-config.pattern-matching=Pattern Matching +service.decompile.impl.decompiler-vineflower-config.try-loop-fix=Try-Loop fix +service.decompile.impl.decompiler-vineflower-config.ternary-in-if=[Experimental] Ternary In If Conditions +service.decompile.impl.decompiler-vineflower-config.decompile-switch-expressions=Decompile Switch Expressions +service.decompile.impl.decompiler-vineflower-config.show-hidden-statements=[Debug] Show hidden statements +service.decompile.impl.decompiler-vineflower-config.override-annotation=Override Annotation +service.decompile.impl.decompiler-vineflower-config.simplify-stack=Second-Pass Stack Simplification +service.decompile.impl.decompiler-vineflower-config.verify-merges=[Experimental] Verify Variable Merges +service.decompile.impl.decompiler-vineflower-config.include-classpath=Include Entire Classpath +service.decompile.impl.decompiler-vineflower-config.include-runtime=Include Java Runtime +service.decompile.impl.decompiler-vineflower-config.explicit-generics=Explicit Generic Arguments +service.decompile.impl.decompiler-vineflower-config.inline-simple-lambdas=Inline Simple Lambdas +service.decompile.impl.decompiler-vineflower-config.log-level=Logging Level +service.decompile.impl.decompiler-vineflower-config.max-time-per-method=[DEPRECATED] Max time to process method +service.decompile.impl.decompiler-vineflower-config.rename-members=Rename Members +service.decompile.impl.decompiler-vineflower-config.user-renamer-class=User Renamer Class +service.decompile.impl.decompiler-vineflower-config.new-line-separator=[DEPRECATED] New Line Seperator +service.decompile.impl.decompiler-vineflower-config.indent-string=Indent String +service.decompile.impl.decompiler-vineflower-config.preferred-line-length=Preferred line length +service.decompile.impl.decompiler-vineflower-config.banner=Banner +service.decompile.impl.decompiler-vineflower-config.error-message=Error Message +service.decompile.impl.decompiler-vineflower-config.thread-count=Thread Count +service.decompile.impl.decompiler-vineflower-config.skip-extra-files=Skip Extra Files +service.decompile.impl.decompiler-vineflower-config.warn-inconsistent-inner-attributes=Warn about inconsistent inner attributes +service.decompile.impl.decompiler-vineflower-config.dump-bytecode-on-error=Dump Bytecode On Error +service.decompile.impl.decompiler-vineflower-config.dump-exception-on-error=Dump Exceptions On Error +service.decompile.impl.decompiler-vineflower-config.decompiler-comments=Decompiler Comments +service.decompile.impl.decompiler-vineflower-config.sourcefile-comments=SourceFile comments +service.decompile.impl.decompiler-vineflower-config.decompile-complex-constant-dynamic=Decompile complex constant-dynamic expressions +service.decompile.impl.decompiler-vineflower-config.force-jsr-inline=Force JSR inline +service.decompile.impl.decompiler-vineflower-config.dump-text-tokens=Dump Text Tokens +service.decompile.impl.decompiler-vineflower-config.remove-imports=Remove Imports +service.decompile.impl.decompiler-vineflower-config.mark-corresponding-synthetics=Mark Corresponding Synthetics service.io=IO service.io.directories-config=Directories service.io.export-config=Exporting @@ -761,6 +772,8 @@ number.match.gte-lt=min <= value < max number.match.gt-lte=min < value <= max number.match.gte-lte=min < value <= max number.match.any-of=numbers.contains(value) +string.match.anything=Anything +string.match.zilch=Nothing string.match.contains=str.contains(value) string.match.contains-ic=str.containsIgnoreCase(value) string.match.ends=str.endsWith(value) diff --git a/recaf-ui/src/main/resources/translations/pl_PL.lang b/recaf-ui/src/main/resources/translations/pl_PL.lang new file mode 100644 index 000000000..6986cb1ea --- /dev/null +++ b/recaf-ui/src/main/resources/translations/pl_PL.lang @@ -0,0 +1,783 @@ +menu.analysis=Analizy +menu.analysis.list-comments=Podejrzyj komentarze +menu.analysis.comment=Komentarz +menu.association.override=Nadpisz język +menu.association.none=Brak skonfigurowanych związków +menu.config=Konfiguracja +menu.config.edit=Edytuj +menu.config.export=Eksportuj +menu.config.import=Importuj +menu.file=Plik +menu.file.attach=Połącz z hostem zdalnym +menu.file.addtoworkspace=Dodaj do obszaru roboczego +menu.file.decompileall=Zdekompiluj wszystkie klasy +menu.file.decompileall.path=Ścieżka wyjściowa +menu.file.openworkspace=Otwórz obszar roboczy +menu.file.openurl=Otwórz z URL +menu.file.exportapp=Eksportuj aplikację +menu.file.exportworkspace=Eksportuj konfigurację obszaru roboczego +menu.file.modifications=Pokaż modyfikacje +menu.file.summary=Pokaż podsumowanie +menu.file.recent=Ostatnie +menu.file.close=Zamknij +menu.file.quit=Zakończ +menu.goto.class=Przejdź do klasy +menu.goto.field=Przejdź do pola +menu.goto.method=Przejdź do metody +menu.goto.instruction=Przejdź do instrukcji +menu.goto.file=Przejdź do pliku +menu.goto.label=Przejdź do etykiety +menu.edit=Edytuj +menu.edit.add.field=Dodaj pole +menu.edit.add.method=Dodaj metodę +menu.edit.add.annotation=Dodaj adnotację +menu.edit.remove.field=Usuń pola +menu.edit.remove.method=Usuń metody +menu.edit.remove.annotation=Usuń adnotacje +menu.edit.assemble.class=Edytuj klasę w asemblerze +menu.edit.assemble.field=Edytuj pole w asemblerze +menu.edit.assemble.method=Edytuj metodę w asemblerze +menu.edit.remove=Usuń +menu.edit.copy=Kopiuj +menu.edit.delete=Usuń +menu.edit.noop=Utwórz no-op +menu.edit.changeversion=Zmień wersję klasy +menu.edit.changeversion.up=Uaktualnij +menu.edit.changeversion.down=Wsteczna kompatybilność +menu.export.class=Eksportuj klasę +menu.help=Pomoc +menu.help.discord=Discord +menu.help.docs=Dokumentacja użytkownika online +menu.help.docsdev=Dokumentacja programisty online +menu.help.github=Github +menu.help.issues=Zgłoś problem +menu.help.sysinfo=Informacje o systemie +menu.refactor=Refaktoryzacja +menu.refactor.move=Przenieś +menu.refactor.rename=Zmień nazwę +menu.search=Szukaj +menu.search.string=Ciągi znaków +menu.search.number=Liczby +menu.search.class.member-references=Odwołania do członków +menu.search.class.type-references=Odwołania do typów +menu.search.method-overrides=Nadpisania metod +menu.search.method-references=Odwołania do metod +menu.search.field-references=Odwołania do pól +menu.search.noresults=Brak wyników +menu.mappings=Mapowania +menu.mappings.apply=Zastosuj +menu.mappings.export=Eksportuj +menu.mappings.export.unsupported=%s (Nieobsługiwane) +menu.mappings.generate=Generuj +menu.mappings.view=Aktualne mapowania +menu.scripting=Skrypty +menu.scripting.list=Lista skryptów +menu.scripting.none-found=Nie znaleziono skryptów +menu.scripting.manage=Zarządzaj skryptami +menu.scripting.new=Nowy skrypt +menu.scripting.edit=Edytuj +menu.scripting.browse=Przeglądaj skrypty +menu.scripting.save=Zapisz skrypt +menu.scripting.execute=Wykonaj +menu.scripting.editor=Edytor skryptów +menu.scripting.author=Autor +menu.scripting.version=Wersja +menu.view=Widok +menu.view.hierarchy=Hierarchia klas +menu.view.hierarchy.children=Potomne +menu.view.hierarchy.parents=Nadrzędne +menu.view.methodcfg=Graf przepływu sterowania +menu.tab.close=Zamknij +menu.tab.closeothers=Zamknij pozostałe +menu.tab.closeall=Zamknij wszystkie +menu.tab.copypath=Kopiuj ścieżkę +menu.image.resetscale=Resetuj skalę +menu.image.center=Wyśrodkuj obraz +menu.hex.copyas=Kopiuj jako... +menu.mode=Zmień widok +menu.mode.class.auto=Automatyczny +menu.mode.class.decompile=Dekompiluj +menu.mode.file.auto=Automatyczny +menu.mode.file.text=Tekst +menu.mode.file.hex=Heksadecymalny +menu.mode.diff.decompile=Dekompiluj +menu.mode.diff.disassemble=Dezassembluj +menu.vm=Wirtualizuj +menu.vm.optimize=Optymalizuj +menu.vm.run=Uruchom +menu.plugin=Wtyczki +menu.plugin.manage=Zarządzaj wtyczkami +menu.plugin.installed=Zainstalowane +menu.plugin.remote=Zdalne +menu.plugin.browse=Przeglądaj wtyczki +menu.plugin.enabled=Włączone +menu.plugin.uninstall=Odinstaluj +menu.plugin.uninstall.warning=Czy na pewno chcesz usunąć tę wtyczkę? + +# Skróty klawiszowe +bind.inputprompt.initial= +bind.inputprompt.finish= +bind.editor.find=Znajdź +bind.editor.rename=Zmień nazwę +bind.editor.replace=Zamień +bind.editor.save=Zapisz +bind.quicknav=Szybka nawigacja + +# Teksty dialogów +dialog.cancel=Anuluj +dialog.close=Zamknij +dialog.confirm=Potwierdź +dialog.finish=Zakończ +dialog.next=Dalej +dialog.previous=Wstecz +dialog.dismiss=Odrzuć +dialog.configure=Konfiguruj +dialog.warning=Ostrzeżenie +dialog.restart=Aby zmienić tę opcję konfiguracji, zaleca się ponowne uruchomienie.\nCzy na pewno chcesz zastosować? +dialog.unknownextension=Nieznane rozszerzenie pliku. Czy chcesz skonfigurować powiązanie języka? + +## Szukaj +dialog.search.type=Wpisz nazwę +dialog.search.member-owner=Typ właściciela elementu +dialog.search.member-name=Nazwa elementu +dialog.search.member-descriptor=Deskryptor elementu + +## Wybór pliku +dialog.title.primary=Podstawowy zasób +dialog.title.supporting=Zasoby pomocnicze +dialog.title.nochanges=Czy eksportować bez zmian? +dialog.file.open=Otwórz +dialog.file.open.directory=Katalogi +dialog.file.open.file=Pliki +dialog.file.export=Eksportuj +dialog.file.save=Zapisz +dialog.file.nothing=Nic nie wybrano +dialog.file.nochanges=Czy chcesz eksportować aplikację, mimo że nie wprowadzono żadnych zmian? +dialog.filefilter.any=Dowolny typ +dialog.filefilter.mapping=Mapowania +dialog.filefilter.input=Aplikacje +dialog.filefilter.workspace=Obszary robocze + +## Przenoszenie pliku +dialog.title.create-workspace=Utwórz obszar roboczy +dialog.title.update-workspace=Obsłuż dane wejściowe obszaru roboczego +dialog.title.close-workspace=Zamknąć obszar roboczy? +dialog.option.create-workspace=Utwórz nowy obszar roboczy +dialog.option.update-workspace=Dodaj do obszaru roboczego + +## Kopiuj klasę/plik +dialog.title.copy-class=Kopiuj klasę +dialog.title.copy-directory=Kopiuj katalog +dialog.title.copy-package=Kopiuj pakiet +dialog.title.copy-field=Kopiuj pole +dialog.title.copy-file=Kopiuj plik +dialog.title.copy-method=Kopiuj metodę +dialog.header.copy-class=Podaj nową nazwę dla skskopiowanej klasy. +dialog.header.copy-directory=Podaj nową nazwę dla skopiowanego katalogu. +dialog.header.copy-package=Podaj nową nazwę dla skopiowanego pakietu. +dialog.header.copy-field=Podaj nową nazwę dla skopiowanego pola. +dialog.header.copy-field-error=Nazwa pola już istnieje.\nProszę wybrać inną nazwę. +dialog.header.copy-file=Podaj nową nazwę dla skopiowanego pliku. +dialog.header.copy-method=Podaj nową nazwę dla skopiowanej metody. +dialog.header.copy-method-error=Nazwa metody już istnieje.\nProszę wybrać inną nazwę. + +## Usuń klasę/plik +dialog.title.delete-class=Usuń klasę +dialog.title.delete-directory=Usuń katalog +dialog.title.delete-field=Usuń pole +dialog.title.delete-file=Usuń plik +dialog.title.delete-method=Usuń metodę +dialog.title.delete-package=Usuń pakiet +dialog.title.delete-resource=Usuń zasób +dialog.header.delete-class=Czy na pewno chcesz usunąć: %s? +dialog.header.delete-directory=Czy na pewno chcesz usunąć: %s? +dialog.header.delete-field=Czy na pewno chcesz usunąć: %s? +dialog.header.delete-file=Czy na pewno chcesz usunąć: %s? +dialog.header.delete-method=Czy na pewno chcesz usunąć: %s? +dialog.header.delete-package=Czy na pewno chcesz usunąć: %s? +dialog.header.delete-resource=Czy na pewno chcesz usunąć: %s? + +## Zmień nazwę klasy/pliku +dialog.title.rename-class=Zmień nazwę klasy +dialog.title.rename-class-warning=Ostrzeżenie +dialog.title.rename-directory=Zmień nazwę katalogu +dialog.title.rename-field=Zmień nazwę pola +dialog.title.rename-file=Zmień nazwę pliku +dialog.title.rename-file-warning=Ostrzeżenie +dialog.title.rename-method=Zmień nazwę metody +dialog.title.rename-package=Zmień nazwę pakietu +dialog.header.rename-class=Podaj nową nazwę dla klasy. +dialog.header.rename-class-error=Nazwa klasy już istnieje.\nProszę wybrać inną nazwę. +dialog.header.rename-package=Podaj nową nazwę dla pakietu. +dialog.header.rename-package-error=Nazwa pakietu już istnieje.\nProszę wybrać inną nazwę. +dialog.header.rename-package-warning=Nazwa pakietu już istnieje.\nMoże to spowodować nadpisanie niektórych klas. +dialog.header.rename-directory=Podaj nową nazwę dla katalogu. +dialog.header.rename-directory-error=Nazwa katalogu już istnieje.\nProszę wybrać inną nazwę. +dialog.header.rename-directory-warning=Nazwa katalogu już istnieje.\nMoże to spowodować nadpisanie niektórych plików. +dialog.header.rename-field=Podaj nową nazwę dla pola. +dialog.header.rename-field-error=Nazwa pola już istnieje.\nProszę wybrać inną nazwę. +dialog.header.rename-file=Podaj nową nazwę dla pliku. +dialog.header.rename-file-error=Nazwa pliku już istnieje.\nProszę wybrać inną nazwę. +dialog.header.rename-method=Podaj nową nazwę dla metody. +dialog.header.rename-method-error=Nazwa metody już istnieje.\nProszę wybrać inną nazwę. + +## Przenieś klasę/plik +dialog.title.move-class=Wybierz pakiet docelowy +dialog.title.move-directory=Wybierz katalog docelowy (nadrzędny) +dialog.title.move-file=Wybierz katalog docelowy +dialog.title.move-package=Wybierz pakiet docelowy (nadrzędny) +dialog.header.move-class=Przenieś klasę do nowego pakietu. +dialog.header.move-directory=Przenieś katalog do nowego katalogu nadrzędnego. +dialog.header.move-file=Przenieś plik do nowego katalogu. +dialog.header.move-package=Przenieś pakiet do nowego pakietu nadrzędnego. + +## Dodaj elementy +dialog.title.add-field=Dodaj pole +dialog.title.add-method=Dodaj metodę +dialog.input.name=Nazwa +dialog.input.desc=Deskryptor +dialog.warn.illegal-name=Nieprawidłowa nazwa +dialog.warn.illegal-desc=Nieprawidłowy format deskryptora +dialog.warn.field-conflict=Nazwa pola już istnieje.\nProszę wybrać inną nazwę. +dialog.warn.method-conflict=Nazwa metody już istnieje.\nProszę wybrać inną nazwę. + +## Akcje VM +dialog.title.vm-invoke-args=Wirtualizuj wywołanie metody +dialog.title.vm-peephole-invoke-args=Zoptymalizuj wirtualizowane wywołanie +dialog.vm.execute=Wykonaj +dialog.vm.optimize=Optymalizuj +dialog.vm.create-dummy=Użyj atrapy +dialog.vm.create-null=Użyj null + +## Dialogi heksadecymalne +dialog.hex.title.insertcount=Wstaw +dialog.hex.header.insertcount=Ile bajtów wstawić? + +# Dialog konwertera podstaw +dialog.conv.title.literal=Literał liczbowy +dialog.conv.title.expression=Wyrażenie liczbowe + +## Szybka nawigacja +dialog.quicknav=Szybka nawigacja +dialog.quicknav.tab.classes=Klasy +dialog.quicknav.tab.members=Elementy +dialog.quicknav.tab.files=Pliki +dialog.quicknav.tab.text=Tekst +dialog.quicknav.tab.commented=Komentarze + +## Dialog błędu +dialog.error.exportclass.title=Nie udało się wyeksportować klasy +dialog.error.exportclass.header=Wystąpił błąd podczas zapisu do miejsca docelowego +dialog.error.exportclass.content=Błąd: +dialog.error.exportworkspace.title=Nie udało się wyeksportować obszaru roboczego +dialog.error.exportworkspace.header=Wystąpił błąd podczas zapisu do miejsca docelowego +dialog.error.exportworkspace.content=Błąd: +dialog.error.loadworkspace.title=Nie udało się załadować obszaru roboczego +dialog.error.loadworkspace.header=Wystąpił błąd podczas odczytu z wybranych plików +dialog.error.loadworkspace.content=Błąd: +dialog.error.loadsupport.title=Nie udało się załadować zasobów pomocniczych +dialog.error.loadsupport.header=Wystąpił błąd podczas odczytu z wybranych plików +dialog.error.loadsupport.content=Błąd: +dialog.error.attach.title=Nie udało się dołączyć do JVM +dialog.error.attach.header=Wystąpił błąd podczas łączenia ze zdalną maszyną wirtualną Java +dialog.error.attach.content=Błąd: + +## Kreator +wizard.chooseaction=Wybierz akcję +wizard.selectprimary=Wybierz podstawowy zasób +wizard.currentworkspace=Aktualny obszar roboczy + + +# Panele +## Powitanie +welcome.title=Witaj +welcome.discord.title=Dołącz do Discorda +welcome.discord.description=Recaf ma grupę na Discordzie do szybszych dyskusji i rozmów o programowaniu +welcome.documentation.title=Dokumentacja +welcome.documentation.description=Przeczytaj dokumentację, aby dowiedzieć się, jak korzystać z różnych funkcji Recafa +welcome.github.title=Recaf na Githubie +welcome.github.description=Sprawdź stronę projektu na GitHubie: kod źródłowy, otwarte problemy i więcej + +## Obszar roboczy +workspace.title=Obszar roboczy +workspace.filter-prompt=Filtruj: Nazwa klasy/pliku... +workspace.info=Informacje + +## Dołącz +attach.unsupported=Dołączanie nie powiodło się +attach.unsupported.detail=Agent dołączania nie mógł się samodzielnie wyodrębnić. +attach.connect=Połącz +attach.tab.properties=Właściwości +attach.tab.classloading=Klasy +attach.tab.compilation=Kompilacja +attach.tab.system=System +attach.tab.runtime=Runtime +attach.tab.thread=Wątki + +## Widok zmian +modifications.none=Brak historii modyfikacji dla elementu +modifications.title=Modyfikacje + +## Obszar Javy +java.decompiling=Dekompilowanie klasy... +java.unparsable=OpenRewrite nie zinterpretował źródła, akcje kontekstowe dostępne tylko na karcie bocznej "Pola i metody" +java.parse-state.error=Błąd parsowania +java.parse-state.error-details=Akcje kontekstowe (prawy przycisk myszy) nie są dostępne, ponieważ parsowanie nie powiodło się.\nMożesz w międzyczasie użyć karty bocznej "Pola i metody". +java.parse-state.initial=Trwa parsowanie... +java.parse-state.initial-details=Akcje kontekstowe (prawy przycisk myszy) nie są dostępne, dopóki parsowanie nie zostanie zakończone.\nMożesz w międzyczasie użyć karty bocznej "Pola i metody". +java.parse-state.new-progress=Ponowne parsowanie... +java.parse-state.new-progress-details=Wprowadzono zmiany, więc trwa nowe parsowanie.\nPodczas konstruowania nowego modelu będzie używany stary model.\nMożesz też użyć karty bocznej "Pola i metody". +java.parse-state.none=Brak treści do parsowania +java.decompile-failure=Nie udało się zdekompilować klasy. Kilka opcji:\n- Zmień dekompilatory\n- Otwórz klasę w asemblerze lub innym widoku\n- Spróbuj zdeobfuskowac klasę i spróbować ponownie +java.decompile-failure.brief=Nie udało się zdekompilować klasy +java.savewitherrors=Wygląda na to, że po raz pierwszy wprowadzasz zmiany, które spowodowały błędy.\nZazwyczaj są one wynikiem tego, że zdekompilowany kod nie jest semantycznie poprawnym kodem Java.\nMusisz rozwiązać te błędy, zanim zmiany zostaną zapisane.\n\nKilka sugestii:\n - Zmień dekompilatory\n - Najedź kursorem na czerwone pola błędów lub kliknij pole błędu u góry, aby zobaczyć, jakie są błędy\n - Użyj asemblera zamiast rekompilacji, aby wprowadzić zmiany +java.savewitherrors.title=Dotyczące błędów rekompilacji +java.decompiler=Dekompilator +java.targetversion=Docelowa wersja kompilacji +java.targetversion.auto=Dopasuj do wersji pliku klasy +java.targetdownsampleversion=Obniż wersję docelową +java.targetdownsampleversion.disabled=Wyłączone +java.targetdebug=Kompiluj z informacjami debugowania +java.info=Informacje o klasie +java.info.version=Wersja klasy +java.info.sourcefile=Nazwa pliku źródłowego + +## Pasek wyszukiwania +find.replace=Zamień +find.replaceall=Zamień wszystkie +find.regexinvalid=Nieprawidłowe wyrażenie regularne +find.regexreplace=Tekst zastępczy + +## Pola i metody +fieldsandmethods.title=Pola i metody +fieldsandmethods.showoutlinedtypes=Pokaż typy elementów +fieldsandmethods.showoutlinedsynths=Pokaż elementy syntetyczne (wygenerowane przez kompilator) +fieldsandmethods.showoutlinedvisibility=Filtruj według widoczności elementu +fieldsandmethods.showoutlinedmembertype=Filtruj według typu elementu +fieldsandmethods.outlinedvisibilityiconposition=Pozycja ikony widoczności +fieldsandmethods.sortalphabetically=Sortuj alfabetycznie +fieldsandmethods.sortbyvisibility=Sortuj według widoczności +fieldsandmethods.filter.prompt=Filtruj: Nazwa pola/metody... + +## Hierarchia +hierarchy.title=Dziedziczenie +hierarchy.children=Potomne +hierarchy.parents=Nadrzędne + +## Logowanie +logging.title=Logowanie + +## Asembler +assembler.title=Asembler +assembler.analysis.title=Analiza +assembler.analysis.stack=Stos +assembler.analysis.variables=Zmienne +assembler.analysis.type=Typ +assembler.analysis.value=Wartość +assembler.playground.title=Java do Bytecode +assembler.playground.comment=// Napisz tutaj kod Java, aby automatycznie przekonwertować go na bytecode\n// Możesz uzyskać dostęp do pól/metod bieżącej klasy,\n// oraz parametrów/zmiennych bieżącej metody. +assembler.variables.title=Zadeklarowane zmienne +assembler.variables.name=Nazwa zmiennej +assembler.variables.type=Typ +assembler.variables.usage=Użycia +assembler.variables.value=Wartość +assembler.variables.empty= +assembler.suggestions.none=Brak sugestii + +## Komentarze +comments.search.prompt=Szukaj komentarza... + +## Wyszukiwanie +search.run=Szukaj +search.results=Wyniki +search.text=Zawartość tekstu +search.textmode=Tryb dopasowania tekstu +search.number=Wartość liczbowa +search.numbermode=Tryb dopasowania liczby +search.refowner=Właściciel elementu +search.refname=Nazwa elementu +search.refdesc=Deskryptor typu elementu + +## Pomoc +help.system=System +help.system.sub=Informacje o systemie operacyjnym +help.java=Java +help.java.sub=Informacje o JVM +help.javafx=JavaFX +help.javafx.sub=Informacje o interfejsie użytkownika JavaFX +help.recaf=Recaf +help.recaf.sub=Informacje o Recafie +help.copy=Kopiuj informacje do schowka +help.opendir=Otwórz katalog Recafa + +## Generator mapowania +mapgen=Generator mapowania +mapgen.genimpl=Konwencja nazewnictwa +mapgen.filter.name=Nazwa +mapgen.filter.class-name=Nazwa klasy +mapgen.filter.owner-name=Nazwa właściciela +mapgen.filter.field-name=Nazwa pola +mapgen.filter.method-name=Nazwa metody +mapgen.filters=Filtry +mapgen.filters.add=Dodaj filtr +mapgen.filters.edit=Edytuj zaznaczone +mapgen.filters.editdone=Gotowe +mapgen.filters.delete=Usuń zaznaczone +mapgen.filters.type=Typ filtra +mapgen.filter.modifiers.tooltip=Modyfikatory są oddzielone spacjami +mapgen.filter.excludealreadymapped=Wyklucz już zmapowane +mapgen.filter.excludemodifier=Wyklucz modyfikatory +mapgen.filter.excludeclasses=Wyklucz klasy +mapgen.filter.excludename=Wyklucz nazwy +mapgen.filter.excludeclass=Wyklucz w klasach +mapgen.filter.excludefield=Wyklucz w polach +mapgen.filter.excludemethod=Wyklucz w metodach +mapgen.filter.includemodifier=Uwzględnij modyfikatory +mapgen.filter.includeclass=Uwzględnij w klasach +mapgen.filter.includefield=Uwzględnij w polach +mapgen.filter.includemethod=Uwzględnij w metodach +mapgen.filter.includewhitespacenames=Uwzględnij spacje +mapgen.filter.includenonasciinames=Uwzględnij znaki spoza ASCII +mapgen.filter.includekeywords=Uwzględnij słowa kluczowe +mapgen.filter.includelong=Uwzględnij długie nazwy +mapgen.filter.includename=Uwzględnij nazwy +mapgen.filter.includeclasses=Uwzględnij klasy +mapgen.title.newfilter=Nowy filtr +mapgen.header.newfilter=Wprowadź zawartość filtra +mapgen.preview.empty=Statystyki wygenerowanych mapowań pojawią się tutaj\n\n\n +mapgen.configure=Konfiguruj +mapgen.configure.nothing=Nic do skonfigurowania +mapgen.generate=Generuj +mapgen.apply=Zastosuj + +## Widok mapowania +mapprog=Postęp mapowania +mapprog.metric.size=Rozmiar pliku klasy +mapprog.metric.membercount=Pola i metody klasy + +# Drzewo +tree.classes=Klasy +tree.files=Pliki +tree.defaultpackage=(Pakiet domyślny) +tree.defaultdirectory=(Katalog główny) +tree.prompt=Przeciągnij tutaj swoje pliki +tree.hidelibs=Ukryj biblioteki +tree.phantoms=Wygenerowane fantomy + +# Usługi +service=Wszystkie usługi +service.analysis=Analiza +service.analysis.comments-config=Komentarze +service.analysis.comments-config.enable-display=Wyświetlaj komentarze w dekompilacji +service.analysis.comments-config.word-wrapping-limit=Limit zawijania słów +service.analysis.graph-calls-config=Graf wywołań +service.analysis.graph-calls-config.active=Włącz przy otwieraniu obszarów roboczych +service.analysis.graph-inheritance-config=Graf dziedziczenia +service.analysis.jphantom-generator-config=JPhantom +service.analysis.jphantom-generator-config.generate-workspace-phantoms=Generuj i dołącz fantomy do obszarów roboczych +service.analysis.search-config=Wyszukiwanie +service.analysis.entry-points=Punkty wejścia +service.analysis.entry-points.none=Nie znaleziono wpisów +service.analysis.anti-decompile=Antydekompilacja +service.analysis.anti-decompile.cyclic=Cykliczne klasy dziedziczenia +service.analysis.anti-decompile.duplicate-annos=Zduplikowane adnotacje +service.analysis.anti-decompile.illegal-annos=Nieprawidłowe adnotacje +service.analysis.anti-decompile.illegal-name=Nieprawidłowe nazwy +service.analysis.anti-decompile.illegal-sig=Nieprawidłowe sygnatury +service.analysis.anti-decompile.label-patch=Popraw %d klas +service.analysis.anti-decompile.label-remove=Usuń %d klas +service.analysis.anti-decompile.long-annos=Długie adnotacje +service.assembler=Asembler +service.assembler.assembler-pipeline.general-config=Ogólne +service.assembler.assembler-pipeline.general-config.disassembly-ast-parse-delay=Opóźnienie parsowania AST +service.assembler.assembler-pipeline.general-config.disassembly-indent=Wcięcia +service.assembler.dalvik-assembler-config=Dalvik +service.assembler.dalvik-assembler-config.value-analysis=Włącz analizę wartości +service.assembler.dalvik-assembler-config.simulate-jvm-calls=Symuluj typowe wywołania JVM +service.assembler.jvm-assembler-config=JVM +service.assembler.jvm-assembler-config.value-analysis=Włącz analizę wartości +service.assembler.jvm-assembler-config.simulate-jvm-calls=Symuluj typowe wywołania JVM +service.compile=Kompilacja +service.compile.java-compiler-config=Javac +service.compile.java-compiler-config.generate-phantoms=Generuj brakujące klasy +service.compile.java-compiler-config.default-emit-debug=Domyślnie dołączaj debugowanie +service.compile.java-compiler-config.default-compile-target-version=Domyślna docelowa wersja klasy +service.compile.java-compiler-config.default-downsample-target-version=Domyślna obniżona wersja klasy +service.debug=Dołącz/Debuguj +service.debug.attach-config=Konfiguracja dołączania +service.debug.attach-config.attach-jmx-bean-agent=Dołącz agenta JMX bean +service.debug.attach-config.passive-scanning=Stan skanowania pasywnego +service.config-manager-config=Menedżer konfiguracji +service.decompile=Dekompilacja +service.decompile.decompilers-config=Menedżer dekompilacji +service.decompile.decompilers-config.pref-android-decompiler=Preferowany dekompilator Androida +service.decompile.decompilers-config.pref-jvm-decompiler=Preferowany dekompilator Javy +service.decompile.decompilers-config.cache-decompilations=Buforuj dekompilacje +service.decompile.decompilers-config.filter-annotations-duplicate=Filtruj zduplikowane adnotacje +service.decompile.decompilers-config.filter-annotations-illegal=Filtruj nieprawidłowe adnotacje +service.decompile.decompilers-config.filter-annotations-long=Filtruj długie adnotacje +service.decompile.decompilers-config.filter-annotations-long-limit=Limit długości adnotacji +service.decompile.decompilers-config.filter-hollow=Filtruj zawartość klasy (wydrążona) +service.decompile.decompilers-config.filter-illegal-signatures=Filtruj nieprawidłowe sygnatury +service.decompile.decompilers-config.filter-names-ascii=Filtruj nazwy spoza ASCII +service.decompile.decompilers-config.filter-strip-debug=Filtruj dane debugowania (zmienne, generyki) +service.decompile.impl=Implementacje +service.decompile.impl.decompiler-cfr-config=CFR +service.decompile.impl.decompiler-cfr-config.aexagg=Próbuj bardziej agresywnie rozszerzać i łączyć wyjątki +service.decompile.impl.decompiler-cfr-config.aexagg2=Próbuj bardziej agresywnie rozszerzać i łączyć wyjątki (może zmienić semantykę) +service.decompile.impl.decompiler-cfr-config.aggressivedocopy=Klonuj kod z niemożliwych skoków do pętli z testem 'first' +service.decompile.impl.decompiler-cfr-config.aggressivedoextension=Zwiń niemożliwe skoki do pętli do-while z testem 'first' +service.decompile.impl.decompiler-cfr-config.aggressiveduff=Zwiń przełączniki w stylu urządzenia duff z dodatkową kontrolą. +service.decompile.impl.decompiler-cfr-config.aggressivesizethreshold=Liczba rozkazów, przy której uruchamiane są agresywne redukcje +service.decompile.impl.decompiler-cfr-config.allowmalformedswitch=Zezwalaj na potencjalnie nieprawidłowe instrukcje switch +service.decompile.impl.decompiler-cfr-config.antiobf=Cofnij różne zaciemnienia +service.decompile.impl.decompiler-cfr-config.arrayiter=Przekształć iterację opartą na tablicach +service.decompile.impl.decompiler-cfr-config.collectioniter=Przekształć iterację opartą na kolekcjach +service.decompile.impl.decompiler-cfr-config.commentmonitors=Zastąp monitory komentarzami - przydatne, jeśli jesteśmy całkowicie zdezorientowani +service.decompile.impl.decompiler-cfr-config.comments=Wypisz komentarze opisujące stan dekompilatora, flagi fallback itp. +service.decompile.impl.decompiler-cfr-config.constobf=Cofnij zaciemnianie stałych +service.decompile.impl.decompiler-cfr-config.decodeenumswitch=Przekształć switch na enum +service.decompile.impl.decompiler-cfr-config.decodefinally=Przekształć instrukcje finally +service.decompile.impl.decompiler-cfr-config.decodelambdas=Odtwórz funkcje lambda +service.decompile.impl.decompiler-cfr-config.decodestringswitch=Przekształć switch na String +service.decompile.impl.decompiler-cfr-config.eclipse=Włącz transformacje, aby lepiej obsługiwać kod Eclipse +service.decompile.impl.decompiler-cfr-config.elidescala=Usuń rzeczy, które nie są pomocne w wyjściu scala (serialVersionUID, @ScalaSignature) +service.decompile.impl.decompiler-cfr-config.forbidanonymousclasses=Nie zezwalaj na anonimowe klasy. +service.decompile.impl.decompiler-cfr-config.forbidmethodscopedclasses=Nie zezwalaj na klasy o zasięgu metody. +service.decompile.impl.decompiler-cfr-config.forceclassfilever=Wymuś wersję pliku klasy (a tym samym Java), jako którą klasy są dekompilowane. +service.decompile.impl.decompiler-cfr-config.forcecondpropagate=Przenieś wyniki deterministycznych skoków z powrotem przez niektóre przypisania stałych +service.decompile.impl.decompiler-cfr-config.forceexceptionprune=Usuń zagnieżdżone programy obsługi wyjątków, jeśli nie zmieniają semantyki +service.decompile.impl.decompiler-cfr-config.forcereturningifs=Przenieś return do miejsca skoku +service.decompile.impl.decompiler-cfr-config.forcetopsort=Wymuś sortowanie bloków podstawowych. Zwykle przydatne tylko w przypadku zaciemnienia. +service.decompile.impl.decompiler-cfr-config.forcetopsortaggress=Wymuś dodatkowe agresywne opcje sortowania +service.decompile.impl.decompiler-cfr-config.forcetopsortnopull=Wymuś, aby sortowanie nie wyciągało bloków try +service.decompile.impl.decompiler-cfr-config.forloopaggcapture=Pozwól pętlom for na agresywne przenoszenie mutacji do sekcji aktualizacji, nawet jeśli nie wydają się być związane z predykatem +service.decompile.impl.decompiler-cfr-config.hidebridgemethods=Ukryj metody pomostowe +service.decompile.impl.decompiler-cfr-config.hidelangimports=Ukryj importy z java.lang. +service.decompile.impl.decompiler-cfr-config.hidelongstrings=Ukryj bardzo długie ciągi znaków - przydatne, jeśli zaciemniacze umieścili fałszywy kod w ciągach znaków +service.decompile.impl.decompiler-cfr-config.hideutf=Ukryj znaki UTF8 - umieść je w cudzysłowie zamiast pokazywać surowe znaki +service.decompile.impl.decompiler-cfr-config.ignoreexceptions=Upuść informacje o wyjątku, jeśli całkowicie utkniesz (UWAGA: zmienia semantykę, niebezpieczne!) +service.decompile.impl.decompiler-cfr-config.ignoreexceptionsalways=Upuść informacje o wyjątku (UWAGA: zmienia semantykę, niebezpieczne!) +service.decompile.impl.decompiler-cfr-config.innerclasses=Dekompiluj klasy wewnętrzne +service.decompile.impl.decompiler-cfr-config.instanceofpattern=Przekształć dopasowania wzorców instanceof +service.decompile.impl.decompiler-cfr-config.j14classobj=Odwróć konstrukcję obiektu klasy Java 1.4 +service.decompile.impl.decompiler-cfr-config.labelledblocks=Zezwalaj na emitowanie kodu, który używa oznaczonych bloków (obsługa dziwnych skoków do przodu) +service.decompile.impl.decompiler-cfr-config.lenient=Bądź trochę bardziej pobłażliwy w sytuacjach, w których normalnie wyrzucilibyśmy wyjątek +service.decompile.impl.decompiler-cfr-config.liftconstructorinit=Podnieś kod inicjalizacji wspólny dla wszystkich konstruktorów do inicjalizacji elementów +service.decompile.impl.decompiler-cfr-config.obfattr=Cofnij zaciemnianie atrybutów +service.decompile.impl.decompiler-cfr-config.obfcontrol=Cofnij zaciemnianie przepływu sterowania +service.decompile.impl.decompiler-cfr-config.override=Generuj adnotacje @Override (jeśli metoda implementuje metodę interfejsu lub przesłania metodę klasy bazowej) +service.decompile.impl.decompiler-cfr-config.previewfeatures=Dekompiluj funkcje podglądu, jeśli klasa została skompilowana z 'javac --enable-preview' +service.decompile.impl.decompiler-cfr-config.pullcodecase=Agresywnie wciągaj kod do instrukcji case +service.decompile.impl.decompiler-cfr-config.recordtypes=Przekształć typy rekordów +service.decompile.impl.decompiler-cfr-config.recover=Pozwól na ustawienie coraz bardziej agresywnych opcji, jeśli dekompilacja się nie powiedzie +service.decompile.impl.decompiler-cfr-config.recovertypeclash=Podziel okresy istnienia, w których analiza spowodowała konflikt typów +service.decompile.impl.decompiler-cfr-config.recovertypehints=Odzyskaj wskazówki dotyczące typów dla iteratorów z pierwszego przejścia +service.decompile.impl.decompiler-cfr-config.reducecondscope=Zmniejsz zakres warunków, ewentualnie generując więcej anonimowych bloków +service.decompile.impl.decompiler-cfr-config.relinkconst=Połącz ponownie stałe - jeśli istnieje wbudowane odwołanie do pola, spróbuj je usunąć. +service.decompile.impl.decompiler-cfr-config.relinkconststring=Połącz ponownie stałe ciągi znaków - jeśli istnieje lokalne odwołanie do ciągu znaków, który pasuje do static final, użyj static final. +service.decompile.impl.decompiler-cfr-config.removebadgenerics=Ukryj generyki, w których ewidentnie się pomyliliśmy i wróć do niegenerycznych +service.decompile.impl.decompiler-cfr-config.removeboilerplate=Usuń sztampowe funkcje - sztampowy konstruktor, deserializacja lambda itp. +service.decompile.impl.decompiler-cfr-config.removedeadconditionals=Usuń kod, którego nie można wykonać. +service.decompile.impl.decompiler-cfr-config.removedeadmethods=Usuń bezsensowne metody - domyślny konstruktor itp. +service.decompile.impl.decompiler-cfr-config.removeinnerclasssynthetics=Usuń (jeśli to możliwe) niejawne odwołania do klas zewnętrznych w klasach wewnętrznych +service.decompile.impl.decompiler-cfr-config.renamedupmembers=Zmień nazwę niejednoznacznych/duplikatów pól. +service.decompile.impl.decompiler-cfr-config.renameenumidents=Zmień nazwę identyfikatorów ENUM, które nie pasują do ich "oczekiwanych" nazw ciągów. +service.decompile.impl.decompiler-cfr-config.renameillegalidents=Zmień nazwę identyfikatorów, które nie są prawidłowymi identyfikatorami Java. +service.decompile.impl.decompiler-cfr-config.renamesmallmembers=Zmień nazwę małych elementów. Uwaga - spowoduje to przerwanie dostępu opartego na odbiciu, więc nie jest automatycznie włączone. +service.decompile.impl.decompiler-cfr-config.sealed=Dekompiluj konstrukcje 'sealed' +service.decompile.impl.decompiler-cfr-config.showinferrable=Udekoruj metody jawnymi typami, jeśli nie są one sugerowane przez argumenty +service.decompile.impl.decompiler-cfr-config.showversion=Pokaż używaną wersję CFR w nagłówku (przydatne do wyłączenia podczas testowania regresji) +service.decompile.impl.decompiler-cfr-config.skipbatchinnerclasses=Podczas przetwarzania wielu plików pomiń klasy wewnętrzne, ponieważ i tak będą przetwarzane jako część klas zewnętrznych. +service.decompile.impl.decompiler-cfr-config.staticinitreturn=Spróbuj usunąć return ze statycznej inicjalizacji +service.decompile.impl.decompiler-cfr-config.stringbuffer=Konwertuj new StringBuffer().append.append.append na string + string + string +service.decompile.impl.decompiler-cfr-config.stringbuilder=Konwertuj new StringBuilder().append.append.append na string + string + string +service.decompile.impl.decompiler-cfr-config.stringconcat=Konwertuj użycia StringConcatFactor na string + string + string +service.decompile.impl.decompiler-cfr-config.sugarasserts=Przekształć wywołania assert +service.decompile.impl.decompiler-cfr-config.sugarboxing=W miarę możliwości usuń bezsensowne opakowania boxing +service.decompile.impl.decompiler-cfr-config.sugarenums=Przekształć enumy +service.decompile.impl.decompiler-cfr-config.sugarretrolambda=W miarę możliwości przekształć użycia retro lambda +service.decompile.impl.decompiler-cfr-config.switchexpression=Przekształć wyrażenie switch +service.decompile.impl.decompiler-cfr-config.tidymonitors=Usuń kod pomocniczy dla monitorów - np. bloki catch tylko po to, aby wyjść z monitora +service.decompile.impl.decompiler-cfr-config.tryresources=Rekonstruuj try-with-resources +service.decompile.impl.decompiler-cfr-config.usenametable=Użyj tabeli nazw zmiennych lokalnych, jeśli istnieje +service.decompile.impl.decompiler-cfr-config.usesignatures=Używaj sygnatur oprócz deskryptorów (gdy nie są one ewidentnie niepoprawne) +service.decompile.impl.decompiler-cfr-config.version=Pokaż aktualną wersję CFR +service.decompile.impl.decompiler-procyon-config=Procyon +service.decompile.impl.decompiler-procyon-config.alwaysGenerateExceptionVariableForCatchBlocks=Zawsze generuj blok catch +service.decompile.impl.decompiler-procyon-config.arePreviewFeaturesEnabled=Włącz funkcje podglądu języka +service.decompile.impl.decompiler-vineflower-config=Vineflower +service.decompile.impl.decompiler-vineflower-config.logging-level=Poziom logowania +service.decompile.impl.decompiler-vineflower-config.remove-bridge=Usuń metody pomostowe +service.decompile.impl.decompiler-vineflower-config.remove-synthetic=Usuń metody i pola syntetyczne +service.decompile.impl.decompiler-vineflower-config.decompile-inner=Dekompiluj klasy wewnętrzne +service.decompile.impl.decompiler-vineflower-config.decompile-java4=Dekompiluj odwołania do klas Java 4 +service.decompile.impl.decompiler-vineflower-config.decompile-assert=Dekompiluj asercje +service.decompile.impl.decompiler-vineflower-config.hide-empty-super=Ukryj puste super() +service.decompile.impl.decompiler-vineflower-config.hide-default-constructor=Ukryj domyślny konstruktor +service.decompile.impl.decompiler-vineflower-config.decompile-generics=Dekompiluj generyki +service.decompile.impl.decompiler-vineflower-config.incorporate-returns=Brak wyjątków w return +service.decompile.impl.decompiler-vineflower-config.ensure-synchronized-monitors=Upewnij się, że zakresy synchronizowane są kompletne +service.decompile.impl.decompiler-vineflower-config.decompile-enums=Dekompiluj wyliczenia +service.decompile.impl.decompiler-vineflower-config.decompile-preview=Dekompiluj funkcje podglądu +service.decompile.impl.decompiler-vineflower-config.remove-getclass=Usuń odwołanie getClass() +service.decompile.impl.decompiler-vineflower-config.keep-literals=Zachowaj literały bez zmian +service.decompile.impl.decompiler-vineflower-config.boolean-as-int=Reprezentuj boolean jako 0/1 +service.decompile.impl.decompiler-vineflower-config.ascii-strings=Znaki ASCII w ciągach +service.decompile.impl.decompiler-vineflower-config.synthetic-not-set=Syntetyczne nie ustawione +service.decompile.impl.decompiler-vineflower-config.undefined-as-object=Traktuj niezdefiniowany typ parametru jako obiekt +service.decompile.impl.decompiler-vineflower-config.use-lvt-names=Używaj nazw LVT +service.decompile.impl.decompiler-vineflower-config.use-method-parameters=Używaj parametrów metody +service.decompile.impl.decompiler-vineflower-config.remove-empty-try-catch=Usuń puste bloki try-catch +service.decompile.impl.decompiler-vineflower-config.decompile-finally=Dekompiluj bloki finally +service.decompile.impl.decompiler-vineflower-config.lambda-to-anonymous-class=Dekompiluj lambdy jako klasy anonimowe +service.decompile.impl.decompiler-vineflower-config.bytecode-source-mapping=Mapowanie bytecode na źródło +service.decompile.impl.decompiler-vineflower-config.dump-code-lines=Zrzuć linie kodu +service.decompile.impl.decompiler-vineflower-config.ignore-invalid-bytecode=Ignoruj nieprawidłowy bytecode +service.decompile.impl.decompiler-vineflower-config.verify-anonymous-classes=Weryfikuj klasy anonimowe +service.decompile.impl.decompiler-vineflower-config.ternary-constant-simplification=Uproszczenie stałych trójskładnikowych +service.decompile.impl.decompiler-vineflower-config.pattern-matching=Dopasowywanie wzorców +service.decompile.impl.decompiler-vineflower-config.try-loop-fix=Naprawianie pętli try +service.decompile.impl.decompiler-vineflower-config.ternary-in-if=Trójskładnikowe w warunkach if (eksperymentalne) +service.decompile.impl.decompiler-vineflower-config.decompile-switch-expressions=Dekompiluj wyrażenia switch +service.decompile.impl.decompiler-vineflower-config.show-hidden-statements=Pokaż ukryte instrukcje (debugowanie) +service.decompile.impl.decompiler-vineflower-config.override-annotation=Adnotacja Override +service.decompile.impl.decompiler-vineflower-config.simplify-stack=Uproszczenie stosu w drugim przebiegu +service.decompile.impl.decompiler-vineflower-config.verify-merges=Weryfikuj scalanie zmiennych (eksperymentalne) +service.decompile.impl.decompiler-vineflower-config.include-classpath=Uwzględnij całą ścieżkę klasy +service.decompile.impl.decompiler-vineflower-config.include-runtime=Uwzględnij środowisko wykonawcze Java +service.decompile.impl.decompiler-vineflower-config.explicit-generics=Jawne argumenty generyczne +service.decompile.impl.decompiler-vineflower-config.inline-simple-lambdas=Wstaw proste lambdy +service.decompile.impl.decompiler-vineflower-config.rename-members=Poziom logowania +service.decompile.impl.decompiler-vineflower-config.mpm=Maksymalna liczba przetwarzanych metod +service.decompile.impl.decompiler-vineflower-config.ren=Zmień nazwy jednostek / elementów +service.decompile.impl.decompiler-vineflower-config.user-renamer-class=Klasa zmiany nazw użytkownika +service.decompile.impl.decompiler-vineflower-config.indent-string=Ciąg wcięcia +service.decompile.impl.decompiler-vineflower-config.pll=Preferowana długość linii +service.decompile.impl.decompiler-vineflower-config.banner=Klasa zmiany nazw użytkownika +service.decompile.impl.decompiler-vineflower-config.error-message=Komunikat o błędzie +service.decompile.impl.decompiler-vineflower-config.thread-count=Liczba wątków +service.decompile.impl.decompiler-vineflower-config.skip-extra-files=Pomiń dodatkowe pliki +service.decompile.impl.decompiler-vineflower-config.warn-inconsistent-inner-attributes=Ostrzegaj o niespójnych atrybutach wewnętrznych +service.decompile.impl.decompiler-vineflower-config.dump-bytecode-on-error=Zrzuć bytecode w przypadku błędu +service.decompile.impl.decompiler-vineflower-config.dump-exception-on-error=Zrzuć wyjątki w przypadku błędu +service.decompile.impl.decompiler-vineflower-config.decompiler-comments=Komentarze dekompilatora +service.decompile.impl.decompiler-vineflower-config.sourcefile-comments=Komentarze do pliku źródłowego +service.decompile.impl.decompiler-vineflower-config.decompile-complex-constant-dynamic=Dekompiluj złożone wyrażenia stało-dynamiczne +service.decompile.impl.decompiler-vineflower-config.force-jsr-inline=Wymuś wstawienie JSR +service.io=We/Wy +service.io.directories-config=Katalogi +service.io.export-config=Eksportowanie +service.io.export-config.bundle-supporting-resources=Dołącz zasoby pomocnicze do wyjścia +service.io.export-config.compression=Strategia kompresji dla zawartości wyjścia +service.io.export-config.create-zip-dir-entries=Utwórz wpisy katalogów ZIP w wyjściu +service.io.export-config.warn-no-changes=Ostrzegaj o eksporcie bez wprowadzonych zmian +service.io.gson-provider-config=Json +service.io.gson-provider-config.pretty-print=Ładne drukowanie +service.io.info-importer-config=Importowanie zawartości +service.io.info-importer-config.skip-class-asm-validation=Pomiń poprawki i walidację klasy +service.io.recent-workspaces-config=Ostatnie obszary robocze +service.io.recent-workspaces-config.last-workspace-export-path=Ostatnia ścieżka eksportu obszaru roboczego +service.io.recent-workspaces-config.last-workspace-open-path=Ostatnia ścieżka otwierania obszaru roboczego +service.io.recent-workspaces-config.max-recent-workspaces=Maksymalna liczba ostatnich ścieżek +service.io.recent-workspaces-config.recent-workspaces=Ostatni obszar roboczy +service.io.recent-workspaces-config.last-class-export-path=Ostatnia ścieżka eksportu klasy +service.io.resource-importer-config=Importowanie archiwum +service.io.resource-importer-config.zip-strategy=Strategia parsowania ZIP +service.io.resource-importer-config.skip-revisited-cen-to-local-links=Pomiń duplikaty wpisów CEN-to-LOC ze strategią JVM +service.mapping=Mapowanie +service.mapping.mapping-aggregator-config=Agregacja mapowania +service.mapping.mapping-formats-config=Formaty mapowania +service.mapping.mapping-generator-config=Generator mapowania +service.mapping.name-gen-provider=Generatory nazw +service.mapping.name-gen-provider.alphabet=Alfabet +service.mapping.name-gen-provider.alphabet.alphabet=Znaki alfabetu +service.mapping.name-gen-provider.alphabet.length=Minimalna długość +service.plugin=Wtyczki +service.plugin.plugin-manager-config=Menedżer wtyczek +service.plugin.plugin-manager-config.scan-on-start=Ładuj przy uruchomieniu +service.plugin.script-manager-config=Menedżer skryptów +service.plugin.script-manager-config.file-watching=Pasywnie skanuj katalog skryptów w poszukiwaniu zmian +service.ui=Interfejs użytkownika +service.ui.bind-config=Skróty klawiszowe +service.ui.bind-config.bundle=Pakiet mapy skrótów klawiszowych +service.ui.class-editing-config=Edycja klasy +service.ui.class-editing-config.default-android-editor=Domyślny edytor dla klas Androida +service.ui.class-editing-config.default-jvm-editor=Domyślny edytor dla klas JVM +service.ui.decompile-pane-config=Panel dekompilacji +service.ui.decompile-pane-config.timeout-seconds=Limit czasu dekompilatora (sekundy) +service.ui.decompile-pane-config.mapping-acceleration=Przyspiesz operacje remapowania +service.ui.member-format-config=Format pola i metody +service.ui.member-format-config.name-type-display=Wyświetlanie nazwy i typu +service.ui.text-format-config=Format tekstu +service.ui.text-format-config.escape=Włącz escape'y tekstu +service.ui.text-format-config.max-length=Maksymalna długość wyświetlanego tekstu +service.ui.text-format-config.shorten=Włącz skracanie tekstu +service.ui.file-type-association-config=Powiązania typów plików +service.ui.file-type-association-config.extensions-to-langs=Mapa rozszerzenia do języka +service.ui.window-manager-config=Menedżer okien +service.ui.workspace-explorer-config=Eksplorator obszaru roboczego +service.ui.workspace-explorer-config.drag-drop-action=Zachowanie przeciągnij i upuść +service.ui.workspace-explorer-config.max-tree-dir-depth=Maksymalna głębokość drzewa +service.ui.language-config=Język +service.ui.language-config.current=Aktualny język + +# Tłumaczenia dopasowań +number.match.equal=wartość == n +number.match.not=wartość != n +number.match.gt=wartość > n +number.match.gte=wartość >= n +number.match.lt=wartość < n +number.match.lte=wartość <= n +number.match.gt-lt=min < wartość < max +number.match.gte-lt=min <= wartość < max +number.match.gt-lte=min < wartość <= max +number.match.gte-lte=min < wartość <= max +number.match.any-of=numbers.contains(wartość) +string.match.contains=str.contains(wartość) +string.match.contains-ic=str.containsIgnoreCase(wartość) +string.match.ends=str.endsWith(wartość) +string.match.ends-ic=str.endsWithIgnoreCase(wartość) +string.match.equal=str.equals(wartość) +string.match.equal-ic=str.equalsIgnoreCase(wartość) +string.match.regex-full=str.matches(wartość) +string.match.regex-partial=str.matchesPartially(wartość) +string.match.starts=str.startsWith(wartość) +string.match.starts-ic=str.startsWithIgnoreCase(wartość) + +# Różne +misc.acknowledge=Potwierdź +misc.all=Wszystkie +misc.none=Brak +misc.ignored=Ignorowane +misc.enabled=Włączone +misc.disabled=Wyłączone +misc.download=Pobierz +misc.clear=Wyczyść +misc.export=Eksportuj +misc.casesensitive=Rozróżnianie wielkości liter +misc.path=Ścieżka +misc.regex=Wyrażenie regularne +misc.member.field=Pole +misc.member.method=Metoda +misc.member.field-n-method=Pole i metoda +misc.member.inner-class=Klasa wewnętrzna +misc.member.inner-interface=Interfejs wewnętrzny +misc.member.inner-enum=Wyliczenie wewnętrzne +misc.member.inner-annotation=Adnotacja wewnętrzna +misc.accessflag.visibility.public=Publiczny +misc.accessflag.visibility.protected=Chroniony +misc.accessflag.visibility.private=Prywatny +misc.accessflag.visibility.package=Pakiet +misc.direction.up=W górę +misc.direction.down=W dół +misc.direction.left=W lewo +misc.direction.right=W prawo +misc.direction.top=Góra +misc.direction.bottom=Dół +misc.position.top=Góra +misc.position.bottom=Dół +misc.position.left=Lewo +misc.position.right=Prawo +misc.position.center=Środek +misc.position.middle=Środe \ No newline at end of file diff --git a/recaf-ui/src/main/resources/translations/zh_CN.lang b/recaf-ui/src/main/resources/translations/zh_CN.lang index 77b5ceb9d..8ac3f9383 100644 --- a/recaf-ui/src/main/resources/translations/zh_CN.lang +++ b/recaf-ui/src/main/resources/translations/zh_CN.lang @@ -604,68 +604,63 @@ service.decompile.impl.decompiler-procyon-config.showSyntheticMembers=Show synth service.decompile.impl.decompiler-procyon-config.simplifyMemberReferences=Simplify member references service.decompile.impl.decompiler-procyon-config.textBlockLineMinimum=Text block minimum lines service.decompile.impl.decompiler-vineflower-config=Vineflower -service.decompile.impl.decompiler-vineflower-config.logging-level=Logging level -service.decompile.impl.decompiler-vineflower-config.rbr=Remove bridge methods -service.decompile.impl.decompiler-vineflower-config.rsy=Remove synthetic methods and fields -service.decompile.impl.decompiler-vineflower-config.din=Decompile inner classes -service.decompile.impl.decompiler-vineflower-config.dc4=Decompile Java 4 class references -service.decompile.impl.decompiler-vineflower-config.das=Decompile assertions -service.decompile.impl.decompiler-vineflower-config.hes=Hide empty super() -service.decompile.impl.decompiler-vineflower-config.hdc=Hide default constructor -service.decompile.impl.decompiler-vineflower-config.dgs=Decompile generics -service.decompile.impl.decompiler-vineflower-config.ner=No exceptions in return -service.decompile.impl.decompiler-vineflower-config.esm=Ensure synchronized ranges are complete -service.decompile.impl.decompiler-vineflower-config.den=Decompile enums -service.decompile.impl.decompiler-vineflower-config.dpr=Decompile preview features -service.decompile.impl.decompiler-vineflower-config.rgn=Remove reference getClass() -service.decompile.impl.decompiler-vineflower-config.lit=Keep literals as is -service.decompile.impl.decompiler-vineflower-config.bto=Represent boolean as 0/1 -service.decompile.impl.decompiler-vineflower-config.asc=ASCII string characters -service.decompile.impl.decompiler-vineflower-config.nns=Synthetic not set -service.decompile.impl.decompiler-vineflower-config.uto=Treat undefined param type as object -service.decompile.impl.decompiler-vineflower-config.udv=Use LVT names -service.decompile.impl.decompiler-vineflower-config.ump=Use method parameters -service.decompile.impl.decompiler-vineflower-config.rer=Remove empty try-catch blocks -service.decompile.impl.decompiler-vineflower-config.fdi=Decompile finally blocks -service.decompile.impl.decompiler-vineflower-config.inn=Resugar IntelliJ IDEA @NotNull -service.decompile.impl.decompiler-vineflower-config.lac=Decompile lambdas as anonymous classes -service.decompile.impl.decompiler-vineflower-config.bsm=Bytecode to source mapping -service.decompile.impl.decompiler-vineflower-config.dcl=Dump code lines -service.decompile.impl.decompiler-vineflower-config.iib=Ignore invalid bytecode -service.decompile.impl.decompiler-vineflower-config.vac=Verify anonymous classes -service.decompile.impl.decompiler-vineflower-config.tcs=Ternary constant simplification -service.decompile.impl.decompiler-vineflower-config.pam=Pattern matching -service.decompile.impl.decompiler-vineflower-config.tlf=Try-loop fix -service.decompile.impl.decompiler-vineflower-config.tco=Ternary in if conditions (experimental) -service.decompile.impl.decompiler-vineflower-config.swe=Decompile switch expressions -service.decompile.impl.decompiler-vineflower-config.shs=Show hidden statements (debug) -service.decompile.impl.decompiler-vineflower-config.ovr=Override annotation -service.decompile.impl.decompiler-vineflower-config.ssp=Second pass stack simplification -service.decompile.impl.decompiler-vineflower-config.vvm=Verify variable merges (experimental) -service.decompile.impl.decompiler-vineflower-config.iec=Include entire classpath -service.decompile.impl.decompiler-vineflower-config.jrt=Include Java runtime -service.decompile.impl.decompiler-vineflower-config.ega=Explicit generic arguments -service.decompile.impl.decompiler-vineflower-config.isl=Inline simple lambdas -service.decompile.impl.decompiler-vineflower-config.log=Logging level -service.decompile.impl.decompiler-vineflower-config.mpm=Max processing method -service.decompile.impl.decompiler-vineflower-config.ren=Rename entities / members -service.decompile.impl.decompiler-vineflower-config.urc=User renamer class -service.decompile.impl.decompiler-vineflower-config.nls=New line separator -service.decompile.impl.decompiler-vineflower-config.ind=Indent string -service.decompile.impl.decompiler-vineflower-config.pll=Preferred line length -service.decompile.impl.decompiler-vineflower-config.ban=User renamer class -service.decompile.impl.decompiler-vineflower-config.erm=Error message -service.decompile.impl.decompiler-vineflower-config.thr=Thread count -service.decompile.impl.decompiler-vineflower-config.jvn=Use JAD style variable naming -service.decompile.impl.decompiler-vineflower-config.jpr=Use JAD style parameter naming -service.decompile.impl.decompiler-vineflower-config.sef=Skip extra files -service.decompile.impl.decompiler-vineflower-config.win=Warn about inconsistent inner attributes -service.decompile.impl.decompiler-vineflower-config.dbe=Dump bytecode on error -service.decompile.impl.decompiler-vineflower-config.dee=Dump exceptions on error -service.decompile.impl.decompiler-vineflower-config.dec=Decompiler comments -service.decompile.impl.decompiler-vineflower-config.sfc=Source file comments -service.decompile.impl.decompiler-vineflower-config.dcc=Decompile complex constant-dynamic expressions -service.decompile.impl.decompiler-vineflower-config.fji=Force JSR inline +service.decompile.impl.decompiler-vineflower-config.log-level=Logging Level +service.decompile.impl.decompiler-vineflower-config.remove-bridge=Remove Bridge Methods +service.decompile.impl.decompiler-vineflower-config.remove-synthetic=Remove Synthetic Methods And Fields +service.decompile.impl.decompiler-vineflower-config.decompile-inner=Decompile Inner Classes +service.decompile.impl.decompiler-vineflower-config.decompile-java4=Decompile Java 4 class references +service.decompile.impl.decompiler-vineflower-config.decompile-assert=Decompile Assertions +service.decompile.impl.decompiler-vineflower-config.hide-empty-super=Hide Empty super() +service.decompile.impl.decompiler-vineflower-config.hide-default-constructor=Hide Default Constructor +service.decompile.impl.decompiler-vineflower-config.decompile-generics=Decompile Generics +service.decompile.impl.decompiler-vineflower-config.incorporate-returns=Incorporate returns in try-catch blocks +service.decompile.impl.decompiler-vineflower-config.ensure-synchronized-monitors=Ensure synchronized ranges are complete +service.decompile.impl.decompiler-vineflower-config.decompile-enums=Decompile Enums +service.decompile.impl.decompiler-vineflower-config.decompile-preview=Decompile Preview Features +service.decompile.impl.decompiler-vineflower-config.remove-getclass=Remove reference getClass() +service.decompile.impl.decompiler-vineflower-config.keep-literals=Keep Literals As Is +service.decompile.impl.decompiler-vineflower-config.boolean-as-int=Represent boolean as 0/1 +service.decompile.impl.decompiler-vineflower-config.ascii-strings=ASCII String Characters +service.decompile.impl.decompiler-vineflower-config.synthetic-not-set=Synthetic Not Set +service.decompile.impl.decompiler-vineflower-config.undefined-as-object=Treat Undefined Param Type As Object +service.decompile.impl.decompiler-vineflower-config.use-lvt-names=Use LVT Names +service.decompile.impl.decompiler-vineflower-config.use-method-parameters=Use Method Parameters +service.decompile.impl.decompiler-vineflower-config.remove-empty-try-catch=Remove Empty try-catch blocks +service.decompile.impl.decompiler-vineflower-config.decompile-finally=Decompile Finally +service.decompile.impl.decompiler-vineflower-config.lambda-to-anonymous-class=Decompile Lambdas as Anonymous Classes +service.decompile.impl.decompiler-vineflower-config.bytecode-source-mapping=Bytecode to Source Mapping +service.decompile.impl.decompiler-vineflower-config.dump-code-lines=Dump Code Lines +service.decompile.impl.decompiler-vineflower-config.ignore-invalid-bytecode=Ignore Invalid Bytecode +service.decompile.impl.decompiler-vineflower-config.verify-anonymous-classes=Verify Anonymous Classes +service.decompile.impl.decompiler-vineflower-config.ternary-constant-simplification=Ternary Constant Simplification +service.decompile.impl.decompiler-vineflower-config.pattern-matching=Pattern Matching +service.decompile.impl.decompiler-vineflower-config.try-loop-fix=Try-Loop fix +service.decompile.impl.decompiler-vineflower-config.ternary-in-if=[Experimental] Ternary In If Conditions +service.decompile.impl.decompiler-vineflower-config.decompile-switch-expressions=Decompile Switch Expressions +service.decompile.impl.decompiler-vineflower-config.show-hidden-statements=[Debug] Show hidden statements +service.decompile.impl.decompiler-vineflower-config.override-annotation=Override Annotation +service.decompile.impl.decompiler-vineflower-config.simplify-stack=Second-Pass Stack Simplification +service.decompile.impl.decompiler-vineflower-config.verify-merges=[Experimental] Verify Variable Merges +service.decompile.impl.decompiler-vineflower-config.include-classpath=Include Entire Classpath +service.decompile.impl.decompiler-vineflower-config.include-runtime=Include Java Runtime +service.decompile.impl.decompiler-vineflower-config.explicit-generics=Explicit Generic Arguments +service.decompile.impl.decompiler-vineflower-config.inline-simple-lambdas=Inline Simple Lambdas +service.decompile.impl.decompiler-vineflower-config.rename-members=Rename Members +service.decompile.impl.decompiler-vineflower-config.user-renamer-class=User Renamer Class +service.decompile.impl.decompiler-vineflower-config.indent-string=Indent String +service.decompile.impl.decompiler-vineflower-config.preferred-line-length=Preferred line length +service.decompile.impl.decompiler-vineflower-config.banner=Banner +service.decompile.impl.decompiler-vineflower-config.error-message=Error Message +service.decompile.impl.decompiler-vineflower-config.thread-count=Thread Count +service.decompile.impl.decompiler-vineflower-config.skip-extra-files=Skip Extra Files +service.decompile.impl.decompiler-vineflower-config.warn-inconsistent-inner-attributes=Warn about inconsistent inner attributes +service.decompile.impl.decompiler-vineflower-config.dump-bytecode-on-error=Dump Bytecode On Error +service.decompile.impl.decompiler-vineflower-config.dump-exception-on-error=Dump Exceptions On Error +service.decompile.impl.decompiler-vineflower-config.decompiler-comments=Decompiler Comments +service.decompile.impl.decompiler-vineflower-config.sourcefile-comments=SourceFile comments +service.decompile.impl.decompiler-vineflower-config.decompile-complex-constant-dynamic=Decompile complex constant-dynamic expressions +service.decompile.impl.decompiler-vineflower-config.force-jsr-inline=Force JSR inline +service.decompile.impl.decompiler-vineflower-config.mark-corresponding-synthetics=Mark Corresponding Synthetics service.io=IO service.io.directories-config=目录 service.io.export-config=导出 diff --git a/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java b/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java index ca2bc85c7..4834afab8 100644 --- a/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java +++ b/recaf-ui/src/test/java/software/coley/recaf/ui/control/tree/WorkspaceTreeNodeTest.java @@ -4,6 +4,7 @@ import org.junit.jupiter.api.Nested; import org.junit.jupiter.api.Test; import software.coley.recaf.info.BasicFileInfo; +import software.coley.recaf.info.FileInfo; import software.coley.recaf.info.JvmClassInfo; import software.coley.recaf.info.properties.BasicPropertyContainer; import software.coley.recaf.path.*; @@ -14,11 +15,15 @@ import software.coley.recaf.workspace.model.BasicWorkspace; import software.coley.recaf.workspace.model.Workspace; import software.coley.recaf.workspace.model.bundle.BasicFileBundle; +import software.coley.recaf.workspace.model.bundle.BasicJvmClassBundle; import software.coley.recaf.workspace.model.bundle.JvmClassBundle; +import software.coley.recaf.workspace.model.resource.WorkspaceFileResource; +import software.coley.recaf.workspace.model.resource.WorkspaceFileResourceBuilder; import software.coley.recaf.workspace.model.resource.WorkspaceResource; import software.coley.recaf.workspace.model.resource.WorkspaceResourceBuilder; import java.io.IOException; +import java.util.Map; import java.util.Objects; import static org.junit.jupiter.api.Assertions.*; @@ -31,6 +36,9 @@ class WorkspaceTreeNodeTest { static Workspace workspace; static WorkspaceResource primaryResource; + static WorkspaceResource resourceWithEmbedded; + static WorkspaceFileResource embeddedResource; + static String embeddedResourcePath = "embedded.jar"; static JvmClassBundle primaryJvmBundle; static JvmClassInfo classA; static JvmClassInfo classB; @@ -92,6 +100,56 @@ static void setup() throws IOException { // This is not ideal, but there's not really any great alternatives either. z2 = p3f.child("//"); z1 = z2.child(new BasicFileInfo("///zero.txt", new byte[0], new BasicPropertyContainer())); + + // Embedded resource containing just 'root.txt' + embeddedResource = new WorkspaceFileResourceBuilder(new BasicJvmClassBundle(), fromFiles(default1.getValue())).build(); + resourceWithEmbedded = new WorkspaceResourceBuilder(new BasicJvmClassBundle(), new BasicFileBundle()) + .withEmbeddedResources(Map.of(embeddedResourcePath, embeddedResource)) + .build(); + } + + @Test + void testPathCreationOfFileInEmbeddedResource() { + Workspace workspace = new BasicWorkspace(resourceWithEmbedded); + WorkspacePathNode workspacePath = PathNodes.workspacePath(workspace); + FilePathNode embeddedFilePath = workspacePath.child(resourceWithEmbedded) + .embeddedChildContainer() + .child(embeddedResource) + .child(embeddedResource.getFileBundle()) + .child(null) + .child(default1.getValue()); + + WorkspaceTreeNode root = new WorkspaceTreeNode(workspacePath); + root.getOrCreateNodeByPath(embeddedFilePath); + + // workspace + WorkspaceTreeNode child = root.getFirstChild(); + assertNotNull(child, "Workspace did not have child"); + + // workspace > resourceWithEmbedded + child = child.getFirstChild(); + assertNotNull(child, "Primary resource did not have child"); + + // workspace > resourceWithEmbedded > embedded-container + child = child.getFirstChild(); + assertNotNull(child, "Embedded container did not have child"); + + // workspace > resourceWithEmbedded > embedded-container > embeddedResource + child = child.getFirstChild(); + assertNotNull(child, "Embedded resource did not have child"); + + // workspace > resourceWithEmbedded > embedded-container > embeddedResource > bundle + child = child.getFirstChild(); + assertNotNull(child, "Embedded bundle did not have child"); + + // workspace > resourceWithEmbedded > embedded-container > embeddedResource > bundle > directory + child = child.getFirstChild(); + assertNotNull(child, "Embedded directory did not have child"); + + // workspace > resourceWithEmbedded > embedded-container > embeddedResource > bundle > directory > file + Object createdPathFile = child.getValue().getValue(); + FileInfo file = default1.getValue(); + assertEquals(file, createdPathFile, "File at end of path not the same"); } @Test