From 5efcadbfff5ee1f6098b013dc214156eead44872 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Thu, 27 Feb 2025 13:57:07 +0200 Subject: [PATCH 01/14] Add an import helper and import interface-injected classes when possible --- .../net/neoforged/jst/api/ImportHelper.java | 145 ++++++++++++++++++ .../jst/api/PostProcessReplacer.java | 38 +++++ .../jst/cli/SourceFileProcessor.java | 5 + .../neoforged/jst/cli/ImportHelperTest.java | 132 ++++++++++++++++ .../InjectInterfacesVisitor.java | 16 +- .../expected/net/Example.java | 4 +- .../expected/net/Example2.java | 4 +- .../generics/expected/com/MyTarget.java | 4 +- .../injected_marker/expected/SomeClass.java | 5 +- .../inner_stubs/expected/ExampleClass.java | 5 +- .../com/example/ExampleInterface.java | 4 +- .../com/example/ExampleInterfaceAdditive.java | 4 +- .../expected/MyTarget.java | 5 +- .../expected/net/me/Example.java | 4 +- .../expected/net/me/Example2.java | 4 +- .../stubs/expected/ExampleClass.java | 5 +- 16 files changed, 369 insertions(+), 15 deletions(-) create mode 100644 api/src/main/java/net/neoforged/jst/api/ImportHelper.java create mode 100644 api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java create mode 100644 cli/src/test/java/net/neoforged/jst/cli/ImportHelperTest.java diff --git a/api/src/main/java/net/neoforged/jst/api/ImportHelper.java b/api/src/main/java/net/neoforged/jst/api/ImportHelper.java new file mode 100644 index 0000000..6ae519a --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/ImportHelper.java @@ -0,0 +1,145 @@ +package net.neoforged.jst.api; + +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiImportStatementBase; +import com.intellij.psi.PsiImportStaticStatement; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiModifier; +import com.intellij.psi.PsiPackage; +import org.jetbrains.annotations.Nullable; +import org.jetbrains.annotations.VisibleForTesting; + +import java.util.HashMap; +import java.util.HashSet; +import java.util.Map; +import java.util.Objects; +import java.util.Set; +import java.util.stream.Collectors; + +/** + * Helper class used to import classes while processing a source file. + * @see ImportHelper#get(PsiFile) + */ +public class ImportHelper implements PostProcessReplacer { + private final PsiJavaFile psiFile; + private final Map importedNames; + + private final Set successfulImports = new HashSet<>(); + + public ImportHelper(PsiJavaFile psiFile) { + this.psiFile = psiFile; + + this.importedNames = new HashMap<>(); + + if (psiFile.getPackageStatement() != null) { + var resolved = psiFile.getPackageStatement().getPackageReference().resolve(); + // We cannot import a class with the name of a class in the package of the file + if (resolved instanceof PsiPackage pkg) { + for (PsiClass cls : pkg.getClasses()) { + importedNames.put(cls.getName(), cls.getQualifiedName()); + } + } + } + + if (psiFile.getImportList() != null) { + for (PsiImportStatementBase stmt : psiFile.getImportList().getImportStatements()) { + var res = stmt.resolve(); + if (res instanceof PsiPackage pkg) { + // Wildcard package imports will reserve all names of top-level classes in the package + for (PsiClass cls : pkg.getClasses()) { + importedNames.put(cls.getName(), cls.getQualifiedName()); + } + } else if (res instanceof PsiClass cls) { + importedNames.put(cls.getName(), cls.getQualifiedName()); + } + } + + for (PsiImportStaticStatement stmt : psiFile.getImportList().getImportStaticStatements()) { + var res = stmt.resolve(); + if (res instanceof PsiMethod method) { + importedNames.put(method.getName(), method.getName()); + } else if (res instanceof PsiField fld) { + importedNames.put(fld.getName(), fld.getName()); + } else if (res instanceof PsiClass cls && stmt.isOnDemand()) { + // On-demand imports are static wildcard imports which will reserve the names of + // - all static methods available through the imported class + for (PsiMethod met : cls.getAllMethods()) { + if (met.getModifierList().hasModifierProperty(PsiModifier.STATIC)) { + importedNames.put(met.getName(), met.getName()); + } + } + + // - all fields available through the imported class + for (PsiField fld : cls.getAllFields()) { + if (fld.getModifierList() != null && fld.getModifierList().hasModifierProperty(PsiModifier.STATIC)) { + importedNames.put(fld.getName(), fld.getName()); + } + } + + // - all inner classes available through the imported class directly + for (PsiClass c : cls.getAllInnerClasses()) { + importedNames.put(c.getName(), c.getQualifiedName()); + } + + // Note: to avoid possible issues, none of the above check for visibility. We prefer to be more conservative to make sure the output sources compile + } + } + } + } + + @VisibleForTesting + public boolean canImport(String name) { + return !importedNames.containsKey(name); + } + + /** + * Attempts to import the given fully qualified class name, returning a reference to it which is either + * its short name (if an import is successful) or the qualified name if not. + */ + public synchronized String importClass(String cls) { + var clsByDot = cls.split("\\."); + // We do not try to import classes in the default package or classes already imported + if (clsByDot.length == 1 || successfulImports.contains(cls)) return clsByDot[clsByDot.length - 1]; + var name = clsByDot[clsByDot.length - 1]; + + if (Objects.equals(importedNames.get(name), cls)) { + return name; + } + + if (canImport(name)) { + successfulImports.add(cls); + return name; + } + + return cls; + } + + @Override + public void process(Replacements replacements) { + if (successfulImports.isEmpty()) return; + + var insertion = successfulImports.stream() + .sorted() + .map(s -> "import " + s + ";") + .collect(Collectors.joining("\n")); + + if (psiFile.getImportList() != null && psiFile.getImportList().getLastChild() != null) { + var lastImport = psiFile.getImportList().getLastChild(); + replacements.insertAfter(lastImport, "\n\n" + insertion); + } else { + replacements.insertBefore(psiFile.getClasses()[0], insertion + "\n\n"); + } + } + + @Nullable + public static ImportHelper get(PsiFile file) { + return file instanceof PsiJavaFile j ? get(j) : null; + } + + public static ImportHelper get(PsiJavaFile file) { + return PostProcessReplacer.getOrCreateReplacer(file, ImportHelper.class, k -> new ImportHelper(file)); + } +} diff --git a/api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java b/api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java new file mode 100644 index 0000000..a270acf --- /dev/null +++ b/api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java @@ -0,0 +1,38 @@ +package net.neoforged.jst.api; + +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiFile; +import org.jetbrains.annotations.UnmodifiableView; + +import java.util.Collections; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.function.Function; + +/** + * A replacer linked to a {@link PsiFile} will run and collect replacements after all {@link SourceTransformer transformers} have processed the file. + */ +public interface PostProcessReplacer { + Key, PostProcessReplacer>> REPLACERS = Key.create("jst.post_process_replacers"); + + /** + * Process replacements in the file after {@link SourceTransformer transformers} have processed it. + */ + void process(Replacements replacements); + + @UnmodifiableView + static Map, PostProcessReplacer> getReplacers(PsiFile file) { + var rep = file.getUserData(REPLACERS); + return rep == null ? Map.of() : Collections.unmodifiableMap(rep); + } + + static T getOrCreateReplacer(PsiFile file, Class type, Function creator) { + var rep = file.getUserData(REPLACERS); + if (rep == null) { + rep = new ConcurrentHashMap<>(); + file.putUserData(REPLACERS, rep); + } + //noinspection unchecked + return (T)rep.computeIfAbsent(type, k -> creator.apply(file)); + } +} diff --git a/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java index 5e7dd06..cb8c9f1 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java +++ b/cli/src/main/java/net/neoforged/jst/cli/SourceFileProcessor.java @@ -6,6 +6,7 @@ import net.neoforged.jst.api.FileSink; import net.neoforged.jst.api.FileSource; import net.neoforged.jst.api.Logger; +import net.neoforged.jst.api.PostProcessReplacer; import net.neoforged.jst.api.Replacement; import net.neoforged.jst.api.Replacements; import net.neoforged.jst.api.SourceTransformer; @@ -153,6 +154,10 @@ private byte[] transformSource(VirtualFile contentRoot, FileEntry entry, List T parseSingleElement(@Language("JAVA") String javaCode, Class type) { + var file = ijEnv.parseFileFromMemory("Test.java", javaCode); + + var elements = PsiTreeUtil.collectElementsOfType(file, type); + assertEquals(1, elements.size()); + return elements.iterator().next(); + } +} diff --git a/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/InjectInterfacesVisitor.java b/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/InjectInterfacesVisitor.java index e24e572..f129240 100644 --- a/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/InjectInterfacesVisitor.java +++ b/interfaceinjection/src/main/java/net/neoforged/jst/interfaceinjection/InjectInterfacesVisitor.java @@ -8,6 +8,7 @@ import com.intellij.psi.PsiWhiteSpace; import com.intellij.psi.util.ClassUtil; import com.intellij.util.containers.MultiMap; +import net.neoforged.jst.api.ImportHelper; import net.neoforged.jst.api.Replacements; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -60,6 +61,8 @@ private void inject(PsiClass psiClass, Collection targets) { return; } + var imports = ImportHelper.get(psiClass.getContainingFile()); + var implementsList = psiClass.isInterface() ? psiClass.getExtendsList() : psiClass.getImplementsList(); var implementedInterfaces = Arrays.stream(implementsList.getReferencedTypes()) .map(PsiClassType::resolve) @@ -71,8 +74,8 @@ private void inject(PsiClass psiClass, Collection targets) { .distinct() .map(stubs::createStub) .filter(iface -> !implementedInterfaces.contains(iface.interfaceDeclaration())) - .map(StubStore.InterfaceInformation::toString) - .map(this::decorate) + .map(iface -> possiblyImport(imports, iface)) + .map(iface -> decorate(imports, iface)) .sorted(Comparator.naturalOrder()) .collect(Collectors.joining(", ")); @@ -94,10 +97,15 @@ private void inject(PsiClass psiClass, Collection targets) { } } - private String decorate(String iface) { + private String possiblyImport(@Nullable ImportHelper helper, StubStore.InterfaceInformation info) { + var interfaceName = helper == null ? info.interfaceDeclaration() : helper.importClass(info.interfaceDeclaration()); + return info.generics().isBlank() ? interfaceName : (interfaceName + "<" + info.generics() + ">"); + } + + private String decorate(@Nullable ImportHelper helper, String iface) { if (marker == null) { return iface; } - return "@" + marker + " " + iface; + return "@" + (helper == null ? marker : helper.importClass(marker)) + " " + iface; } } diff --git a/tests/data/interfaceinjection/additive_injection/expected/net/Example.java b/tests/data/interfaceinjection/additive_injection/expected/net/Example.java index 1552f09..6d73e0e 100644 --- a/tests/data/interfaceinjection/additive_injection/expected/net/Example.java +++ b/tests/data/interfaceinjection/additive_injection/expected/net/Example.java @@ -1,4 +1,6 @@ package net; -public class Example implements Runnable, com.example.InjectedInterface { +import com.example.InjectedInterface; + +public class Example implements Runnable, InjectedInterface { } diff --git a/tests/data/interfaceinjection/additive_injection/expected/net/Example2.java b/tests/data/interfaceinjection/additive_injection/expected/net/Example2.java index c1a7710..7766d9d 100644 --- a/tests/data/interfaceinjection/additive_injection/expected/net/Example2.java +++ b/tests/data/interfaceinjection/additive_injection/expected/net/Example2.java @@ -2,5 +2,7 @@ import java.util.*; -public class Example2 implements Runnable, Consumer, com.example.InjectedInterface { +import com.example.InjectedInterface; + +public class Example2 implements Runnable, Consumer, InjectedInterface { } diff --git a/tests/data/interfaceinjection/generics/expected/com/MyTarget.java b/tests/data/interfaceinjection/generics/expected/com/MyTarget.java index 220e439..dbbc084 100644 --- a/tests/data/interfaceinjection/generics/expected/com/MyTarget.java +++ b/tests/data/interfaceinjection/generics/expected/com/MyTarget.java @@ -1,4 +1,6 @@ package com; -public class MyTarget implements com.InjectedGeneric> { +import com.InjectedGeneric; + +public class MyTarget implements InjectedGeneric> { } diff --git a/tests/data/interfaceinjection/injected_marker/expected/SomeClass.java b/tests/data/interfaceinjection/injected_marker/expected/SomeClass.java index 76b9e8b..cd0c44e 100644 --- a/tests/data/interfaceinjection/injected_marker/expected/SomeClass.java +++ b/tests/data/interfaceinjection/injected_marker/expected/SomeClass.java @@ -1,2 +1,5 @@ -public class SomeClass implements @com.markers.InjectedMarker com.example.InjectedInterface { +import com.example.InjectedInterface; +import com.markers.InjectedMarker; + +public class SomeClass implements @InjectedMarker InjectedInterface { } diff --git a/tests/data/interfaceinjection/inner_stubs/expected/ExampleClass.java b/tests/data/interfaceinjection/inner_stubs/expected/ExampleClass.java index b6c1385..d0b871f 100644 --- a/tests/data/interfaceinjection/inner_stubs/expected/ExampleClass.java +++ b/tests/data/interfaceinjection/inner_stubs/expected/ExampleClass.java @@ -1,2 +1,5 @@ -public class ExampleClass implements com.example.InjectedInterface.Inner, com.example.InjectedInterface.Inner.SubInner { +import com.example.InjectedInterface.Inner; +import com.example.InjectedInterface.Inner.SubInner; + +public class ExampleClass implements Inner, SubInner { } diff --git a/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterface.java b/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterface.java index 2f6baad..4f512d2 100644 --- a/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterface.java +++ b/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterface.java @@ -1,4 +1,6 @@ package com.example; -public interface ExampleInterface extends com.example.InjectedInterface { +import com.example.InjectedInterface; + +public interface ExampleInterface extends InjectedInterface { } diff --git a/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterfaceAdditive.java b/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterfaceAdditive.java index c62ac5e..eae8f5f 100644 --- a/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterfaceAdditive.java +++ b/tests/data/interfaceinjection/interface_target/expected/com/example/ExampleInterfaceAdditive.java @@ -1,4 +1,6 @@ package com.example; -public interface ExampleInterfaceAdditive extends Runnable, com.example.InjectedInterface { +import com.example.InjectedInterface; + +public interface ExampleInterfaceAdditive extends Runnable, InjectedInterface { } diff --git a/tests/data/interfaceinjection/multiple_interfaces/expected/MyTarget.java b/tests/data/interfaceinjection/multiple_interfaces/expected/MyTarget.java index a502b5e..1a879b8 100644 --- a/tests/data/interfaceinjection/multiple_interfaces/expected/MyTarget.java +++ b/tests/data/interfaceinjection/multiple_interfaces/expected/MyTarget.java @@ -1,2 +1,5 @@ -public class MyTarget implements com.example.I1, com.example.I2 { +import com.example.I1; +import com.example.I2; + +public class MyTarget implements I1, I2 { } diff --git a/tests/data/interfaceinjection/simple_injection/expected/net/me/Example.java b/tests/data/interfaceinjection/simple_injection/expected/net/me/Example.java index 4ef26d8..17d3c71 100644 --- a/tests/data/interfaceinjection/simple_injection/expected/net/me/Example.java +++ b/tests/data/interfaceinjection/simple_injection/expected/net/me/Example.java @@ -1,4 +1,6 @@ package net.me; -public class Example implements com.example.InjectedInterface { +import com.example.InjectedInterface; + +public class Example implements InjectedInterface { } diff --git a/tests/data/interfaceinjection/simple_injection/expected/net/me/Example2.java b/tests/data/interfaceinjection/simple_injection/expected/net/me/Example2.java index ab2a748..d282a7e 100644 --- a/tests/data/interfaceinjection/simple_injection/expected/net/me/Example2.java +++ b/tests/data/interfaceinjection/simple_injection/expected/net/me/Example2.java @@ -1,4 +1,6 @@ package net.me; -public class Example2 extends Object implements com.example.InjectedInterface { +import com.example.InjectedInterface; + +public class Example2 extends Object implements InjectedInterface { } diff --git a/tests/data/interfaceinjection/stubs/expected/ExampleClass.java b/tests/data/interfaceinjection/stubs/expected/ExampleClass.java index 9cb6328..b5f091f 100644 --- a/tests/data/interfaceinjection/stubs/expected/ExampleClass.java +++ b/tests/data/interfaceinjection/stubs/expected/ExampleClass.java @@ -1,2 +1,5 @@ -public class ExampleClass implements InjectedRootInterface, com.example.II2, com.example.InjectedInterface { +import com.example.II2; +import com.example.InjectedInterface; + +public class ExampleClass implements II2, InjectedInterface, InjectedRootInterface { } From 8aebffc47ca3c91da8aea2c1d1556785f64f2aff Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Thu, 27 Feb 2025 14:05:32 +0200 Subject: [PATCH 02/14] Fix test --- cli/src/test/java/net/neoforged/jst/cli/ImportHelperTest.java | 1 + 1 file changed, 1 insertion(+) diff --git a/cli/src/test/java/net/neoforged/jst/cli/ImportHelperTest.java b/cli/src/test/java/net/neoforged/jst/cli/ImportHelperTest.java index c5d02dc..4f28026 100644 --- a/cli/src/test/java/net/neoforged/jst/cli/ImportHelperTest.java +++ b/cli/src/test/java/net/neoforged/jst/cli/ImportHelperTest.java @@ -106,6 +106,7 @@ class MyClass { package java.lang.annotation; import java.util.*; + import a.b.c.Thing; import com.hello.world.HelloWorld; From ac1a5fc208e24cb510da272b10e7068b5b2cb8d4 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Thu, 27 Feb 2025 15:08:10 +0200 Subject: [PATCH 03/14] Remove needless thread safety --- api/src/main/java/net/neoforged/jst/api/ImportHelper.java | 2 +- .../main/java/net/neoforged/jst/api/PostProcessReplacer.java | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/api/src/main/java/net/neoforged/jst/api/ImportHelper.java b/api/src/main/java/net/neoforged/jst/api/ImportHelper.java index 6ae519a..760bcb2 100644 --- a/api/src/main/java/net/neoforged/jst/api/ImportHelper.java +++ b/api/src/main/java/net/neoforged/jst/api/ImportHelper.java @@ -99,7 +99,7 @@ public boolean canImport(String name) { * Attempts to import the given fully qualified class name, returning a reference to it which is either * its short name (if an import is successful) or the qualified name if not. */ - public synchronized String importClass(String cls) { + public String importClass(String cls) { var clsByDot = cls.split("\\."); // We do not try to import classes in the default package or classes already imported if (clsByDot.length == 1 || successfulImports.contains(cls)) return clsByDot[clsByDot.length - 1]; diff --git a/api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java b/api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java index a270acf..26b5d73 100644 --- a/api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java +++ b/api/src/main/java/net/neoforged/jst/api/PostProcessReplacer.java @@ -5,8 +5,8 @@ import org.jetbrains.annotations.UnmodifiableView; import java.util.Collections; +import java.util.IdentityHashMap; import java.util.Map; -import java.util.concurrent.ConcurrentHashMap; import java.util.function.Function; /** @@ -29,7 +29,7 @@ static Map, PostProcessReplacer> getReplacers(PsiFile file) { static T getOrCreateReplacer(PsiFile file, Class type, Function creator) { var rep = file.getUserData(REPLACERS); if (rep == null) { - rep = new ConcurrentHashMap<>(); + rep = new IdentityHashMap<>(); file.putUserData(REPLACERS, rep); } //noinspection unchecked From 97151509d1684e9ce2791c317fc38ce5591b858c Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Mon, 17 Mar 2025 18:12:05 +0200 Subject: [PATCH 04/14] Initial work on unpick --- .../net/neoforged/jst/api/ImportHelper.java | 2 + cli/build.gradle | 1 + settings.gradle | 1 + tests/data/unpick/const/def.unpick | 15 + .../const/expected/com/example/Constants.java | 9 + .../unpick/const/expected/com/stuff/Uses.java | 21 + .../const/source/com/example/Constants.java | 9 + .../unpick/const/source/com/stuff/Uses.java | 19 + tests/data/unpick/formats/def.unpick | 21 + .../formats/expected/com/example/Example.java | 17 + .../formats/source/com/example/Example.java | 17 + tests/data/unpick/scoped/def.unpick | 11 + .../unpick/scoped/expected/com/Outsider.java | 17 + .../scoped/expected/com/example/Example.java | 12 + .../scoped/expected/com/example/Example2.java | 5 + .../unpick/scoped/source/com/Outsider.java | 17 + .../scoped/source/com/example/Example.java | 12 + .../scoped/source/com/example/Example2.java | 5 + .../net/neoforged/jst/tests/EmbeddedTest.java | 29 + unpick/build.gradle | 7 + .../unpickv3parser/UnpickParseException.java | 17 + .../unpickv3parser/UnpickV3Reader.java | 1232 +++++++++++++++++ .../unpickv3parser/UnpickV3Remapper.java | 240 ++++ .../unpickv3parser/UnpickV3Writer.java | 379 +++++ .../unpickv3parser/tree/DataType.java | 5 + .../unpickv3parser/tree/GroupConstant.java | 13 + .../unpickv3parser/tree/GroupDefinition.java | 35 + .../unpickv3parser/tree/GroupFormat.java | 5 + .../unpickv3parser/tree/GroupScope.java | 41 + .../unpickv3parser/tree/GroupType.java | 5 + .../unpickv3parser/tree/Literal.java | 87 ++ .../unpickv3parser/tree/TargetField.java | 15 + .../unpickv3parser/tree/TargetMethod.java | 28 + .../unpickv3parser/tree/UnpickV3Visitor.java | 12 + .../tree/expr/BinaryExpression.java | 37 + .../tree/expr/CastExpression.java | 23 + .../unpickv3parser/tree/expr/Expression.java | 10 + .../tree/expr/ExpressionTransformer.java | 27 + .../tree/expr/ExpressionVisitor.java | 29 + .../tree/expr/FieldExpression.java | 29 + .../tree/expr/LiteralExpression.java | 24 + .../tree/expr/ParenExpression.java | 19 + .../tree/expr/UnaryExpression.java | 25 + .../net/neoforged/jst/unpick/IntegerType.java | 72 + .../jst/unpick/UnpickCollection.java | 235 ++++ .../neoforged/jst/unpick/UnpickPlugin.java | 16 + .../jst/unpick/UnpickTransformer.java | 71 + .../neoforged/jst/unpick/UnpickVisitor.java | 398 ++++++ ....neoforged.jst.api.SourceTransformerPlugin | 1 + 49 files changed, 3377 insertions(+) create mode 100644 tests/data/unpick/const/def.unpick create mode 100644 tests/data/unpick/const/expected/com/example/Constants.java create mode 100644 tests/data/unpick/const/expected/com/stuff/Uses.java create mode 100644 tests/data/unpick/const/source/com/example/Constants.java create mode 100644 tests/data/unpick/const/source/com/stuff/Uses.java create mode 100644 tests/data/unpick/formats/def.unpick create mode 100644 tests/data/unpick/formats/expected/com/example/Example.java create mode 100644 tests/data/unpick/formats/source/com/example/Example.java create mode 100644 tests/data/unpick/scoped/def.unpick create mode 100644 tests/data/unpick/scoped/expected/com/Outsider.java create mode 100644 tests/data/unpick/scoped/expected/com/example/Example.java create mode 100644 tests/data/unpick/scoped/expected/com/example/Example2.java create mode 100644 tests/data/unpick/scoped/source/com/Outsider.java create mode 100644 tests/data/unpick/scoped/source/com/example/Example.java create mode 100644 tests/data/unpick/scoped/source/com/example/Example2.java create mode 100644 unpick/build.gradle create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java create mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java create mode 100644 unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java create mode 100644 unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java create mode 100644 unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java create mode 100644 unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java create mode 100644 unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java create mode 100644 unpick/src/main/resources/META-INF/services/net.neoforged.jst.api.SourceTransformerPlugin diff --git a/api/src/main/java/net/neoforged/jst/api/ImportHelper.java b/api/src/main/java/net/neoforged/jst/api/ImportHelper.java index 760bcb2..a098a46 100644 --- a/api/src/main/java/net/neoforged/jst/api/ImportHelper.java +++ b/api/src/main/java/net/neoforged/jst/api/ImportHelper.java @@ -101,6 +101,8 @@ public boolean canImport(String name) { */ public String importClass(String cls) { var clsByDot = cls.split("\\."); + if (clsByDot.length == 3 && Objects.equals(clsByDot[0], "java") && Objects.equals(clsByDot[1], "lang")) return clsByDot[2]; // Special case - java.lang classes do not need to be imported + // We do not try to import classes in the default package or classes already imported if (clsByDot.length == 1 || successfulImports.contains(cls)) return clsByDot[clsByDot.length - 1]; var name = clsByDot[clsByDot.length - 1]; diff --git a/cli/build.gradle b/cli/build.gradle index a59db0d..efb6d92 100644 --- a/cli/build.gradle +++ b/cli/build.gradle @@ -34,6 +34,7 @@ dependencies { include project(":parchment") include project(":accesstransformers") include project(':interfaceinjection') + include project(':unpick') testImplementation platform("org.junit:junit-bom:$junit_version") testImplementation 'org.junit.jupiter:junit-jupiter' diff --git a/settings.gradle b/settings.gradle index 7ff6427..9b84342 100644 --- a/settings.gradle +++ b/settings.gradle @@ -38,3 +38,4 @@ include 'parchment' include 'tests' include 'accesstransformers' include 'interfaceinjection' +include 'unpick' diff --git a/tests/data/unpick/const/def.unpick b/tests/data/unpick/const/def.unpick new file mode 100644 index 0000000..ef188cd --- /dev/null +++ b/tests/data/unpick/const/def.unpick @@ -0,0 +1,15 @@ +unpick v3 + +const String + "1.21.4" = com.example.Constants.VERSION + +const strict float + 3.1415927 = java.lang.Math.PI + 1.0471976 = java.lang.Math.PI / 3 + +const float + 2.5 = com.example.Constants.FLOAT_CT + +const long + 34 = com.example.Constants.LONG_VAL + 70 = (com.example.Constants.LONG_VAL + 1) * 2 diff --git a/tests/data/unpick/const/expected/com/example/Constants.java b/tests/data/unpick/const/expected/com/example/Constants.java new file mode 100644 index 0000000..4110cac --- /dev/null +++ b/tests/data/unpick/const/expected/com/example/Constants.java @@ -0,0 +1,9 @@ +package com.example; + +public class Constants { + public static final String VERSION = "1.21.4"; + + public static final float FLOAT_CT = 2.5; + + public static final long LONG_VAL = 34L; +} diff --git a/tests/data/unpick/const/expected/com/stuff/Uses.java b/tests/data/unpick/const/expected/com/stuff/Uses.java new file mode 100644 index 0000000..b14567b --- /dev/null +++ b/tests/data/unpick/const/expected/com/stuff/Uses.java @@ -0,0 +1,21 @@ +package com.stuff; + +import com.example.Constants; + +public class Uses { + public String fld = Constants.VERSION; + + void run() { + String s = Constants.VERSION + "2"; + + float f = Math.PI; + + f = Math.PI / 3; + + double d = 3.1415927d; // PI unpick is strict float so this should not be replaced + + d = Constants.FLOAT_CT; // but the other float unpick isn't so this double literal should be replaced + + System.out.println(Long.toHexString((Constants.LONG_VAL + 1) * 2)); + } +} diff --git a/tests/data/unpick/const/source/com/example/Constants.java b/tests/data/unpick/const/source/com/example/Constants.java new file mode 100644 index 0000000..4110cac --- /dev/null +++ b/tests/data/unpick/const/source/com/example/Constants.java @@ -0,0 +1,9 @@ +package com.example; + +public class Constants { + public static final String VERSION = "1.21.4"; + + public static final float FLOAT_CT = 2.5; + + public static final long LONG_VAL = 34L; +} diff --git a/tests/data/unpick/const/source/com/stuff/Uses.java b/tests/data/unpick/const/source/com/stuff/Uses.java new file mode 100644 index 0000000..6a87a12 --- /dev/null +++ b/tests/data/unpick/const/source/com/stuff/Uses.java @@ -0,0 +1,19 @@ +package com.stuff; + +public class Uses { + public String fld = "1.21.4"; + + void run() { + String s = "1.21.4" + "2"; + + float f = 3.1415927f; + + f = 1.0471976f; + + double d = 3.1415927d; // PI unpick is strict float so this should not be replaced + + d = 2.5d; // but the other float unpick isn't so this double literal should be replaced + + System.out.println(Long.toHexString(70L)); + } +} diff --git a/tests/data/unpick/formats/def.unpick b/tests/data/unpick/formats/def.unpick new file mode 100644 index 0000000..402c80e --- /dev/null +++ b/tests/data/unpick/formats/def.unpick @@ -0,0 +1,21 @@ +unpick v3 + +const int HEXInt + format = hex +target_method com.example.Example acceptHex(I)V + param 0 HEXInt + +const int BINInt + format = binary +target_method com.example.Example acceptBin(I)V + param 0 BINInt + +const int OCTInt + format = octal +target_method com.example.Example acceptOct(I)V + param 0 OCTInt + +const int CharInt + format = char +target_method com.example.Example acceptChar(C)V + param 0 CharInt diff --git a/tests/data/unpick/formats/expected/com/example/Example.java b/tests/data/unpick/formats/expected/com/example/Example.java new file mode 100644 index 0000000..6f935ce --- /dev/null +++ b/tests/data/unpick/formats/expected/com/example/Example.java @@ -0,0 +1,17 @@ +package com.example; + +public class Example { + + void execute() { + acceptHex(0xA505); + acceptBin(0b1010100111010110000); + acceptOct(017350); + acceptChar('d'); + } + + void acceptHex(int hex) {} + void acceptBin(int b) {} + void acceptOct(int oct) {} + + void acceptChar(char c) {} +} diff --git a/tests/data/unpick/formats/source/com/example/Example.java b/tests/data/unpick/formats/source/com/example/Example.java new file mode 100644 index 0000000..fd6484e --- /dev/null +++ b/tests/data/unpick/formats/source/com/example/Example.java @@ -0,0 +1,17 @@ +package com.example; + +public class Example { + + void execute() { + acceptHex(42245); + acceptBin(347824); + acceptOct(7912); + acceptChar(100); + } + + void acceptHex(int hex) {} + void acceptBin(int b) {} + void acceptOct(int oct) {} + + void acceptChar(char c) {} +} diff --git a/tests/data/unpick/scoped/def.unpick b/tests/data/unpick/scoped/def.unpick new file mode 100644 index 0000000..5388b09 --- /dev/null +++ b/tests/data/unpick/scoped/def.unpick @@ -0,0 +1,11 @@ +unpick v3 + +scoped class com.example.Example const int + 472 = com.example.Example.V1 + 84 = com.example.Example.V2 + +scoped method com.Outsider anotherExecute ()V const int + 12 = com.Outsider.DIFFERENT_CONSTANT + +scoped package com.example const int + 4 = com.example.Example.FOUR diff --git a/tests/data/unpick/scoped/expected/com/Outsider.java b/tests/data/unpick/scoped/expected/com/Outsider.java new file mode 100644 index 0000000..f0fde68 --- /dev/null +++ b/tests/data/unpick/scoped/expected/com/Outsider.java @@ -0,0 +1,17 @@ +package com; + +public class Outsider { + private static final int DIFFERENT_CONST = 12; + + public void execute() { + int i = 472; // This should NOT be unpicked to Example.V1 + + int j = 12; // This should NOT be unpicked to DIFFERENT_CONST + + int k = 4; // This should NOT be unpicked to Example.FOUR since it's outside the package + } + + public void anotherExecute() { + int i = Outsider.DIFFERENT_CONSTANT; // This should be replaced with DIFFERENT_CONST + } +} diff --git a/tests/data/unpick/scoped/expected/com/example/Example.java b/tests/data/unpick/scoped/expected/com/example/Example.java new file mode 100644 index 0000000..a94273b --- /dev/null +++ b/tests/data/unpick/scoped/expected/com/example/Example.java @@ -0,0 +1,12 @@ +package com.example; + +public class Example { + private static final int V1 = 472, V2 = 84; + static final int FOUR = 4; + + void execute() { + System.out.println(Example.V1); + + System.out.println(Example.V2); + } +} diff --git a/tests/data/unpick/scoped/expected/com/example/Example2.java b/tests/data/unpick/scoped/expected/com/example/Example2.java new file mode 100644 index 0000000..d091542 --- /dev/null +++ b/tests/data/unpick/scoped/expected/com/example/Example2.java @@ -0,0 +1,5 @@ +package com.example; + +public class Example2 { + private final int fld = Example.FOUR; +} diff --git a/tests/data/unpick/scoped/source/com/Outsider.java b/tests/data/unpick/scoped/source/com/Outsider.java new file mode 100644 index 0000000..c6fa2d7 --- /dev/null +++ b/tests/data/unpick/scoped/source/com/Outsider.java @@ -0,0 +1,17 @@ +package com; + +public class Outsider { + private static final int DIFFERENT_CONST = 12; + + public void execute() { + int i = 472; // This should NOT be unpicked to Example.V1 + + int j = 12; // This should NOT be unpicked to DIFFERENT_CONST + + int k = 4; // This should NOT be unpicked to Example.FOUR since it's outside the package + } + + public void anotherExecute() { + int i = 12; // This should be replaced with DIFFERENT_CONST + } +} diff --git a/tests/data/unpick/scoped/source/com/example/Example.java b/tests/data/unpick/scoped/source/com/example/Example.java new file mode 100644 index 0000000..19f68fe --- /dev/null +++ b/tests/data/unpick/scoped/source/com/example/Example.java @@ -0,0 +1,12 @@ +package com.example; + +public class Example { + private static final int V1 = 472, V2 = 84; + static final int FOUR = 4; + + void execute() { + System.out.println(472); + + System.out.println(84); + } +} diff --git a/tests/data/unpick/scoped/source/com/example/Example2.java b/tests/data/unpick/scoped/source/com/example/Example2.java new file mode 100644 index 0000000..5a63ce2 --- /dev/null +++ b/tests/data/unpick/scoped/source/com/example/Example2.java @@ -0,0 +1,5 @@ +package com.example; + +public class Example2 { + private final int fld = 4; +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java index 842fc84..591e208 100644 --- a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -340,6 +340,24 @@ void testGenerics() throws Exception { } } + @Nested + class Unpick { + @Test + void testConst() throws Exception { + runUnpickTest("const"); + } + + @Test + void testFormats() throws Exception { + runUnpickTest("formats"); + } + + @Test + void testScoped() throws Exception { + runUnpickTest("scoped"); + } + } + protected final void runInterfaceInjectionTest(String testDirName, Path tempDir, String... additionalArgs) throws Exception { var stub = tempDir.resolve("jst-" + testDirName + "-stub.jar"); testDirName = "interfaceinjection/" + testDirName; @@ -356,6 +374,17 @@ protected final void runInterfaceInjectionTest(String testDirName, Path tempDir, } } + protected final void runUnpickTest(String testDirName, String... additionalArgs) throws Exception { + testDirName = "unpick/" + testDirName; + var testDir = testDataRoot.resolve(testDirName); + var inputPath = testDir.resolve("def.unpick"); + + var args = new ArrayList<>(Arrays.asList("--enable-unpick", "--unpick-data", inputPath.toString())); + args.addAll(Arrays.asList(additionalArgs)); + + runTest(testDirName, UnaryOperator.identity(), args.toArray(String[]::new)); + } + protected final void runATTest(String testDirName, final String... extraArgs) throws Exception { testDirName = "accesstransformer/" + testDirName; var atPath = testDataRoot.resolve(testDirName).resolve("accesstransformer.cfg"); diff --git a/unpick/build.gradle b/unpick/build.gradle new file mode 100644 index 0000000..6032fe2 --- /dev/null +++ b/unpick/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'java-library' +} + +dependencies { + implementation project(':api') +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java new file mode 100644 index 0000000..fff2402 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java @@ -0,0 +1,17 @@ +package net.earthcomputer.unpickv3parser; + +import java.io.IOException; + +/** + * Thrown when a syntax error is found in a .unpick file. + */ +public class UnpickParseException extends IOException { + public final int line; + public final int column; + + public UnpickParseException(String message, int line, int column) { + super(line + ":" + column + ": " + message); + this.line = line; + this.column = column; + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java new file mode 100644 index 0000000..336889a --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java @@ -0,0 +1,1232 @@ +package net.earthcomputer.unpickv3parser; + +import net.earthcomputer.unpickv3parser.tree.DataType; +import net.earthcomputer.unpickv3parser.tree.GroupConstant; +import net.earthcomputer.unpickv3parser.tree.GroupDefinition; +import net.earthcomputer.unpickv3parser.tree.GroupFormat; +import net.earthcomputer.unpickv3parser.tree.GroupScope; +import net.earthcomputer.unpickv3parser.tree.GroupType; +import net.earthcomputer.unpickv3parser.tree.Literal; +import net.earthcomputer.unpickv3parser.tree.TargetField; +import net.earthcomputer.unpickv3parser.tree.TargetMethod; +import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; +import net.earthcomputer.unpickv3parser.tree.expr.BinaryExpression; +import net.earthcomputer.unpickv3parser.tree.expr.CastExpression; +import net.earthcomputer.unpickv3parser.tree.expr.Expression; +import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; +import net.earthcomputer.unpickv3parser.tree.expr.LiteralExpression; +import net.earthcomputer.unpickv3parser.tree.expr.ParenExpression; +import net.earthcomputer.unpickv3parser.tree.expr.UnaryExpression; +import org.jetbrains.annotations.Nullable; + +import java.io.IOException; +import java.io.LineNumberReader; +import java.io.Reader; +import java.util.ArrayList; +import java.util.EnumMap; +import java.util.HashMap; +import java.util.List; +import java.util.Map; +import java.util.Stack; + +/** + * Performs syntax checking and basic semantic checking on .unpick v3 format text, and allows its structure to be + * visited by instances of {@link UnpickV3Visitor}. + */ +public final class UnpickV3Reader implements AutoCloseable { + private static final int MAX_PARSE_DEPTH = 64; + private static final EnumMap PRECEDENCES = new EnumMap<>(BinaryExpression.Operator.class); + static { + PRECEDENCES.put(BinaryExpression.Operator.BIT_OR, 0); + PRECEDENCES.put(BinaryExpression.Operator.BIT_XOR, 1); + PRECEDENCES.put(BinaryExpression.Operator.BIT_AND, 2); + PRECEDENCES.put(BinaryExpression.Operator.BIT_SHIFT_LEFT, 3); + PRECEDENCES.put(BinaryExpression.Operator.BIT_SHIFT_RIGHT, 3); + PRECEDENCES.put(BinaryExpression.Operator.BIT_SHIFT_RIGHT_UNSIGNED, 3); + PRECEDENCES.put(BinaryExpression.Operator.ADD, 4); + PRECEDENCES.put(BinaryExpression.Operator.SUBTRACT, 4); + PRECEDENCES.put(BinaryExpression.Operator.MULTIPLY, 5); + PRECEDENCES.put(BinaryExpression.Operator.DIVIDE, 5); + PRECEDENCES.put(BinaryExpression.Operator.MODULO, 5); + } + + private final LineNumberReader reader; + private String line; + private int column; + private int lastTokenLine; + private int lastTokenColumn; + private TokenType lastTokenType; + private String nextToken; + private ParseState nextTokenState; + private String nextToken2; + private ParseState nextToken2State; + + public UnpickV3Reader(Reader reader) { + this.reader = new LineNumberReader(reader); + } + + public void accept(UnpickV3Visitor visitor) throws IOException { + line = reader.readLine(); + if (!"unpick v3".equals(line)) { + throw parseError("Missing version marker", 1, 0); + } + column = line.length(); + + nextToken(); // newline + + while (true) { + String token = nextToken(); + if (lastTokenType == TokenType.EOF) { + break; + } + parseUnpickItem(visitor, token); + } + } + + private void parseUnpickItem(UnpickV3Visitor visitor, String token) throws IOException { + if (lastTokenType != TokenType.IDENTIFIER) { + throw expectedTokenError("unpick item", token); + } + + switch (token) { + case "target_field": + visitor.visitTargetField(parseTargetField()); + break; + case "target_method": + visitor.visitTargetMethod(parseTargetMethod()); + break; + case "scoped": + GroupScope scope = parseGroupScope(); + token = nextToken("group type", TokenType.IDENTIFIER); + switch (token) { + case "const": + visitor.visitGroupDefinition(parseGroupDefinition(scope, GroupType.CONST)); + break; + case "flag": + visitor.visitGroupDefinition(parseGroupDefinition(scope, GroupType.FLAG)); + break; + default: + throw expectedTokenError("group type", token); + } + break; + case "const": + visitor.visitGroupDefinition(parseGroupDefinition(GroupScope.Global.INSTANCE, GroupType.CONST)); + break; + case "flag": + visitor.visitGroupDefinition(parseGroupDefinition(GroupScope.Global.INSTANCE, GroupType.FLAG)); + break; + default: + throw expectedTokenError("unpick item", token); + } + } + + private GroupScope parseGroupScope() throws IOException { + String token = nextToken("group scope type", TokenType.IDENTIFIER); + switch (token) { + case "package": + return new GroupScope.Package(parseClassName("package name")); + case "class": + return new GroupScope.Class(parseClassName()); + case "method": + String className = parseClassName(); + String methodName = parseMethodName(); + String methodDesc = nextToken(TokenType.METHOD_DESCRIPTOR); + return new GroupScope.Method(className, methodName, methodDesc); + default: + throw expectedTokenError("group scope type", token); + } + } + + private GroupDefinition parseGroupDefinition(GroupScope scope, GroupType type) throws IOException { + int typeLine = lastTokenLine; + int typeColumn = lastTokenColumn; + + boolean strict = false; + if ("strict".equals(peekToken())) { + nextToken(); + strict = true; + } + + DataType dataType = parseDataType(); + if (!isDataTypeValidInGroup(dataType)) { + throw parseError("Data type not allowed in group: " + dataType); + } + if (type == GroupType.FLAG && dataType != DataType.INT && dataType != DataType.LONG) { + throw parseError("Data type not allowed for flag constants"); + } + + String name = peekTokenType() == TokenType.IDENTIFIER ? nextToken() : null; + if (name == null && type != GroupType.CONST) { + throw parseError("Non-const group type used for default group", typeLine, typeColumn); + } + + List constants = new ArrayList<>(); + GroupFormat format = null; + + while (true) { + String token = nextToken(); + if (lastTokenType == TokenType.EOF) { + break; + } + if (lastTokenType != TokenType.NEWLINE) { + throw expectedTokenError("'\\n'", token); + } + + if (peekTokenType() != TokenType.INDENT) { + break; + } + nextToken(); + + token = nextToken(); + switch (lastTokenType) { + case IDENTIFIER: + if (!"format".equals(token)) { + throw expectedTokenError("constant", token); + } + if (format != null) { + throw parseError("Duplicate format declaration"); + } + expectToken("="); + format = parseGroupFormat(); + break; + case OPERATOR: case INTEGER: case DOUBLE: case CHAR: case STRING: + int constantLine = lastTokenLine; + int constantColumn = lastTokenColumn; + GroupConstant constant = parseGroupConstant(token); + if (!isMatchingConstantType(dataType, constant.key)) { + throw parseError("Constant type not valid for group data type", constantLine, constantColumn); + } + if (isDuplicateConstantKey(constants, constant)) { + throw parseError("Duplicate constant key", constantLine, constantColumn); + } + constants.add(constant); + break; + default: + throw expectedTokenError("constant", token); + } + } + + return new GroupDefinition(scope, type, strict, dataType, name, constants, format); + } + + private static boolean isDataTypeValidInGroup(DataType type) { + return type == DataType.INT || type == DataType.LONG || type == DataType.FLOAT || type == DataType.DOUBLE || type == DataType.STRING; + } + + private static boolean isMatchingConstantType(DataType type, Literal.ConstantKey constantKey) { + if (constantKey instanceof Literal.Long) { + return type != DataType.STRING; + } else if (constantKey instanceof Literal.Double) { + return type == DataType.FLOAT || type == DataType.DOUBLE; + } else if (constantKey instanceof Literal.String) { + return type == DataType.STRING; + } else { + throw new AssertionError("Unknown group constant type: " + constantKey.getClass().getName()); + } + } + + private static boolean isDuplicateConstantKey(List constants, GroupConstant newConstant) { + if (newConstant.key instanceof Literal.Long) { + long newValue = ((Literal.Long) newConstant.key).value; + for (GroupConstant constant : constants) { + if (constant.key instanceof Literal.Long && ((Literal.Long) constant.key).value == newValue) { + return true; + } + if (constant.key instanceof Literal.Double && ((Literal.Double) constant.key).value == newValue) { + return true; + } + } + } else if (newConstant.key instanceof Literal.Double) { + double newValue = ((Literal.Double) newConstant.key).value; + for (GroupConstant constant : constants) { + if (constant.key instanceof Literal.Long && ((Literal.Long) constant.key).value == newValue) { + return true; + } + if (constant.key instanceof Literal.Double && ((Literal.Double) constant.key).value == newValue) { + return true; + } + } + } else if (newConstant.key instanceof Literal.String) { + String newValue = ((Literal.String) newConstant.key).value; + for (GroupConstant constant : constants) { + if (constant.key instanceof Literal.String && ((Literal.String) constant.key).value.equals(newValue)) { + return true; + } + } + } + + return false; + } + + private GroupFormat parseGroupFormat() throws IOException { + String token = nextToken("group format", TokenType.IDENTIFIER); + switch (token) { + case "decimal": + return GroupFormat.DECIMAL; + case "hex": + return GroupFormat.HEX; + case "binary": + return GroupFormat.BINARY; + case "octal": + return GroupFormat.OCTAL; + case "char": + return GroupFormat.CHAR; + default: + throw expectedTokenError("group format", token); + } + } + + private GroupConstant parseGroupConstant(String token) throws IOException { + Literal.ConstantKey key = parseGroupConstantKey(token); + expectToken("="); + Expression value = parseExpression(0); + return new GroupConstant(key, value); + } + + private Literal.ConstantKey parseGroupConstantKey(String token) throws IOException { + boolean negative = false; + if ("-".equals(token)) { + negative = true; + token = nextToken(); + } + + switch (lastTokenType) { + case INTEGER: + ParsedLong parsedLong = parseLong(token, negative); + return new Literal.Long(parsedLong.value, parsedLong.radix); + case DOUBLE: + return new Literal.Double(parseDouble(token, negative)); + case CHAR: + return new Literal.Long(unquoteChar(token)); + case STRING: + return new Literal.String(unquoteString(token)); + default: + throw expectedTokenError("number", token); + } + } + + private Expression parseExpression(int parseDepth) throws IOException { + // Shunting yard algorithm for parsing with operator precedence: https://stackoverflow.com/a/47717/11071180 + Stack operandStack = new Stack<>(); + Stack operatorStack = new Stack<>(); + + operandStack.push(parseUnaryExpression(parseDepth, false)); + + parseLoop: + while (true) { + BinaryExpression.Operator operator; + switch (peekToken()) { + case "|": + operator = BinaryExpression.Operator.BIT_OR; + break; + case "^": + operator = BinaryExpression.Operator.BIT_XOR; + break; + case "&": + operator = BinaryExpression.Operator.BIT_AND; + break; + case "<<": + operator = BinaryExpression.Operator.BIT_SHIFT_LEFT; + break; + case ">>": + operator = BinaryExpression.Operator.BIT_SHIFT_RIGHT; + break; + case ">>>": + operator = BinaryExpression.Operator.BIT_SHIFT_RIGHT_UNSIGNED; + break; + case "+": + operator = BinaryExpression.Operator.ADD; + break; + case "-": + operator = BinaryExpression.Operator.SUBTRACT; + break; + case "*": + operator = BinaryExpression.Operator.MULTIPLY; + break; + case "/": + operator = BinaryExpression.Operator.DIVIDE; + break; + case "%": + operator = BinaryExpression.Operator.MODULO; + break; + default: + break parseLoop; + } + nextToken(); // consume the operator + + int ourPrecedence = PRECEDENCES.get(operator); + while (!operatorStack.isEmpty() && ourPrecedence <= PRECEDENCES.get(operatorStack.peek())) { + BinaryExpression.Operator op = operatorStack.pop(); + Expression rhs = operandStack.pop(); + Expression lhs = operandStack.pop(); + operandStack.push(new BinaryExpression(lhs, rhs, op)); + } + + operatorStack.push(operator); + operandStack.push(parseUnaryExpression(parseDepth, false)); + } + + Expression result = operandStack.pop(); + while (!operatorStack.isEmpty()) { + result = new BinaryExpression(operandStack.pop(), result, operatorStack.pop()); + } + + return result; + } + + private Expression parseUnaryExpression(int parseDepth, boolean negative) throws IOException { + if (parseDepth > MAX_PARSE_DEPTH) { + throw parseError("max parse depth reached"); + } + + String token = nextToken(); + switch (token) { + case "-": + return new UnaryExpression(parseUnaryExpression(parseDepth + 1, true), UnaryExpression.Operator.NEGATE); + case "~": + return new UnaryExpression(parseUnaryExpression(parseDepth + 1, false), UnaryExpression.Operator.BIT_NOT); + case "(": + boolean parseAsCast = peekTokenType() == TokenType.IDENTIFIER && ")".equals(peekToken2()); + if (parseAsCast) { + DataType castType = parseDataType(); + nextToken(); // close paren + return new CastExpression(castType, parseUnaryExpression(parseDepth + 1, false)); + } else { + Expression expression = parseExpression(parseDepth + 1); + expectToken(")"); + return new ParenExpression(expression); + } + } + + switch (lastTokenType) { + case IDENTIFIER: + return parseFieldExpression(token); + case INTEGER: + ParsedInteger parsedInt = parseInt(token, negative); + return new LiteralExpression(new Literal.Integer(negative ? -parsedInt.value : parsedInt.value, parsedInt.radix)); + case LONG: + ParsedLong parsedLong = parseLong(token, negative); + return new LiteralExpression(new Literal.Long(negative ? -parsedLong.value : parsedLong.value, parsedLong.radix)); + case FLOAT: + float parsedFloat = parseFloat(token, negative); + return new LiteralExpression(new Literal.Float(negative ? -parsedFloat : parsedFloat)); + case DOUBLE: + double parsedDouble = parseDouble(token, negative); + return new LiteralExpression(new Literal.Double(negative ? -parsedDouble : parsedDouble)); + case CHAR: + return new LiteralExpression(new Literal.Character(unquoteChar(token))); + case STRING: + return new LiteralExpression(new Literal.String(unquoteString(token))); + default: + throw expectedTokenError("expression", token); + } + } + + private FieldExpression parseFieldExpression(String token) throws IOException { + expectToken("."); + String className = token + "." + parseClassName("field name"); + + // the field name has been joined to the class name, split it off + int dotIndex = className.lastIndexOf('.'); + String fieldName = className.substring(dotIndex + 1); + className = className.substring(0, dotIndex); + + boolean isStatic = true; + DataType fieldType = null; + if (":".equals(peekToken())) { + nextToken(); + if ("instance".equals(peekToken())) { + nextToken(); + isStatic = false; + if (":".equals(peekToken())) { + nextToken(); + fieldType = parseDataType(); + } + } else { + fieldType = parseDataType(); + } + } + + return new FieldExpression(className, fieldName, fieldType, isStatic); + } + + private TargetField parseTargetField() throws IOException { + String className = parseClassName(); + String fieldName = nextToken(TokenType.IDENTIFIER); + String fieldDesc = nextToken(TokenType.FIELD_DESCRIPTOR); + String groupName = nextToken(TokenType.IDENTIFIER); + String token = nextToken(); + if (lastTokenType != TokenType.NEWLINE && lastTokenType != TokenType.EOF) { + throw expectedTokenError("'\n'", token); + } + return new TargetField(className, fieldName, fieldDesc, groupName); + } + + private TargetMethod parseTargetMethod() throws IOException { + String className = parseClassName(); + String methodName = parseMethodName(); + String methodDesc = nextToken(TokenType.METHOD_DESCRIPTOR); + + Map paramGroups = new HashMap<>(); + String returnGroup = null; + + while (true) { + String token = nextToken(); + if (lastTokenType == TokenType.EOF) { + break; + } + if (lastTokenType != TokenType.NEWLINE) { + throw expectedTokenError("'\\n'", token); + } + + if (peekTokenType() != TokenType.INDENT) { + break; + } + nextToken(); + + token = nextToken("target method item", TokenType.IDENTIFIER); + switch (token) { + case "param": + int paramIndex = parseInt(nextToken(TokenType.INTEGER), false).value; + if (paramGroups.containsKey(paramIndex)) { + throw parseError("Specified parameter " + paramIndex + " twice"); + } + paramGroups.put(paramIndex, nextToken(TokenType.IDENTIFIER)); + break; + case "return": + if (returnGroup != null) { + throw parseError("Specified return group twice"); + } + returnGroup = nextToken(TokenType.IDENTIFIER); + break; + default: + throw expectedTokenError("target method item", token); + } + } + + return new TargetMethod(className, methodName, methodDesc, paramGroups, returnGroup); + } + + private DataType parseDataType() throws IOException { + String token = nextToken("data type", TokenType.IDENTIFIER); + switch (token) { + case "byte": + return DataType.BYTE; + case "short": + return DataType.SHORT; + case "int": + return DataType.INT; + case "long": + return DataType.LONG; + case "float": + return DataType.FLOAT; + case "double": + return DataType.DOUBLE; + case "char": + return DataType.CHAR; + case "String": + return DataType.STRING; + default: + throw expectedTokenError("data type", token); + } + } + + private String parseClassName() throws IOException { + return parseClassName("class name"); + } + + private String parseClassName(String expected) throws IOException { + StringBuilder result = new StringBuilder(nextToken(expected, TokenType.IDENTIFIER)); + while (".".equals(peekToken())) { + nextToken(); + result.append('.').append(nextToken(TokenType.IDENTIFIER)); + } + return result.toString(); + } + + private String parseMethodName() throws IOException { + String token = nextToken(); + if (lastTokenType == TokenType.IDENTIFIER) { + return token; + } + if ("<".equals(token)) { + token = nextToken(TokenType.IDENTIFIER); + if (!"init".equals(token) && !"clinit".equals(token)) { + throw expectedTokenError("identifier", token); + } + expectToken(">"); + return "<" + token + ">"; + } + throw expectedTokenError("identifier", token); + } + + private ParsedInteger parseInt(String string, boolean negative) throws UnpickParseException { + int radix; + if (string.startsWith("0x") || string.startsWith("0X")) { + radix = 16; + string = string.substring(2); + } else if (string.startsWith("0b") || string.startsWith("0B")) { + radix = 2; + string = string.substring(2); + } else if (string.startsWith("0") && string.length() > 1) { + radix = 8; + string = string.substring(1); + } else { + radix = 10; + } + + try { + return new ParsedInteger(Integer.parseInt(negative ? "-" + string : string, radix), radix); + } catch (NumberFormatException ignore) { + } + + // try unsigned parsing in other radixes + if (!negative && radix != 10) { + try { + return new ParsedInteger(Integer.parseUnsignedInt(string, radix), radix); + } catch (NumberFormatException ignore) { + } + } + + throw parseError("Integer out of bounds"); + } + + private static final class ParsedInteger { + final int value; + final int radix; + + private ParsedInteger(int value, int radix) { + this.value = value; + this.radix = radix; + } + } + + private ParsedLong parseLong(String string, boolean negative) throws UnpickParseException { + if (string.endsWith("l") || string.endsWith("L")) { + string = string.substring(0, string.length() - 1); + } + + int radix; + if (string.startsWith("0x") || string.startsWith("0X")) { + radix = 16; + string = string.substring(2); + } else if (string.startsWith("0b") || string.startsWith("0B")) { + radix = 2; + string = string.substring(2); + } else if (string.startsWith("0") && string.length() > 1) { + radix = 8; + string = string.substring(1); + } else { + radix = 10; + } + + try { + return new ParsedLong(Long.parseLong(negative ? "-" + string : string, radix), radix); + } catch (NumberFormatException ignore) { + } + + // try unsigned parsing in other radixes + if (!negative && radix != 10) { + try { + return new ParsedLong(Long.parseUnsignedLong(string, radix), radix); + } catch (NumberFormatException ignore) { + } + } + + throw parseError("Long out of bounds"); + } + + private static final class ParsedLong { + final long value; + final int radix; + + private ParsedLong(long value, int radix) { + this.value = value; + this.radix = radix; + } + } + + private float parseFloat(String string, boolean negative) throws UnpickParseException { + if (string.endsWith("f") || string.endsWith("F")) { + string = string.substring(0, string.length() - 1); + } + try { + float result = Float.parseFloat(string); + return negative ? -result : result; + } catch (NumberFormatException e) { + throw parseError("Invalid float"); + } + } + + private double parseDouble(String string, boolean negative) throws UnpickParseException { + try { + double result = Double.parseDouble(string); + return negative ? -result : result; + } catch (NumberFormatException e) { + throw parseError("Invalid double"); + } + } + + private static char unquoteChar(String string) { + return unquoteString(string).charAt(0); + } + + private static String unquoteString(String string) { + StringBuilder result = new StringBuilder(string.length() - 2); + for (int i = 1; i < string.length() - 1; i++) { + if (string.charAt(i) == '\\') { + i++; + switch (string.charAt(i)) { + case 'u': + do { + i++; + } while (string.charAt(i) == 'u'); + result.append((char) Integer.parseInt(string.substring(i, i + 4), 16)); + i += 3; + break; + case 'b': + result.append('\b'); + break; + case 't': + result.append('\t'); + break; + case 'n': + result.append('\n'); + break; + case 'f': + result.append('\f'); + break; + case 'r': + result.append('\r'); + break; + case '"': + result.append('"'); + break; + case '\'': + result.append('\''); + break; + case '\\': + result.append('\\'); + break; + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': + char c; + int count = 0; + int maxCount = string.charAt(i) <= '3' ? 3 : 2; + while (count < maxCount && (c = string.charAt(i + count)) >= '0' && c <= '7') { + count++; + } + result.append((char) Integer.parseInt(string.substring(i, i + count), 8)); + i += count - 1; + break; + default: + throw new AssertionError("Unexpected escape sequence in string"); + } + } else { + result.append(string.charAt(i)); + } + } + return result.toString(); + } + + // region Tokenizer + + private TokenType peekTokenType() throws IOException { + ParseState state = new ParseState(this); + nextToken = nextToken(); + nextTokenState = new ParseState(this); + state.restore(this); + return nextTokenState.lastTokenType; + } + + private String peekToken() throws IOException { + ParseState state = new ParseState(this); + nextToken = nextToken(); + nextTokenState = new ParseState(this); + state.restore(this); + return nextToken; + } + + private String peekToken2() throws IOException { + ParseState state = new ParseState(this); + String nextToken = nextToken(); + ParseState nextTokenState = new ParseState(this); + nextToken2 = nextToken(); + nextToken2State = new ParseState(this); + this.nextToken = nextToken; + this.nextTokenState = nextTokenState; + state.restore(this); + return nextToken2; + } + + private void expectToken(String expected) throws IOException { + String token = nextToken(); + if (!expected.equals(token)) { + throw expectedTokenError(UnpickV3Writer.quoteString(expected, '\''), token); + } + } + + private String nextToken() throws IOException { + return nextTokenInner(null); + } + + private String nextToken(TokenType type) throws IOException { + return nextToken(type.name, type); + } + + private String nextToken(String expected, TokenType type) throws IOException { + String token = nextTokenInner(type); + if (lastTokenType != type) { + throw expectedTokenError(expected, token); + } + return token; + } + + private String nextTokenInner(@Nullable TokenType typeHint) throws IOException { + if (nextTokenState != null) { + String tok = nextToken; + nextToken = nextToken2; + nextToken2 = null; + nextTokenState.restore(this); + nextTokenState = nextToken2State; + nextToken2State = null; + return tok; + } + + if (lastTokenType == TokenType.EOF) { + return null; + } + + // newline token (skipping comment and whitespace) + while (column < line.length() && Character.isWhitespace(line.charAt(column))) { + column++; + } + if (column < line.length() && line.charAt(column) == '#') { + column = line.length(); + } + if (column == line.length() && lastTokenType != TokenType.NEWLINE) { + lastTokenColumn = column; + lastTokenLine = reader.getLineNumber(); + lastTokenType = TokenType.NEWLINE; + return "\n"; + } + + // skip whitespace and comments, handle indent token + boolean seenIndent = false; + while (true) { + if (column == line.length() || line.charAt(column) == '#') { + seenIndent = false; + line = reader.readLine(); + column = 0; + if (line == null) { + lastTokenColumn = column; + lastTokenLine = reader.getLineNumber(); + lastTokenType = TokenType.EOF; + return null; + } + } else if (Character.isWhitespace(line.charAt(column))) { + seenIndent = column == 0; + do { + column++; + } while (column < line.length() && Character.isWhitespace(line.charAt(column))); + } else { + break; + } + } + if (seenIndent) { + lastTokenColumn = 0; + lastTokenLine = reader.getLineNumber(); + lastTokenType = TokenType.INDENT; + return line.substring(0, column); + } + + lastTokenColumn = column; + lastTokenLine = reader.getLineNumber(); + + if (typeHint == TokenType.FIELD_DESCRIPTOR) { + if (skipFieldDescriptor(true)) { + return line.substring(lastTokenColumn, column); + } + } + + if (typeHint == TokenType.METHOD_DESCRIPTOR) { + if (skipMethodDescriptor()) { + return line.substring(lastTokenColumn, column); + } + } + + if (skipNumber()) { + if (column < line.length() && isIdentifierChar(line.charAt(column))) { + throw parseErrorInToken("Unexpected character in number: " + line.charAt(column)); + } + return line.substring(lastTokenColumn, column); + } + + if (skipIdentifier()) { + return line.substring(lastTokenColumn, column); + } + + if (skipString('\'', true)) { + lastTokenType = TokenType.CHAR; + return line.substring(lastTokenColumn, column); + } + + if (skipString('"', false)) { + lastTokenType = TokenType.STRING; + return line.substring(lastTokenColumn, column); + } + + char c = line.charAt(column); + column++; + if (c == '<') { + if (column < line.length() && line.charAt(column) == '<') { + column++; + } + } else if (c == '>') { + if (column < line.length() && line.charAt(column) == '>') { + column++; + if (column < line.length() && line.charAt(column) == '>') { + column++; + } + } + } + + lastTokenType = TokenType.OPERATOR; + return line.substring(lastTokenColumn, column); + } + + private boolean skipFieldDescriptor(boolean startOfToken) throws UnpickParseException { + // array descriptors + while (column < line.length() && line.charAt(column) == '[') { + startOfToken = false; + column++; + } + + // first character of main part of descriptor + if (column == line.length() || isTokenEnd(line.charAt(column))) { + throw parseErrorInToken("Unexpected end to descriptor"); + } + switch (line.charAt(column)) { + // primitive types + case 'B': case 'C': case 'D': case 'F': case 'I': case 'J': case 'S': case 'Z': + column++; + break; + // class types + case 'L': + column++; + + // class name + char c; + while (column < line.length() && (c = line.charAt(column)) != ';' && !isTokenEnd(c)) { + if (c == '.' || c == '[') { + throw parseErrorInToken("Illegal character in descriptor: " + c); + } + column++; + } + + // semicolon + if (column == line.length() || isTokenEnd(line.charAt(column))) { + throw parseErrorInToken("Unexpected end of descriptor"); + } + column++; + break; + default: + if (!startOfToken) { + throw parseErrorInToken("Illegal character in descriptor: " + line.charAt(column)); + } + return false; + } + + lastTokenType = TokenType.FIELD_DESCRIPTOR; + return true; + } + + private boolean skipMethodDescriptor() throws UnpickParseException { + if (line.charAt(column) != '(') { + return false; + } + column++; + + // parameter types + while (column < line.length() && line.charAt(column) != ')' && !isTokenEnd(line.charAt(column))) { + skipFieldDescriptor(false); + } + if (column == line.length() || isTokenEnd(line.charAt(column))) { + throw parseErrorInToken("Unexpected end of descriptor"); + } + column++; + + // return type + if (column == line.length() || isTokenEnd(line.charAt(column))) { + throw parseErrorInToken("Unexpected end of descriptor"); + } + if (line.charAt(column) == 'V') { + column++; + } else { + skipFieldDescriptor(false); + } + + lastTokenType = TokenType.METHOD_DESCRIPTOR; + return true; + } + + private boolean skipNumber() throws UnpickParseException { + if (line.charAt(column) < '0' || line.charAt(column) > '9') { + return false; + } + + // hex numbers + if (line.startsWith("0x", column) || line.startsWith("0X", column)) { + column += 2; + char c; + boolean seenDigit = false; + while (column < line.length() && ((c = line.charAt(column)) >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F')) { + seenDigit = true; + column++; + } + if (!seenDigit) { + throw parseErrorInToken("Unexpected end of integer"); + } + detectIntegerType(); + return true; + } + + // binary numbers + if (line.startsWith("0b", column) || line.startsWith("0B", column)) { + column += 2; + char c; + boolean seenDigit = false; + while (column < line.length() && ((c = line.charAt(column)) == '0' || c == '1')) { + seenDigit = true; + column++; + } + if (!seenDigit) { + throw parseErrorInToken("Unexpected end of integer"); + } + detectIntegerType(); + return true; + } + + // lookahead a decimal number + int endOfInteger = column; + char c; + do { + endOfInteger++; + } while (endOfInteger < line.length() && (c = line.charAt(endOfInteger)) >= '0' && c <= '9'); + + // floats and doubles + if (endOfInteger < line.length() && line.charAt(endOfInteger) == '.') { + column = endOfInteger + 1; + + // fractional part + boolean seenFracDigit = false; + while (column < line.length() && (c = line.charAt(column)) >= '0' && c <= '9') { + seenFracDigit = true; + column++; + } + if (!seenFracDigit) { + throw parseErrorInToken("Unexpected end of float"); + } + + // exponent + if (column < line.length() && ((c = line.charAt(column)) == 'e' || c == 'E')) { + column++; + if (column < line.length() && (c = line.charAt(column)) >= '+' && c <= '-') { + column++; + } + + boolean seenExponentDigit = false; + while (column < line.length() && ((c = line.charAt(column)) >= '0' && c <= '9')) { + seenExponentDigit = true; + column++; + } + if (!seenExponentDigit) { + throw parseErrorInToken("Unexpected end of float"); + } + } + + boolean isFloat = column < line.length() && ((c = line.charAt(column)) == 'f' || c == 'F'); + if (isFloat) { + column++; + } + lastTokenType = isFloat ? TokenType.FLOAT : TokenType.DOUBLE; + return true; + } + + // octal numbers (we'll count 0 itself as an octal) + if (line.charAt(column) == '0') { + column++; + while (column < line.length() && (c = line.charAt(column)) >= '0' && c <= '7') { + column++; + } + detectIntegerType(); + return true; + } + + // decimal numbers + column = endOfInteger; + detectIntegerType(); + return true; + } + + private void detectIntegerType() { + char c; + boolean isLong = column < line.length() && ((c = line.charAt(column)) == 'l' || c == 'L'); + if (isLong) { + column++; + } + lastTokenType = isLong ? TokenType.LONG : TokenType.INTEGER; + } + + private boolean skipIdentifier() { + if (!isIdentifierChar(line.charAt(column))) { + return false; + } + + do { + column++; + } while (column < line.length() && isIdentifierChar(line.charAt(column))); + + lastTokenType = TokenType.IDENTIFIER; + return true; + } + + private boolean skipString(char quoteChar, boolean singleChar) throws UnpickParseException { + if (line.charAt(column) != quoteChar) { + return false; + } + column++; + + boolean seenChar = false; + while (column < line.length() && line.charAt(column) != quoteChar) { + if (singleChar && seenChar) { + throw parseErrorInToken("Multiple characters in char literal"); + } + seenChar = true; + + if (line.charAt(column) == '\\') { + column++; + if (column == line.length()) { + throw parseErrorInToken("Unexpected end of string"); + } + char c = line.charAt(column); + switch (c) { + case 'u': + do { + column++; + } while (column < line.length() && line.charAt(column) == 'u'); + for (int i = 0; i < 4; i++) { + if (column == line.length()) { + throw parseErrorInToken("Unexpected end of string"); + } + c = line.charAt(column); + if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) { + throw parseErrorInToken("Illegal character in unicode escape sequence"); + } + column++; + } + break; + case 'b': case 't': case 'n': case 'f': case 'r': case '"': case '\'': case '\\': + column++; + break; + case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': + column++; + int maxOctalDigits = c <= '3' ? 3 : 2; + for (int i = 1; i < maxOctalDigits && column < line.length() && (c = line.charAt(column)) >= '0' && c <= '7'; i++) { + column++; + } + break; + default: + throw parseErrorInToken("Illegal escape sequence \\" + c); + } + } else { + column++; + } + } + + if (column == line.length()) { + throw parseErrorInToken("Unexpected end of string"); + } + + if (singleChar && !seenChar) { + throw parseErrorInToken("No character in char literal"); + } + + column++; + return true; + } + + private static boolean isTokenEnd(char c) { + return Character.isWhitespace(c) || c == '#'; + } + + private static boolean isIdentifierChar(char c) { + return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '$'; + } + + // endregion + + private UnpickParseException expectedTokenError(String expected, String token) { + if (lastTokenType == TokenType.EOF) { + return parseError("Expected " + expected + " before eof token"); + } else { + return parseError("Expected " + expected + " before " + UnpickV3Writer.quoteString(token, '\'') + " token"); + } + } + + private UnpickParseException parseError(String message) { + return parseError(message, lastTokenLine, lastTokenColumn); + } + + private UnpickParseException parseErrorInToken(String message) { + return parseError(message, reader.getLineNumber(), column); + } + + private UnpickParseException parseError(String message, int lineNumber, int column) { + return new UnpickParseException(message, lineNumber, column + 1); + } + + @Override + public void close() throws IOException { + reader.close(); + } + + private static class ParseState { + private final int lastTokenLine; + private final int lastTokenColumn; + private final TokenType lastTokenType; + + ParseState(UnpickV3Reader reader) { + this.lastTokenLine = reader.lastTokenLine; + this.lastTokenColumn = reader.lastTokenColumn; + this.lastTokenType = reader.lastTokenType; + } + + void restore(UnpickV3Reader reader) { + reader.lastTokenLine = lastTokenLine; + reader.lastTokenColumn = lastTokenColumn; + reader.lastTokenType = lastTokenType; + } + } + + private enum TokenType { + IDENTIFIER("identifier"), + DOUBLE("double"), + FLOAT("float"), + INTEGER("integer"), + LONG("long"), + CHAR("char"), + STRING("string"), + INDENT("indent"), + NEWLINE("newline"), + FIELD_DESCRIPTOR("field descriptor"), + METHOD_DESCRIPTOR("method descriptor"), + OPERATOR("operator"), + EOF("eof"); + + final String name; + + TokenType(String name) { + this.name = name; + } + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java new file mode 100644 index 0000000..e5eaba8 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java @@ -0,0 +1,240 @@ +package net.earthcomputer.unpickv3parser; + +import net.earthcomputer.unpickv3parser.tree.DataType; +import net.earthcomputer.unpickv3parser.tree.GroupConstant; +import net.earthcomputer.unpickv3parser.tree.GroupDefinition; +import net.earthcomputer.unpickv3parser.tree.GroupScope; +import net.earthcomputer.unpickv3parser.tree.TargetField; +import net.earthcomputer.unpickv3parser.tree.TargetMethod; +import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; +import net.earthcomputer.unpickv3parser.tree.expr.Expression; +import net.earthcomputer.unpickv3parser.tree.expr.ExpressionTransformer; +import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; + +import java.util.Collections; +import java.util.List; +import java.util.Map; +import java.util.Objects; +import java.util.stream.Collectors; + +/** + * Remaps all class, field, and method names in a .unpick v3 file. Visitor methods will be called on the downstream + * visitor with the remapped names. + */ +public class UnpickV3Remapper extends UnpickV3Visitor { + private final UnpickV3Visitor downstream; + private final Map> classesInPackage; + private final Map classMappings; + private final Map fieldMappings; + private final Map methodMappings; + + /** + * Warning: class names use "." format, not "/" format. {@code classesInPackage} should contain all the classes in + * each package, including unmapped ones. The classes in this map are unqualified by the package name (because the + * package name is already in the key of the map entry). + */ + public UnpickV3Remapper( + UnpickV3Visitor downstream, + Map> classesInPackage, + Map classMappings, + Map fieldMappings, + Map methodMappings + ) { + this.downstream = downstream; + this.classesInPackage = classesInPackage; + this.classMappings = classMappings; + this.fieldMappings = fieldMappings; + this.methodMappings = methodMappings; + } + + @Override + public void visitGroupDefinition(GroupDefinition groupDefinition) { + GroupScope oldScope = groupDefinition.scope; + List scopes; + if (oldScope instanceof GroupScope.Global) { + scopes = Collections.singletonList(oldScope); + } else if (oldScope instanceof GroupScope.Package) { + String pkg = ((GroupScope.Package) oldScope).packageName; + scopes = classesInPackage.getOrDefault(pkg, Collections.emptyList()).stream() + .map(cls -> new GroupScope.Class(mapClassName(pkg + "." + cls))) + .collect(Collectors.toList()); + } else if (oldScope instanceof GroupScope.Class) { + scopes = Collections.singletonList(new GroupScope.Class(mapClassName(((GroupScope.Class) oldScope).className))); + } else if (oldScope instanceof GroupScope.Method) { + GroupScope.Method methodScope = (GroupScope.Method) oldScope; + String className = mapClassName(methodScope.className); + String methodName = mapMethodName(methodScope.className, methodScope.methodName, methodScope.methodDesc); + String methodDesc = mapDescriptor(methodScope.methodDesc); + scopes = Collections.singletonList(new GroupScope.Method(className, methodName, methodDesc)); + } else { + throw new AssertionError("Unknown group scope type: " + oldScope.getClass().getName()); + } + + List constants = groupDefinition.constants.stream() + .map(constant -> new GroupConstant(constant.key, constant.value.transform(new ExpressionRemapper(groupDefinition.dataType)))) + .collect(Collectors.toList()); + + for (GroupScope scope : scopes) { + downstream.visitGroupDefinition(new GroupDefinition(scope, groupDefinition.type, groupDefinition.strict, groupDefinition.dataType, groupDefinition.name, constants, groupDefinition.format)); + } + } + + @Override + public void visitTargetField(TargetField targetField) { + String className = mapClassName(targetField.className); + String fieldName = mapFieldName(targetField.className, targetField.fieldName, targetField.fieldDesc); + String fieldDesc = mapDescriptor(targetField.fieldDesc); + downstream.visitTargetField(new TargetField(className, fieldName, fieldDesc, targetField.groupName)); + } + + @Override + public void visitTargetMethod(TargetMethod targetMethod) { + String className = mapClassName(targetMethod.className); + String methodName = mapMethodName(targetMethod.className, targetMethod.methodName, targetMethod.methodDesc); + String methodDesc = mapDescriptor(targetMethod.methodDesc); + downstream.visitTargetMethod(new TargetMethod(className, methodName, methodDesc, targetMethod.paramGroups, targetMethod.returnGroup)); + } + + private String mapClassName(String className) { + return classMappings.getOrDefault(className, className); + } + + private String mapFieldName(String className, String fieldName, String fieldDesc) { + return fieldMappings.getOrDefault(new FieldKey(className, fieldName, fieldDesc), fieldName); + } + + private String mapMethodName(String className, String methodName, String methodDesc) { + return methodMappings.getOrDefault(new MethodKey(className, methodName, methodDesc), methodName); + } + + private String mapDescriptor(String descriptor) { + StringBuilder mappedDescriptor = new StringBuilder(); + + int semicolonIndex = 0; + int lIndex; + while ((lIndex = descriptor.indexOf('L', semicolonIndex)) != -1) { + mappedDescriptor.append(descriptor, semicolonIndex, lIndex + 1); + semicolonIndex = descriptor.indexOf(';', lIndex); + if (semicolonIndex == -1) { + throw new AssertionError("Invalid descriptor: " + descriptor); + } + String className = descriptor.substring(lIndex + 1, semicolonIndex).replace('/', '.'); + mappedDescriptor.append(mapClassName(className).replace('.', '/')); + } + + return mappedDescriptor.append(descriptor, semicolonIndex, descriptor.length()).toString(); + } + + public static final class FieldKey { + public final String className; + public final String fieldName; + public final String fieldDesc; + + /** + * Warning: class name uses "." format, not "/" format + */ + public FieldKey(String className, String fieldName, String fieldDesc) { + this.className = className; + this.fieldName = fieldName; + this.fieldDesc = fieldDesc; + } + + @Override + public int hashCode() { + return Objects.hash(className, fieldName, fieldDesc); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof FieldKey)) { + return false; + } + FieldKey other = (FieldKey) o; + return className.equals(other.className) && fieldName.equals(other.fieldName) && fieldDesc.equals(other.fieldDesc); + } + + @Override + public String toString() { + return className + "." + fieldName + ":" + fieldDesc; + } + } + + public static final class MethodKey { + public final String className; + public final String methodName; + public final String methodDesc; + + /** + * Warning: class name uses "." format, not "/" format + */ + public MethodKey(String className, String methodName, String methodDesc) { + this.className = className; + this.methodName = methodName; + this.methodDesc = methodDesc; + } + + @Override + public int hashCode() { + return Objects.hash(className, methodName, methodDesc); + } + + @Override + public boolean equals(Object o) { + if (!(o instanceof MethodKey)) { + return false; + } + MethodKey other = (MethodKey) o; + return className.equals(other.className) && methodName.equals(other.methodName) && methodDesc.equals(other.methodDesc); + } + + @Override + public String toString() { + return className + "." + methodName + methodDesc; + } + } + + private class ExpressionRemapper extends ExpressionTransformer { + private final DataType groupDataType; + + ExpressionRemapper(DataType groupDataType) { + this.groupDataType = groupDataType; + } + + @Override + public Expression transformFieldExpression(FieldExpression fieldExpression) { + String fieldDesc; + switch (fieldExpression.fieldType == null ? groupDataType : fieldExpression.fieldType) { + case BYTE: + fieldDesc = "B"; + break; + case SHORT: + fieldDesc = "S"; + break; + case INT: + fieldDesc = "I"; + break; + case LONG: + fieldDesc = "J"; + break; + case FLOAT: + fieldDesc = "F"; + break; + case DOUBLE: + fieldDesc = "D"; + break; + case CHAR: + fieldDesc = "C"; + break; + case STRING: + fieldDesc = "Ljava/lang/String;"; + break; + default: + throw new AssertionError("Unknown data type: " + fieldExpression.fieldType); + } + + String className = mapClassName(fieldExpression.className); + String fieldName = mapFieldName(fieldExpression.className, fieldExpression.fieldName, fieldDesc); + return new FieldExpression(className, fieldName, fieldExpression.fieldType, fieldExpression.isStatic); + } + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java new file mode 100644 index 0000000..121a449 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java @@ -0,0 +1,379 @@ +package net.earthcomputer.unpickv3parser; + +import net.earthcomputer.unpickv3parser.tree.DataType; +import net.earthcomputer.unpickv3parser.tree.GroupConstant; +import net.earthcomputer.unpickv3parser.tree.GroupDefinition; +import net.earthcomputer.unpickv3parser.tree.GroupScope; +import net.earthcomputer.unpickv3parser.tree.Literal; +import net.earthcomputer.unpickv3parser.tree.TargetField; +import net.earthcomputer.unpickv3parser.tree.TargetMethod; +import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; +import net.earthcomputer.unpickv3parser.tree.expr.BinaryExpression; +import net.earthcomputer.unpickv3parser.tree.expr.CastExpression; +import net.earthcomputer.unpickv3parser.tree.expr.ExpressionVisitor; +import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; +import net.earthcomputer.unpickv3parser.tree.expr.LiteralExpression; +import net.earthcomputer.unpickv3parser.tree.expr.ParenExpression; +import net.earthcomputer.unpickv3parser.tree.expr.UnaryExpression; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +/** + * A visitor that generates .unpick v3 format text. Useful for programmatically writing .unpick v3 format files; + * or remapping them, when used as the delegate for an instance of {@link UnpickV3Remapper}. + */ +public final class UnpickV3Writer extends UnpickV3Visitor { + private static final String LINE_SEPARATOR = System.lineSeparator(); + private final String indent; + private final StringBuilder output = new StringBuilder("unpick v3").append(LINE_SEPARATOR); + + public UnpickV3Writer() { + this("\t"); + } + + public UnpickV3Writer(String indent) { + this.indent = indent; + } + + @Override + public void visitGroupDefinition(GroupDefinition groupDefinition) { + output.append(LINE_SEPARATOR); + + if (!(groupDefinition.scope instanceof GroupScope.Global)) { + writeGroupScope(groupDefinition.scope); + output.append(" "); + } + + writeLowerCaseEnum(groupDefinition.type); + output.append(" "); + + if (groupDefinition.strict) { + output.append("strict "); + } + + writeDataType(groupDefinition.dataType); + + if (groupDefinition.name != null) { + output.append(" ").append(groupDefinition.name); + } + + output.append(LINE_SEPARATOR); + + if (groupDefinition.format != null) { + output.append(indent).append("format = "); + writeLowerCaseEnum(groupDefinition.format); + output.append(LINE_SEPARATOR); + } + + for (GroupConstant constant : groupDefinition.constants) { + writeGroupConstant(constant); + } + } + + private void writeGroupScope(GroupScope scope) { + output.append("scoped "); + if (scope instanceof GroupScope.Package) { + output.append("package ").append(((GroupScope.Package) scope).packageName); + } else if (scope instanceof GroupScope.Class) { + output.append("class ").append(((GroupScope.Class) scope).className); + } else if (scope instanceof GroupScope.Method) { + GroupScope.Method methodScope = (GroupScope.Method) scope; + output.append("method ") + .append(methodScope.className) + .append(" ") + .append(methodScope.methodName) + .append(" ") + .append(methodScope.methodDesc); + } else { + throw new AssertionError("Unknown group scope type: " + scope.getClass().getName()); + } + } + + private void writeGroupConstant(GroupConstant constant) { + output.append(indent); + writeGroupConstantKey(constant.key); + output.append(" = "); + constant.value.accept(new ExpressionWriter()); + output.append(LINE_SEPARATOR); + } + + private void writeGroupConstantKey(Literal.ConstantKey constantKey) { + if (constantKey instanceof Literal.Long) { + Literal.Long longLiteral = (Literal.Long) constantKey; + if (longLiteral.radix == 10) { + // treat base 10 as signed + output.append(longLiteral.value); + } else { + writeRadixPrefix(longLiteral.radix); + output.append(Long.toUnsignedString(longLiteral.value, longLiteral.radix)); + } + } else if (constantKey instanceof Literal.Double) { + output.append(((Literal.Double) constantKey).value); + } else if (constantKey instanceof Literal.String) { + output.append(quoteString(((Literal.String) constantKey).value, '"')); + } else { + throw new AssertionError("Unknown group constant key type: " + constantKey.getClass().getName()); + } + } + + @Override + public void visitTargetField(TargetField targetField) { + output.append(LINE_SEPARATOR) + .append("target_field ") + .append(targetField.className) + .append(" ") + .append(targetField.fieldName) + .append(" ") + .append(targetField.fieldDesc) + .append(" ") + .append(targetField.groupName) + .append(LINE_SEPARATOR); + } + + @Override + public void visitTargetMethod(TargetMethod targetMethod) { + output.append(LINE_SEPARATOR) + .append("target_method ") + .append(targetMethod.className) + .append(" ") + .append(targetMethod.methodName) + .append(" ") + .append(targetMethod.methodDesc) + .append(LINE_SEPARATOR); + + List> paramGroups = new ArrayList<>(targetMethod.paramGroups.entrySet()); + paramGroups.sort(Map.Entry.comparingByKey()); + for (Map.Entry paramGroup : paramGroups) { + output.append(indent) + .append("param ") + .append(paramGroup.getKey()) + .append(" ") + .append(paramGroup.getValue()) + .append(LINE_SEPARATOR); + } + + if (targetMethod.returnGroup != null) { + output.append(indent) + .append("return ") + .append(targetMethod.returnGroup) + .append(LINE_SEPARATOR); + } + } + + private void writeRadixPrefix(int radix) { + switch (radix) { + case 10: + break; + case 16: + output.append("0x"); + break; + case 8: + output.append("0"); + break; + case 2: + output.append("0b"); + break; + default: + throw new AssertionError("Illegal radix: " + radix); + } + } + + private void writeDataType(DataType dataType) { + if (dataType == DataType.STRING) { + output.append("String"); + } else { + writeLowerCaseEnum(dataType); + } + } + + private void writeLowerCaseEnum(Enum enumValue) { + output.append(enumValue.name().toLowerCase(Locale.ROOT)); + } + + static String quoteString(String string, char quoteChar) { + StringBuilder result = new StringBuilder(string.length() + 2).append(quoteChar); + + for (int i = 0; i < string.length(); i++) { + char c = string.charAt(i); + switch (c) { + case '\b': + result.append("\\b"); + break; + case '\t': + result.append("\\t"); + break; + case '\n': + result.append("\\n"); + break; + case '\f': + result.append("\\f"); + break; + case '\r': + result.append("\\r"); + break; + case '\\': + result.append("\\\\"); + break; + default: + if (c == quoteChar) { + result.append("\\").append(c); + } else if (isPrintable(c)) { + result.append(c); + } else if (c <= 255) { + result.append('\\').append(Integer.toOctalString(c)); + } else { + result.append("\\u").append(String.format("%04x", (int) c)); + } + } + } + + return result.append(quoteChar).toString(); + } + + private static boolean isPrintable(char ch) { + switch (Character.getType(ch)) { + case Character.UPPERCASE_LETTER: + case Character.LOWERCASE_LETTER: + case Character.TITLECASE_LETTER: + case Character.MODIFIER_LETTER: + case Character.OTHER_LETTER: + case Character.NON_SPACING_MARK: + case Character.ENCLOSING_MARK: + case Character.COMBINING_SPACING_MARK: + case Character.DECIMAL_DIGIT_NUMBER: + case Character.LETTER_NUMBER: + case Character.OTHER_NUMBER: + case Character.SPACE_SEPARATOR: + case Character.DASH_PUNCTUATION: + case Character.START_PUNCTUATION: + case Character.END_PUNCTUATION: + case Character.CONNECTOR_PUNCTUATION: + case Character.OTHER_PUNCTUATION: + case Character.MATH_SYMBOL: + case Character.CURRENCY_SYMBOL: + case Character.MODIFIER_SYMBOL: + case Character.OTHER_SYMBOL: + case Character.INITIAL_QUOTE_PUNCTUATION: + case Character.FINAL_QUOTE_PUNCTUATION: + return true; + } + return false; + } + + public String getOutput() { + return output.toString(); + } + + private final class ExpressionWriter extends ExpressionVisitor { + @Override + public void visitBinaryExpression(BinaryExpression binaryExpression) { + binaryExpression.lhs.accept(this); + switch (binaryExpression.operator) { + case BIT_OR: + output.append(" | "); + break; + case BIT_XOR: + output.append(" ^ "); + break; + case BIT_AND: + output.append(" & "); + break; + case BIT_SHIFT_LEFT: + output.append(" << "); + break; + case BIT_SHIFT_RIGHT: + output.append(" >> "); + break; + case BIT_SHIFT_RIGHT_UNSIGNED: + output.append(" >>> "); + break; + case ADD: + output.append(" + "); + break; + case SUBTRACT: + output.append(" - "); + break; + case MULTIPLY: + output.append(" * "); + break; + case DIVIDE: + output.append(" / "); + break; + case MODULO: + output.append(" % "); + break; + default: + throw new AssertionError("Unknown operator: " + binaryExpression.operator); + } + binaryExpression.rhs.accept(this); + } + + @Override + public void visitCastExpression(CastExpression castExpression) { + output.append('('); + writeDataType(castExpression.castType); + output.append(") "); + castExpression.operand.accept(this); + } + + @Override + public void visitFieldExpression(FieldExpression fieldExpression) { + output.append(fieldExpression.className).append('.').append(fieldExpression.fieldName); + if (!fieldExpression.isStatic) { + output.append(":instance"); + } + if (fieldExpression.fieldType != null) { + output.append(':'); + writeDataType(fieldExpression.fieldType); + } + } + + @Override + public void visitLiteralExpression(LiteralExpression literalExpression) { + if (literalExpression.literal instanceof Literal.Integer) { + Literal.Integer literalInteger = (Literal.Integer) literalExpression.literal; + writeRadixPrefix(literalInteger.radix); + output.append(Integer.toUnsignedString(literalInteger.value, literalInteger.radix)); + } else if (literalExpression.literal instanceof Literal.Long) { + Literal.Long literalLong = (Literal.Long) literalExpression.literal; + writeRadixPrefix(literalLong.radix); + output.append(Long.toUnsignedString(literalLong.value, literalLong.radix)).append('L'); + } else if (literalExpression.literal instanceof Literal.Float) { + output.append(((Literal.Float) literalExpression.literal).value).append('F'); + } else if (literalExpression.literal instanceof Literal.Double) { + output.append(((Literal.Double) literalExpression.literal).value); + } else if (literalExpression.literal instanceof Literal.Character) { + output.append(quoteString(String.valueOf(((Literal.Character) literalExpression.literal).value), '\'')); + } else if (literalExpression.literal instanceof Literal.String) { + output.append(quoteString(((Literal.String) literalExpression.literal).value, '"')); + } else { + throw new AssertionError("Unknown literal: " + literalExpression.literal); + } + } + + @Override + public void visitParenExpression(ParenExpression parenExpression) { + output.append('('); + parenExpression.expression.accept(this); + output.append(')'); + } + + @Override + public void visitUnaryExpression(UnaryExpression unaryExpression) { + switch (unaryExpression.operator) { + case NEGATE: + output.append('-'); + break; + case BIT_NOT: + output.append('~'); + break; + default: + throw new AssertionError("Unknown operator: " + unaryExpression.operator); + } + unaryExpression.operand.accept(this); + } + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java new file mode 100644 index 0000000..c057aa0 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java @@ -0,0 +1,5 @@ +package net.earthcomputer.unpickv3parser.tree; + +public enum DataType { + BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, CHAR, STRING +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java new file mode 100644 index 0000000..3b6c96c --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java @@ -0,0 +1,13 @@ +package net.earthcomputer.unpickv3parser.tree; + +import net.earthcomputer.unpickv3parser.tree.expr.Expression; + +public final class GroupConstant { + public final Literal.ConstantKey key; + public final Expression value; + + public GroupConstant(Literal.ConstantKey key, Expression value) { + this.key = key; + this.value = value; + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java new file mode 100644 index 0000000..10d31bd --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java @@ -0,0 +1,35 @@ +package net.earthcomputer.unpickv3parser.tree; + +import org.jetbrains.annotations.Nullable; + +import java.util.List; + +public final class GroupDefinition { + public final GroupScope scope; + public final GroupType type; + public final boolean strict; + public final DataType dataType; + @Nullable + public final String name; + public final List constants; + @Nullable + public final GroupFormat format; + + public GroupDefinition( + GroupScope scope, + GroupType type, + boolean strict, + DataType dataType, + @Nullable String name, + List constants, + @Nullable GroupFormat format + ) { + this.scope = scope; + this.type = type; + this.strict = strict; + this.dataType = dataType; + this.name = name; + this.constants = constants; + this.format = format; + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java new file mode 100644 index 0000000..8d88068 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java @@ -0,0 +1,5 @@ +package net.earthcomputer.unpickv3parser.tree; + +public enum GroupFormat { + DECIMAL, HEX, BINARY, OCTAL, CHAR +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java new file mode 100644 index 0000000..b299886 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java @@ -0,0 +1,41 @@ +package net.earthcomputer.unpickv3parser.tree; + +public abstract class GroupScope { + private GroupScope() { + } + + public static final class Global extends GroupScope { + public static final Global INSTANCE = new Global(); + + private Global() { + } + } + + public static final class Package extends GroupScope { + public final String packageName; + + public Package(String packageName) { + this.packageName = packageName; + } + } + + public static final class Class extends GroupScope { + public final String className; + + public Class(String className) { + this.className = className; + } + } + + public static final class Method extends GroupScope { + public final String className; + public final String methodName; + public final String methodDesc; + + public Method(String className, String methodName, String methodDesc) { + this.className = className; + this.methodName = methodName; + this.methodDesc = methodDesc; + } + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java new file mode 100644 index 0000000..0a3629b --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java @@ -0,0 +1,5 @@ +package net.earthcomputer.unpickv3parser.tree; + +public enum GroupType { + CONST, FLAG +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java new file mode 100644 index 0000000..0ede19e --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java @@ -0,0 +1,87 @@ +package net.earthcomputer.unpickv3parser.tree; + +public abstract class Literal { + private Literal() { + } + + public static abstract class ConstantKey extends Literal { + private ConstantKey() { + } + } + + public static abstract class NumberConstant extends ConstantKey { + private NumberConstant() {} + + public abstract Number asNumber(); + } + + public static final class Integer extends Literal { + public final int value; + public final int radix; + + public Integer(int value) { + this(value, 10); + } + + public Integer(int value, int radix) { + this.value = value; + this.radix = radix; + } + } + + public static final class Long extends NumberConstant { + public final long value; + public final int radix; + + public Long(long value) { + this(value, 10); + } + + public Long(long value, int radix) { + this.value = value; + this.radix = radix; + } + + @Override + public Number asNumber() { + return value; + } + } + + public static final class Float extends Literal { + public final float value; + + public Float(float value) { + this.value = value; + } + } + + public static final class Double extends NumberConstant { + public final double value; + + public Double(double value) { + this.value = value; + } + + @Override + public Number asNumber() { + return value; + } + } + + public static final class Character extends Literal { + public final char value; + + public Character(char value) { + this.value = value; + } + } + + public static final class String extends ConstantKey { + public final java.lang.String value; + + public String(java.lang.String value) { + this.value = value; + } + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java new file mode 100644 index 0000000..359ea3b --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java @@ -0,0 +1,15 @@ +package net.earthcomputer.unpickv3parser.tree; + +public final class TargetField { + public final String className; + public final String fieldName; + public final String fieldDesc; + public final String groupName; + + public TargetField(String className, String fieldName, String fieldDesc, String groupName) { + this.className = className; + this.fieldName = fieldName; + this.fieldDesc = fieldDesc; + this.groupName = groupName; + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java new file mode 100644 index 0000000..b21a5c7 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java @@ -0,0 +1,28 @@ +package net.earthcomputer.unpickv3parser.tree; + +import org.jetbrains.annotations.Nullable; + +import java.util.Map; + +public final class TargetMethod { + public final String className; + public final String methodName; + public final String methodDesc; + public final Map paramGroups; + @Nullable + public final String returnGroup; + + public TargetMethod( + String className, + String methodName, + String methodDesc, + Map paramGroups, + @Nullable String returnGroup + ) { + this.className = className; + this.methodName = methodName; + this.methodDesc = methodDesc; + this.paramGroups = paramGroups; + this.returnGroup = returnGroup; + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java new file mode 100644 index 0000000..dbbb302 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java @@ -0,0 +1,12 @@ +package net.earthcomputer.unpickv3parser.tree; + +public abstract class UnpickV3Visitor { + public void visitGroupDefinition(GroupDefinition groupDefinition) { + } + + public void visitTargetField(TargetField targetField) { + } + + public void visitTargetMethod(TargetMethod targetMethod) { + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java new file mode 100644 index 0000000..022ea0d --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java @@ -0,0 +1,37 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +public final class BinaryExpression extends Expression { + public final Expression lhs; + public final Expression rhs; + public final Operator operator; + + public BinaryExpression(Expression lhs, Expression rhs, Operator operator) { + this.lhs = lhs; + this.rhs = rhs; + this.operator = operator; + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visitBinaryExpression(this); + } + + @Override + public Expression transform(ExpressionTransformer transformer) { + return transformer.transformBinaryExpression(this); + } + + public enum Operator { + BIT_OR, + BIT_XOR, + BIT_AND, + BIT_SHIFT_LEFT, + BIT_SHIFT_RIGHT, + BIT_SHIFT_RIGHT_UNSIGNED, + ADD, + SUBTRACT, + MULTIPLY, + DIVIDE, + MODULO, + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java new file mode 100644 index 0000000..67e7e82 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java @@ -0,0 +1,23 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +import net.earthcomputer.unpickv3parser.tree.DataType; + +public final class CastExpression extends Expression { + public final DataType castType; + public final Expression operand; + + public CastExpression(DataType castType, Expression operand) { + this.castType = castType; + this.operand = operand; + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visitCastExpression(this); + } + + @Override + public Expression transform(ExpressionTransformer transformer) { + return transformer.transformCastExpression(this); + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java new file mode 100644 index 0000000..07f9da1 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java @@ -0,0 +1,10 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +public abstract class Expression { + Expression() { + } + + public abstract void accept(ExpressionVisitor visitor); + + public abstract Expression transform(ExpressionTransformer transformer); +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java new file mode 100644 index 0000000..7e0fd88 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java @@ -0,0 +1,27 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +public abstract class ExpressionTransformer { + public Expression transformBinaryExpression(BinaryExpression binaryExpression) { + return new BinaryExpression(binaryExpression.lhs.transform(this), binaryExpression.rhs.transform(this), binaryExpression.operator); + } + + public Expression transformCastExpression(CastExpression castExpression) { + return new CastExpression(castExpression.castType, castExpression.operand.transform(this)); + } + + public Expression transformFieldExpression(FieldExpression fieldExpression) { + return fieldExpression; + } + + public Expression transformLiteralExpression(LiteralExpression literalExpression) { + return literalExpression; + } + + public Expression transformParenExpression(ParenExpression parenExpression) { + return new ParenExpression(parenExpression.expression.transform(this)); + } + + public Expression transformUnaryExpression(UnaryExpression unaryExpression) { + return new UnaryExpression(unaryExpression.operand.transform(this), unaryExpression.operator); + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java new file mode 100644 index 0000000..f6a9e98 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java @@ -0,0 +1,29 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +/** + * Visitor for an {@link Expression}. By default, recursively visits sub-expressions. + */ +public abstract class ExpressionVisitor { + public void visitBinaryExpression(BinaryExpression binaryExpression) { + binaryExpression.lhs.accept(this); + binaryExpression.rhs.accept(this); + } + + public void visitCastExpression(CastExpression castExpression) { + castExpression.operand.accept(this); + } + + public void visitFieldExpression(FieldExpression fieldExpression) { + } + + public void visitLiteralExpression(LiteralExpression literalExpression) { + } + + public void visitParenExpression(ParenExpression parenExpression) { + parenExpression.expression.accept(this); + } + + public void visitUnaryExpression(UnaryExpression unaryExpression) { + unaryExpression.operand.accept(this); + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java new file mode 100644 index 0000000..648f752 --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java @@ -0,0 +1,29 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +import net.earthcomputer.unpickv3parser.tree.DataType; +import org.jetbrains.annotations.Nullable; + +public final class FieldExpression extends Expression { + public final String className; + public final String fieldName; + @Nullable + public final DataType fieldType; + public final boolean isStatic; + + public FieldExpression(String className, String fieldName, @Nullable DataType fieldType, boolean isStatic) { + this.className = className; + this.fieldName = fieldName; + this.fieldType = fieldType; + this.isStatic = isStatic; + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visitFieldExpression(this); + } + + @Override + public Expression transform(ExpressionTransformer transformer) { + return transformer.transformFieldExpression(this); + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java new file mode 100644 index 0000000..a6022ba --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java @@ -0,0 +1,24 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +import net.earthcomputer.unpickv3parser.tree.Literal; + +public final class LiteralExpression extends Expression { + /** + * Note: this literal is always positive. Integers are to be interpreted as unsigned values. + */ + public final Literal literal; + + public LiteralExpression(Literal literal) { + this.literal = literal; + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visitLiteralExpression(this); + } + + @Override + public Expression transform(ExpressionTransformer transformer) { + return transformer.transformLiteralExpression(this); + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java new file mode 100644 index 0000000..6bda33e --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java @@ -0,0 +1,19 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +public final class ParenExpression extends Expression { + public final Expression expression; + + public ParenExpression(Expression expression) { + this.expression = expression; + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visitParenExpression(this); + } + + @Override + public Expression transform(ExpressionTransformer transformer) { + return transformer.transformParenExpression(this); + } +} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java new file mode 100644 index 0000000..012f68a --- /dev/null +++ b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java @@ -0,0 +1,25 @@ +package net.earthcomputer.unpickv3parser.tree.expr; + +public final class UnaryExpression extends Expression { + public final Expression operand; + public final Operator operator; + + public UnaryExpression(Expression operand, Operator operator) { + this.operand = operand; + this.operator = operator; + } + + @Override + public void accept(ExpressionVisitor visitor) { + visitor.visitUnaryExpression(this); + } + + @Override + public Expression transform(ExpressionTransformer transformer) { + return transformer.transformUnaryExpression(this); + } + + public enum Operator { + NEGATE, BIT_NOT, + } +} diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java b/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java new file mode 100644 index 0000000..739b6c9 --- /dev/null +++ b/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java @@ -0,0 +1,72 @@ +package net.neoforged.jst.unpick; + +import net.earthcomputer.unpickv3parser.tree.DataType; + +public enum IntegerType { + BYTE(DataType.BYTE, false) { + @Override + public Number cast(Number in) { + return in.byteValue(); + } + }, + SHORT(DataType.SHORT, false, BYTE) { + @Override + public Number cast(Number in) { + return in.shortValue(); + } + }, + INT(DataType.INT, true, SHORT) { + @Override + public long toUnsignedLong(Number number) { + return Integer.toUnsignedLong(number.intValue()); + } + + @Override + public Number negate(Number number) { + return ~number.intValue(); + } + + @Override + public Number cast(Number in) { + return in.intValue(); + } + }, + LONG(DataType.LONG, true, INT) { + @Override + public Number cast(Number in) { + return in.longValue(); + } + }, + FLOAT(DataType.FLOAT, false, INT) { + @Override + public Number cast(Number in) { + return in.floatValue(); + } + }, + DOUBLE(DataType.DOUBLE, false, FLOAT) { + @Override + public Number cast(Number in) { + return in.doubleValue(); + } + }; + + public final DataType dataType; + public final boolean supportsFlag; + public final IntegerType[] widenFrom; + + IntegerType(DataType dataType, boolean supportsFlag, IntegerType... widenFrom) { + this.dataType = dataType; + this.supportsFlag = supportsFlag; + this.widenFrom = widenFrom; + } + + public abstract Number cast(Number in); + + public long toUnsignedLong(Number number) { + return number.longValue(); + } + + public Number negate(Number number) { + return ~number.longValue(); + } +} diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java new file mode 100644 index 0000000..9d414d6 --- /dev/null +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java @@ -0,0 +1,235 @@ +package net.neoforged.jst.unpick; + +import com.intellij.openapi.util.Key; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiJavaFile; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.search.GlobalSearchScope; +import com.intellij.util.containers.MultiMap; +import net.earthcomputer.unpickv3parser.tree.DataType; +import net.earthcomputer.unpickv3parser.tree.GroupDefinition; +import net.earthcomputer.unpickv3parser.tree.GroupFormat; +import net.earthcomputer.unpickv3parser.tree.GroupScope; +import net.earthcomputer.unpickv3parser.tree.GroupType; +import net.earthcomputer.unpickv3parser.tree.Literal; +import net.earthcomputer.unpickv3parser.tree.TargetField; +import net.earthcomputer.unpickv3parser.tree.TargetMethod; +import net.earthcomputer.unpickv3parser.tree.expr.Expression; +import net.neoforged.jst.api.PsiHelper; +import net.neoforged.jst.api.TransformContext; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.HashSet; +import java.util.IdentityHashMap; +import java.util.List; +import java.util.Map; +import java.util.Optional; +import java.util.Set; +import java.util.function.Predicate; +import java.util.stream.Collectors; + +public class UnpickCollection { + private static final Key> UNPICK_DEFINITION = Key.create("unpick.method_definition"); + private static final Key UNPICK_FIELD_TARGET = Key.create("unpick.field_target"); + + private final Set possibleTargetNames = new HashSet<>(); + private final Map groups; + + private final List global; + + private final MultiMap byPackage; + private final MultiMap byClass; + + private final Map> methodScopes; + + public UnpickCollection(TransformContext context, Map groups, List fields, List methods) { + this.groups = new HashMap<>(groups.size()); + groups.forEach((k, v) -> this.groups.put(k.name(), Group.create(v))); + + var facade = context.environment().getPsiFacade(); + var project = context.environment().getPsiManager().getProject(); + + var projectScope = GlobalSearchScope.projectScope(project); + + global = new ArrayList<>(); + + byPackage = new MultiMap<>(); + byClass = new MultiMap<>(); + + methodScopes = new IdentityHashMap<>(); + + groups.forEach((s, def) -> { + var gr = Group.create(def); + if (def.scope instanceof GroupScope.Package pkg) { + byPackage.putValue(pkg.packageName, gr); + } else if (def.scope instanceof GroupScope.Class cls) { + byClass.putValue(cls.className, gr); + } else if (def.scope instanceof GroupScope.Global && def.name == null) { + global.add(gr); + } else if (def.scope instanceof GroupScope.Method mtd) { + var cls = facade.findClass(mtd.className, projectScope); + if (cls == null) return; + + for (PsiMethod clsMethod : cls.getMethods()) { + if (clsMethod.getName().equals(mtd.methodName) && PsiHelper.getBinaryMethodSignature(clsMethod).equals(mtd.methodDesc)) { + methodScopes.computeIfAbsent(clsMethod, k -> new ArrayList<>()).add(gr); + } + } + } + }); + + for (var field : fields) { + var cls = facade.findClass(field.className, projectScope); + if (cls == null) continue; + + var fld = cls.findFieldByName(field.fieldName, true); + if (fld != null) { + fld.putUserData(UNPICK_FIELD_TARGET, field); + } + } + + for (var method : methods) { + var cls = facade.findClass(method.className, projectScope); + if (cls == null) continue; + + possibleTargetNames.add(method.methodName); + + for (PsiMethod clsMethod : cls.getMethods()) { + if (clsMethod.getName().equals(method.methodName) && PsiHelper.getBinaryMethodSignature(clsMethod).equals(method.methodDesc)) { + clsMethod.putUserData(UNPICK_DEFINITION, Optional.of(method)); + } + } + } + } + + public boolean forEachInScope(PsiClass cls, @Nullable PsiMethod scope, Predicate pred) { + if (scope != null) { + var metScoped = methodScopes.get(scope); + if (metScoped != null) { + for (Group group : metScoped) { + if (pred.test(group)) return true; + } + } + } + + var clsName = cls.getQualifiedName(); + if (clsName != null) { + for (Group group : byClass.get(clsName)) { + if (pred.test(group)) { + return true; + } + } + } + + var par = cls.getParent(); + while (par != null && !(par instanceof PsiJavaFile)) { + par = par.getParent(); + } + + if (par instanceof PsiJavaFile file) { + for (Group group : byPackage.get(file.getPackageName())) { + if (pred.test(group)) { + return true; + } + } + } + + for (Group group : global) { + if (pred.test(group)) { + return true; + } + } + + return false; + } + + @SuppressWarnings("OptionalAssignedToNull") + @Nullable + public TargetMethod getDefinitionsFor(PsiMethod method) { + if (!possibleTargetNames.contains(method.getName())) return null; + var data = method.getUserData(UNPICK_DEFINITION); + if (data == null) { + synchronized (this) { + if (method.getParent() instanceof PsiClass cls) { + for (PsiClass iface : cls.getInterfaces()) { + var met = iface.findMethodBySignature(method, true); + if (met != null) { + var parent = getDefinitionsFor(met); + if (parent != null) { + data = Optional.of(parent); + break; + } + } + } + + if (data == null && cls.getSuperClass() != null) { + var met = cls.getSuperClass().findMethodBySignature(method, true); + if (met != null) { + var parent = getDefinitionsFor(met); + if (parent != null) { + data = Optional.of(parent); + } + } + } + } + + if (data == null) data = Optional.empty(); + method.putUserData(UNPICK_DEFINITION, data); + } + } + return data.orElse(null); + } + + @Nullable + public Group getGroup(String id) { + return groups.get(id); + } + + public record Group( + DataType data, + boolean strict, + @Nullable GroupFormat format, + GroupType type, + Map constants + ) { + public static Group create(GroupDefinition def) { + return new Group( + def.dataType, + def.strict, + def.format, + def.type, + def.constants.stream() + .collect(Collectors.toMap( + g -> getKey(g.key, def.dataType), + g -> g.value + )) + ); + } + + private static Object getKey(Literal.ConstantKey key, DataType type) { + if (key instanceof Literal.NumberConstant nct) { + var val = nct.asNumber(); + return switch (type) { + case CHAR -> (char)val.intValue(); + case BYTE -> val.byteValue(); + case SHORT -> val.shortValue(); + case INT -> val.intValue(); + case LONG -> val.longValue(); + case FLOAT -> val.floatValue(); + case DOUBLE -> val.doubleValue(); + case STRING -> throw null; + }; + } + if (key.getClass() == Literal.String.class) { + return ((Literal.String) key).value; + } + return null; + } + } + + public record TypedKey(DataType type, GroupScope scope, @Nullable String name) { + + } +} diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java new file mode 100644 index 0000000..0468d64 --- /dev/null +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java @@ -0,0 +1,16 @@ +package net.neoforged.jst.unpick; + +import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.api.SourceTransformerPlugin; + +public class UnpickPlugin implements SourceTransformerPlugin { + @Override + public String getName() { + return "unpick"; + } + + @Override + public SourceTransformer createTransformer() { + return new UnpickTransformer(); + } +} diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java new file mode 100644 index 0000000..f9ae193 --- /dev/null +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java @@ -0,0 +1,71 @@ +package net.neoforged.jst.unpick; + +import com.intellij.psi.PsiFile; +import net.earthcomputer.unpickv3parser.UnpickV3Reader; +import net.earthcomputer.unpickv3parser.tree.GroupDefinition; +import net.earthcomputer.unpickv3parser.tree.TargetField; +import net.earthcomputer.unpickv3parser.tree.TargetMethod; +import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; +import net.neoforged.jst.api.Replacements; +import net.neoforged.jst.api.SourceTransformer; +import net.neoforged.jst.api.TransformContext; +import picocli.CommandLine; + +import java.io.IOException; +import java.io.UncheckedIOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; + +public class UnpickTransformer implements SourceTransformer { + @CommandLine.Option(names = "--unpick-data", description = "The paths to read unpick definition files from") + public List paths = new ArrayList<>(); + + private UnpickCollection collection; + + @Override + public void beforeRun(TransformContext context) { + var groups = new HashMap(); + var fields = new ArrayList(); + var methods = new ArrayList(); + + for (Path path : paths) { + try (var reader = Files.newBufferedReader(path)) { + new UnpickV3Reader(reader).accept(new UnpickV3Visitor() { + @Override + public void visitGroupDefinition(GroupDefinition groupDefinition) { + groups.merge(new UnpickCollection.TypedKey(groupDefinition.dataType, groupDefinition.scope, groupDefinition.name), groupDefinition, UnpickTransformer.this::merge); + } + + @Override + public void visitTargetField(TargetField targetField) { + fields.add(targetField); + } + + @Override + public void visitTargetMethod(TargetMethod targetMethod) { + methods.add(targetMethod); + } + }); + } catch (IOException exception) { + context.logger().error("Failed to read unpick definition file: %s", exception.getMessage()); + throw new UncheckedIOException(exception); + } + } + + this.collection = new UnpickCollection(context, groups, fields, methods); + } + + @Override + public void visitFile(PsiFile psiFile, Replacements replacements) { + new UnpickVisitor(psiFile, collection, replacements).visitFile(psiFile); + } + + private GroupDefinition merge(GroupDefinition first, GroupDefinition second) { + // TODO - validate they can be merged + first.constants.addAll(second.constants); + return first; + } +} diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java new file mode 100644 index 0000000..b0c5978 --- /dev/null +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -0,0 +1,398 @@ +package net.neoforged.jst.unpick; + +import com.intellij.openapi.util.Key; +import com.intellij.psi.JavaTokenType; +import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; +import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiJavaToken; +import com.intellij.psi.PsiLocalVariable; +import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiRecursiveElementVisitor; +import com.intellij.psi.PsiReferenceExpression; +import net.earthcomputer.unpickv3parser.tree.GroupFormat; +import net.earthcomputer.unpickv3parser.tree.GroupType; +import net.earthcomputer.unpickv3parser.tree.Literal; +import net.earthcomputer.unpickv3parser.tree.TargetMethod; +import net.earthcomputer.unpickv3parser.tree.expr.BinaryExpression; +import net.earthcomputer.unpickv3parser.tree.expr.CastExpression; +import net.earthcomputer.unpickv3parser.tree.expr.Expression; +import net.earthcomputer.unpickv3parser.tree.expr.ExpressionVisitor; +import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; +import net.earthcomputer.unpickv3parser.tree.expr.LiteralExpression; +import net.earthcomputer.unpickv3parser.tree.expr.ParenExpression; +import net.earthcomputer.unpickv3parser.tree.expr.UnaryExpression; +import net.neoforged.jst.api.ImportHelper; +import net.neoforged.jst.api.Replacements; +import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.function.Predicate; + +public class UnpickVisitor extends PsiRecursiveElementVisitor { + private static final Key UNPICK_WAS_REPLACED = Key.create("unpick.was_replaced"); + + private final PsiFile file; + private final UnpickCollection collection; + private final Replacements replacements; + + public UnpickVisitor(PsiFile file, UnpickCollection collection, Replacements replacements) { + this.file = file; + this.collection = collection; + this.replacements = replacements; + } + + private PsiClass classContext; + + @Nullable + private PsiMethod methodContext; + + @Nullable + private PsiField fieldContext; + + @Nullable + private PsiMethod calledMethod; + + @Nullable + private TargetMethod cachedDefinition; + + private int currentParamIndex; + + @Override + public void visitElement(@NotNull PsiElement element) { + if (element instanceof PsiClass cls) { + var oldCls = classContext; + this.classContext = cls; + cls.acceptChildren(this); + this.classContext = oldCls; + return; + } else if (element instanceof PsiMethod met) { + if (met.getBody() != null) { + var oldCtx = this.methodContext; + this.methodContext = met; + met.getBody().acceptChildren(this); + this.methodContext = oldCtx; + } + return; + } else if (element instanceof PsiField fld) { + var oldCtx = this.fieldContext; + this.fieldContext = fld; + fld.acceptChildren(this); + this.fieldContext = oldCtx; + return; + } else if (element instanceof PsiJavaToken tok) { + if (Boolean.TRUE.equals(tok.getUserData(UNPICK_WAS_REPLACED))) return; + + if (tok.getTokenType() == JavaTokenType.STRING_LITERAL) { + var val = tok.getText().substring(1); // Remove starting " + final var finalVal = val.substring(0, val.length() - 1); // Remove leading " + forInScope(group -> { + var ct = group.constants().get(finalVal); + if (ct != null && checkNotRecursive(ct)) { + replacements.replace(element, write(ct)); + tok.putUserData(UNPICK_WAS_REPLACED, true); + return true; + } + return false; + }); + } else if (tok.getTokenType() == JavaTokenType.INTEGER_LITERAL) { + var val = Integer.parseInt(tok.getText()); + replaceLiteral(tok, val, IntegerType.INT); + } else if (tok.getTokenType() == JavaTokenType.LONG_LITERAL) { + var val = Long.parseLong(removeSuffix(tok.getText(), "l")); + replaceLiteral(tok, val, IntegerType.LONG); + } else if (tok.getTokenType() == JavaTokenType.DOUBLE_LITERAL) { + var val = Double.parseDouble(removeSuffix(tok.getText(), "d")); + replaceLiteral(tok, val, IntegerType.DOUBLE); + } else if (tok.getTokenType() == JavaTokenType.FLOAT_LITERAL) { + var val = Float.parseFloat(removeSuffix(tok.getText(), "f")); + replaceLiteral(tok, val, IntegerType.FLOAT); + } + + return; + } else if (element instanceof PsiMethodCallExpression call) { + var ref = call.getMethodExpression().resolve(); + if (ref instanceof PsiMethod met) { + var oldMet = calledMethod; + calledMethod = met; + cachedDefinition = null; + + for (int i = 0; i < call.getArgumentList().getExpressions().length; i++) { + var oldIndex = currentParamIndex; + currentParamIndex = i; + + // TODO - we might want to rethink this, right now we rewalk everything with new context, + // but we could instead collect the variables from the start + if (call.getArgumentList().getExpressions()[i] instanceof PsiReferenceExpression refEx) { + var resolved = refEx.resolve(); + if (resolved instanceof PsiLocalVariable localVar && methodContext.getBody() != null) { + if (localVar.getInitializer() != null) { + super.visitElement(localVar.getInitializer()); + } + + methodContext.getBody().acceptChildren(new PsiRecursiveElementVisitor() { + @Override + public void visitElement(@NotNull PsiElement element) { + if (element instanceof PsiAssignmentExpression as && as.getOperationSign().getTokenType() == JavaTokenType.EQ) { + if (as.getLExpression() instanceof PsiReferenceExpression ref && ref.resolve() == localVar && as.getRExpression() != null) { + UnpickVisitor.this.visitElement(as.getRExpression()); + } + return; + } + super.visitElement(element); + } + }); + continue; + } + } + + // TODO - we need to handle return unpicks + + super.visitElement(call.getArgumentList().getExpressions()[i]); + + currentParamIndex = oldIndex; + } + calledMethod = oldMet; + } + return; + } + super.visitElement(element); + } + + private boolean replaceLiteral(PsiElement element, Number number, IntegerType type) { + return forInScope(group -> { + if (group.type() == GroupType.CONST) { + var ct = group.constants().get(number); + if (ct != null && checkNotRecursive(ct)) { + replacements.replace(element, write(ct)); + element.putUserData(UNPICK_WAS_REPLACED, true); + return true; + } + } + + if (group.type() == GroupType.FLAG && type.supportsFlag) { + var flag = generateFlag(group, number.longValue(), type); + if (flag != null) { + replacements.replace(element, flag); + element.putUserData(UNPICK_WAS_REPLACED, true); + return true; + } + } + + if (group.format() != null) { + replacements.replace(element, formatAs(number, group.format())); + element.putUserData(UNPICK_WAS_REPLACED, true); + return true; + } + + for (IntegerType from : type.widenFrom) { + var lower = from.cast(number); + if (lower.doubleValue() == number.doubleValue()) { + if (replaceLiteral(element, lower, from)) { + return true; + } + } + } + + return false; + }); + } + + private boolean checkNotRecursive(Expression expression) { + if (fieldContext != null && expression instanceof FieldExpression fld) { + return !(fld.className.equals(classContext.getQualifiedName()) && fld.fieldName.equals(fieldContext.getName())); + } + return true; + } + + private boolean forInScope(Predicate apply) { + if (calledMethod != null) { + if (cachedDefinition == null) { + cachedDefinition = collection.getDefinitionsFor(calledMethod); + } + if (cachedDefinition != null) { + var grId = cachedDefinition.paramGroups.get(currentParamIndex); + if (grId != null) { + var gr = collection.getGroup(grId); + if (gr != null && apply.test(gr)) { + return true; + } + } + } + } + return collection.forEachInScope(classContext, methodContext, apply); + } + + private String write(Expression expression) { + StringBuilder s = new StringBuilder(); + expression.accept(new ExpressionVisitor() { + @Override + public void visitFieldExpression(FieldExpression fieldExpression) { + var cls = imports().importClass(fieldExpression.className); + s.append(cls).append('.').append(fieldExpression.fieldName); + } + + @Override + public void visitParenExpression(ParenExpression parenExpression) { + s.append('(') + .append(write(parenExpression.expression)) + .append(')'); + } + + @Override + public void visitLiteralExpression(LiteralExpression literalExpression) { + if (literalExpression.literal instanceof Literal.String str) { + s.append('\"').append(str.value).append('\"'); // TODO - escape + } else if (literalExpression.literal instanceof Literal.Integer i) { + s.append(i.value); + } else if (literalExpression.literal instanceof Literal.Long l) { + s.append(l.value).append('l'); + } else if (literalExpression.literal instanceof Literal.Double d) { + s.append(d).append('d'); + } else if (literalExpression.literal instanceof Literal.Float f) { + s.append(f).append('f'); + } + } + + @Override + public void visitCastExpression(CastExpression castExpression) { + s.append('('); + s.append(switch (castExpression.castType) { + case INT -> "int"; + case CHAR -> "char"; + case DOUBLE -> "double"; + case BYTE -> "byte"; + case LONG -> "long"; + case FLOAT -> "float"; + case SHORT -> "short"; + case STRING -> "String"; + }); + s.append(')'); + s.append(write(castExpression.operand)); + } + + @Override + public void visitBinaryExpression(BinaryExpression binaryExpression) { + s.append(write(binaryExpression.lhs)); + switch (binaryExpression.operator) { + case ADD -> s.append(" + "); + case DIVIDE -> s.append(" / "); + case MODULO -> s.append(" % "); + case MULTIPLY -> s.append(" * "); + case SUBTRACT -> s.append(" - "); + + case BIT_AND -> s.append(" & "); + case BIT_OR -> s.append(" | "); + case BIT_XOR -> s.append(" ^ "); + + case BIT_SHIFT_LEFT -> s.append(" << "); + case BIT_SHIFT_RIGHT -> s.append(" >> "); + case BIT_SHIFT_RIGHT_UNSIGNED -> s.append(" >>> "); + } + s.append(write(binaryExpression.rhs)); + } + + @Override + public void visitUnaryExpression(UnaryExpression unaryExpression) { + switch (unaryExpression.operator) { + case NEGATE -> s.append("!"); + case BIT_NOT -> s.append("~"); + } + s.append(write(unaryExpression.operand)); + } + }); + return s.toString(); + } + + private String formatAs(Number value, GroupFormat format) { + return switch (format) { + case HEX -> { + if (value instanceof Integer) yield "0x" + Integer.toHexString(value.intValue()).toUpperCase(Locale.ROOT); + else if (value instanceof Long) yield "0x" + Long.toHexString(value.longValue()).toUpperCase(Locale.ROOT) + "l"; + else if (value instanceof Double) yield Double.toHexString(value.longValue()) + "d"; + else if (value instanceof Float) yield Float.toHexString(value.longValue()) + "f"; + yield value.toString(); + } + case OCTAL -> { + if (value instanceof Integer) yield "0" + Integer.toOctalString(value.intValue()); + else if (value instanceof Long) yield "0" + Long.toOctalString(value.longValue()) + "l"; + yield value.toString(); + } + case BINARY -> { + if (value instanceof Integer) yield "0b" + Integer.toBinaryString(value.intValue()); + else if (value instanceof Long) yield "0b" + Long.toBinaryString(value.longValue()) + "l"; + yield value.toString(); + } + case CHAR -> "'" + ((char) value.intValue()) + "'"; + + default -> value.toString(); + }; + } + + private ImportHelper imports() { + return ImportHelper.get(file); + } + + @Nullable + private String generateFlag(UnpickCollection.Group group, long val, IntegerType type) { + List orConstants = new ArrayList<>(); + long orResidual = getConstantsEncompassing(val, type, group, orConstants); + long negatedLiteral = type.toUnsignedLong(type.negate(val)); + List negatedConstants = new ArrayList<>(); + long negatedResidual = getConstantsEncompassing(negatedLiteral, type, group, negatedConstants); + + boolean negated = negatedResidual == 0 && (orResidual != 0 || negatedConstants.size() < orConstants.size()); + List constants = negated ? negatedConstants : orConstants; + if (constants.isEmpty()) + return null; + + long residual = negated ? negatedResidual : orResidual; + + StringBuilder replacement = new StringBuilder(write(constants.get(0))); + for (int i = 1; i < constants.size(); i++) { + replacement.append(" | "); + replacement.append(write(constants.get(i))); + } + + if (residual != 0) { + replacement.append(" | ").append(residual); + } + + if (negated) { + return "~" + replacement; + } + + return replacement.toString(); + } + + /** + * Adds the constants that encompass {@code literal} to {@code constantsOut}. + * Returns the residual (bits set in the literal not covered by the returned constants). + */ + private static long getConstantsEncompassing(long literal, IntegerType unsign, UnpickCollection.Group group, List constantsOut) { + long residual = literal; + for (var constant : group.constants().entrySet()) { + long val = unsign.toUnsignedLong((Number) constant.getKey()); + if ((val & residual) != 0 && (val & literal) == val) { + residual &= ~val; + constantsOut.add(constant.getValue()); + if (residual == 0) + break; + } + } + return residual; + } + + private static String removeSuffix(String in, String suffix) { + if (in.endsWith(suffix) || in.endsWith(suffix.toUpperCase(Locale.ROOT))) { + return in.substring(0, in.length() - 1); + } + return in; + } +} diff --git a/unpick/src/main/resources/META-INF/services/net.neoforged.jst.api.SourceTransformerPlugin b/unpick/src/main/resources/META-INF/services/net.neoforged.jst.api.SourceTransformerPlugin new file mode 100644 index 0000000..da41d00 --- /dev/null +++ b/unpick/src/main/resources/META-INF/services/net.neoforged.jst.api.SourceTransformerPlugin @@ -0,0 +1 @@ +net.neoforged.jst.unpick.UnpickPlugin From a2096a04c222fb857c60c4d8e423e59ca0731d57 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Mon, 17 Mar 2025 23:24:56 +0200 Subject: [PATCH 05/14] Continue unpicking --- .../java/net/neoforged/jst/api/PsiHelper.java | 13 ++ .../neoforged/jst/cli/io/FolderFileSink.java | 3 + .../jst/unpick/UnpickCollection.java | 9 ++ .../neoforged/jst/unpick/UnpickVisitor.java | 150 ++++++++++++------ 4 files changed, 127 insertions(+), 48 deletions(-) diff --git a/api/src/main/java/net/neoforged/jst/api/PsiHelper.java b/api/src/main/java/net/neoforged/jst/api/PsiHelper.java index 3e864c3..35bdfc9 100644 --- a/api/src/main/java/net/neoforged/jst/api/PsiHelper.java +++ b/api/src/main/java/net/neoforged/jst/api/PsiHelper.java @@ -2,11 +2,14 @@ import com.intellij.lang.jvm.types.JvmPrimitiveTypeKind; import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiModifier; import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiParameterListOwner; import com.intellij.psi.PsiPrimitiveType; +import com.intellij.psi.PsiReferenceExpression; import com.intellij.psi.PsiTypeParameter; import com.intellij.psi.PsiTypes; import com.intellij.psi.PsiWhiteSpace; @@ -17,6 +20,7 @@ import com.intellij.psi.util.PsiTreeUtil; import com.intellij.util.containers.ObjectIntHashMap; import org.jetbrains.annotations.NotNull; +import org.jetbrains.annotations.Nullable; import java.util.ArrayList; import java.util.Iterator; @@ -242,4 +246,13 @@ public static int getLastLineLength(PsiWhiteSpace psiWhiteSpace) { return psiWhiteSpace.getTextLength(); } } + + @Nullable + public static PsiElement resolve(PsiReferenceExpression expression) { + try { + return expression.resolve(); + } catch (Exception ignored) { + return null; + } + } } diff --git a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java index 5c943cb..c6cd5eb 100644 --- a/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java +++ b/cli/src/main/java/net/neoforged/jst/cli/io/FolderFileSink.java @@ -17,6 +17,9 @@ public void putDirectory(String relativePath) throws IOException { @Override public void putFile(String relativePath, FileTime lastModified, byte[] content) throws IOException { var targetPath = path.resolve(relativePath); + if (targetPath.getParent() != null) { + Files.createDirectories(targetPath.getParent()); + } Files.write(targetPath, content); Files.setLastModifiedTime(targetPath, lastModified); } diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java index 9d414d6..fa6b01e 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java @@ -2,6 +2,7 @@ import com.intellij.openapi.util.Key; import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiElement; import com.intellij.psi.PsiJavaFile; import com.intellij.psi.PsiMethod; import com.intellij.psi.search.GlobalSearchScope; @@ -44,6 +45,11 @@ public class UnpickCollection { private final Map> methodScopes; + // This list only exists to keep the base elements in memory and prevent them from being GC'd and therefore losing their user data + // JavaPsiFacade#findClass uses a soft key and soft value map + @SuppressWarnings({"FieldCanBeLocal", "MismatchedQueryAndUpdateOfCollection"}) + private final List baseElements; + public UnpickCollection(TransformContext context, Map groups, List fields, List methods) { this.groups = new HashMap<>(groups.size()); groups.forEach((k, v) -> this.groups.put(k.name(), Group.create(v))); @@ -59,6 +65,7 @@ public UnpickCollection(TransformContext context, Map byClass = new MultiMap<>(); methodScopes = new IdentityHashMap<>(); + baseElements = new ArrayList<>(); groups.forEach((s, def) -> { var gr = Group.create(def); @@ -87,6 +94,7 @@ public UnpickCollection(TransformContext context, Map var fld = cls.findFieldByName(field.fieldName, true); if (fld != null) { fld.putUserData(UNPICK_FIELD_TARGET, field); + baseElements.add(fld); } } @@ -99,6 +107,7 @@ public UnpickCollection(TransformContext context, Map for (PsiMethod clsMethod : cls.getMethods()) { if (clsMethod.getName().equals(method.methodName) && PsiHelper.getBinaryMethodSignature(clsMethod).equals(method.methodDesc)) { clsMethod.putUserData(UNPICK_DEFINITION, Optional.of(method)); + baseElements.add(clsMethod); } } } diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java index b0c5978..9023c1c 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -3,14 +3,17 @@ import com.intellij.openapi.util.Key; import com.intellij.psi.JavaTokenType; import com.intellij.psi.PsiAssignmentExpression; +import com.intellij.psi.PsiCallExpression; import com.intellij.psi.PsiClass; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiField; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiJavaToken; +import com.intellij.psi.PsiLiteralExpression; import com.intellij.psi.PsiLocalVariable; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiPrefixExpression; import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiReferenceExpression; import net.earthcomputer.unpickv3parser.tree.GroupFormat; @@ -26,6 +29,7 @@ import net.earthcomputer.unpickv3parser.tree.expr.ParenExpression; import net.earthcomputer.unpickv3parser.tree.expr.UnaryExpression; import net.neoforged.jst.api.ImportHelper; +import net.neoforged.jst.api.PsiHelper; import net.neoforged.jst.api.Replacements; import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.Nullable; @@ -87,37 +91,10 @@ public void visitElement(@NotNull PsiElement element) { this.fieldContext = oldCtx; return; } else if (element instanceof PsiJavaToken tok) { - if (Boolean.TRUE.equals(tok.getUserData(UNPICK_WAS_REPLACED))) return; - - if (tok.getTokenType() == JavaTokenType.STRING_LITERAL) { - var val = tok.getText().substring(1); // Remove starting " - final var finalVal = val.substring(0, val.length() - 1); // Remove leading " - forInScope(group -> { - var ct = group.constants().get(finalVal); - if (ct != null && checkNotRecursive(ct)) { - replacements.replace(element, write(ct)); - tok.putUserData(UNPICK_WAS_REPLACED, true); - return true; - } - return false; - }); - } else if (tok.getTokenType() == JavaTokenType.INTEGER_LITERAL) { - var val = Integer.parseInt(tok.getText()); - replaceLiteral(tok, val, IntegerType.INT); - } else if (tok.getTokenType() == JavaTokenType.LONG_LITERAL) { - var val = Long.parseLong(removeSuffix(tok.getText(), "l")); - replaceLiteral(tok, val, IntegerType.LONG); - } else if (tok.getTokenType() == JavaTokenType.DOUBLE_LITERAL) { - var val = Double.parseDouble(removeSuffix(tok.getText(), "d")); - replaceLiteral(tok, val, IntegerType.DOUBLE); - } else if (tok.getTokenType() == JavaTokenType.FLOAT_LITERAL) { - var val = Float.parseFloat(removeSuffix(tok.getText(), "f")); - replaceLiteral(tok, val, IntegerType.FLOAT); - } - + visitToken(tok); return; } else if (element instanceof PsiMethodCallExpression call) { - var ref = call.getMethodExpression().resolve(); + PsiElement ref = PsiHelper.resolve(call.getMethodExpression()); if (ref instanceof PsiMethod met) { var oldMet = calledMethod; calledMethod = met; @@ -130,31 +107,31 @@ public void visitElement(@NotNull PsiElement element) { // TODO - we might want to rethink this, right now we rewalk everything with new context, // but we could instead collect the variables from the start if (call.getArgumentList().getExpressions()[i] instanceof PsiReferenceExpression refEx) { - var resolved = refEx.resolve(); - if (resolved instanceof PsiLocalVariable localVar && methodContext.getBody() != null) { + PsiElement resolved = PsiHelper.resolve(refEx); + if (resolved instanceof PsiLocalVariable localVar && methodContext != null && methodContext.getBody() != null) { if (localVar.getInitializer() != null) { - super.visitElement(localVar.getInitializer()); + localVar.getInitializer().accept(limitedDirectVisitor()); } - methodContext.getBody().acceptChildren(new PsiRecursiveElementVisitor() { + new PsiRecursiveElementVisitor() { @Override public void visitElement(@NotNull PsiElement element) { - if (element instanceof PsiAssignmentExpression as && as.getOperationSign().getTokenType() == JavaTokenType.EQ) { - if (as.getLExpression() instanceof PsiReferenceExpression ref && ref.resolve() == localVar && as.getRExpression() != null) { - UnpickVisitor.this.visitElement(as.getRExpression()); + if (element instanceof PsiAssignmentExpression as) { + if (as.getOperationSign().getTokenType() == JavaTokenType.EQ && as.getLExpression() instanceof PsiReferenceExpression ref && PsiHelper.resolve(ref) == localVar && as.getRExpression() != null) { + as.getRExpression().accept(limitedDirectVisitor()); } return; } super.visitElement(element); } - }); + }.visitElement(methodContext.getBody()); continue; } } // TODO - we need to handle return unpicks - super.visitElement(call.getArgumentList().getExpressions()[i]); + this.visitElement(call.getArgumentList().getExpressions()[i]); currentParamIndex = oldIndex; } @@ -162,18 +139,83 @@ public void visitElement(@NotNull PsiElement element) { } return; } - super.visitElement(element); + + element.acceptChildren(this); } - private boolean replaceLiteral(PsiElement element, Number number, IntegerType type) { - return forInScope(group -> { - if (group.type() == GroupType.CONST) { - var ct = group.constants().get(number); + private PsiRecursiveElementVisitor limitedDirectVisitor() { + return new PsiRecursiveElementVisitor() { + @Override + public void visitElement(@NotNull PsiElement element) { + if (element instanceof PsiCallExpression) { + return; // We do not want to try to further replace constants inside method calls, that's why we're limited to direct elements + } + if (element instanceof PsiJavaToken tok) { + visitToken(tok); + return; + } + super.visitElement(element); + } + }; + } + + private void visitToken(PsiJavaToken tok) { + if (Boolean.TRUE.equals(tok.getUserData(UNPICK_WAS_REPLACED))) return; + + if (tok.getTokenType() == JavaTokenType.STRING_LITERAL) { + var val = tok.getText().substring(1); // Remove starting " + final var finalVal = val.substring(0, val.length() - 1); // Remove leading " + forInScope(group -> { + var ct = group.constants().get(finalVal); if (ct != null && checkNotRecursive(ct)) { - replacements.replace(element, write(ct)); - element.putUserData(UNPICK_WAS_REPLACED, true); + replacements.replace(tok, write(ct)); + tok.putUserData(UNPICK_WAS_REPLACED, true); return true; } + return false; + }); + } else if (tok.getTokenType() == JavaTokenType.INTEGER_LITERAL) { + int val; + if (tok.getText().toLowerCase(Locale.ROOT).startsWith("0x")) { + val = Integer.parseUnsignedInt(tok.getText().substring(2), 16); + } else if (tok.getText().toLowerCase(Locale.ROOT).startsWith("0b")) { + val = Integer.parseUnsignedInt(tok.getText().substring(2), 2); + } else { + val = Integer.parseUnsignedInt(tok.getText()); + } + if (isUnaryMinus(tok)) val = -val; + replaceLiteral(tok, val, IntegerType.INT); + } else if (tok.getTokenType() == JavaTokenType.LONG_LITERAL) { + var val = Long.parseLong(removeSuffix(tok.getText(), "l")); + if (isUnaryMinus(tok)) val = -val; + replaceLiteral(tok, val, IntegerType.LONG); + } else if (tok.getTokenType() == JavaTokenType.DOUBLE_LITERAL) { + var val = Double.parseDouble(removeSuffix(tok.getText(), "d")); + if (isUnaryMinus(tok)) val = -val; + replaceLiteral(tok, val, IntegerType.DOUBLE); + } else if (tok.getTokenType() == JavaTokenType.FLOAT_LITERAL) { + var val = Float.parseFloat(removeSuffix(tok.getText(), "f")); + if (isUnaryMinus(tok)) val = -val; + replaceLiteral(tok, val, IntegerType.FLOAT); + } + } + + private void replaceLiteral(PsiJavaToken element, Number number, IntegerType type) { + replaceLiteral(element, number, type, false); + } + + private boolean replaceLiteral(PsiJavaToken element, Number number, IntegerType type, boolean denyStrict) { + return forInScope(group -> { + // If we need to deny strict conversion (so if this is a conversion) we shall do so + if (group.strict() && denyStrict) return false; + + // We'll try a direct constant first, even if it's a flag + var ct = group.constants().get(number); + if (ct != null && checkNotRecursive(ct)) { + replacements.replace(element, write(ct)); + element.putUserData(UNPICK_WAS_REPLACED, true); + replaceMinus(element); + return true; } if (group.type() == GroupType.FLAG && type.supportsFlag) { @@ -181,12 +223,14 @@ private boolean replaceLiteral(PsiElement element, Number number, IntegerType ty if (flag != null) { replacements.replace(element, flag); element.putUserData(UNPICK_WAS_REPLACED, true); + replaceMinus(element); return true; } } if (group.format() != null) { replacements.replace(element, formatAs(number, group.format())); + replaceMinus(element); element.putUserData(UNPICK_WAS_REPLACED, true); return true; } @@ -194,7 +238,7 @@ private boolean replaceLiteral(PsiElement element, Number number, IntegerType ty for (IntegerType from : type.widenFrom) { var lower = from.cast(number); if (lower.doubleValue() == number.doubleValue()) { - if (replaceLiteral(element, lower, from)) { + if (replaceLiteral(element, lower, from, true)) { return true; } } @@ -204,6 +248,16 @@ private boolean replaceLiteral(PsiElement element, Number number, IntegerType ty }); } + private boolean isUnaryMinus(PsiJavaToken tok) { + return tok.getParent() instanceof PsiLiteralExpression lit && lit.getParent() instanceof PsiPrefixExpression ex && ex.getOperationTokenType() == JavaTokenType.MINUS; + } + + private void replaceMinus(PsiJavaToken tok) { + if (tok.getParent() instanceof PsiLiteralExpression lit && lit.getParent() instanceof PsiPrefixExpression ex && ex.getOperationTokenType() == JavaTokenType.MINUS) { + replacements.remove(ex.getOperationSign()); + } + } + private boolean checkNotRecursive(Expression expression) { if (fieldContext != null && expression instanceof FieldExpression fld) { return !(fld.className.equals(classContext.getQualifiedName()) && fld.fieldName.equals(fieldContext.getName())); @@ -315,8 +369,8 @@ private String formatAs(Number value, GroupFormat format) { case HEX -> { if (value instanceof Integer) yield "0x" + Integer.toHexString(value.intValue()).toUpperCase(Locale.ROOT); else if (value instanceof Long) yield "0x" + Long.toHexString(value.longValue()).toUpperCase(Locale.ROOT) + "l"; - else if (value instanceof Double) yield Double.toHexString(value.longValue()) + "d"; - else if (value instanceof Float) yield Float.toHexString(value.longValue()) + "f"; + else if (value instanceof Double) yield Double.toHexString(value.doubleValue()) + "d"; + else if (value instanceof Float) yield Float.toHexString(value.floatValue()) + "f"; yield value.toString(); } case OCTAL -> { From 9248ff4be86cfb39b94e30ee54629dfed46e2e5c Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Fri, 18 Jul 2025 01:05:01 +0300 Subject: [PATCH 06/14] Unpick v3 --- build.gradle | 2 +- tests/data/unpick/const/def.unpick | 21 +- .../unpick/const/expected/com/stuff/Uses.java | 2 +- .../unpick/const/source/com/stuff/Uses.java | 6 +- tests/data/unpick/formats/def.unpick | 16 +- tests/data/unpick/scoped/def.unpick | 17 +- .../unpick/scoped/expected/com/Outsider.java | 2 +- unpick/build.gradle | 1 + .../unpickv3parser/UnpickParseException.java | 17 - .../unpickv3parser/UnpickV3Reader.java | 1232 ----------------- .../unpickv3parser/UnpickV3Remapper.java | 240 ---- .../unpickv3parser/UnpickV3Writer.java | 379 ----- .../unpickv3parser/tree/DataType.java | 5 - .../unpickv3parser/tree/GroupConstant.java | 13 - .../unpickv3parser/tree/GroupDefinition.java | 35 - .../unpickv3parser/tree/GroupFormat.java | 5 - .../unpickv3parser/tree/GroupScope.java | 41 - .../unpickv3parser/tree/GroupType.java | 5 - .../unpickv3parser/tree/Literal.java | 87 -- .../unpickv3parser/tree/TargetField.java | 15 - .../unpickv3parser/tree/TargetMethod.java | 28 - .../unpickv3parser/tree/UnpickV3Visitor.java | 12 - .../tree/expr/BinaryExpression.java | 37 - .../tree/expr/CastExpression.java | 23 - .../unpickv3parser/tree/expr/Expression.java | 10 - .../tree/expr/ExpressionTransformer.java | 27 - .../tree/expr/ExpressionVisitor.java | 29 - .../tree/expr/FieldExpression.java | 29 - .../tree/expr/LiteralExpression.java | 24 - .../tree/expr/ParenExpression.java | 19 - .../tree/expr/UnaryExpression.java | 25 - .../net/neoforged/jst/unpick/IntegerType.java | 302 +++- .../jst/unpick/UnpickCollection.java | 223 ++- .../jst/unpick/UnpickTransformer.java | 21 +- .../neoforged/jst/unpick/UnpickVisitor.java | 51 +- 35 files changed, 518 insertions(+), 2483 deletions(-) delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java delete mode 100644 unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java diff --git a/build.gradle b/build.gradle index 1d0102a..8f75a38 100644 --- a/build.gradle +++ b/build.gradle @@ -23,7 +23,7 @@ subprojects { java { toolchain { - languageVersion = JavaLanguageVersion.of(17) + languageVersion = JavaLanguageVersion.of(21) } } diff --git a/tests/data/unpick/const/def.unpick b/tests/data/unpick/const/def.unpick index ef188cd..57ec0c9 100644 --- a/tests/data/unpick/const/def.unpick +++ b/tests/data/unpick/const/def.unpick @@ -1,15 +1,16 @@ unpick v3 -const String - "1.21.4" = com.example.Constants.VERSION +group String + com.example.Constants.VERSION -const strict float - 3.1415927 = java.lang.Math.PI - 1.0471976 = java.lang.Math.PI / 3 +group float + @strict + java.lang.Math.PI + java.lang.Math.PI / 3 -const float - 2.5 = com.example.Constants.FLOAT_CT +group float + com.example.Constants.FLOAT_CT -const long - 34 = com.example.Constants.LONG_VAL - 70 = (com.example.Constants.LONG_VAL + 1) * 2 +group long + com.example.Constants.LONG_VAL + (com.example.Constants.LONG_VAL + 1) * 2 diff --git a/tests/data/unpick/const/expected/com/stuff/Uses.java b/tests/data/unpick/const/expected/com/stuff/Uses.java index b14567b..5614a9b 100644 --- a/tests/data/unpick/const/expected/com/stuff/Uses.java +++ b/tests/data/unpick/const/expected/com/stuff/Uses.java @@ -12,7 +12,7 @@ void run() { f = Math.PI / 3; - double d = 3.1415927d; // PI unpick is strict float so this should not be replaced + double d = 3.141592653589793d; // PI unpick is strict float so this should not be replaced d = Constants.FLOAT_CT; // but the other float unpick isn't so this double literal should be replaced diff --git a/tests/data/unpick/const/source/com/stuff/Uses.java b/tests/data/unpick/const/source/com/stuff/Uses.java index 6a87a12..6a61a1f 100644 --- a/tests/data/unpick/const/source/com/stuff/Uses.java +++ b/tests/data/unpick/const/source/com/stuff/Uses.java @@ -6,11 +6,11 @@ public class Uses { void run() { String s = "1.21.4" + "2"; - float f = 3.1415927f; + float f = 3.141592653589793f; - f = 1.0471976f; + f = 1.0471975511965976f; - double d = 3.1415927d; // PI unpick is strict float so this should not be replaced + double d = 3.141592653589793d; // PI unpick is strict float so this should not be replaced d = 2.5d; // but the other float unpick isn't so this double literal should be replaced diff --git a/tests/data/unpick/formats/def.unpick b/tests/data/unpick/formats/def.unpick index 402c80e..d385a4d 100644 --- a/tests/data/unpick/formats/def.unpick +++ b/tests/data/unpick/formats/def.unpick @@ -1,21 +1,21 @@ unpick v3 -const int HEXInt - format = hex +group int HEXInt + @format hex target_method com.example.Example acceptHex(I)V param 0 HEXInt -const int BINInt - format = binary +group int BINInt + @format binary target_method com.example.Example acceptBin(I)V param 0 BINInt -const int OCTInt - format = octal +group int OCTInt + @format octal target_method com.example.Example acceptOct(I)V param 0 OCTInt -const int CharInt - format = char +group int CharInt + @format char target_method com.example.Example acceptChar(C)V param 0 CharInt diff --git a/tests/data/unpick/scoped/def.unpick b/tests/data/unpick/scoped/def.unpick index 5388b09..b2ee490 100644 --- a/tests/data/unpick/scoped/def.unpick +++ b/tests/data/unpick/scoped/def.unpick @@ -1,11 +1,14 @@ unpick v3 -scoped class com.example.Example const int - 472 = com.example.Example.V1 - 84 = com.example.Example.V2 +group int + @scope class com.example.Example + com.example.Example.V1 + com.example.Example.V2 -scoped method com.Outsider anotherExecute ()V const int - 12 = com.Outsider.DIFFERENT_CONSTANT +group int + @scope method com.Outsider anotherExecute ()V + com.Outsider.DIFFERENT_CONST -scoped package com.example const int - 4 = com.example.Example.FOUR +group int + @scope package com.example + com.example.Example.FOUR diff --git a/tests/data/unpick/scoped/expected/com/Outsider.java b/tests/data/unpick/scoped/expected/com/Outsider.java index f0fde68..847befd 100644 --- a/tests/data/unpick/scoped/expected/com/Outsider.java +++ b/tests/data/unpick/scoped/expected/com/Outsider.java @@ -12,6 +12,6 @@ public void execute() { } public void anotherExecute() { - int i = Outsider.DIFFERENT_CONSTANT; // This should be replaced with DIFFERENT_CONST + int i = Outsider.DIFFERENT_CONST; // This should be replaced with DIFFERENT_CONST } } diff --git a/unpick/build.gradle b/unpick/build.gradle index 6032fe2..9871314 100644 --- a/unpick/build.gradle +++ b/unpick/build.gradle @@ -4,4 +4,5 @@ plugins { dependencies { implementation project(':api') + implementation 'net.fabricmc.unpick:unpick-format-utils:3.0.0-beta.8' } diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java deleted file mode 100644 index fff2402..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickParseException.java +++ /dev/null @@ -1,17 +0,0 @@ -package net.earthcomputer.unpickv3parser; - -import java.io.IOException; - -/** - * Thrown when a syntax error is found in a .unpick file. - */ -public class UnpickParseException extends IOException { - public final int line; - public final int column; - - public UnpickParseException(String message, int line, int column) { - super(line + ":" + column + ": " + message); - this.line = line; - this.column = column; - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java deleted file mode 100644 index 336889a..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Reader.java +++ /dev/null @@ -1,1232 +0,0 @@ -package net.earthcomputer.unpickv3parser; - -import net.earthcomputer.unpickv3parser.tree.DataType; -import net.earthcomputer.unpickv3parser.tree.GroupConstant; -import net.earthcomputer.unpickv3parser.tree.GroupDefinition; -import net.earthcomputer.unpickv3parser.tree.GroupFormat; -import net.earthcomputer.unpickv3parser.tree.GroupScope; -import net.earthcomputer.unpickv3parser.tree.GroupType; -import net.earthcomputer.unpickv3parser.tree.Literal; -import net.earthcomputer.unpickv3parser.tree.TargetField; -import net.earthcomputer.unpickv3parser.tree.TargetMethod; -import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; -import net.earthcomputer.unpickv3parser.tree.expr.BinaryExpression; -import net.earthcomputer.unpickv3parser.tree.expr.CastExpression; -import net.earthcomputer.unpickv3parser.tree.expr.Expression; -import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; -import net.earthcomputer.unpickv3parser.tree.expr.LiteralExpression; -import net.earthcomputer.unpickv3parser.tree.expr.ParenExpression; -import net.earthcomputer.unpickv3parser.tree.expr.UnaryExpression; -import org.jetbrains.annotations.Nullable; - -import java.io.IOException; -import java.io.LineNumberReader; -import java.io.Reader; -import java.util.ArrayList; -import java.util.EnumMap; -import java.util.HashMap; -import java.util.List; -import java.util.Map; -import java.util.Stack; - -/** - * Performs syntax checking and basic semantic checking on .unpick v3 format text, and allows its structure to be - * visited by instances of {@link UnpickV3Visitor}. - */ -public final class UnpickV3Reader implements AutoCloseable { - private static final int MAX_PARSE_DEPTH = 64; - private static final EnumMap PRECEDENCES = new EnumMap<>(BinaryExpression.Operator.class); - static { - PRECEDENCES.put(BinaryExpression.Operator.BIT_OR, 0); - PRECEDENCES.put(BinaryExpression.Operator.BIT_XOR, 1); - PRECEDENCES.put(BinaryExpression.Operator.BIT_AND, 2); - PRECEDENCES.put(BinaryExpression.Operator.BIT_SHIFT_LEFT, 3); - PRECEDENCES.put(BinaryExpression.Operator.BIT_SHIFT_RIGHT, 3); - PRECEDENCES.put(BinaryExpression.Operator.BIT_SHIFT_RIGHT_UNSIGNED, 3); - PRECEDENCES.put(BinaryExpression.Operator.ADD, 4); - PRECEDENCES.put(BinaryExpression.Operator.SUBTRACT, 4); - PRECEDENCES.put(BinaryExpression.Operator.MULTIPLY, 5); - PRECEDENCES.put(BinaryExpression.Operator.DIVIDE, 5); - PRECEDENCES.put(BinaryExpression.Operator.MODULO, 5); - } - - private final LineNumberReader reader; - private String line; - private int column; - private int lastTokenLine; - private int lastTokenColumn; - private TokenType lastTokenType; - private String nextToken; - private ParseState nextTokenState; - private String nextToken2; - private ParseState nextToken2State; - - public UnpickV3Reader(Reader reader) { - this.reader = new LineNumberReader(reader); - } - - public void accept(UnpickV3Visitor visitor) throws IOException { - line = reader.readLine(); - if (!"unpick v3".equals(line)) { - throw parseError("Missing version marker", 1, 0); - } - column = line.length(); - - nextToken(); // newline - - while (true) { - String token = nextToken(); - if (lastTokenType == TokenType.EOF) { - break; - } - parseUnpickItem(visitor, token); - } - } - - private void parseUnpickItem(UnpickV3Visitor visitor, String token) throws IOException { - if (lastTokenType != TokenType.IDENTIFIER) { - throw expectedTokenError("unpick item", token); - } - - switch (token) { - case "target_field": - visitor.visitTargetField(parseTargetField()); - break; - case "target_method": - visitor.visitTargetMethod(parseTargetMethod()); - break; - case "scoped": - GroupScope scope = parseGroupScope(); - token = nextToken("group type", TokenType.IDENTIFIER); - switch (token) { - case "const": - visitor.visitGroupDefinition(parseGroupDefinition(scope, GroupType.CONST)); - break; - case "flag": - visitor.visitGroupDefinition(parseGroupDefinition(scope, GroupType.FLAG)); - break; - default: - throw expectedTokenError("group type", token); - } - break; - case "const": - visitor.visitGroupDefinition(parseGroupDefinition(GroupScope.Global.INSTANCE, GroupType.CONST)); - break; - case "flag": - visitor.visitGroupDefinition(parseGroupDefinition(GroupScope.Global.INSTANCE, GroupType.FLAG)); - break; - default: - throw expectedTokenError("unpick item", token); - } - } - - private GroupScope parseGroupScope() throws IOException { - String token = nextToken("group scope type", TokenType.IDENTIFIER); - switch (token) { - case "package": - return new GroupScope.Package(parseClassName("package name")); - case "class": - return new GroupScope.Class(parseClassName()); - case "method": - String className = parseClassName(); - String methodName = parseMethodName(); - String methodDesc = nextToken(TokenType.METHOD_DESCRIPTOR); - return new GroupScope.Method(className, methodName, methodDesc); - default: - throw expectedTokenError("group scope type", token); - } - } - - private GroupDefinition parseGroupDefinition(GroupScope scope, GroupType type) throws IOException { - int typeLine = lastTokenLine; - int typeColumn = lastTokenColumn; - - boolean strict = false; - if ("strict".equals(peekToken())) { - nextToken(); - strict = true; - } - - DataType dataType = parseDataType(); - if (!isDataTypeValidInGroup(dataType)) { - throw parseError("Data type not allowed in group: " + dataType); - } - if (type == GroupType.FLAG && dataType != DataType.INT && dataType != DataType.LONG) { - throw parseError("Data type not allowed for flag constants"); - } - - String name = peekTokenType() == TokenType.IDENTIFIER ? nextToken() : null; - if (name == null && type != GroupType.CONST) { - throw parseError("Non-const group type used for default group", typeLine, typeColumn); - } - - List constants = new ArrayList<>(); - GroupFormat format = null; - - while (true) { - String token = nextToken(); - if (lastTokenType == TokenType.EOF) { - break; - } - if (lastTokenType != TokenType.NEWLINE) { - throw expectedTokenError("'\\n'", token); - } - - if (peekTokenType() != TokenType.INDENT) { - break; - } - nextToken(); - - token = nextToken(); - switch (lastTokenType) { - case IDENTIFIER: - if (!"format".equals(token)) { - throw expectedTokenError("constant", token); - } - if (format != null) { - throw parseError("Duplicate format declaration"); - } - expectToken("="); - format = parseGroupFormat(); - break; - case OPERATOR: case INTEGER: case DOUBLE: case CHAR: case STRING: - int constantLine = lastTokenLine; - int constantColumn = lastTokenColumn; - GroupConstant constant = parseGroupConstant(token); - if (!isMatchingConstantType(dataType, constant.key)) { - throw parseError("Constant type not valid for group data type", constantLine, constantColumn); - } - if (isDuplicateConstantKey(constants, constant)) { - throw parseError("Duplicate constant key", constantLine, constantColumn); - } - constants.add(constant); - break; - default: - throw expectedTokenError("constant", token); - } - } - - return new GroupDefinition(scope, type, strict, dataType, name, constants, format); - } - - private static boolean isDataTypeValidInGroup(DataType type) { - return type == DataType.INT || type == DataType.LONG || type == DataType.FLOAT || type == DataType.DOUBLE || type == DataType.STRING; - } - - private static boolean isMatchingConstantType(DataType type, Literal.ConstantKey constantKey) { - if (constantKey instanceof Literal.Long) { - return type != DataType.STRING; - } else if (constantKey instanceof Literal.Double) { - return type == DataType.FLOAT || type == DataType.DOUBLE; - } else if (constantKey instanceof Literal.String) { - return type == DataType.STRING; - } else { - throw new AssertionError("Unknown group constant type: " + constantKey.getClass().getName()); - } - } - - private static boolean isDuplicateConstantKey(List constants, GroupConstant newConstant) { - if (newConstant.key instanceof Literal.Long) { - long newValue = ((Literal.Long) newConstant.key).value; - for (GroupConstant constant : constants) { - if (constant.key instanceof Literal.Long && ((Literal.Long) constant.key).value == newValue) { - return true; - } - if (constant.key instanceof Literal.Double && ((Literal.Double) constant.key).value == newValue) { - return true; - } - } - } else if (newConstant.key instanceof Literal.Double) { - double newValue = ((Literal.Double) newConstant.key).value; - for (GroupConstant constant : constants) { - if (constant.key instanceof Literal.Long && ((Literal.Long) constant.key).value == newValue) { - return true; - } - if (constant.key instanceof Literal.Double && ((Literal.Double) constant.key).value == newValue) { - return true; - } - } - } else if (newConstant.key instanceof Literal.String) { - String newValue = ((Literal.String) newConstant.key).value; - for (GroupConstant constant : constants) { - if (constant.key instanceof Literal.String && ((Literal.String) constant.key).value.equals(newValue)) { - return true; - } - } - } - - return false; - } - - private GroupFormat parseGroupFormat() throws IOException { - String token = nextToken("group format", TokenType.IDENTIFIER); - switch (token) { - case "decimal": - return GroupFormat.DECIMAL; - case "hex": - return GroupFormat.HEX; - case "binary": - return GroupFormat.BINARY; - case "octal": - return GroupFormat.OCTAL; - case "char": - return GroupFormat.CHAR; - default: - throw expectedTokenError("group format", token); - } - } - - private GroupConstant parseGroupConstant(String token) throws IOException { - Literal.ConstantKey key = parseGroupConstantKey(token); - expectToken("="); - Expression value = parseExpression(0); - return new GroupConstant(key, value); - } - - private Literal.ConstantKey parseGroupConstantKey(String token) throws IOException { - boolean negative = false; - if ("-".equals(token)) { - negative = true; - token = nextToken(); - } - - switch (lastTokenType) { - case INTEGER: - ParsedLong parsedLong = parseLong(token, negative); - return new Literal.Long(parsedLong.value, parsedLong.radix); - case DOUBLE: - return new Literal.Double(parseDouble(token, negative)); - case CHAR: - return new Literal.Long(unquoteChar(token)); - case STRING: - return new Literal.String(unquoteString(token)); - default: - throw expectedTokenError("number", token); - } - } - - private Expression parseExpression(int parseDepth) throws IOException { - // Shunting yard algorithm for parsing with operator precedence: https://stackoverflow.com/a/47717/11071180 - Stack operandStack = new Stack<>(); - Stack operatorStack = new Stack<>(); - - operandStack.push(parseUnaryExpression(parseDepth, false)); - - parseLoop: - while (true) { - BinaryExpression.Operator operator; - switch (peekToken()) { - case "|": - operator = BinaryExpression.Operator.BIT_OR; - break; - case "^": - operator = BinaryExpression.Operator.BIT_XOR; - break; - case "&": - operator = BinaryExpression.Operator.BIT_AND; - break; - case "<<": - operator = BinaryExpression.Operator.BIT_SHIFT_LEFT; - break; - case ">>": - operator = BinaryExpression.Operator.BIT_SHIFT_RIGHT; - break; - case ">>>": - operator = BinaryExpression.Operator.BIT_SHIFT_RIGHT_UNSIGNED; - break; - case "+": - operator = BinaryExpression.Operator.ADD; - break; - case "-": - operator = BinaryExpression.Operator.SUBTRACT; - break; - case "*": - operator = BinaryExpression.Operator.MULTIPLY; - break; - case "/": - operator = BinaryExpression.Operator.DIVIDE; - break; - case "%": - operator = BinaryExpression.Operator.MODULO; - break; - default: - break parseLoop; - } - nextToken(); // consume the operator - - int ourPrecedence = PRECEDENCES.get(operator); - while (!operatorStack.isEmpty() && ourPrecedence <= PRECEDENCES.get(operatorStack.peek())) { - BinaryExpression.Operator op = operatorStack.pop(); - Expression rhs = operandStack.pop(); - Expression lhs = operandStack.pop(); - operandStack.push(new BinaryExpression(lhs, rhs, op)); - } - - operatorStack.push(operator); - operandStack.push(parseUnaryExpression(parseDepth, false)); - } - - Expression result = operandStack.pop(); - while (!operatorStack.isEmpty()) { - result = new BinaryExpression(operandStack.pop(), result, operatorStack.pop()); - } - - return result; - } - - private Expression parseUnaryExpression(int parseDepth, boolean negative) throws IOException { - if (parseDepth > MAX_PARSE_DEPTH) { - throw parseError("max parse depth reached"); - } - - String token = nextToken(); - switch (token) { - case "-": - return new UnaryExpression(parseUnaryExpression(parseDepth + 1, true), UnaryExpression.Operator.NEGATE); - case "~": - return new UnaryExpression(parseUnaryExpression(parseDepth + 1, false), UnaryExpression.Operator.BIT_NOT); - case "(": - boolean parseAsCast = peekTokenType() == TokenType.IDENTIFIER && ")".equals(peekToken2()); - if (parseAsCast) { - DataType castType = parseDataType(); - nextToken(); // close paren - return new CastExpression(castType, parseUnaryExpression(parseDepth + 1, false)); - } else { - Expression expression = parseExpression(parseDepth + 1); - expectToken(")"); - return new ParenExpression(expression); - } - } - - switch (lastTokenType) { - case IDENTIFIER: - return parseFieldExpression(token); - case INTEGER: - ParsedInteger parsedInt = parseInt(token, negative); - return new LiteralExpression(new Literal.Integer(negative ? -parsedInt.value : parsedInt.value, parsedInt.radix)); - case LONG: - ParsedLong parsedLong = parseLong(token, negative); - return new LiteralExpression(new Literal.Long(negative ? -parsedLong.value : parsedLong.value, parsedLong.radix)); - case FLOAT: - float parsedFloat = parseFloat(token, negative); - return new LiteralExpression(new Literal.Float(negative ? -parsedFloat : parsedFloat)); - case DOUBLE: - double parsedDouble = parseDouble(token, negative); - return new LiteralExpression(new Literal.Double(negative ? -parsedDouble : parsedDouble)); - case CHAR: - return new LiteralExpression(new Literal.Character(unquoteChar(token))); - case STRING: - return new LiteralExpression(new Literal.String(unquoteString(token))); - default: - throw expectedTokenError("expression", token); - } - } - - private FieldExpression parseFieldExpression(String token) throws IOException { - expectToken("."); - String className = token + "." + parseClassName("field name"); - - // the field name has been joined to the class name, split it off - int dotIndex = className.lastIndexOf('.'); - String fieldName = className.substring(dotIndex + 1); - className = className.substring(0, dotIndex); - - boolean isStatic = true; - DataType fieldType = null; - if (":".equals(peekToken())) { - nextToken(); - if ("instance".equals(peekToken())) { - nextToken(); - isStatic = false; - if (":".equals(peekToken())) { - nextToken(); - fieldType = parseDataType(); - } - } else { - fieldType = parseDataType(); - } - } - - return new FieldExpression(className, fieldName, fieldType, isStatic); - } - - private TargetField parseTargetField() throws IOException { - String className = parseClassName(); - String fieldName = nextToken(TokenType.IDENTIFIER); - String fieldDesc = nextToken(TokenType.FIELD_DESCRIPTOR); - String groupName = nextToken(TokenType.IDENTIFIER); - String token = nextToken(); - if (lastTokenType != TokenType.NEWLINE && lastTokenType != TokenType.EOF) { - throw expectedTokenError("'\n'", token); - } - return new TargetField(className, fieldName, fieldDesc, groupName); - } - - private TargetMethod parseTargetMethod() throws IOException { - String className = parseClassName(); - String methodName = parseMethodName(); - String methodDesc = nextToken(TokenType.METHOD_DESCRIPTOR); - - Map paramGroups = new HashMap<>(); - String returnGroup = null; - - while (true) { - String token = nextToken(); - if (lastTokenType == TokenType.EOF) { - break; - } - if (lastTokenType != TokenType.NEWLINE) { - throw expectedTokenError("'\\n'", token); - } - - if (peekTokenType() != TokenType.INDENT) { - break; - } - nextToken(); - - token = nextToken("target method item", TokenType.IDENTIFIER); - switch (token) { - case "param": - int paramIndex = parseInt(nextToken(TokenType.INTEGER), false).value; - if (paramGroups.containsKey(paramIndex)) { - throw parseError("Specified parameter " + paramIndex + " twice"); - } - paramGroups.put(paramIndex, nextToken(TokenType.IDENTIFIER)); - break; - case "return": - if (returnGroup != null) { - throw parseError("Specified return group twice"); - } - returnGroup = nextToken(TokenType.IDENTIFIER); - break; - default: - throw expectedTokenError("target method item", token); - } - } - - return new TargetMethod(className, methodName, methodDesc, paramGroups, returnGroup); - } - - private DataType parseDataType() throws IOException { - String token = nextToken("data type", TokenType.IDENTIFIER); - switch (token) { - case "byte": - return DataType.BYTE; - case "short": - return DataType.SHORT; - case "int": - return DataType.INT; - case "long": - return DataType.LONG; - case "float": - return DataType.FLOAT; - case "double": - return DataType.DOUBLE; - case "char": - return DataType.CHAR; - case "String": - return DataType.STRING; - default: - throw expectedTokenError("data type", token); - } - } - - private String parseClassName() throws IOException { - return parseClassName("class name"); - } - - private String parseClassName(String expected) throws IOException { - StringBuilder result = new StringBuilder(nextToken(expected, TokenType.IDENTIFIER)); - while (".".equals(peekToken())) { - nextToken(); - result.append('.').append(nextToken(TokenType.IDENTIFIER)); - } - return result.toString(); - } - - private String parseMethodName() throws IOException { - String token = nextToken(); - if (lastTokenType == TokenType.IDENTIFIER) { - return token; - } - if ("<".equals(token)) { - token = nextToken(TokenType.IDENTIFIER); - if (!"init".equals(token) && !"clinit".equals(token)) { - throw expectedTokenError("identifier", token); - } - expectToken(">"); - return "<" + token + ">"; - } - throw expectedTokenError("identifier", token); - } - - private ParsedInteger parseInt(String string, boolean negative) throws UnpickParseException { - int radix; - if (string.startsWith("0x") || string.startsWith("0X")) { - radix = 16; - string = string.substring(2); - } else if (string.startsWith("0b") || string.startsWith("0B")) { - radix = 2; - string = string.substring(2); - } else if (string.startsWith("0") && string.length() > 1) { - radix = 8; - string = string.substring(1); - } else { - radix = 10; - } - - try { - return new ParsedInteger(Integer.parseInt(negative ? "-" + string : string, radix), radix); - } catch (NumberFormatException ignore) { - } - - // try unsigned parsing in other radixes - if (!negative && radix != 10) { - try { - return new ParsedInteger(Integer.parseUnsignedInt(string, radix), radix); - } catch (NumberFormatException ignore) { - } - } - - throw parseError("Integer out of bounds"); - } - - private static final class ParsedInteger { - final int value; - final int radix; - - private ParsedInteger(int value, int radix) { - this.value = value; - this.radix = radix; - } - } - - private ParsedLong parseLong(String string, boolean negative) throws UnpickParseException { - if (string.endsWith("l") || string.endsWith("L")) { - string = string.substring(0, string.length() - 1); - } - - int radix; - if (string.startsWith("0x") || string.startsWith("0X")) { - radix = 16; - string = string.substring(2); - } else if (string.startsWith("0b") || string.startsWith("0B")) { - radix = 2; - string = string.substring(2); - } else if (string.startsWith("0") && string.length() > 1) { - radix = 8; - string = string.substring(1); - } else { - radix = 10; - } - - try { - return new ParsedLong(Long.parseLong(negative ? "-" + string : string, radix), radix); - } catch (NumberFormatException ignore) { - } - - // try unsigned parsing in other radixes - if (!negative && radix != 10) { - try { - return new ParsedLong(Long.parseUnsignedLong(string, radix), radix); - } catch (NumberFormatException ignore) { - } - } - - throw parseError("Long out of bounds"); - } - - private static final class ParsedLong { - final long value; - final int radix; - - private ParsedLong(long value, int radix) { - this.value = value; - this.radix = radix; - } - } - - private float parseFloat(String string, boolean negative) throws UnpickParseException { - if (string.endsWith("f") || string.endsWith("F")) { - string = string.substring(0, string.length() - 1); - } - try { - float result = Float.parseFloat(string); - return negative ? -result : result; - } catch (NumberFormatException e) { - throw parseError("Invalid float"); - } - } - - private double parseDouble(String string, boolean negative) throws UnpickParseException { - try { - double result = Double.parseDouble(string); - return negative ? -result : result; - } catch (NumberFormatException e) { - throw parseError("Invalid double"); - } - } - - private static char unquoteChar(String string) { - return unquoteString(string).charAt(0); - } - - private static String unquoteString(String string) { - StringBuilder result = new StringBuilder(string.length() - 2); - for (int i = 1; i < string.length() - 1; i++) { - if (string.charAt(i) == '\\') { - i++; - switch (string.charAt(i)) { - case 'u': - do { - i++; - } while (string.charAt(i) == 'u'); - result.append((char) Integer.parseInt(string.substring(i, i + 4), 16)); - i += 3; - break; - case 'b': - result.append('\b'); - break; - case 't': - result.append('\t'); - break; - case 'n': - result.append('\n'); - break; - case 'f': - result.append('\f'); - break; - case 'r': - result.append('\r'); - break; - case '"': - result.append('"'); - break; - case '\'': - result.append('\''); - break; - case '\\': - result.append('\\'); - break; - case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': - char c; - int count = 0; - int maxCount = string.charAt(i) <= '3' ? 3 : 2; - while (count < maxCount && (c = string.charAt(i + count)) >= '0' && c <= '7') { - count++; - } - result.append((char) Integer.parseInt(string.substring(i, i + count), 8)); - i += count - 1; - break; - default: - throw new AssertionError("Unexpected escape sequence in string"); - } - } else { - result.append(string.charAt(i)); - } - } - return result.toString(); - } - - // region Tokenizer - - private TokenType peekTokenType() throws IOException { - ParseState state = new ParseState(this); - nextToken = nextToken(); - nextTokenState = new ParseState(this); - state.restore(this); - return nextTokenState.lastTokenType; - } - - private String peekToken() throws IOException { - ParseState state = new ParseState(this); - nextToken = nextToken(); - nextTokenState = new ParseState(this); - state.restore(this); - return nextToken; - } - - private String peekToken2() throws IOException { - ParseState state = new ParseState(this); - String nextToken = nextToken(); - ParseState nextTokenState = new ParseState(this); - nextToken2 = nextToken(); - nextToken2State = new ParseState(this); - this.nextToken = nextToken; - this.nextTokenState = nextTokenState; - state.restore(this); - return nextToken2; - } - - private void expectToken(String expected) throws IOException { - String token = nextToken(); - if (!expected.equals(token)) { - throw expectedTokenError(UnpickV3Writer.quoteString(expected, '\''), token); - } - } - - private String nextToken() throws IOException { - return nextTokenInner(null); - } - - private String nextToken(TokenType type) throws IOException { - return nextToken(type.name, type); - } - - private String nextToken(String expected, TokenType type) throws IOException { - String token = nextTokenInner(type); - if (lastTokenType != type) { - throw expectedTokenError(expected, token); - } - return token; - } - - private String nextTokenInner(@Nullable TokenType typeHint) throws IOException { - if (nextTokenState != null) { - String tok = nextToken; - nextToken = nextToken2; - nextToken2 = null; - nextTokenState.restore(this); - nextTokenState = nextToken2State; - nextToken2State = null; - return tok; - } - - if (lastTokenType == TokenType.EOF) { - return null; - } - - // newline token (skipping comment and whitespace) - while (column < line.length() && Character.isWhitespace(line.charAt(column))) { - column++; - } - if (column < line.length() && line.charAt(column) == '#') { - column = line.length(); - } - if (column == line.length() && lastTokenType != TokenType.NEWLINE) { - lastTokenColumn = column; - lastTokenLine = reader.getLineNumber(); - lastTokenType = TokenType.NEWLINE; - return "\n"; - } - - // skip whitespace and comments, handle indent token - boolean seenIndent = false; - while (true) { - if (column == line.length() || line.charAt(column) == '#') { - seenIndent = false; - line = reader.readLine(); - column = 0; - if (line == null) { - lastTokenColumn = column; - lastTokenLine = reader.getLineNumber(); - lastTokenType = TokenType.EOF; - return null; - } - } else if (Character.isWhitespace(line.charAt(column))) { - seenIndent = column == 0; - do { - column++; - } while (column < line.length() && Character.isWhitespace(line.charAt(column))); - } else { - break; - } - } - if (seenIndent) { - lastTokenColumn = 0; - lastTokenLine = reader.getLineNumber(); - lastTokenType = TokenType.INDENT; - return line.substring(0, column); - } - - lastTokenColumn = column; - lastTokenLine = reader.getLineNumber(); - - if (typeHint == TokenType.FIELD_DESCRIPTOR) { - if (skipFieldDescriptor(true)) { - return line.substring(lastTokenColumn, column); - } - } - - if (typeHint == TokenType.METHOD_DESCRIPTOR) { - if (skipMethodDescriptor()) { - return line.substring(lastTokenColumn, column); - } - } - - if (skipNumber()) { - if (column < line.length() && isIdentifierChar(line.charAt(column))) { - throw parseErrorInToken("Unexpected character in number: " + line.charAt(column)); - } - return line.substring(lastTokenColumn, column); - } - - if (skipIdentifier()) { - return line.substring(lastTokenColumn, column); - } - - if (skipString('\'', true)) { - lastTokenType = TokenType.CHAR; - return line.substring(lastTokenColumn, column); - } - - if (skipString('"', false)) { - lastTokenType = TokenType.STRING; - return line.substring(lastTokenColumn, column); - } - - char c = line.charAt(column); - column++; - if (c == '<') { - if (column < line.length() && line.charAt(column) == '<') { - column++; - } - } else if (c == '>') { - if (column < line.length() && line.charAt(column) == '>') { - column++; - if (column < line.length() && line.charAt(column) == '>') { - column++; - } - } - } - - lastTokenType = TokenType.OPERATOR; - return line.substring(lastTokenColumn, column); - } - - private boolean skipFieldDescriptor(boolean startOfToken) throws UnpickParseException { - // array descriptors - while (column < line.length() && line.charAt(column) == '[') { - startOfToken = false; - column++; - } - - // first character of main part of descriptor - if (column == line.length() || isTokenEnd(line.charAt(column))) { - throw parseErrorInToken("Unexpected end to descriptor"); - } - switch (line.charAt(column)) { - // primitive types - case 'B': case 'C': case 'D': case 'F': case 'I': case 'J': case 'S': case 'Z': - column++; - break; - // class types - case 'L': - column++; - - // class name - char c; - while (column < line.length() && (c = line.charAt(column)) != ';' && !isTokenEnd(c)) { - if (c == '.' || c == '[') { - throw parseErrorInToken("Illegal character in descriptor: " + c); - } - column++; - } - - // semicolon - if (column == line.length() || isTokenEnd(line.charAt(column))) { - throw parseErrorInToken("Unexpected end of descriptor"); - } - column++; - break; - default: - if (!startOfToken) { - throw parseErrorInToken("Illegal character in descriptor: " + line.charAt(column)); - } - return false; - } - - lastTokenType = TokenType.FIELD_DESCRIPTOR; - return true; - } - - private boolean skipMethodDescriptor() throws UnpickParseException { - if (line.charAt(column) != '(') { - return false; - } - column++; - - // parameter types - while (column < line.length() && line.charAt(column) != ')' && !isTokenEnd(line.charAt(column))) { - skipFieldDescriptor(false); - } - if (column == line.length() || isTokenEnd(line.charAt(column))) { - throw parseErrorInToken("Unexpected end of descriptor"); - } - column++; - - // return type - if (column == line.length() || isTokenEnd(line.charAt(column))) { - throw parseErrorInToken("Unexpected end of descriptor"); - } - if (line.charAt(column) == 'V') { - column++; - } else { - skipFieldDescriptor(false); - } - - lastTokenType = TokenType.METHOD_DESCRIPTOR; - return true; - } - - private boolean skipNumber() throws UnpickParseException { - if (line.charAt(column) < '0' || line.charAt(column) > '9') { - return false; - } - - // hex numbers - if (line.startsWith("0x", column) || line.startsWith("0X", column)) { - column += 2; - char c; - boolean seenDigit = false; - while (column < line.length() && ((c = line.charAt(column)) >= '0' && c <= '9' || c >= 'a' && c <= 'f' || c >= 'A' && c <= 'F')) { - seenDigit = true; - column++; - } - if (!seenDigit) { - throw parseErrorInToken("Unexpected end of integer"); - } - detectIntegerType(); - return true; - } - - // binary numbers - if (line.startsWith("0b", column) || line.startsWith("0B", column)) { - column += 2; - char c; - boolean seenDigit = false; - while (column < line.length() && ((c = line.charAt(column)) == '0' || c == '1')) { - seenDigit = true; - column++; - } - if (!seenDigit) { - throw parseErrorInToken("Unexpected end of integer"); - } - detectIntegerType(); - return true; - } - - // lookahead a decimal number - int endOfInteger = column; - char c; - do { - endOfInteger++; - } while (endOfInteger < line.length() && (c = line.charAt(endOfInteger)) >= '0' && c <= '9'); - - // floats and doubles - if (endOfInteger < line.length() && line.charAt(endOfInteger) == '.') { - column = endOfInteger + 1; - - // fractional part - boolean seenFracDigit = false; - while (column < line.length() && (c = line.charAt(column)) >= '0' && c <= '9') { - seenFracDigit = true; - column++; - } - if (!seenFracDigit) { - throw parseErrorInToken("Unexpected end of float"); - } - - // exponent - if (column < line.length() && ((c = line.charAt(column)) == 'e' || c == 'E')) { - column++; - if (column < line.length() && (c = line.charAt(column)) >= '+' && c <= '-') { - column++; - } - - boolean seenExponentDigit = false; - while (column < line.length() && ((c = line.charAt(column)) >= '0' && c <= '9')) { - seenExponentDigit = true; - column++; - } - if (!seenExponentDigit) { - throw parseErrorInToken("Unexpected end of float"); - } - } - - boolean isFloat = column < line.length() && ((c = line.charAt(column)) == 'f' || c == 'F'); - if (isFloat) { - column++; - } - lastTokenType = isFloat ? TokenType.FLOAT : TokenType.DOUBLE; - return true; - } - - // octal numbers (we'll count 0 itself as an octal) - if (line.charAt(column) == '0') { - column++; - while (column < line.length() && (c = line.charAt(column)) >= '0' && c <= '7') { - column++; - } - detectIntegerType(); - return true; - } - - // decimal numbers - column = endOfInteger; - detectIntegerType(); - return true; - } - - private void detectIntegerType() { - char c; - boolean isLong = column < line.length() && ((c = line.charAt(column)) == 'l' || c == 'L'); - if (isLong) { - column++; - } - lastTokenType = isLong ? TokenType.LONG : TokenType.INTEGER; - } - - private boolean skipIdentifier() { - if (!isIdentifierChar(line.charAt(column))) { - return false; - } - - do { - column++; - } while (column < line.length() && isIdentifierChar(line.charAt(column))); - - lastTokenType = TokenType.IDENTIFIER; - return true; - } - - private boolean skipString(char quoteChar, boolean singleChar) throws UnpickParseException { - if (line.charAt(column) != quoteChar) { - return false; - } - column++; - - boolean seenChar = false; - while (column < line.length() && line.charAt(column) != quoteChar) { - if (singleChar && seenChar) { - throw parseErrorInToken("Multiple characters in char literal"); - } - seenChar = true; - - if (line.charAt(column) == '\\') { - column++; - if (column == line.length()) { - throw parseErrorInToken("Unexpected end of string"); - } - char c = line.charAt(column); - switch (c) { - case 'u': - do { - column++; - } while (column < line.length() && line.charAt(column) == 'u'); - for (int i = 0; i < 4; i++) { - if (column == line.length()) { - throw parseErrorInToken("Unexpected end of string"); - } - c = line.charAt(column); - if ((c < '0' || c > '9') && (c < 'a' || c > 'f') && (c < 'A' || c > 'F')) { - throw parseErrorInToken("Illegal character in unicode escape sequence"); - } - column++; - } - break; - case 'b': case 't': case 'n': case 'f': case 'r': case '"': case '\'': case '\\': - column++; - break; - case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': - column++; - int maxOctalDigits = c <= '3' ? 3 : 2; - for (int i = 1; i < maxOctalDigits && column < line.length() && (c = line.charAt(column)) >= '0' && c <= '7'; i++) { - column++; - } - break; - default: - throw parseErrorInToken("Illegal escape sequence \\" + c); - } - } else { - column++; - } - } - - if (column == line.length()) { - throw parseErrorInToken("Unexpected end of string"); - } - - if (singleChar && !seenChar) { - throw parseErrorInToken("No character in char literal"); - } - - column++; - return true; - } - - private static boolean isTokenEnd(char c) { - return Character.isWhitespace(c) || c == '#'; - } - - private static boolean isIdentifierChar(char c) { - return c >= 'a' && c <= 'z' || c >= 'A' && c <= 'Z' || c >= '0' && c <= '9' || c == '_' || c == '$'; - } - - // endregion - - private UnpickParseException expectedTokenError(String expected, String token) { - if (lastTokenType == TokenType.EOF) { - return parseError("Expected " + expected + " before eof token"); - } else { - return parseError("Expected " + expected + " before " + UnpickV3Writer.quoteString(token, '\'') + " token"); - } - } - - private UnpickParseException parseError(String message) { - return parseError(message, lastTokenLine, lastTokenColumn); - } - - private UnpickParseException parseErrorInToken(String message) { - return parseError(message, reader.getLineNumber(), column); - } - - private UnpickParseException parseError(String message, int lineNumber, int column) { - return new UnpickParseException(message, lineNumber, column + 1); - } - - @Override - public void close() throws IOException { - reader.close(); - } - - private static class ParseState { - private final int lastTokenLine; - private final int lastTokenColumn; - private final TokenType lastTokenType; - - ParseState(UnpickV3Reader reader) { - this.lastTokenLine = reader.lastTokenLine; - this.lastTokenColumn = reader.lastTokenColumn; - this.lastTokenType = reader.lastTokenType; - } - - void restore(UnpickV3Reader reader) { - reader.lastTokenLine = lastTokenLine; - reader.lastTokenColumn = lastTokenColumn; - reader.lastTokenType = lastTokenType; - } - } - - private enum TokenType { - IDENTIFIER("identifier"), - DOUBLE("double"), - FLOAT("float"), - INTEGER("integer"), - LONG("long"), - CHAR("char"), - STRING("string"), - INDENT("indent"), - NEWLINE("newline"), - FIELD_DESCRIPTOR("field descriptor"), - METHOD_DESCRIPTOR("method descriptor"), - OPERATOR("operator"), - EOF("eof"); - - final String name; - - TokenType(String name) { - this.name = name; - } - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java deleted file mode 100644 index e5eaba8..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Remapper.java +++ /dev/null @@ -1,240 +0,0 @@ -package net.earthcomputer.unpickv3parser; - -import net.earthcomputer.unpickv3parser.tree.DataType; -import net.earthcomputer.unpickv3parser.tree.GroupConstant; -import net.earthcomputer.unpickv3parser.tree.GroupDefinition; -import net.earthcomputer.unpickv3parser.tree.GroupScope; -import net.earthcomputer.unpickv3parser.tree.TargetField; -import net.earthcomputer.unpickv3parser.tree.TargetMethod; -import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; -import net.earthcomputer.unpickv3parser.tree.expr.Expression; -import net.earthcomputer.unpickv3parser.tree.expr.ExpressionTransformer; -import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; - -import java.util.Collections; -import java.util.List; -import java.util.Map; -import java.util.Objects; -import java.util.stream.Collectors; - -/** - * Remaps all class, field, and method names in a .unpick v3 file. Visitor methods will be called on the downstream - * visitor with the remapped names. - */ -public class UnpickV3Remapper extends UnpickV3Visitor { - private final UnpickV3Visitor downstream; - private final Map> classesInPackage; - private final Map classMappings; - private final Map fieldMappings; - private final Map methodMappings; - - /** - * Warning: class names use "." format, not "/" format. {@code classesInPackage} should contain all the classes in - * each package, including unmapped ones. The classes in this map are unqualified by the package name (because the - * package name is already in the key of the map entry). - */ - public UnpickV3Remapper( - UnpickV3Visitor downstream, - Map> classesInPackage, - Map classMappings, - Map fieldMappings, - Map methodMappings - ) { - this.downstream = downstream; - this.classesInPackage = classesInPackage; - this.classMappings = classMappings; - this.fieldMappings = fieldMappings; - this.methodMappings = methodMappings; - } - - @Override - public void visitGroupDefinition(GroupDefinition groupDefinition) { - GroupScope oldScope = groupDefinition.scope; - List scopes; - if (oldScope instanceof GroupScope.Global) { - scopes = Collections.singletonList(oldScope); - } else if (oldScope instanceof GroupScope.Package) { - String pkg = ((GroupScope.Package) oldScope).packageName; - scopes = classesInPackage.getOrDefault(pkg, Collections.emptyList()).stream() - .map(cls -> new GroupScope.Class(mapClassName(pkg + "." + cls))) - .collect(Collectors.toList()); - } else if (oldScope instanceof GroupScope.Class) { - scopes = Collections.singletonList(new GroupScope.Class(mapClassName(((GroupScope.Class) oldScope).className))); - } else if (oldScope instanceof GroupScope.Method) { - GroupScope.Method methodScope = (GroupScope.Method) oldScope; - String className = mapClassName(methodScope.className); - String methodName = mapMethodName(methodScope.className, methodScope.methodName, methodScope.methodDesc); - String methodDesc = mapDescriptor(methodScope.methodDesc); - scopes = Collections.singletonList(new GroupScope.Method(className, methodName, methodDesc)); - } else { - throw new AssertionError("Unknown group scope type: " + oldScope.getClass().getName()); - } - - List constants = groupDefinition.constants.stream() - .map(constant -> new GroupConstant(constant.key, constant.value.transform(new ExpressionRemapper(groupDefinition.dataType)))) - .collect(Collectors.toList()); - - for (GroupScope scope : scopes) { - downstream.visitGroupDefinition(new GroupDefinition(scope, groupDefinition.type, groupDefinition.strict, groupDefinition.dataType, groupDefinition.name, constants, groupDefinition.format)); - } - } - - @Override - public void visitTargetField(TargetField targetField) { - String className = mapClassName(targetField.className); - String fieldName = mapFieldName(targetField.className, targetField.fieldName, targetField.fieldDesc); - String fieldDesc = mapDescriptor(targetField.fieldDesc); - downstream.visitTargetField(new TargetField(className, fieldName, fieldDesc, targetField.groupName)); - } - - @Override - public void visitTargetMethod(TargetMethod targetMethod) { - String className = mapClassName(targetMethod.className); - String methodName = mapMethodName(targetMethod.className, targetMethod.methodName, targetMethod.methodDesc); - String methodDesc = mapDescriptor(targetMethod.methodDesc); - downstream.visitTargetMethod(new TargetMethod(className, methodName, methodDesc, targetMethod.paramGroups, targetMethod.returnGroup)); - } - - private String mapClassName(String className) { - return classMappings.getOrDefault(className, className); - } - - private String mapFieldName(String className, String fieldName, String fieldDesc) { - return fieldMappings.getOrDefault(new FieldKey(className, fieldName, fieldDesc), fieldName); - } - - private String mapMethodName(String className, String methodName, String methodDesc) { - return methodMappings.getOrDefault(new MethodKey(className, methodName, methodDesc), methodName); - } - - private String mapDescriptor(String descriptor) { - StringBuilder mappedDescriptor = new StringBuilder(); - - int semicolonIndex = 0; - int lIndex; - while ((lIndex = descriptor.indexOf('L', semicolonIndex)) != -1) { - mappedDescriptor.append(descriptor, semicolonIndex, lIndex + 1); - semicolonIndex = descriptor.indexOf(';', lIndex); - if (semicolonIndex == -1) { - throw new AssertionError("Invalid descriptor: " + descriptor); - } - String className = descriptor.substring(lIndex + 1, semicolonIndex).replace('/', '.'); - mappedDescriptor.append(mapClassName(className).replace('.', '/')); - } - - return mappedDescriptor.append(descriptor, semicolonIndex, descriptor.length()).toString(); - } - - public static final class FieldKey { - public final String className; - public final String fieldName; - public final String fieldDesc; - - /** - * Warning: class name uses "." format, not "/" format - */ - public FieldKey(String className, String fieldName, String fieldDesc) { - this.className = className; - this.fieldName = fieldName; - this.fieldDesc = fieldDesc; - } - - @Override - public int hashCode() { - return Objects.hash(className, fieldName, fieldDesc); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof FieldKey)) { - return false; - } - FieldKey other = (FieldKey) o; - return className.equals(other.className) && fieldName.equals(other.fieldName) && fieldDesc.equals(other.fieldDesc); - } - - @Override - public String toString() { - return className + "." + fieldName + ":" + fieldDesc; - } - } - - public static final class MethodKey { - public final String className; - public final String methodName; - public final String methodDesc; - - /** - * Warning: class name uses "." format, not "/" format - */ - public MethodKey(String className, String methodName, String methodDesc) { - this.className = className; - this.methodName = methodName; - this.methodDesc = methodDesc; - } - - @Override - public int hashCode() { - return Objects.hash(className, methodName, methodDesc); - } - - @Override - public boolean equals(Object o) { - if (!(o instanceof MethodKey)) { - return false; - } - MethodKey other = (MethodKey) o; - return className.equals(other.className) && methodName.equals(other.methodName) && methodDesc.equals(other.methodDesc); - } - - @Override - public String toString() { - return className + "." + methodName + methodDesc; - } - } - - private class ExpressionRemapper extends ExpressionTransformer { - private final DataType groupDataType; - - ExpressionRemapper(DataType groupDataType) { - this.groupDataType = groupDataType; - } - - @Override - public Expression transformFieldExpression(FieldExpression fieldExpression) { - String fieldDesc; - switch (fieldExpression.fieldType == null ? groupDataType : fieldExpression.fieldType) { - case BYTE: - fieldDesc = "B"; - break; - case SHORT: - fieldDesc = "S"; - break; - case INT: - fieldDesc = "I"; - break; - case LONG: - fieldDesc = "J"; - break; - case FLOAT: - fieldDesc = "F"; - break; - case DOUBLE: - fieldDesc = "D"; - break; - case CHAR: - fieldDesc = "C"; - break; - case STRING: - fieldDesc = "Ljava/lang/String;"; - break; - default: - throw new AssertionError("Unknown data type: " + fieldExpression.fieldType); - } - - String className = mapClassName(fieldExpression.className); - String fieldName = mapFieldName(fieldExpression.className, fieldExpression.fieldName, fieldDesc); - return new FieldExpression(className, fieldName, fieldExpression.fieldType, fieldExpression.isStatic); - } - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java deleted file mode 100644 index 121a449..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/UnpickV3Writer.java +++ /dev/null @@ -1,379 +0,0 @@ -package net.earthcomputer.unpickv3parser; - -import net.earthcomputer.unpickv3parser.tree.DataType; -import net.earthcomputer.unpickv3parser.tree.GroupConstant; -import net.earthcomputer.unpickv3parser.tree.GroupDefinition; -import net.earthcomputer.unpickv3parser.tree.GroupScope; -import net.earthcomputer.unpickv3parser.tree.Literal; -import net.earthcomputer.unpickv3parser.tree.TargetField; -import net.earthcomputer.unpickv3parser.tree.TargetMethod; -import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; -import net.earthcomputer.unpickv3parser.tree.expr.BinaryExpression; -import net.earthcomputer.unpickv3parser.tree.expr.CastExpression; -import net.earthcomputer.unpickv3parser.tree.expr.ExpressionVisitor; -import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; -import net.earthcomputer.unpickv3parser.tree.expr.LiteralExpression; -import net.earthcomputer.unpickv3parser.tree.expr.ParenExpression; -import net.earthcomputer.unpickv3parser.tree.expr.UnaryExpression; - -import java.util.ArrayList; -import java.util.List; -import java.util.Locale; -import java.util.Map; - -/** - * A visitor that generates .unpick v3 format text. Useful for programmatically writing .unpick v3 format files; - * or remapping them, when used as the delegate for an instance of {@link UnpickV3Remapper}. - */ -public final class UnpickV3Writer extends UnpickV3Visitor { - private static final String LINE_SEPARATOR = System.lineSeparator(); - private final String indent; - private final StringBuilder output = new StringBuilder("unpick v3").append(LINE_SEPARATOR); - - public UnpickV3Writer() { - this("\t"); - } - - public UnpickV3Writer(String indent) { - this.indent = indent; - } - - @Override - public void visitGroupDefinition(GroupDefinition groupDefinition) { - output.append(LINE_SEPARATOR); - - if (!(groupDefinition.scope instanceof GroupScope.Global)) { - writeGroupScope(groupDefinition.scope); - output.append(" "); - } - - writeLowerCaseEnum(groupDefinition.type); - output.append(" "); - - if (groupDefinition.strict) { - output.append("strict "); - } - - writeDataType(groupDefinition.dataType); - - if (groupDefinition.name != null) { - output.append(" ").append(groupDefinition.name); - } - - output.append(LINE_SEPARATOR); - - if (groupDefinition.format != null) { - output.append(indent).append("format = "); - writeLowerCaseEnum(groupDefinition.format); - output.append(LINE_SEPARATOR); - } - - for (GroupConstant constant : groupDefinition.constants) { - writeGroupConstant(constant); - } - } - - private void writeGroupScope(GroupScope scope) { - output.append("scoped "); - if (scope instanceof GroupScope.Package) { - output.append("package ").append(((GroupScope.Package) scope).packageName); - } else if (scope instanceof GroupScope.Class) { - output.append("class ").append(((GroupScope.Class) scope).className); - } else if (scope instanceof GroupScope.Method) { - GroupScope.Method methodScope = (GroupScope.Method) scope; - output.append("method ") - .append(methodScope.className) - .append(" ") - .append(methodScope.methodName) - .append(" ") - .append(methodScope.methodDesc); - } else { - throw new AssertionError("Unknown group scope type: " + scope.getClass().getName()); - } - } - - private void writeGroupConstant(GroupConstant constant) { - output.append(indent); - writeGroupConstantKey(constant.key); - output.append(" = "); - constant.value.accept(new ExpressionWriter()); - output.append(LINE_SEPARATOR); - } - - private void writeGroupConstantKey(Literal.ConstantKey constantKey) { - if (constantKey instanceof Literal.Long) { - Literal.Long longLiteral = (Literal.Long) constantKey; - if (longLiteral.radix == 10) { - // treat base 10 as signed - output.append(longLiteral.value); - } else { - writeRadixPrefix(longLiteral.radix); - output.append(Long.toUnsignedString(longLiteral.value, longLiteral.radix)); - } - } else if (constantKey instanceof Literal.Double) { - output.append(((Literal.Double) constantKey).value); - } else if (constantKey instanceof Literal.String) { - output.append(quoteString(((Literal.String) constantKey).value, '"')); - } else { - throw new AssertionError("Unknown group constant key type: " + constantKey.getClass().getName()); - } - } - - @Override - public void visitTargetField(TargetField targetField) { - output.append(LINE_SEPARATOR) - .append("target_field ") - .append(targetField.className) - .append(" ") - .append(targetField.fieldName) - .append(" ") - .append(targetField.fieldDesc) - .append(" ") - .append(targetField.groupName) - .append(LINE_SEPARATOR); - } - - @Override - public void visitTargetMethod(TargetMethod targetMethod) { - output.append(LINE_SEPARATOR) - .append("target_method ") - .append(targetMethod.className) - .append(" ") - .append(targetMethod.methodName) - .append(" ") - .append(targetMethod.methodDesc) - .append(LINE_SEPARATOR); - - List> paramGroups = new ArrayList<>(targetMethod.paramGroups.entrySet()); - paramGroups.sort(Map.Entry.comparingByKey()); - for (Map.Entry paramGroup : paramGroups) { - output.append(indent) - .append("param ") - .append(paramGroup.getKey()) - .append(" ") - .append(paramGroup.getValue()) - .append(LINE_SEPARATOR); - } - - if (targetMethod.returnGroup != null) { - output.append(indent) - .append("return ") - .append(targetMethod.returnGroup) - .append(LINE_SEPARATOR); - } - } - - private void writeRadixPrefix(int radix) { - switch (radix) { - case 10: - break; - case 16: - output.append("0x"); - break; - case 8: - output.append("0"); - break; - case 2: - output.append("0b"); - break; - default: - throw new AssertionError("Illegal radix: " + radix); - } - } - - private void writeDataType(DataType dataType) { - if (dataType == DataType.STRING) { - output.append("String"); - } else { - writeLowerCaseEnum(dataType); - } - } - - private void writeLowerCaseEnum(Enum enumValue) { - output.append(enumValue.name().toLowerCase(Locale.ROOT)); - } - - static String quoteString(String string, char quoteChar) { - StringBuilder result = new StringBuilder(string.length() + 2).append(quoteChar); - - for (int i = 0; i < string.length(); i++) { - char c = string.charAt(i); - switch (c) { - case '\b': - result.append("\\b"); - break; - case '\t': - result.append("\\t"); - break; - case '\n': - result.append("\\n"); - break; - case '\f': - result.append("\\f"); - break; - case '\r': - result.append("\\r"); - break; - case '\\': - result.append("\\\\"); - break; - default: - if (c == quoteChar) { - result.append("\\").append(c); - } else if (isPrintable(c)) { - result.append(c); - } else if (c <= 255) { - result.append('\\').append(Integer.toOctalString(c)); - } else { - result.append("\\u").append(String.format("%04x", (int) c)); - } - } - } - - return result.append(quoteChar).toString(); - } - - private static boolean isPrintable(char ch) { - switch (Character.getType(ch)) { - case Character.UPPERCASE_LETTER: - case Character.LOWERCASE_LETTER: - case Character.TITLECASE_LETTER: - case Character.MODIFIER_LETTER: - case Character.OTHER_LETTER: - case Character.NON_SPACING_MARK: - case Character.ENCLOSING_MARK: - case Character.COMBINING_SPACING_MARK: - case Character.DECIMAL_DIGIT_NUMBER: - case Character.LETTER_NUMBER: - case Character.OTHER_NUMBER: - case Character.SPACE_SEPARATOR: - case Character.DASH_PUNCTUATION: - case Character.START_PUNCTUATION: - case Character.END_PUNCTUATION: - case Character.CONNECTOR_PUNCTUATION: - case Character.OTHER_PUNCTUATION: - case Character.MATH_SYMBOL: - case Character.CURRENCY_SYMBOL: - case Character.MODIFIER_SYMBOL: - case Character.OTHER_SYMBOL: - case Character.INITIAL_QUOTE_PUNCTUATION: - case Character.FINAL_QUOTE_PUNCTUATION: - return true; - } - return false; - } - - public String getOutput() { - return output.toString(); - } - - private final class ExpressionWriter extends ExpressionVisitor { - @Override - public void visitBinaryExpression(BinaryExpression binaryExpression) { - binaryExpression.lhs.accept(this); - switch (binaryExpression.operator) { - case BIT_OR: - output.append(" | "); - break; - case BIT_XOR: - output.append(" ^ "); - break; - case BIT_AND: - output.append(" & "); - break; - case BIT_SHIFT_LEFT: - output.append(" << "); - break; - case BIT_SHIFT_RIGHT: - output.append(" >> "); - break; - case BIT_SHIFT_RIGHT_UNSIGNED: - output.append(" >>> "); - break; - case ADD: - output.append(" + "); - break; - case SUBTRACT: - output.append(" - "); - break; - case MULTIPLY: - output.append(" * "); - break; - case DIVIDE: - output.append(" / "); - break; - case MODULO: - output.append(" % "); - break; - default: - throw new AssertionError("Unknown operator: " + binaryExpression.operator); - } - binaryExpression.rhs.accept(this); - } - - @Override - public void visitCastExpression(CastExpression castExpression) { - output.append('('); - writeDataType(castExpression.castType); - output.append(") "); - castExpression.operand.accept(this); - } - - @Override - public void visitFieldExpression(FieldExpression fieldExpression) { - output.append(fieldExpression.className).append('.').append(fieldExpression.fieldName); - if (!fieldExpression.isStatic) { - output.append(":instance"); - } - if (fieldExpression.fieldType != null) { - output.append(':'); - writeDataType(fieldExpression.fieldType); - } - } - - @Override - public void visitLiteralExpression(LiteralExpression literalExpression) { - if (literalExpression.literal instanceof Literal.Integer) { - Literal.Integer literalInteger = (Literal.Integer) literalExpression.literal; - writeRadixPrefix(literalInteger.radix); - output.append(Integer.toUnsignedString(literalInteger.value, literalInteger.radix)); - } else if (literalExpression.literal instanceof Literal.Long) { - Literal.Long literalLong = (Literal.Long) literalExpression.literal; - writeRadixPrefix(literalLong.radix); - output.append(Long.toUnsignedString(literalLong.value, literalLong.radix)).append('L'); - } else if (literalExpression.literal instanceof Literal.Float) { - output.append(((Literal.Float) literalExpression.literal).value).append('F'); - } else if (literalExpression.literal instanceof Literal.Double) { - output.append(((Literal.Double) literalExpression.literal).value); - } else if (literalExpression.literal instanceof Literal.Character) { - output.append(quoteString(String.valueOf(((Literal.Character) literalExpression.literal).value), '\'')); - } else if (literalExpression.literal instanceof Literal.String) { - output.append(quoteString(((Literal.String) literalExpression.literal).value, '"')); - } else { - throw new AssertionError("Unknown literal: " + literalExpression.literal); - } - } - - @Override - public void visitParenExpression(ParenExpression parenExpression) { - output.append('('); - parenExpression.expression.accept(this); - output.append(')'); - } - - @Override - public void visitUnaryExpression(UnaryExpression unaryExpression) { - switch (unaryExpression.operator) { - case NEGATE: - output.append('-'); - break; - case BIT_NOT: - output.append('~'); - break; - default: - throw new AssertionError("Unknown operator: " + unaryExpression.operator); - } - unaryExpression.operand.accept(this); - } - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java deleted file mode 100644 index c057aa0..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/DataType.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -public enum DataType { - BYTE, SHORT, INT, LONG, FLOAT, DOUBLE, CHAR, STRING -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java deleted file mode 100644 index 3b6c96c..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupConstant.java +++ /dev/null @@ -1,13 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -import net.earthcomputer.unpickv3parser.tree.expr.Expression; - -public final class GroupConstant { - public final Literal.ConstantKey key; - public final Expression value; - - public GroupConstant(Literal.ConstantKey key, Expression value) { - this.key = key; - this.value = value; - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java deleted file mode 100644 index 10d31bd..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupDefinition.java +++ /dev/null @@ -1,35 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -import org.jetbrains.annotations.Nullable; - -import java.util.List; - -public final class GroupDefinition { - public final GroupScope scope; - public final GroupType type; - public final boolean strict; - public final DataType dataType; - @Nullable - public final String name; - public final List constants; - @Nullable - public final GroupFormat format; - - public GroupDefinition( - GroupScope scope, - GroupType type, - boolean strict, - DataType dataType, - @Nullable String name, - List constants, - @Nullable GroupFormat format - ) { - this.scope = scope; - this.type = type; - this.strict = strict; - this.dataType = dataType; - this.name = name; - this.constants = constants; - this.format = format; - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java deleted file mode 100644 index 8d88068..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupFormat.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -public enum GroupFormat { - DECIMAL, HEX, BINARY, OCTAL, CHAR -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java deleted file mode 100644 index b299886..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupScope.java +++ /dev/null @@ -1,41 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -public abstract class GroupScope { - private GroupScope() { - } - - public static final class Global extends GroupScope { - public static final Global INSTANCE = new Global(); - - private Global() { - } - } - - public static final class Package extends GroupScope { - public final String packageName; - - public Package(String packageName) { - this.packageName = packageName; - } - } - - public static final class Class extends GroupScope { - public final String className; - - public Class(String className) { - this.className = className; - } - } - - public static final class Method extends GroupScope { - public final String className; - public final String methodName; - public final String methodDesc; - - public Method(String className, String methodName, String methodDesc) { - this.className = className; - this.methodName = methodName; - this.methodDesc = methodDesc; - } - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java deleted file mode 100644 index 0a3629b..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/GroupType.java +++ /dev/null @@ -1,5 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -public enum GroupType { - CONST, FLAG -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java deleted file mode 100644 index 0ede19e..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/Literal.java +++ /dev/null @@ -1,87 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -public abstract class Literal { - private Literal() { - } - - public static abstract class ConstantKey extends Literal { - private ConstantKey() { - } - } - - public static abstract class NumberConstant extends ConstantKey { - private NumberConstant() {} - - public abstract Number asNumber(); - } - - public static final class Integer extends Literal { - public final int value; - public final int radix; - - public Integer(int value) { - this(value, 10); - } - - public Integer(int value, int radix) { - this.value = value; - this.radix = radix; - } - } - - public static final class Long extends NumberConstant { - public final long value; - public final int radix; - - public Long(long value) { - this(value, 10); - } - - public Long(long value, int radix) { - this.value = value; - this.radix = radix; - } - - @Override - public Number asNumber() { - return value; - } - } - - public static final class Float extends Literal { - public final float value; - - public Float(float value) { - this.value = value; - } - } - - public static final class Double extends NumberConstant { - public final double value; - - public Double(double value) { - this.value = value; - } - - @Override - public Number asNumber() { - return value; - } - } - - public static final class Character extends Literal { - public final char value; - - public Character(char value) { - this.value = value; - } - } - - public static final class String extends ConstantKey { - public final java.lang.String value; - - public String(java.lang.String value) { - this.value = value; - } - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java deleted file mode 100644 index 359ea3b..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetField.java +++ /dev/null @@ -1,15 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -public final class TargetField { - public final String className; - public final String fieldName; - public final String fieldDesc; - public final String groupName; - - public TargetField(String className, String fieldName, String fieldDesc, String groupName) { - this.className = className; - this.fieldName = fieldName; - this.fieldDesc = fieldDesc; - this.groupName = groupName; - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java deleted file mode 100644 index b21a5c7..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/TargetMethod.java +++ /dev/null @@ -1,28 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -import org.jetbrains.annotations.Nullable; - -import java.util.Map; - -public final class TargetMethod { - public final String className; - public final String methodName; - public final String methodDesc; - public final Map paramGroups; - @Nullable - public final String returnGroup; - - public TargetMethod( - String className, - String methodName, - String methodDesc, - Map paramGroups, - @Nullable String returnGroup - ) { - this.className = className; - this.methodName = methodName; - this.methodDesc = methodDesc; - this.paramGroups = paramGroups; - this.returnGroup = returnGroup; - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java deleted file mode 100644 index dbbb302..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/UnpickV3Visitor.java +++ /dev/null @@ -1,12 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree; - -public abstract class UnpickV3Visitor { - public void visitGroupDefinition(GroupDefinition groupDefinition) { - } - - public void visitTargetField(TargetField targetField) { - } - - public void visitTargetMethod(TargetMethod targetMethod) { - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java deleted file mode 100644 index 022ea0d..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/BinaryExpression.java +++ /dev/null @@ -1,37 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -public final class BinaryExpression extends Expression { - public final Expression lhs; - public final Expression rhs; - public final Operator operator; - - public BinaryExpression(Expression lhs, Expression rhs, Operator operator) { - this.lhs = lhs; - this.rhs = rhs; - this.operator = operator; - } - - @Override - public void accept(ExpressionVisitor visitor) { - visitor.visitBinaryExpression(this); - } - - @Override - public Expression transform(ExpressionTransformer transformer) { - return transformer.transformBinaryExpression(this); - } - - public enum Operator { - BIT_OR, - BIT_XOR, - BIT_AND, - BIT_SHIFT_LEFT, - BIT_SHIFT_RIGHT, - BIT_SHIFT_RIGHT_UNSIGNED, - ADD, - SUBTRACT, - MULTIPLY, - DIVIDE, - MODULO, - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java deleted file mode 100644 index 67e7e82..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/CastExpression.java +++ /dev/null @@ -1,23 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -import net.earthcomputer.unpickv3parser.tree.DataType; - -public final class CastExpression extends Expression { - public final DataType castType; - public final Expression operand; - - public CastExpression(DataType castType, Expression operand) { - this.castType = castType; - this.operand = operand; - } - - @Override - public void accept(ExpressionVisitor visitor) { - visitor.visitCastExpression(this); - } - - @Override - public Expression transform(ExpressionTransformer transformer) { - return transformer.transformCastExpression(this); - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java deleted file mode 100644 index 07f9da1..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/Expression.java +++ /dev/null @@ -1,10 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -public abstract class Expression { - Expression() { - } - - public abstract void accept(ExpressionVisitor visitor); - - public abstract Expression transform(ExpressionTransformer transformer); -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java deleted file mode 100644 index 7e0fd88..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionTransformer.java +++ /dev/null @@ -1,27 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -public abstract class ExpressionTransformer { - public Expression transformBinaryExpression(BinaryExpression binaryExpression) { - return new BinaryExpression(binaryExpression.lhs.transform(this), binaryExpression.rhs.transform(this), binaryExpression.operator); - } - - public Expression transformCastExpression(CastExpression castExpression) { - return new CastExpression(castExpression.castType, castExpression.operand.transform(this)); - } - - public Expression transformFieldExpression(FieldExpression fieldExpression) { - return fieldExpression; - } - - public Expression transformLiteralExpression(LiteralExpression literalExpression) { - return literalExpression; - } - - public Expression transformParenExpression(ParenExpression parenExpression) { - return new ParenExpression(parenExpression.expression.transform(this)); - } - - public Expression transformUnaryExpression(UnaryExpression unaryExpression) { - return new UnaryExpression(unaryExpression.operand.transform(this), unaryExpression.operator); - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java deleted file mode 100644 index f6a9e98..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ExpressionVisitor.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -/** - * Visitor for an {@link Expression}. By default, recursively visits sub-expressions. - */ -public abstract class ExpressionVisitor { - public void visitBinaryExpression(BinaryExpression binaryExpression) { - binaryExpression.lhs.accept(this); - binaryExpression.rhs.accept(this); - } - - public void visitCastExpression(CastExpression castExpression) { - castExpression.operand.accept(this); - } - - public void visitFieldExpression(FieldExpression fieldExpression) { - } - - public void visitLiteralExpression(LiteralExpression literalExpression) { - } - - public void visitParenExpression(ParenExpression parenExpression) { - parenExpression.expression.accept(this); - } - - public void visitUnaryExpression(UnaryExpression unaryExpression) { - unaryExpression.operand.accept(this); - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java deleted file mode 100644 index 648f752..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/FieldExpression.java +++ /dev/null @@ -1,29 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -import net.earthcomputer.unpickv3parser.tree.DataType; -import org.jetbrains.annotations.Nullable; - -public final class FieldExpression extends Expression { - public final String className; - public final String fieldName; - @Nullable - public final DataType fieldType; - public final boolean isStatic; - - public FieldExpression(String className, String fieldName, @Nullable DataType fieldType, boolean isStatic) { - this.className = className; - this.fieldName = fieldName; - this.fieldType = fieldType; - this.isStatic = isStatic; - } - - @Override - public void accept(ExpressionVisitor visitor) { - visitor.visitFieldExpression(this); - } - - @Override - public Expression transform(ExpressionTransformer transformer) { - return transformer.transformFieldExpression(this); - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java deleted file mode 100644 index a6022ba..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/LiteralExpression.java +++ /dev/null @@ -1,24 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -import net.earthcomputer.unpickv3parser.tree.Literal; - -public final class LiteralExpression extends Expression { - /** - * Note: this literal is always positive. Integers are to be interpreted as unsigned values. - */ - public final Literal literal; - - public LiteralExpression(Literal literal) { - this.literal = literal; - } - - @Override - public void accept(ExpressionVisitor visitor) { - visitor.visitLiteralExpression(this); - } - - @Override - public Expression transform(ExpressionTransformer transformer) { - return transformer.transformLiteralExpression(this); - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java deleted file mode 100644 index 6bda33e..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/ParenExpression.java +++ /dev/null @@ -1,19 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -public final class ParenExpression extends Expression { - public final Expression expression; - - public ParenExpression(Expression expression) { - this.expression = expression; - } - - @Override - public void accept(ExpressionVisitor visitor) { - visitor.visitParenExpression(this); - } - - @Override - public Expression transform(ExpressionTransformer transformer) { - return transformer.transformParenExpression(this); - } -} diff --git a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java b/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java deleted file mode 100644 index 012f68a..0000000 --- a/unpick/src/main/java/net/earthcomputer/unpickv3parser/tree/expr/UnaryExpression.java +++ /dev/null @@ -1,25 +0,0 @@ -package net.earthcomputer.unpickv3parser.tree.expr; - -public final class UnaryExpression extends Expression { - public final Expression operand; - public final Operator operator; - - public UnaryExpression(Expression operand, Operator operator) { - this.operand = operand; - this.operator = operator; - } - - @Override - public void accept(ExpressionVisitor visitor) { - visitor.visitUnaryExpression(this); - } - - @Override - public Expression transform(ExpressionTransformer transformer) { - return transformer.transformUnaryExpression(this); - } - - public enum Operator { - NEGATE, BIT_NOT, - } -} diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java b/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java index 739b6c9..6df67aa 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java @@ -1,29 +1,138 @@ package net.neoforged.jst.unpick; -import net.earthcomputer.unpickv3parser.tree.DataType; +import daomephsta.unpick.constantmappers.datadriven.tree.DataType; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; public enum IntegerType { - BYTE(DataType.BYTE, false) { + BYTE(DataType.BYTE, Byte.class, false) { @Override public Number cast(Number in) { return in.byteValue(); } + + @Override + public Number divide(Number a, Number b) { + return a.byteValue() / b.byteValue(); + } + + @Override + public Number multiply(Number a, Number b) { + return a.byteValue() * b.byteValue(); + } + + @Override + public Number add(Number a, Number b) { + return a.byteValue() + b.byteValue(); + } + + @Override + public Number subtract(Number a, Number b) { + return a.byteValue() - b.byteValue(); + } + + @Override + public Number modulo(Number a, Number b) { + return a.byteValue() % b.byteValue(); + } + + @Override + public Number or(Number a, Number b) { + return a.byteValue() | b.byteValue(); + } + + @Override + public Number xor(Number a, Number b) { + return a.byteValue() ^ b.byteValue(); + } + + @Override + public Number and(Number a, Number b) { + return a.byteValue() & b.byteValue(); + } + + @Override + public Number lshift(Number a, Number b) { + return a.byteValue() << b.byteValue(); + } + + @Override + public Number rshift(Number a, Number b) { + return a.byteValue() >> b.byteValue(); + } + + @Override + public Number rshiftUnsigned(Number a, Number b) { + return a.byteValue() >>> b.byteValue(); + } }, - SHORT(DataType.SHORT, false, BYTE) { + SHORT(DataType.SHORT, Short.class, false, BYTE) { @Override public Number cast(Number in) { return in.shortValue(); } - }, - INT(DataType.INT, true, SHORT) { + @Override - public long toUnsignedLong(Number number) { - return Integer.toUnsignedLong(number.intValue()); + public Number divide(Number a, Number b) { + return a.shortValue() / b.shortValue(); } @Override - public Number negate(Number number) { - return ~number.intValue(); + public Number multiply(Number a, Number b) { + return a.shortValue() * b.shortValue(); + } + + @Override + public Number add(Number a, Number b) { + return a.shortValue() + b.shortValue(); + } + + @Override + public Number subtract(Number a, Number b) { + return a.shortValue() - b.shortValue(); + } + + @Override + public Number modulo(Number a, Number b) { + return a.shortValue() % b.shortValue(); + } + + @Override + public Number or(Number a, Number b) { + return a.shortValue() | b.shortValue(); + } + + @Override + public Number xor(Number a, Number b) { + return a.shortValue() ^ b.shortValue(); + } + + @Override + public Number and(Number a, Number b) { + return a.shortValue() & b.shortValue(); + } + + @Override + public Number lshift(Number a, Number b) { + return a.shortValue() << b.shortValue(); + } + + @Override + public Number rshift(Number a, Number b) { + return a.shortValue() >> b.shortValue(); + } + + @Override + public Number rshiftUnsigned(Number a, Number b) { + return a.shortValue() >>> b.shortValue(); + } + }, + INT(DataType.INT, Integer.class, true, SHORT) { + @Override + public long toUnsignedLong(Number number) { + return Integer.toUnsignedLong(number.intValue()); } @Override @@ -31,31 +140,152 @@ public Number cast(Number in) { return in.intValue(); } }, - LONG(DataType.LONG, true, INT) { + LONG(DataType.LONG, Long.class, true, INT) { + @Override + public Number negate(Number number) { + return ~number.longValue(); + } + @Override public Number cast(Number in) { return in.longValue(); } + + @Override + public Number divide(Number a, Number b) { + return a.longValue() / b.longValue(); + } + + @Override + public Number multiply(Number a, Number b) { + return a.longValue() * b.longValue(); + } + + @Override + public Number add(Number a, Number b) { + return a.longValue() + b.longValue(); + } + + @Override + public Number subtract(Number a, Number b) { + return a.longValue() - b.longValue(); + } + + @Override + public Number modulo(Number a, Number b) { + return a.longValue() % b.longValue(); + } + + @Override + public Number or(Number a, Number b) { + return a.longValue() | b.longValue(); + } + + @Override + public Number xor(Number a, Number b) { + return a.longValue() ^ b.longValue(); + } + + @Override + public Number and(Number a, Number b) { + return a.longValue() & b.longValue(); + } + + @Override + public Number lshift(Number a, Number b) { + return a.longValue() << b.longValue(); + } + + @Override + public Number rshift(Number a, Number b) { + return a.longValue() >> b.longValue(); + } + + @Override + public Number rshiftUnsigned(Number a, Number b) { + return a.longValue() >>> b.longValue(); + } }, - FLOAT(DataType.FLOAT, false, INT) { + FLOAT(DataType.FLOAT, Float.class, false, INT) { @Override public Number cast(Number in) { return in.floatValue(); } + + @Override + public Number divide(Number a, Number b) { + return a.floatValue() / b.floatValue(); + } + + @Override + public Number multiply(Number a, Number b) { + return a.floatValue() * b.floatValue(); + } + + @Override + public Number add(Number a, Number b) { + return a.floatValue() + b.floatValue(); + } + + @Override + public Number subtract(Number a, Number b) { + return a.floatValue() - b.floatValue(); + } + + @Override + public Number modulo(Number a, Number b) { + return a.floatValue() % b.floatValue(); + } }, - DOUBLE(DataType.DOUBLE, false, FLOAT) { + DOUBLE(DataType.DOUBLE, Double.class, false, FLOAT) { @Override public Number cast(Number in) { return in.doubleValue(); } + + @Override + public Number divide(Number a, Number b) { + return a.doubleValue() / b.doubleValue(); + } + + @Override + public Number multiply(Number a, Number b) { + return a.doubleValue() * b.doubleValue(); + } + + @Override + public Number add(Number a, Number b) { + return a.doubleValue() + b.doubleValue(); + } + + @Override + public Number subtract(Number a, Number b) { + return a.doubleValue() - b.doubleValue(); + } + + @Override + public Number modulo(Number a, Number b) { + return a.doubleValue() % b.doubleValue(); + } }; + public static final Map, IntegerType> TYPES; + static { + var types = new HashMap, IntegerType>(); + for (IntegerType value : values()) { + types.put(value.classType, value); + } + TYPES = Collections.unmodifiableMap(types); + } + public final DataType dataType; + public final Class classType; public final boolean supportsFlag; public final IntegerType[] widenFrom; - IntegerType(DataType dataType, boolean supportsFlag, IntegerType... widenFrom) { + IntegerType(DataType dataType, Class classType, boolean supportsFlag, IntegerType... widenFrom) { this.dataType = dataType; + this.classType = classType; this.supportsFlag = supportsFlag; this.widenFrom = widenFrom; } @@ -67,6 +297,50 @@ public long toUnsignedLong(Number number) { } public Number negate(Number number) { - return ~number.longValue(); + return ~number.intValue(); + } + + public Number divide(Number a, Number b) { + return a.intValue() / b.intValue(); + } + + public Number multiply(Number a, Number b) { + return a.intValue() * b.intValue(); + } + + public Number add(Number a, Number b) { + return a.intValue() + b.intValue(); + } + + public Number subtract(Number a, Number b) { + return a.intValue() - b.intValue(); + } + + public Number modulo(Number a, Number b) { + return a.intValue() % b.intValue(); + } + + public Number or(Number a, Number b) { + return a.intValue() | b.intValue(); + } + + public Number xor(Number a, Number b) { + return a.intValue() ^ b.intValue(); + } + + public Number and(Number a, Number b) { + return a.intValue() & b.intValue(); + } + + public Number lshift(Number a, Number b) { + return a.intValue() << b.intValue(); + } + + public Number rshift(Number a, Number b) { + return a.intValue() >> b.intValue(); + } + + public Number rshiftUnsigned(Number a, Number b) { + return a.intValue() >>> b.intValue(); } } diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java index fa6b01e..034998a 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java @@ -1,26 +1,38 @@ package net.neoforged.jst.unpick; +import com.intellij.lang.jvm.JvmModifier; import com.intellij.openapi.util.Key; +import com.intellij.psi.JavaPsiFacade; import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiClassType; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiField; import com.intellij.psi.PsiJavaFile; import com.intellij.psi.PsiMethod; +import com.intellij.psi.PsiType; +import com.intellij.psi.PsiTypes; import com.intellij.psi.search.GlobalSearchScope; import com.intellij.util.containers.MultiMap; -import net.earthcomputer.unpickv3parser.tree.DataType; -import net.earthcomputer.unpickv3parser.tree.GroupDefinition; -import net.earthcomputer.unpickv3parser.tree.GroupFormat; -import net.earthcomputer.unpickv3parser.tree.GroupScope; -import net.earthcomputer.unpickv3parser.tree.GroupType; -import net.earthcomputer.unpickv3parser.tree.Literal; -import net.earthcomputer.unpickv3parser.tree.TargetField; -import net.earthcomputer.unpickv3parser.tree.TargetMethod; -import net.earthcomputer.unpickv3parser.tree.expr.Expression; +import daomephsta.unpick.constantmappers.datadriven.tree.DataType; +import daomephsta.unpick.constantmappers.datadriven.tree.GroupDefinition; +import daomephsta.unpick.constantmappers.datadriven.tree.GroupFormat; +import daomephsta.unpick.constantmappers.datadriven.tree.GroupScope; +import daomephsta.unpick.constantmappers.datadriven.tree.Literal; +import daomephsta.unpick.constantmappers.datadriven.tree.TargetField; +import daomephsta.unpick.constantmappers.datadriven.tree.TargetMethod; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.BinaryExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.CastExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.Expression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.FieldExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.LiteralExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.ParenExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.UnaryExpression; import net.neoforged.jst.api.PsiHelper; import net.neoforged.jst.api.TransformContext; import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.HashMap; import java.util.HashSet; import java.util.IdentityHashMap; @@ -29,14 +41,13 @@ import java.util.Optional; import java.util.Set; import java.util.function.Predicate; -import java.util.stream.Collectors; public class UnpickCollection { private static final Key> UNPICK_DEFINITION = Key.create("unpick.method_definition"); private static final Key UNPICK_FIELD_TARGET = Key.create("unpick.field_target"); private final Set possibleTargetNames = new HashSet<>(); - private final Map groups; + private final MultiMap groups; private final List global; @@ -50,9 +61,8 @@ public class UnpickCollection { @SuppressWarnings({"FieldCanBeLocal", "MismatchedQueryAndUpdateOfCollection"}) private final List baseElements; - public UnpickCollection(TransformContext context, Map groups, List fields, List methods) { - this.groups = new HashMap<>(groups.size()); - groups.forEach((k, v) -> this.groups.put(k.name(), Group.create(v))); + public UnpickCollection(TransformContext context, Map> groups, List fields, List methods) { + this.groups = new MultiMap<>(new HashMap<>(groups.size())); var facade = context.environment().getPsiFacade(); var project = context.environment().getPsiManager().getProject(); @@ -67,31 +77,39 @@ public UnpickCollection(TransformContext context, Map methodScopes = new IdentityHashMap<>(); baseElements = new ArrayList<>(); - groups.forEach((s, def) -> { - var gr = Group.create(def); - if (def.scope instanceof GroupScope.Package pkg) { - byPackage.putValue(pkg.packageName, gr); - } else if (def.scope instanceof GroupScope.Class cls) { - byClass.putValue(cls.className, gr); - } else if (def.scope instanceof GroupScope.Global && def.name == null) { - global.add(gr); - } else if (def.scope instanceof GroupScope.Method mtd) { - var cls = facade.findClass(mtd.className, projectScope); - if (cls == null) return; - - for (PsiMethod clsMethod : cls.getMethods()) { - if (clsMethod.getName().equals(mtd.methodName) && PsiHelper.getBinaryMethodSignature(clsMethod).equals(mtd.methodDesc)) { - methodScopes.computeIfAbsent(clsMethod, k -> new ArrayList<>()).add(gr); + groups.forEach((key, defs) -> { + for (GroupDefinition def : defs) { + var gr = Group.create(def, facade, projectScope); + if (key.isGlobal()) { + global.add(gr); + } else { + this.groups.putValue(key.name(), gr); + + for (var scope : def.scopes()) { + switch (scope) { + case GroupScope.Package(var packageName) -> byPackage.putValue(packageName, gr); + case GroupScope.Class(var cls) -> byClass.putValue(cls, gr); + case GroupScope.Method(var className, var method, var desc) -> { + var cls = facade.findClass(className, projectScope); + if (cls == null) return; + + for (PsiMethod clsMethod : cls.getMethods()) { + if (clsMethod.getName().equals(method) && PsiHelper.getBinaryMethodSignature(clsMethod).equals(desc)) { + methodScopes.computeIfAbsent(clsMethod, k -> new ArrayList<>()).add(gr); + } + } + } + } } } } }); for (var field : fields) { - var cls = facade.findClass(field.className, projectScope); + var cls = facade.findClass(field.className(), projectScope); if (cls == null) continue; - var fld = cls.findFieldByName(field.fieldName, true); + var fld = cls.findFieldByName(field.fieldName(), true); if (fld != null) { fld.putUserData(UNPICK_FIELD_TARGET, field); baseElements.add(fld); @@ -99,13 +117,13 @@ public UnpickCollection(TransformContext context, Map } for (var method : methods) { - var cls = facade.findClass(method.className, projectScope); + var cls = facade.findClass(method.className(), projectScope); if (cls == null) continue; - possibleTargetNames.add(method.methodName); + possibleTargetNames.add(method.methodName()); for (PsiMethod clsMethod : cls.getMethods()) { - if (clsMethod.getName().equals(method.methodName) && PsiHelper.getBinaryMethodSignature(clsMethod).equals(method.methodDesc)) { + if (clsMethod.getName().equals(method.methodName()) && PsiHelper.getBinaryMethodSignature(clsMethod).equals(method.methodDesc())) { clsMethod.putUserData(UNPICK_DEFINITION, Optional.of(method)); baseElements.add(clsMethod); } @@ -191,54 +209,129 @@ public TargetMethod getDefinitionsFor(PsiMethod method) { return data.orElse(null); } - @Nullable - public Group getGroup(String id) { + public Collection getGroups(String id) { return groups.get(id); } public record Group( DataType data, boolean strict, + boolean flag, @Nullable GroupFormat format, - GroupType type, Map constants ) { - public static Group create(GroupDefinition def) { + public static Group create(GroupDefinition def, JavaPsiFacade facade, GlobalSearchScope scope) { + var constants = HashMap.newHashMap(def.constants().size()); + for (Expression constant : def.constants()) { + var value = resolveConstant(constant, facade, scope); + constants.put(cast(value, def.dataType()), constant); + } return new Group( - def.dataType, - def.strict, - def.format, - def.type, - def.constants.stream() - .collect(Collectors.toMap( - g -> getKey(g.key, def.dataType), - g -> g.value - )) + def.dataType(), + def.strict(), + def.flags(), + def.format(), + constants ); } - private static Object getKey(Literal.ConstantKey key, DataType type) { - if (key instanceof Literal.NumberConstant nct) { - var val = nct.asNumber(); - return switch (type) { - case CHAR -> (char)val.intValue(); - case BYTE -> val.byteValue(); - case SHORT -> val.shortValue(); - case INT -> val.intValue(); - case LONG -> val.longValue(); - case FLOAT -> val.floatValue(); - case DOUBLE -> val.doubleValue(); - case STRING -> throw null; + private static Object resolveConstant(Expression expression, JavaPsiFacade facade, GlobalSearchScope scope) { + if (expression instanceof FieldExpression fieldEx) { + var clazz = facade.findClass(fieldEx.className, scope); + if (clazz != null) { + for (PsiField field : clazz.getAllFields()) { + if (fieldEx.isStatic != field.hasModifier(JvmModifier.STATIC)) continue; + if (fieldEx.fieldType != null && !sameType(fieldEx.fieldType, field.getType())) continue; + if (fieldEx.fieldName.equals(field.getName())) { + return field.computeConstantValue(); + } + } + } + throw new IllegalArgumentException("Cannot find field named " + fieldEx.className + " of type " + fieldEx.fieldType + " in class " + fieldEx.className); + } else if (expression instanceof LiteralExpression literalExpression) { + return switch (literalExpression.literal) { + case Literal.Character(var ch) -> ch; + case Literal.Integer i -> i.value(); + case Literal.Long l -> l.value(); + case Literal.Float(var f) -> f; + case Literal.Double(var d) -> d; + case Literal.String(var s) -> s; }; + } else if (expression instanceof ParenExpression parenExpression) { + return resolveConstant(parenExpression.expression, facade, scope); + } else if (expression instanceof CastExpression castExpression) { + return cast(resolveConstant(castExpression.operand, facade, scope), castExpression.castType); + } else if (expression instanceof UnaryExpression unaryExpression) { + var value = (Number) resolveConstant(unaryExpression.operand, facade, scope); + return switch (unaryExpression.operator) { + case NEGATE -> value; // TODO - we can't negate numbers? + case BIT_NOT -> value instanceof Long ? ~value.longValue() : ~value.intValue(); + }; + } else if (expression instanceof BinaryExpression binaryExpression) { + var lhs = resolveConstant(binaryExpression.lhs, facade, scope); + var rhs = resolveConstant(binaryExpression.rhs, facade, scope); + + if (lhs instanceof Number l && rhs instanceof Number r) { + var type = IntegerType.TYPES.get(l.getClass()); + return switch (binaryExpression.operator) { + case ADD -> type.add(l, r); + case DIVIDE -> type.divide(l, r); + case MODULO -> type.modulo(l, r); + case MULTIPLY -> type.multiply(l, r); + case SUBTRACT -> type.subtract(l, r); + + case BIT_AND -> type.and(l, r); + case BIT_OR -> type.or(l, r); + case BIT_XOR -> type.xor(l, r); + + case BIT_SHIFT_LEFT -> type.lshift(l, r); + case BIT_SHIFT_RIGHT -> type.rshift(l, r); + case BIT_SHIFT_RIGHT_UNSIGNED -> type.rshiftUnsigned(l, r); + }; + } + + if (lhs instanceof String lS && rhs instanceof String rS && binaryExpression.operator == BinaryExpression.Operator.ADD) { + return lS + rS; + } + + throw new IllegalArgumentException("Cannot resolve expression: " + binaryExpression + ". Operands of type " + lhs.getClass() + " and " + rhs.getClass() + " do not support operator " + binaryExpression.operator); } - if (key.getClass() == Literal.String.class) { - return ((Literal.String) key).value; - } - return null; + + throw new IllegalArgumentException("Unknown Expression of type " + expression.getClass() + ": " + expression); } - } - public record TypedKey(DataType type, GroupScope scope, @Nullable String name) { + private static Object cast(Object in, DataType type) { + return switch (type) { + case BYTE -> ((Number) in).byteValue(); + case CHAR -> Character.valueOf((char)((Number) in).byteValue()); + case SHORT -> ((Number) in).shortValue(); + case INT -> ((Number) in).intValue(); + case LONG -> ((Number) in).longValue(); + case FLOAT -> ((Number) in).floatValue(); + case DOUBLE -> ((Number) in).doubleValue(); + case CLASS -> (Class) in; + case STRING -> in.toString(); + }; + } + private static boolean sameType(DataType type, PsiType fieldType) { + return switch (type) { + case BYTE -> fieldType.equals(PsiTypes.byteType()); + case CHAR -> fieldType.equals(PsiTypes.charType()); + case SHORT -> fieldType.equals(PsiTypes.shortType()); + case INT -> fieldType.equals(PsiTypes.intType()); + case LONG -> fieldType.equals(PsiTypes.longType()); + case FLOAT -> fieldType.equals(PsiTypes.floatType()); + case DOUBLE -> fieldType.equals(PsiTypes.doubleType()); + case CLASS -> ((PsiClassType) fieldType).resolve().getQualifiedName().equals("java.lang.Class"); + case STRING -> ((PsiClassType) fieldType).resolve().getQualifiedName().equals("java.lang.String"); + }; + } + } + + public record TypedKey(DataType type, List scopes, @Nullable String name) { + public boolean isGlobal() { + return name == null && scopes.isEmpty(); + } } } diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java index f9ae193..102cbe3 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickTransformer.java @@ -1,11 +1,11 @@ package net.neoforged.jst.unpick; import com.intellij.psi.PsiFile; -import net.earthcomputer.unpickv3parser.UnpickV3Reader; -import net.earthcomputer.unpickv3parser.tree.GroupDefinition; -import net.earthcomputer.unpickv3parser.tree.TargetField; -import net.earthcomputer.unpickv3parser.tree.TargetMethod; -import net.earthcomputer.unpickv3parser.tree.UnpickV3Visitor; +import daomephsta.unpick.constantmappers.datadriven.parser.v3.UnpickV3Reader; +import daomephsta.unpick.constantmappers.datadriven.tree.GroupDefinition; +import daomephsta.unpick.constantmappers.datadriven.tree.TargetField; +import daomephsta.unpick.constantmappers.datadriven.tree.TargetMethod; +import daomephsta.unpick.constantmappers.datadriven.tree.UnpickV3Visitor; import net.neoforged.jst.api.Replacements; import net.neoforged.jst.api.SourceTransformer; import net.neoforged.jst.api.TransformContext; @@ -27,7 +27,7 @@ public class UnpickTransformer implements SourceTransformer { @Override public void beforeRun(TransformContext context) { - var groups = new HashMap(); + var groups = new HashMap>(); var fields = new ArrayList(); var methods = new ArrayList(); @@ -36,7 +36,8 @@ public void beforeRun(TransformContext context) { new UnpickV3Reader(reader).accept(new UnpickV3Visitor() { @Override public void visitGroupDefinition(GroupDefinition groupDefinition) { - groups.merge(new UnpickCollection.TypedKey(groupDefinition.dataType, groupDefinition.scope, groupDefinition.name), groupDefinition, UnpickTransformer.this::merge); + groups.computeIfAbsent(new UnpickCollection.TypedKey(groupDefinition.dataType(), groupDefinition.scopes(), groupDefinition.name()), k -> new ArrayList<>()) + .add(groupDefinition); } @Override @@ -62,10 +63,4 @@ public void visitTargetMethod(TargetMethod targetMethod) { public void visitFile(PsiFile psiFile, Replacements replacements) { new UnpickVisitor(psiFile, collection, replacements).visitFile(psiFile); } - - private GroupDefinition merge(GroupDefinition first, GroupDefinition second) { - // TODO - validate they can be merged - first.constants.addAll(second.constants); - return first; - } } diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java index 9023c1c..0999076 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -16,18 +16,17 @@ import com.intellij.psi.PsiPrefixExpression; import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiReferenceExpression; -import net.earthcomputer.unpickv3parser.tree.GroupFormat; -import net.earthcomputer.unpickv3parser.tree.GroupType; -import net.earthcomputer.unpickv3parser.tree.Literal; -import net.earthcomputer.unpickv3parser.tree.TargetMethod; -import net.earthcomputer.unpickv3parser.tree.expr.BinaryExpression; -import net.earthcomputer.unpickv3parser.tree.expr.CastExpression; -import net.earthcomputer.unpickv3parser.tree.expr.Expression; -import net.earthcomputer.unpickv3parser.tree.expr.ExpressionVisitor; -import net.earthcomputer.unpickv3parser.tree.expr.FieldExpression; -import net.earthcomputer.unpickv3parser.tree.expr.LiteralExpression; -import net.earthcomputer.unpickv3parser.tree.expr.ParenExpression; -import net.earthcomputer.unpickv3parser.tree.expr.UnaryExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.GroupFormat; +import daomephsta.unpick.constantmappers.datadriven.tree.Literal; +import daomephsta.unpick.constantmappers.datadriven.tree.TargetMethod; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.BinaryExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.CastExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.Expression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.ExpressionVisitor; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.FieldExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.LiteralExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.ParenExpression; +import daomephsta.unpick.constantmappers.datadriven.tree.expr.UnaryExpression; import net.neoforged.jst.api.ImportHelper; import net.neoforged.jst.api.PsiHelper; import net.neoforged.jst.api.Replacements; @@ -37,6 +36,7 @@ import java.util.ArrayList; import java.util.List; import java.util.Locale; +import java.util.Objects; import java.util.function.Predicate; public class UnpickVisitor extends PsiRecursiveElementVisitor { @@ -218,7 +218,7 @@ private boolean replaceLiteral(PsiJavaToken element, Number number, IntegerType return true; } - if (group.type() == GroupType.FLAG && type.supportsFlag) { + if (group.flag() && type.supportsFlag) { var flag = generateFlag(group, number.longValue(), type); if (flag != null) { replacements.replace(element, flag); @@ -260,7 +260,7 @@ private void replaceMinus(PsiJavaToken tok) { private boolean checkNotRecursive(Expression expression) { if (fieldContext != null && expression instanceof FieldExpression fld) { - return !(fld.className.equals(classContext.getQualifiedName()) && fld.fieldName.equals(fieldContext.getName())); + return !(fld.className.equals(classContext.getQualifiedName()) && Objects.equals(fld.fieldName, fieldContext.getName())); } return true; } @@ -271,11 +271,15 @@ private boolean forInScope(Predicate apply) { cachedDefinition = collection.getDefinitionsFor(calledMethod); } if (cachedDefinition != null) { - var grId = cachedDefinition.paramGroups.get(currentParamIndex); - if (grId != null) { - var gr = collection.getGroup(grId); - if (gr != null && apply.test(gr)) { - return true; + var groupId = cachedDefinition.paramGroups().get(currentParamIndex); + if (groupId != null) { + var groups = collection.getGroups(groupId); + if (!groups.isEmpty()) { + for (UnpickCollection.Group group : groups) { + if (apply.test(group)) { + return true; + } + } } } } @@ -301,12 +305,12 @@ public void visitParenExpression(ParenExpression parenExpression) { @Override public void visitLiteralExpression(LiteralExpression literalExpression) { - if (literalExpression.literal instanceof Literal.String str) { - s.append('\"').append(str.value).append('\"'); // TODO - escape + if (literalExpression.literal instanceof Literal.String(String value)) { + s.append('\"').append(value.replace("\"", "\\\"")).append('\"'); } else if (literalExpression.literal instanceof Literal.Integer i) { - s.append(i.value); + s.append(i.value()); } else if (literalExpression.literal instanceof Literal.Long l) { - s.append(l.value).append('l'); + s.append(l.value()).append('l'); } else if (literalExpression.literal instanceof Literal.Double d) { s.append(d).append('d'); } else if (literalExpression.literal instanceof Literal.Float f) { @@ -326,6 +330,7 @@ public void visitCastExpression(CastExpression castExpression) { case FLOAT -> "float"; case SHORT -> "short"; case STRING -> "String"; + case CLASS -> "Class"; }); s.append(')'); s.append(write(castExpression.operand)); From 338db58a690861ce307aa60dd974dca8c11fbfc7 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Fri, 18 Jul 2025 01:28:12 +0300 Subject: [PATCH 07/14] Update UnpickVisitor.java --- .../neoforged/jst/unpick/UnpickVisitor.java | 46 +++++++++++-------- 1 file changed, 27 insertions(+), 19 deletions(-) diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java index 0999076..98c8119 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -5,6 +5,7 @@ import com.intellij.psi.PsiAssignmentExpression; import com.intellij.psi.PsiCallExpression; import com.intellij.psi.PsiClass; +import com.intellij.psi.PsiCodeBlock; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiField; import com.intellij.psi.PsiFile; @@ -56,15 +57,13 @@ public UnpickVisitor(PsiFile file, UnpickCollection collection, Replacements rep @Nullable private PsiMethod methodContext; - @Nullable private PsiField fieldContext; @Nullable private PsiMethod calledMethod; - @Nullable - private TargetMethod cachedDefinition; + private TargetMethod methodCallTarget; private int currentParamIndex; @@ -98,17 +97,19 @@ public void visitElement(@NotNull PsiElement element) { if (ref instanceof PsiMethod met) { var oldMet = calledMethod; calledMethod = met; - cachedDefinition = null; + methodCallTarget = null; for (int i = 0; i < call.getArgumentList().getExpressions().length; i++) { var oldIndex = currentParamIndex; currentParamIndex = i; - // TODO - we might want to rethink this, right now we rewalk everything with new context, - // but we could instead collect the variables from the start - if (call.getArgumentList().getExpressions()[i] instanceof PsiReferenceExpression refEx) { + // If any parameter of the method call is directly referencing local var we re-walk the entire method body + // and apply unpick with the context of the method being called to all of its assignments (including the initialiser) + if (call.getArgumentList().getExpressions()[i] instanceof PsiReferenceExpression refEx && methodContext != null) { + PsiCodeBlock body = methodContext.getBody(); PsiElement resolved = PsiHelper.resolve(refEx); - if (resolved instanceof PsiLocalVariable localVar && methodContext != null && methodContext.getBody() != null) { + + if (body != null && resolved instanceof PsiLocalVariable localVar) { if (localVar.getInitializer() != null) { localVar.getInitializer().accept(limitedDirectVisitor()); } @@ -124,7 +125,7 @@ public void visitElement(@NotNull PsiElement element) { } super.visitElement(element); } - }.visitElement(methodContext.getBody()); + }.visitElement(body); continue; } } @@ -143,6 +144,12 @@ public void visitElement(@NotNull PsiElement element) { element.acceptChildren(this); } + /** + * {@return an element visitor that visits only tokens outside of call expressions} + * This can be used when there is no need to handle call expressions as they would have already + * been handled or will be handled - for instance, when re-applying unpick for local variable initialisers, + * but with more context. + */ private PsiRecursiveElementVisitor limitedDirectVisitor() { return new PsiRecursiveElementVisitor() { @Override @@ -186,15 +193,15 @@ private void visitToken(PsiJavaToken tok) { if (isUnaryMinus(tok)) val = -val; replaceLiteral(tok, val, IntegerType.INT); } else if (tok.getTokenType() == JavaTokenType.LONG_LITERAL) { - var val = Long.parseLong(removeSuffix(tok.getText(), "l")); + var val = Long.parseLong(removeSuffix(tok.getText(), 'l')); if (isUnaryMinus(tok)) val = -val; replaceLiteral(tok, val, IntegerType.LONG); } else if (tok.getTokenType() == JavaTokenType.DOUBLE_LITERAL) { - var val = Double.parseDouble(removeSuffix(tok.getText(), "d")); + var val = Double.parseDouble(removeSuffix(tok.getText(), 'd')); if (isUnaryMinus(tok)) val = -val; replaceLiteral(tok, val, IntegerType.DOUBLE); } else if (tok.getTokenType() == JavaTokenType.FLOAT_LITERAL) { - var val = Float.parseFloat(removeSuffix(tok.getText(), "f")); + var val = Float.parseFloat(removeSuffix(tok.getText(), 'f')); if (isUnaryMinus(tok)) val = -val; replaceLiteral(tok, val, IntegerType.FLOAT); } @@ -267,11 +274,11 @@ private boolean checkNotRecursive(Expression expression) { private boolean forInScope(Predicate apply) { if (calledMethod != null) { - if (cachedDefinition == null) { - cachedDefinition = collection.getDefinitionsFor(calledMethod); + if (methodCallTarget == null) { + methodCallTarget = collection.getDefinitionsFor(calledMethod); } - if (cachedDefinition != null) { - var groupId = cachedDefinition.paramGroups().get(currentParamIndex); + if (methodCallTarget != null) { + var groupId = methodCallTarget.paramGroups().get(currentParamIndex); if (groupId != null) { var groups = collection.getGroups(groupId); if (!groups.isEmpty()) { @@ -413,7 +420,7 @@ private String generateFlag(UnpickCollection.Group group, long val, IntegerType long residual = negated ? negatedResidual : orResidual; - StringBuilder replacement = new StringBuilder(write(constants.get(0))); + StringBuilder replacement = new StringBuilder(write(constants.getFirst())); for (int i = 1; i < constants.size(); i++) { replacement.append(" | "); replacement.append(write(constants.get(i))); @@ -448,8 +455,9 @@ private static long getConstantsEncompassing(long literal, IntegerType unsign, U return residual; } - private static String removeSuffix(String in, String suffix) { - if (in.endsWith(suffix) || in.endsWith(suffix.toUpperCase(Locale.ROOT))) { + private static String removeSuffix(String in, char suffix) { + var lastChar = in.charAt(in.length() - 1); + if (lastChar == suffix || lastChar == Character.toUpperCase(suffix)) { return in.substring(0, in.length() - 1); } return in; From 374622b9317c5c40b69a15fd1645387608c934a4 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Fri, 18 Jul 2025 02:03:44 +0300 Subject: [PATCH 08/14] Some comments --- .../{IntegerType.java => NumberType.java} | 32 +++++++++++++++---- .../jst/unpick/UnpickCollection.java | 4 +-- .../neoforged/jst/unpick/UnpickVisitor.java | 24 ++++++++------ 3 files changed, 42 insertions(+), 18 deletions(-) rename unpick/src/main/java/net/neoforged/jst/unpick/{IntegerType.java => NumberType.java} (87%) diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java b/unpick/src/main/java/net/neoforged/jst/unpick/NumberType.java similarity index 87% rename from unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java rename to unpick/src/main/java/net/neoforged/jst/unpick/NumberType.java index 6df67aa..a521629 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/IntegerType.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/NumberType.java @@ -6,7 +6,15 @@ import java.util.HashMap; import java.util.Map; -public enum IntegerType { +/** + * Class that is used to compute mathematical operations between number types while operating on {@link Number}s. + *

+ * Each operation has a method that is overridden as needed by each number type to ensure that + * special behaviour is preserved (e.g. we cannot add two bytes as longs and then attempt to cast back + * to byte because we need to make sure that the addition overflows as as byte; the same + * applies to floats and doubles). + */ +public enum NumberType { BYTE(DataType.BYTE, Byte.class, false) { @Override public Number cast(Number in) { @@ -269,10 +277,10 @@ public Number modulo(Number a, Number b) { } }; - public static final Map, IntegerType> TYPES; + public static final Map, NumberType> TYPES; static { - var types = new HashMap, IntegerType>(); - for (IntegerType value : values()) { + var types = new HashMap, NumberType>(); + for (NumberType value : values()) { types.put(value.classType, value); } TYPES = Collections.unmodifiableMap(types); @@ -280,10 +288,20 @@ public Number modulo(Number a, Number b) { public final DataType dataType; public final Class classType; + /** + * Whether this number type can be treated as a bit flag - only {@code true} for {@link #INT} and {@link #LONG}. + */ public final boolean supportsFlag; - public final IntegerType[] widenFrom; - - IntegerType(DataType dataType, Class classType, boolean supportsFlag, IntegerType... widenFrom) { + /** + * Number types that can be converted to this type without needing an explicit cast: + *

+ * byte -> short -> int -> long + *

+ * int -> float -> double + */ + public final NumberType[] widenFrom; + + NumberType(DataType dataType, Class classType, boolean supportsFlag, NumberType... widenFrom) { this.dataType = dataType; this.classType = classType; this.supportsFlag = supportsFlag; diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java index 034998a..1c5c397 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java @@ -264,7 +264,7 @@ private static Object resolveConstant(Expression expression, JavaPsiFacade facad } else if (expression instanceof UnaryExpression unaryExpression) { var value = (Number) resolveConstant(unaryExpression.operand, facade, scope); return switch (unaryExpression.operator) { - case NEGATE -> value; // TODO - we can't negate numbers? + case NEGATE -> NumberType.TYPES.get(value.getClass()).negate(value); case BIT_NOT -> value instanceof Long ? ~value.longValue() : ~value.intValue(); }; } else if (expression instanceof BinaryExpression binaryExpression) { @@ -272,7 +272,7 @@ private static Object resolveConstant(Expression expression, JavaPsiFacade facad var rhs = resolveConstant(binaryExpression.rhs, facade, scope); if (lhs instanceof Number l && rhs instanceof Number r) { - var type = IntegerType.TYPES.get(l.getClass()); + var type = NumberType.TYPES.get(l.getClass()); return switch (binaryExpression.operator) { case ADD -> type.add(l, r); case DIVIDE -> type.divide(l, r); diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java index 98c8119..1a47bcc 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -191,27 +191,27 @@ private void visitToken(PsiJavaToken tok) { val = Integer.parseUnsignedInt(tok.getText()); } if (isUnaryMinus(tok)) val = -val; - replaceLiteral(tok, val, IntegerType.INT); + replaceLiteral(tok, val, NumberType.INT); } else if (tok.getTokenType() == JavaTokenType.LONG_LITERAL) { var val = Long.parseLong(removeSuffix(tok.getText(), 'l')); if (isUnaryMinus(tok)) val = -val; - replaceLiteral(tok, val, IntegerType.LONG); + replaceLiteral(tok, val, NumberType.LONG); } else if (tok.getTokenType() == JavaTokenType.DOUBLE_LITERAL) { var val = Double.parseDouble(removeSuffix(tok.getText(), 'd')); if (isUnaryMinus(tok)) val = -val; - replaceLiteral(tok, val, IntegerType.DOUBLE); + replaceLiteral(tok, val, NumberType.DOUBLE); } else if (tok.getTokenType() == JavaTokenType.FLOAT_LITERAL) { var val = Float.parseFloat(removeSuffix(tok.getText(), 'f')); if (isUnaryMinus(tok)) val = -val; - replaceLiteral(tok, val, IntegerType.FLOAT); + replaceLiteral(tok, val, NumberType.FLOAT); } } - private void replaceLiteral(PsiJavaToken element, Number number, IntegerType type) { + private void replaceLiteral(PsiJavaToken element, Number number, NumberType type) { replaceLiteral(element, number, type, false); } - private boolean replaceLiteral(PsiJavaToken element, Number number, IntegerType type, boolean denyStrict) { + private boolean replaceLiteral(PsiJavaToken element, Number number, NumberType type, boolean denyStrict) { return forInScope(group -> { // If we need to deny strict conversion (so if this is a conversion) we shall do so if (group.strict() && denyStrict) return false; @@ -225,7 +225,10 @@ private boolean replaceLiteral(PsiJavaToken element, Number number, IntegerType return true; } + // Next, try if this group is a flag and the number type supports flags (ints and longs) we try to generate the flag combination if (group.flag() && type.supportsFlag) { + // We generate flags for ints based on their long value as + // longs are a superset of ints and as such we can reduce code duplication var flag = generateFlag(group, number.longValue(), type); if (flag != null) { replacements.replace(element, flag); @@ -235,6 +238,8 @@ private boolean replaceLiteral(PsiJavaToken element, Number number, IntegerType } } + // As a fallback, if the group has a specific format but the + // value of the token does not have a constant we format the token if (group.format() != null) { replacements.replace(element, formatAs(number, group.format())); replaceMinus(element); @@ -242,7 +247,8 @@ private boolean replaceLiteral(PsiJavaToken element, Number number, IntegerType return true; } - for (IntegerType from : type.widenFrom) { + // Finally we try to apply non-strict widening from lower number types + for (NumberType from : type.widenFrom) { var lower = from.cast(number); if (lower.doubleValue() == number.doubleValue()) { if (replaceLiteral(element, lower, from, true)) { @@ -406,7 +412,7 @@ private ImportHelper imports() { } @Nullable - private String generateFlag(UnpickCollection.Group group, long val, IntegerType type) { + private String generateFlag(UnpickCollection.Group group, long val, NumberType type) { List orConstants = new ArrayList<>(); long orResidual = getConstantsEncompassing(val, type, group, orConstants); long negatedLiteral = type.toUnsignedLong(type.negate(val)); @@ -441,7 +447,7 @@ private String generateFlag(UnpickCollection.Group group, long val, IntegerType * Adds the constants that encompass {@code literal} to {@code constantsOut}. * Returns the residual (bits set in the literal not covered by the returned constants). */ - private static long getConstantsEncompassing(long literal, IntegerType unsign, UnpickCollection.Group group, List constantsOut) { + private static long getConstantsEncompassing(long literal, NumberType unsign, UnpickCollection.Group group, List constantsOut) { long residual = literal; for (var constant : group.constants().entrySet()) { long val = unsign.toUnsignedLong((Number) constant.getKey()); From 0956f3c72366c6d266692bd6990931efedb398c0 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Fri, 18 Jul 2025 19:09:43 +0300 Subject: [PATCH 09/14] Handle returns --- tests/data/unpick/local_variables/def.unpick | 10 ++ .../expected/com/example/Example.java | 21 ++++ .../source/com/example/Example.java | 21 ++++ .../net/neoforged/jst/tests/EmbeddedTest.java | 5 + .../neoforged/jst/unpick/UnpickVisitor.java | 113 ++++++++++++------ 5 files changed, 135 insertions(+), 35 deletions(-) create mode 100644 tests/data/unpick/local_variables/def.unpick create mode 100644 tests/data/unpick/local_variables/expected/com/example/Example.java create mode 100644 tests/data/unpick/local_variables/source/com/example/Example.java diff --git a/tests/data/unpick/local_variables/def.unpick b/tests/data/unpick/local_variables/def.unpick new file mode 100644 index 0000000..7b17ee5 --- /dev/null +++ b/tests/data/unpick/local_variables/def.unpick @@ -0,0 +1,10 @@ +unpick v3 + +group int Color + @format hex + com.example.Example.RED + com.example.Example.PURPLE + com.example.Example.PINK + +target_method com.example.Example setColor(I)V + param 0 Color diff --git a/tests/data/unpick/local_variables/expected/com/example/Example.java b/tests/data/unpick/local_variables/expected/com/example/Example.java new file mode 100644 index 0000000..4cb8f37 --- /dev/null +++ b/tests/data/unpick/local_variables/expected/com/example/Example.java @@ -0,0 +1,21 @@ +package com.example; + +public class Example { + public static final int + RED = 0xFF0000, + PURPLE = 0x800080, + PINK = 0xFFC0CB; + + public static void acceptColor(int in) { + int color = 0xD7837F; + if (in < 0) { + color = Example.PURPLE; + } else { + color = in == 0x0 ? Example.RED : Example.PINK; + } + + setColor(color); + } + + public static void setColor(int color) {} +} diff --git a/tests/data/unpick/local_variables/source/com/example/Example.java b/tests/data/unpick/local_variables/source/com/example/Example.java new file mode 100644 index 0000000..8413564 --- /dev/null +++ b/tests/data/unpick/local_variables/source/com/example/Example.java @@ -0,0 +1,21 @@ +package com.example; + +public class Example { + public static final int + RED = 0xFF0000, + PURPLE = 0x800080, + PINK = 0xFFC0CB; + + public static void acceptColor(int in) { + int color = 14123903; + if (in < 0) { + color = 8388736; + } else { + color = in == 0 ? 16711680 : 16761035; + } + + setColor(color); + } + + public static void setColor(int color) {} +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java index b08d28a..faa254c 100644 --- a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -368,6 +368,11 @@ void testFormats() throws Exception { void testScoped() throws Exception { runUnpickTest("scoped"); } + + @Test + void testLocalVariables() throws Exception { + runUnpickTest("local_variables"); + } } protected final void runInterfaceInjectionTest(String testDirName, Path tempDir, String... additionalArgs) throws Exception { diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java index 1a47bcc..a8d55c2 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -7,6 +7,7 @@ import com.intellij.psi.PsiClass; import com.intellij.psi.PsiCodeBlock; import com.intellij.psi.PsiElement; +import com.intellij.psi.PsiExpression; import com.intellij.psi.PsiField; import com.intellij.psi.PsiFile; import com.intellij.psi.PsiJavaToken; @@ -17,6 +18,7 @@ import com.intellij.psi.PsiPrefixExpression; import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiReferenceExpression; +import com.intellij.psi.PsiReturnStatement; import daomephsta.unpick.constantmappers.datadriven.tree.GroupFormat; import daomephsta.unpick.constantmappers.datadriven.tree.Literal; import daomephsta.unpick.constantmappers.datadriven.tree.TargetMethod; @@ -66,6 +68,7 @@ public UnpickVisitor(PsiFile file, UnpickCollection collection, Replacements rep private TargetMethod methodCallTarget; private int currentParamIndex; + private boolean withinReturnStatement; @Override public void visitElement(@NotNull PsiElement element) { @@ -79,8 +82,12 @@ public void visitElement(@NotNull PsiElement element) { if (met.getBody() != null) { var oldCtx = this.methodContext; this.methodContext = met; + var oldInReturn = this.withinReturnStatement; + this.withinReturnStatement = false; + met.getBody().acceptChildren(this); this.methodContext = oldCtx; + this.withinReturnStatement = oldInReturn; } return; } else if (element instanceof PsiField fld) { @@ -105,45 +112,53 @@ public void visitElement(@NotNull PsiElement element) { // If any parameter of the method call is directly referencing local var we re-walk the entire method body // and apply unpick with the context of the method being called to all of its assignments (including the initialiser) - if (call.getArgumentList().getExpressions()[i] instanceof PsiReferenceExpression refEx && methodContext != null) { - PsiCodeBlock body = methodContext.getBody(); - PsiElement resolved = PsiHelper.resolve(refEx); - - if (body != null && resolved instanceof PsiLocalVariable localVar) { - if (localVar.getInitializer() != null) { - localVar.getInitializer().accept(limitedDirectVisitor()); - } - - new PsiRecursiveElementVisitor() { - @Override - public void visitElement(@NotNull PsiElement element) { - if (element instanceof PsiAssignmentExpression as) { - if (as.getOperationSign().getTokenType() == JavaTokenType.EQ && as.getLExpression() instanceof PsiReferenceExpression ref && PsiHelper.resolve(ref) == localVar && as.getRExpression() != null) { - as.getRExpression().accept(limitedDirectVisitor()); - } - return; - } - super.visitElement(element); - } - }.visitElement(body); - continue; - } - } - - // TODO - we need to handle return unpicks - - this.visitElement(call.getArgumentList().getExpressions()[i]); + acceptPossibleLocalVarReference(call.getArgumentList().getExpressions()[i]); currentParamIndex = oldIndex; } calledMethod = oldMet; } return; + } else if (element instanceof PsiReturnStatement returnStatement) { + this.withinReturnStatement = true; + acceptPossibleLocalVarReference(returnStatement.getReturnValue()); + this.withinReturnStatement = false; } element.acceptChildren(this); } + private void acceptPossibleLocalVarReference(PsiExpression expression) { + if (expression instanceof PsiReferenceExpression refEx && methodContext != null) { + PsiCodeBlock body = methodContext.getBody(); + PsiElement resolved = PsiHelper.resolve(refEx); + + if (body != null && resolved instanceof PsiLocalVariable localVar) { + if (localVar.getInitializer() != null) { + localVar.getInitializer().accept(limitedDirectVisitor()); + } + + new PsiRecursiveElementVisitor() { + @Override + public void visitElement(@NotNull PsiElement element) { + if (element instanceof PsiAssignmentExpression as) { + if (as.getOperationSign().getTokenType() == JavaTokenType.EQ && as.getLExpression() instanceof PsiReferenceExpression ref && PsiHelper.resolve(ref) == localVar && as.getRExpression() != null) { + as.getRExpression().accept(limitedDirectVisitor()); + } + return; + } + super.visitElement(element); + } + }.visitElement(body); + return; + } + } + + if (expression != null) { + visitElement(expression); + } + } + /** * {@return an element visitor that visits only tokens outside of call expressions} * This can be used when there is no need to handle call expressions as they would have already @@ -182,18 +197,31 @@ private void visitToken(PsiJavaToken tok) { return false; }); } else if (tok.getTokenType() == JavaTokenType.INTEGER_LITERAL) { + var text = tok.getText().toLowerCase(Locale.ROOT); + int val; - if (tok.getText().toLowerCase(Locale.ROOT).startsWith("0x")) { - val = Integer.parseUnsignedInt(tok.getText().substring(2), 16); - } else if (tok.getText().toLowerCase(Locale.ROOT).startsWith("0b")) { - val = Integer.parseUnsignedInt(tok.getText().substring(2), 2); + if (text.startsWith("0x")) { + val = Integer.parseUnsignedInt(text.substring(2), 16); + } else if (text.startsWith("0b")) { + val = Integer.parseUnsignedInt(text.substring(2), 2); } else { - val = Integer.parseUnsignedInt(tok.getText()); + val = Integer.parseUnsignedInt(text); } + if (isUnaryMinus(tok)) val = -val; replaceLiteral(tok, val, NumberType.INT); } else if (tok.getTokenType() == JavaTokenType.LONG_LITERAL) { - var val = Long.parseLong(removeSuffix(tok.getText(), 'l')); + var text = removeSuffix(tok.getText(), 'l').toLowerCase(Locale.ROOT); + + long val; + if (text.startsWith("0x")) { + val = Long.parseUnsignedLong(text.substring(2), 16); + } else if (text.startsWith("0b")) { + val = Long.parseUnsignedLong(text.substring(2), 2); + } else { + val = Long.parseUnsignedLong(text); + } + if (isUnaryMinus(tok)) val = -val; replaceLiteral(tok, val, NumberType.LONG); } else if (tok.getTokenType() == JavaTokenType.DOUBLE_LITERAL) { @@ -288,7 +316,7 @@ private boolean forInScope(Predicate apply) { if (groupId != null) { var groups = collection.getGroups(groupId); if (!groups.isEmpty()) { - for (UnpickCollection.Group group : groups) { + for (var group : groups) { if (apply.test(group)) { return true; } @@ -297,6 +325,21 @@ private boolean forInScope(Predicate apply) { } } } + + if (withinReturnStatement && methodContext != null) { + var contextDefinitions = collection.getDefinitionsFor(methodContext); + if (contextDefinitions != null && contextDefinitions.returnGroup() != null) { + var groups = collection.getGroups(contextDefinitions.returnGroup()); + if (!groups.isEmpty()) { + for (var group : groups) { + if (apply.test(group)) { + return true; + } + } + } + } + } + return collection.forEachInScope(classContext, methodContext, apply); } From 25ed976cde0506ad86c088601e110b588fedef48 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sat, 19 Jul 2025 00:37:34 +0300 Subject: [PATCH 10/14] Add test for returns --- tests/data/unpick/returns/def.unpick | 9 ++++++++ .../returns/expected/com/example/Example.java | 21 +++++++++++++++++++ .../returns/source/com/example/Example.java | 21 +++++++++++++++++++ .../net/neoforged/jst/tests/EmbeddedTest.java | 5 +++++ 4 files changed, 56 insertions(+) create mode 100644 tests/data/unpick/returns/def.unpick create mode 100644 tests/data/unpick/returns/expected/com/example/Example.java create mode 100644 tests/data/unpick/returns/source/com/example/Example.java diff --git a/tests/data/unpick/returns/def.unpick b/tests/data/unpick/returns/def.unpick new file mode 100644 index 0000000..fcc4cff --- /dev/null +++ b/tests/data/unpick/returns/def.unpick @@ -0,0 +1,9 @@ +unpick v3 + +group int Constant + com.example.Example.ONE + com.example.Example.TWO + com.example.Example.FOUR + +target_method com.example.Example getNumber(ZZ)I + return Constant diff --git a/tests/data/unpick/returns/expected/com/example/Example.java b/tests/data/unpick/returns/expected/com/example/Example.java new file mode 100644 index 0000000..23d63ca --- /dev/null +++ b/tests/data/unpick/returns/expected/com/example/Example.java @@ -0,0 +1,21 @@ +package com.example; + +public class Example { + public static final int + ONE = 1, + TWO = 2, + FOUR = 4; + + public static int getNumber(boolean odd, boolean b) { + int value = 0; + if (odd) { + value = Example.ONE; + } else if (b) { + return Example.FOUR; + } else { + value = Example.TWO; + } + + return value; + } +} diff --git a/tests/data/unpick/returns/source/com/example/Example.java b/tests/data/unpick/returns/source/com/example/Example.java new file mode 100644 index 0000000..8ecd9a4 --- /dev/null +++ b/tests/data/unpick/returns/source/com/example/Example.java @@ -0,0 +1,21 @@ +package com.example; + +public class Example { + public static final int + ONE = 1, + TWO = 2, + FOUR = 4; + + public static int getNumber(boolean odd, boolean b) { + int value = 0; + if (odd) { + value = 1; + } else if (b) { + return 4; + } else { + value = 2; + } + + return value; + } +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java index faa254c..fa6b590 100644 --- a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -373,6 +373,11 @@ void testScoped() throws Exception { void testLocalVariables() throws Exception { runUnpickTest("local_variables"); } + + @Test + void testReturns() throws Exception { + runUnpickTest("returns"); + } } protected final void runInterfaceInjectionTest(String testDirName, Path tempDir, String... additionalArgs) throws Exception { From f00f4122f3bb272e0a87c513c0c3639162980190 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sat, 19 Jul 2025 00:59:06 +0300 Subject: [PATCH 11/14] Add test for flags --- tests/data/unpick/flags/def.unpick | 11 +++++++++++ .../flags/expected/com/example/Example.java | 18 ++++++++++++++++++ .../flags/source/com/example/Example.java | 18 ++++++++++++++++++ .../net/neoforged/jst/tests/EmbeddedTest.java | 5 +++++ 4 files changed, 52 insertions(+) create mode 100644 tests/data/unpick/flags/def.unpick create mode 100644 tests/data/unpick/flags/expected/com/example/Example.java create mode 100644 tests/data/unpick/flags/source/com/example/Example.java diff --git a/tests/data/unpick/flags/def.unpick b/tests/data/unpick/flags/def.unpick new file mode 100644 index 0000000..febf0ad --- /dev/null +++ b/tests/data/unpick/flags/def.unpick @@ -0,0 +1,11 @@ +unpick v3 + +group int Flags + @flags + com.example.Example.FLAG_1 + com.example.Example.FLAG_2 + com.example.Example.FLAG_3 + com.example.Example.FLAG_4 + +target_method com.example.Example applyFlags(I)V + param 0 Flags diff --git a/tests/data/unpick/flags/expected/com/example/Example.java b/tests/data/unpick/flags/expected/com/example/Example.java new file mode 100644 index 0000000..c403eaa --- /dev/null +++ b/tests/data/unpick/flags/expected/com/example/Example.java @@ -0,0 +1,18 @@ +package com.example; + +public class Example { + public static final int + FLAG_1 = 2, + FLAG_2 = 4, + FLAG_3 = 8, + FLAG_4 = 16; + + public static void main(String[] args) { + applyFlags(Example.FLAG_1); + applyFlags(Example.FLAG_1 | Example.FLAG_2); + applyFlags(Example.FLAG_3 | Example.FLAG_4 | Example.FLAG_1 | 129); + applyFlags(-1); + } + + public static void applyFlags(int flags) {} +} diff --git a/tests/data/unpick/flags/source/com/example/Example.java b/tests/data/unpick/flags/source/com/example/Example.java new file mode 100644 index 0000000..9222826 --- /dev/null +++ b/tests/data/unpick/flags/source/com/example/Example.java @@ -0,0 +1,18 @@ +package com.example; + +public class Example { + public static final int + FLAG_1 = 2, + FLAG_2 = 4, + FLAG_3 = 8, + FLAG_4 = 16; + + public static void main(String[] args) { + applyFlags(2); + applyFlags(6); + applyFlags(155); + applyFlags(-1); + } + + public static void applyFlags(int flags) {} +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java index fa6b590..b03b72f 100644 --- a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -378,6 +378,11 @@ void testLocalVariables() throws Exception { void testReturns() throws Exception { runUnpickTest("returns"); } + + @Test + void testFlags() throws Exception { + runUnpickTest("flags"); + } } protected final void runInterfaceInjectionTest(String testDirName, Path tempDir, String... additionalArgs) throws Exception { From 63397fbc262702edc3a95d484cb769fd5e36b0aa Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sat, 19 Jul 2025 01:29:31 +0300 Subject: [PATCH 12/14] Fix method params not being recognized as local variables --- tests/data/unpick/statements/def.unpick | 11 +++++++++++ .../expected/com/example/Example.java | 19 +++++++++++++++++++ .../source/com/example/Example.java | 19 +++++++++++++++++++ .../net/neoforged/jst/tests/EmbeddedTest.java | 5 +++++ .../neoforged/jst/unpick/UnpickVisitor.java | 11 +++++++---- 5 files changed, 61 insertions(+), 4 deletions(-) create mode 100644 tests/data/unpick/statements/def.unpick create mode 100644 tests/data/unpick/statements/expected/com/example/Example.java create mode 100644 tests/data/unpick/statements/source/com/example/Example.java diff --git a/tests/data/unpick/statements/def.unpick b/tests/data/unpick/statements/def.unpick new file mode 100644 index 0000000..cc15b23 --- /dev/null +++ b/tests/data/unpick/statements/def.unpick @@ -0,0 +1,11 @@ +unpick v3 + +group int Flags + @flags + com.example.Example.ONE + com.example.Example.TWO + com.example.Example.THREE + com.example.Example.FOUR + +target_method com.example.Example accept(I)V + param 0 Flags diff --git a/tests/data/unpick/statements/expected/com/example/Example.java b/tests/data/unpick/statements/expected/com/example/Example.java new file mode 100644 index 0000000..6c199f1 --- /dev/null +++ b/tests/data/unpick/statements/expected/com/example/Example.java @@ -0,0 +1,19 @@ +package com.example; + +public class Example { + public static final int + ONE = 1 << 0, + TWO = 1 << 1, + THREE = 1 << 2, + FOUR = 1 << 3; + + public static void run(int val) { + accept(((val & Example.FOUR) != 0) ? val | Example.THREE : val | Example.ONE); + + val = Example.TWO; + + accept(val); + } + + public static void accept(int value) {} +} diff --git a/tests/data/unpick/statements/source/com/example/Example.java b/tests/data/unpick/statements/source/com/example/Example.java new file mode 100644 index 0000000..b1bf000 --- /dev/null +++ b/tests/data/unpick/statements/source/com/example/Example.java @@ -0,0 +1,19 @@ +package com.example; + +public class Example { + public static final int + ONE = 1 << 0, + TWO = 1 << 1, + THREE = 1 << 2, + FOUR = 1 << 3; + + public static void run(int val) { + accept(((val & 8) != 0) ? val | 4 : val | 1); + + val = 2; + + accept(val); + } + + public static void accept(int value) {} +} diff --git a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java index b03b72f..0e85c35 100644 --- a/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java +++ b/tests/src/test/java/net/neoforged/jst/tests/EmbeddedTest.java @@ -383,6 +383,11 @@ void testReturns() throws Exception { void testFlags() throws Exception { runUnpickTest("flags"); } + + @Test + void testStatements() throws Exception { + runUnpickTest("statements"); + } } protected final void runInterfaceInjectionTest(String testDirName, Path tempDir, String... additionalArgs) throws Exception { diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java index a8d55c2..f808294 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -15,10 +15,12 @@ import com.intellij.psi.PsiLocalVariable; import com.intellij.psi.PsiMethod; import com.intellij.psi.PsiMethodCallExpression; +import com.intellij.psi.PsiParameter; import com.intellij.psi.PsiPrefixExpression; import com.intellij.psi.PsiRecursiveElementVisitor; import com.intellij.psi.PsiReferenceExpression; import com.intellij.psi.PsiReturnStatement; +import com.intellij.psi.PsiVariable; import daomephsta.unpick.constantmappers.datadriven.tree.GroupFormat; import daomephsta.unpick.constantmappers.datadriven.tree.Literal; import daomephsta.unpick.constantmappers.datadriven.tree.TargetMethod; @@ -133,16 +135,17 @@ private void acceptPossibleLocalVarReference(PsiExpression expression) { PsiCodeBlock body = methodContext.getBody(); PsiElement resolved = PsiHelper.resolve(refEx); - if (body != null && resolved instanceof PsiLocalVariable localVar) { - if (localVar.getInitializer() != null) { - localVar.getInitializer().accept(limitedDirectVisitor()); + if (body != null && (resolved instanceof PsiLocalVariable || resolved instanceof PsiParameter)) { + var var = (PsiVariable) resolved; + if (var.getInitializer() != null) { + var.getInitializer().accept(limitedDirectVisitor()); } new PsiRecursiveElementVisitor() { @Override public void visitElement(@NotNull PsiElement element) { if (element instanceof PsiAssignmentExpression as) { - if (as.getOperationSign().getTokenType() == JavaTokenType.EQ && as.getLExpression() instanceof PsiReferenceExpression ref && PsiHelper.resolve(ref) == localVar && as.getRExpression() != null) { + if (as.getOperationSign().getTokenType() == JavaTokenType.EQ && as.getLExpression() instanceof PsiReferenceExpression ref && PsiHelper.resolve(ref) == var && as.getRExpression() != null) { as.getRExpression().accept(limitedDirectVisitor()); } return; From 26e1153b5cdd3559d225c62e7b6da51f27d14c08 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Sat, 19 Jul 2025 02:49:48 +0300 Subject: [PATCH 13/14] Refactor to mostly use a stack --- .../jst/unpick/UnpickCollection.java | 50 ++-- .../neoforged/jst/unpick/UnpickVisitor.java | 250 +++++++++++------- 2 files changed, 167 insertions(+), 133 deletions(-) diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java index 1c5c397..b68ba64 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickCollection.java @@ -54,7 +54,7 @@ public class UnpickCollection { private final MultiMap byPackage; private final MultiMap byClass; - private final Map> methodScopes; + private final MultiMap methodScopes; // This list only exists to keep the base elements in memory and prevent them from being GC'd and therefore losing their user data // JavaPsiFacade#findClass uses a soft key and soft value map @@ -74,7 +74,7 @@ public UnpickCollection(TransformContext context, Map(); byClass = new MultiMap<>(); - methodScopes = new IdentityHashMap<>(); + methodScopes = new MultiMap<>(new IdentityHashMap<>()); baseElements = new ArrayList<>(); groups.forEach((key, defs) -> { @@ -95,7 +95,7 @@ public UnpickCollection(TransformContext context, Map new ArrayList<>()).add(gr); + methodScopes.putValue(clsMethod, gr); } } } @@ -131,45 +131,25 @@ public UnpickCollection(TransformContext context, Map pred) { - if (scope != null) { - var metScoped = methodScopes.get(scope); - if (metScoped != null) { - for (Group group : metScoped) { - if (pred.test(group)) return true; - } - } - } - + public Collection getClassContext(PsiClass cls) { var clsName = cls.getQualifiedName(); if (clsName != null) { - for (Group group : byClass.get(clsName)) { - if (pred.test(group)) { - return true; - } - } + return byClass.get(clsName); } + return List.of(); + } - var par = cls.getParent(); - while (par != null && !(par instanceof PsiJavaFile)) { - par = par.getParent(); - } - if (par instanceof PsiJavaFile file) { - for (Group group : byPackage.get(file.getPackageName())) { - if (pred.test(group)) { - return true; - } - } - } + public Collection getPackageContext(PsiJavaFile file) { + return byPackage.get(file.getPackageName()); + } - for (Group group : global) { - if (pred.test(group)) { - return true; - } - } + public Collection getMethodContext(PsiMethod method) { + return methodScopes.get(method); + } - return false; + public Collection getGlobalContext() { + return global; } @SuppressWarnings("OptionalAssignedToNull") diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java index f808294..1225cd5 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickVisitor.java @@ -5,11 +5,11 @@ import com.intellij.psi.PsiAssignmentExpression; import com.intellij.psi.PsiCallExpression; import com.intellij.psi.PsiClass; -import com.intellij.psi.PsiCodeBlock; import com.intellij.psi.PsiElement; import com.intellij.psi.PsiExpression; import com.intellij.psi.PsiField; import com.intellij.psi.PsiFile; +import com.intellij.psi.PsiJavaFile; import com.intellij.psi.PsiJavaToken; import com.intellij.psi.PsiLiteralExpression; import com.intellij.psi.PsiLocalVariable; @@ -39,9 +39,11 @@ import org.jetbrains.annotations.Nullable; import java.util.ArrayList; +import java.util.Collection; import java.util.List; import java.util.Locale; import java.util.Objects; +import java.util.Stack; import java.util.function.Predicate; public class UnpickVisitor extends PsiRecursiveElementVisitor { @@ -57,102 +59,134 @@ public UnpickVisitor(PsiFile file, UnpickCollection collection, Replacements rep this.replacements = replacements; } - private PsiClass classContext; - @Nullable private PsiMethod methodContext; @Nullable private PsiField fieldContext; @Nullable - private PsiMethod calledMethod; - @Nullable - private TargetMethod methodCallTarget; + private TargetMethod calledMethodContext; + private int currentParameterIndex; - private int currentParamIndex; - private boolean withinReturnStatement; + private final Stack> contextStack = new Stack<>(); @Override public void visitElement(@NotNull PsiElement element) { - if (element instanceof PsiClass cls) { - var oldCls = classContext; - this.classContext = cls; - cls.acceptChildren(this); - this.classContext = oldCls; - return; - } else if (element instanceof PsiMethod met) { - if (met.getBody() != null) { + Collection additionalContext = List.of(); + + switch (element) { + case PsiJavaFile javaFile -> additionalContext = collection.getPackageContext(javaFile); + case PsiClass cls -> additionalContext = collection.getClassContext(cls); + case PsiMethod met when met.getBody() != null -> { var oldCtx = this.methodContext; + contextStack.push(collection.getMethodContext(met)); this.methodContext = met; - var oldInReturn = this.withinReturnStatement; - this.withinReturnStatement = false; - met.getBody().acceptChildren(this); this.methodContext = oldCtx; - this.withinReturnStatement = oldInReturn; + contextStack.pop(); + return; } - return; - } else if (element instanceof PsiField fld) { - var oldCtx = this.fieldContext; - this.fieldContext = fld; - fld.acceptChildren(this); - this.fieldContext = oldCtx; - return; - } else if (element instanceof PsiJavaToken tok) { - visitToken(tok); - return; - } else if (element instanceof PsiMethodCallExpression call) { - PsiElement ref = PsiHelper.resolve(call.getMethodExpression()); - if (ref instanceof PsiMethod met) { - var oldMet = calledMethod; - calledMethod = met; - methodCallTarget = null; - - for (int i = 0; i < call.getArgumentList().getExpressions().length; i++) { - var oldIndex = currentParamIndex; - currentParamIndex = i; - - // If any parameter of the method call is directly referencing local var we re-walk the entire method body - // and apply unpick with the context of the method being called to all of its assignments (including the initialiser) - acceptPossibleLocalVarReference(call.getArgumentList().getExpressions()[i]); - - currentParamIndex = oldIndex; - } - calledMethod = oldMet; + case PsiField fld -> { + var oldCtx = this.fieldContext; + this.fieldContext = fld; + fld.acceptChildren(this); + this.fieldContext = oldCtx; + return; } - return; - } else if (element instanceof PsiReturnStatement returnStatement) { - this.withinReturnStatement = true; - acceptPossibleLocalVarReference(returnStatement.getReturnValue()); - this.withinReturnStatement = false; - } - element.acceptChildren(this); - } + case PsiJavaToken tok -> { + visitToken(tok); + return; + } - private void acceptPossibleLocalVarReference(PsiExpression expression) { - if (expression instanceof PsiReferenceExpression refEx && methodContext != null) { - PsiCodeBlock body = methodContext.getBody(); - PsiElement resolved = PsiHelper.resolve(refEx); + case PsiMethodCallExpression call -> { + PsiElement ref = PsiHelper.resolve(call.getMethodExpression()); + if (ref instanceof PsiMethod met) { + var oldCtx = this.calledMethodContext; + var oldIdx = this.currentParameterIndex; + + // We replace the old context to avoid nesting as it can produce weird artifacts + // when parameter expression are themselves other method calls since "invasive" + // (i.e. number formatting) rules of this group would apply to its parameters + this.calledMethodContext = collection.getDefinitionsFor(met); + for (int i = 0; i < call.getArgumentList().getExpressions().length; i++) { + this.currentParameterIndex = i; + // If any parameter of the method call is directly referencing local var we re-walk the entire method body + // and apply unpick with the context of the method being called to all of its assignments (including the initialiser) + acceptPossibleLocalVarReference(call.getArgumentList().getExpressions()[i]); + } - if (body != null && (resolved instanceof PsiLocalVariable || resolved instanceof PsiParameter)) { - var var = (PsiVariable) resolved; - if (var.getInitializer() != null) { - var.getInitializer().accept(limitedDirectVisitor()); + this.calledMethodContext = oldCtx; + this.currentParameterIndex = oldIdx; + return; } + } + case PsiReturnStatement returnStatement when methodContext != null -> { + var contextDefinitions = collection.getDefinitionsFor(methodContext); + if (contextDefinitions != null && contextDefinitions.returnGroup() != null) { + var groups = collection.getGroups(contextDefinitions.returnGroup()); + if (!groups.isEmpty()) { + contextStack.push(groups); + acceptPossibleLocalVarReference(returnStatement.getReturnValue()); + contextStack.pop(); + return; + } + } + } - new PsiRecursiveElementVisitor() { - @Override - public void visitElement(@NotNull PsiElement element) { - if (element instanceof PsiAssignmentExpression as) { - if (as.getOperationSign().getTokenType() == JavaTokenType.EQ && as.getLExpression() instanceof PsiReferenceExpression ref && PsiHelper.resolve(ref) == var && as.getRExpression() != null) { - as.getRExpression().accept(limitedDirectVisitor()); - } + case PsiLocalVariable localVar + when localVar.getInitializer() != null && localVar.getInitializer() instanceof PsiMethodCallExpression methodCall -> { + acceptReturnFlow(localVar, methodCall); + return; + } + + case PsiAssignmentExpression assignment -> { + if (assignment.getOperationSign().getTokenType() == JavaTokenType.EQ && assignment.getLExpression() instanceof PsiReferenceExpression ref) { + var referencedVariable = PsiHelper.resolve(ref); + if (referencedVariable instanceof PsiLocalVariable || referencedVariable instanceof PsiParameter) { + if (assignment.getLExpression() instanceof PsiMethodCallExpression methodCall) { + acceptReturnFlow((PsiVariable) referencedVariable, methodCall); return; } - super.visitElement(element); } - }.visitElement(body); + } + } + + default -> {} + } + + if (additionalContext.isEmpty()) { + element.acceptChildren(this); + } else { + contextStack.push(additionalContext); + element.acceptChildren(this); + contextStack.pop(); + } + } + + private void acceptReturnFlow(PsiVariable variable, PsiMethodCallExpression expression) { + var flowingFrom = expression.resolveMethod(); + if (flowingFrom != null) { + var target = collection.getDefinitionsFor(flowingFrom); + if (target != null && target.returnGroup() != null) { + var groups = collection.getGroups(target.returnGroup()); + if (!groups.isEmpty()) { + contextStack.push(groups); + visitVariableAssignments(variable); + contextStack.pop(); + return; + } + } + } + expression.acceptChildren(this); + } + + private void acceptPossibleLocalVarReference(PsiExpression expression) { + if (expression instanceof PsiReferenceExpression refEx) { + PsiElement resolved = PsiHelper.resolve(refEx); + + if (resolved instanceof PsiLocalVariable || resolved instanceof PsiParameter) { + visitVariableAssignments((PsiVariable) resolved); return; } } @@ -162,6 +196,28 @@ public void visitElement(@NotNull PsiElement element) { } } + private void visitVariableAssignments(PsiVariable var) { + if (var.getInitializer() != null) { + var.getInitializer().accept(limitedDirectVisitor()); + } + + var body = methodContext == null ? null : methodContext.getBody(); + if (body != null) { + new PsiRecursiveElementVisitor() { + @Override + public void visitElement(@NotNull PsiElement element) { + if (element instanceof PsiAssignmentExpression as) { + if (as.getOperationSign().getTokenType() == JavaTokenType.EQ && as.getLExpression() instanceof PsiReferenceExpression ref && PsiHelper.resolve(ref) == var && as.getRExpression() != null) { + as.getRExpression().accept(limitedDirectVisitor()); + } + return; + } + super.visitElement(element); + } + }.visitElement(body); + } + } + /** * {@return an element visitor that visits only tokens outside of call expressions} * This can be used when there is no need to handle call expressions as they would have already @@ -303,47 +359,45 @@ private void replaceMinus(PsiJavaToken tok) { } private boolean checkNotRecursive(Expression expression) { - if (fieldContext != null && expression instanceof FieldExpression fld) { - return !(fld.className.equals(classContext.getQualifiedName()) && Objects.equals(fld.fieldName, fieldContext.getName())); + if (fieldContext != null && fieldContext.getContainingClass() != null && expression instanceof FieldExpression fld) { + return !(fld.className.equals(fieldContext.getContainingClass().getQualifiedName()) && Objects.equals(fld.fieldName, fieldContext.getName())); } return true; } private boolean forInScope(Predicate apply) { - if (calledMethod != null) { - if (methodCallTarget == null) { - methodCallTarget = collection.getDefinitionsFor(calledMethod); - } - if (methodCallTarget != null) { - var groupId = methodCallTarget.paramGroups().get(currentParamIndex); - if (groupId != null) { - var groups = collection.getGroups(groupId); - if (!groups.isEmpty()) { - for (var group : groups) { - if (apply.test(group)) { - return true; - } + if (calledMethodContext != null) { + var paramGroupId = this.calledMethodContext.paramGroups().get(currentParameterIndex); + if (paramGroupId != null) { + var paramGroups = collection.getGroups(paramGroupId); + if (!paramGroups.isEmpty()) { + for (var group : paramGroups) { + if (apply.test(group)) { + return true; } } } } } - if (withinReturnStatement && methodContext != null) { - var contextDefinitions = collection.getDefinitionsFor(methodContext); - if (contextDefinitions != null && contextDefinitions.returnGroup() != null) { - var groups = collection.getGroups(contextDefinitions.returnGroup()); - if (!groups.isEmpty()) { - for (var group : groups) { - if (apply.test(group)) { - return true; - } + if (!contextStack.isEmpty()) { + // Walk and apply the context stack in reverse (e.g. we first apply the method scope, then the class scope and finally the package scope) + for (int i = contextStack.size() - 1; i >= 0; i--) { + for (var group : contextStack.get(i)) { + if (apply.test(group)) { + return true; } } } } - return collection.forEachInScope(classContext, methodContext, apply); + for (var group : collection.getGlobalContext()) { + if (apply.test(group)) { + return true; + } + } + + return false; } private String write(Expression expression) { From 4e8263a8807cab77ca8916bbe5a08cb9cb84a954 Mon Sep 17 00:00:00 2001 From: Matyrobbrt Date: Wed, 6 Aug 2025 16:45:02 +0300 Subject: [PATCH 14/14] Update docs --- README.md | 19 ++++++++++++++----- .../neoforged/jst/unpick/UnpickPlugin.java | 2 ++ 2 files changed, 16 insertions(+), 5 deletions(-) diff --git a/README.md b/README.md index 7550448..5ed587f 100644 --- a/README.md +++ b/README.md @@ -53,24 +53,27 @@ To create the executable jar with your custom transformer, you should shadow the Note that this tool is not intended to be run by users directly. Rather it is integrated into the [NeoGradle](https://github.com/neoforged/NeoGradle) build process. -It can be invoked as a standalone executable Jar-File. Java 17 is required. +It can be invoked as a standalone executable Jar-File. Java 21 is required. ``` -Usage: jst [-hV] [--in-format=] [--libraries-list=] +Usage: jst [-hV] [--debug] [--in-format=] [--libraries-list=] [--max-queue-depth=] [--out-format=] - [--classpath=]... [--ignore-prefix=]... - [--enable-parchment --parchment-mappings= [--[no-]parchment-javadoc] + [--problems-report=] [--classpath=]... + [--ignore-prefix=]... [--enable-parchment + --parchment-mappings= [--[no-]parchment-javadoc] [--parchment-conflict-prefix=]] [--enable-accesstransformers --access-transformer= [--access-transformer=]... [--access-transformer-validation=]] [--enable-interface-injection [--interface-injection-stubs=] [--interface-injection-marker=] - [--interface-injection-data=]...] INPUT OUTPUT + [--interface-injection-data=]...] [--enable-unpick [--unpick-data=]...] + INPUT OUTPUT INPUT Path to a single Java-file, a source-archive or a folder containing the source to transform. OUTPUT Path to where the resulting source should be placed. --classpath= Additional classpath entries to use. Is combined with --libraries-list. + --debug Print additional debugging information -h, --help Show this help message and exit. --ignore-prefix= Do not apply transformations to paths that start with any of these @@ -89,6 +92,8 @@ Usage: jst [-hV] [--in-format=] [--libraries-list=] --out-format= Specify the format of OUTPUT explicitly. Allows the same options as --in-format. + --problems-report= + Write problems to this report file. -V, --version Print version information and exit. Plugin - parchment --enable-parchment Enable parchment @@ -116,6 +121,10 @@ Plugin - interface-injection injected interfaces --interface-injection-stubs= The path to a zip to save interface stubs in +Plugin - unpick + --enable-unpick Enable unpick + --unpick-data= + The paths to read unpick definition files from ``` ## Licenses diff --git a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java index 0468d64..0a0dde6 100644 --- a/unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java +++ b/unpick/src/main/java/net/neoforged/jst/unpick/UnpickPlugin.java @@ -2,7 +2,9 @@ import net.neoforged.jst.api.SourceTransformer; import net.neoforged.jst.api.SourceTransformerPlugin; +import org.jetbrains.annotations.ApiStatus; +@ApiStatus.Experimental public class UnpickPlugin implements SourceTransformerPlugin { @Override public String getName() {