diff --git a/recaf-core/src/main/java/me/coley/recaf/parse/JavaParserHelper.java b/recaf-core/src/main/java/me/coley/recaf/parse/JavaParserHelper.java index 6f2385f7c..e1c06a2f6 100644 --- a/recaf-core/src/main/java/me/coley/recaf/parse/JavaParserHelper.java +++ b/recaf-core/src/main/java/me/coley/recaf/parse/JavaParserHelper.java @@ -1,21 +1,22 @@ package me.coley.recaf.parse; import com.github.javaparser.*; +import com.github.javaparser.ParserConfiguration.LanguageLevel; import com.github.javaparser.ast.CompilationUnit; import com.github.javaparser.ast.Node; import com.github.javaparser.ast.body.*; import com.github.javaparser.ast.expr.Expression; import com.github.javaparser.ast.expr.ObjectCreationExpr; import com.github.javaparser.ast.expr.SimpleName; -import jregex.Matcher; +import java.util.Arrays; import me.coley.recaf.Controller; import me.coley.recaf.code.FieldInfo; import me.coley.recaf.code.ItemInfo; import me.coley.recaf.code.LiteralExpressionInfo; import me.coley.recaf.code.MethodInfo; import me.coley.recaf.parse.evaluation.ExpressionEvaluator; -import me.coley.recaf.util.RegexUtil; -import me.coley.recaf.util.StringUtil; +import me.coley.recaf.util.logging.Logging; +import org.slf4j.Logger; import java.util.List; import java.util.Optional; @@ -26,13 +27,16 @@ * @author Matt Coley */ public class JavaParserHelper { + + private static final Logger logger = Logging.get(JavaParserHelper.class); + private final WorkspaceSymbolSolver symbolSolver; private final JavaParser parser; private JavaParserHelper(WorkspaceSymbolSolver symbolSolver) { this.symbolSolver = symbolSolver; parser = new JavaParser(new ParserConfiguration() - .setLanguageLevel(ParserConfiguration.LanguageLevel.JAVA_16) + .setLanguageLevel(LanguageLevel.JAVA_16) .setSymbolResolver(this.symbolSolver)); } @@ -82,12 +86,20 @@ public ParseResult parseClass(String code, boolean tryRecover) // There is a compilation unit, but is it usable? CompilationUnit unit = result.getResult().get(); if (tryRecover && isInvalidCompilationUnit(unit)) { + logger.info("{} problems found when parsing class", result.getProblems().size()); + for (Problem problem : result.getProblems()) { + logger.info("\t - {}", problem); + } // Its not usable at all, attempt to recover return new JavaParserRecovery(this).parseClassWithRecovery(code, problems); } else { // The unit is usable, but may contain some localized problems. // Check if we want to try to resolve any reported problems. if (tryRecover && !result.getProblems().isEmpty()) { + logger.info("{} problems found when parsing class", result.getProblems().size()); + for (Problem problem : result.getProblems()) { + logger.info("\t - {}", problem); + } return new JavaParserRecovery(this).parseClassWithRecovery(code, problems); } // Update unit and roll with whatever we got. @@ -104,7 +116,7 @@ public ParseResult parseClass(String code, boolean tryRecover) * JavaParser {@link com.github.javaparser.resolution.types.ResolvedReferenceType} will fail if there * is a mismatch in generic type arguments. If we're resolving from workspace references, those do not get * their type arguments generated, so it expects {@code 0} but our decompiled source may specify {@code 1} or - * more. This regular expression matches an equal number of items between two {@code <>} pairs. + * more. This code will match any generic type declaration (between two {@code <>} pairs). *
* Examples of valid matches: *
@@ -112,6 +124,7 @@ public ParseResult parseClass(String code, boolean tryRecover)
 	 *     >>
 	 *      | Foo>
 	 * 
+ * But will not match if the content is inside a string. * We then replace the matched content with an equal number of spaces so that none of the text positions * become offset. * @@ -121,14 +134,80 @@ public ParseResult parseClass(String code, boolean tryRecover) * @return Code with generics content replaced with spaces. */ private String filterGenerics(String code) { - // This isn't perfect, but should replace most basic generic type usage - Matcher matcher = RegexUtil.getMatcher("(?:<)((?:(?!\\1).)*>)", code); - while (matcher.find()) { - String temp = code.substring(0, matcher.start()); - String filler = StringUtil.repeat(" ", matcher.length()); - code = temp + filler + code.substring(matcher.end()); + char[] codeAsCharArray = code.toCharArray(); + int nestedGenericLevel = 0; + boolean isEscaped = false; + boolean isString = false; + boolean isCharEscaped = false; + + char lastChar = Character.MIN_VALUE; + + int beginReplacement = -1; + for (int i = 0; i < codeAsCharArray.length; i++) { + if (i > 0) lastChar = codeAsCharArray[i - 1]; + var currentChar = codeAsCharArray[i]; + if (isEscaped) { + isEscaped = false; + continue; + } + if (currentChar == '\\') { + isEscaped = true; + } else if (currentChar == '\"') { + isString = !isString; + } else { + if (isCharEscaped) { + if (currentChar == '\'') { + isCharEscaped = false; + } + continue; + } + if (isString) { + continue; + } + if (Character.isSpaceChar(currentChar)) continue; + + if (currentChar == '\'') { + isCharEscaped = true; + } else if (currentChar == '<') { + if (beginReplacement == -1) { + beginReplacement = i; + } + nestedGenericLevel++; + } else if (currentChar == '>') { + if (beginReplacement > -1) { + nestedGenericLevel--; + if (nestedGenericLevel == 0) { + Arrays.fill(codeAsCharArray, beginReplacement, i + 1, ' '); + beginReplacement = -1; + } + } + } else if (isNotAGenericDeclaration(currentChar, lastChar)) { + //we are not a generic type declaration + if (nestedGenericLevel > 0) { + beginReplacement = -1; + nestedGenericLevel = 0; + } + } + } } - return code; + if (nestedGenericLevel != 0) { + System.out.println(" -- nested generic level is not 0 at EOF: " + nestedGenericLevel); + } + return new String(codeAsCharArray); + } + + /** + * @param currentChar + * The current char being read + * @param lastChar + * The last char read + * @return {@code true} if we can be sure that we are not in a generic declaration; {@code false} otherwise + */ + private boolean isNotAGenericDeclaration(char currentChar, char lastChar) { + if (currentChar == '[' || currentChar == ']' || currentChar == ',') return false; + if (currentChar == '&') return lastChar == '&'; + if (currentChar == '|') return lastChar == '|'; + return ! Character.isJavaIdentifierPart((int)currentChar); } /** diff --git a/recaf-core/src/test/java/me/coley/recaf/parse/JavaParserGenericFilterTests.java b/recaf-core/src/test/java/me/coley/recaf/parse/JavaParserGenericFilterTests.java new file mode 100644 index 000000000..05949016c --- /dev/null +++ b/recaf-core/src/test/java/me/coley/recaf/parse/JavaParserGenericFilterTests.java @@ -0,0 +1,192 @@ +package me.coley.recaf.parse; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertTrue; +import static org.junit.jupiter.api.Assertions.fail; + +import java.lang.reflect.Method; +import me.coley.recaf.Controller; +import me.coley.recaf.presentation.EmptyPresentation; +import me.coley.recaf.workspace.Workspace; +import me.coley.recaf.workspace.resource.Resources; +import me.coley.recaf.workspace.resource.RuntimeResource; +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.parallel.Execution; +import org.junit.jupiter.api.parallel.ExecutionMode; + +/** + * Tests for {@link JavaParserHelper} + */ +@Execution(ExecutionMode.SAME_THREAD) +public class JavaParserGenericFilterTests { + + private static Controller controller; + private static Method filterGenerics; + + @BeforeAll + static void setup() { + Workspace workspace = new Workspace(new Resources(RuntimeResource.get())); + controller = new Controller(new EmptyPresentation()); + controller.setWorkspace(workspace); + try { + filterGenerics = JavaParserHelper.class.getDeclaredMethod("filterGenerics", String.class); + filterGenerics.setAccessible(true); + } catch (Exception e) { + fail("Failed to access private method 'filterGenerics'", e); + } + } + + private static String filterGeneric(String source) { + try { + return (String) filterGenerics.invoke(JavaParserHelper.create(controller), source); + } catch (Exception e) { + fail("Failed to invoke private method 'filterGenerics'", e); + return null; + } + } + + + @Test + void removeGenericInitialisation() { + String code = "class Clazz {\n" + + " public void test() {\n" + + " final Map content = new HashMap();\n" + + " }\n" + + "}"; + var res = filterGeneric(code); + assertEquals("class Clazz {\n" + + " public void test() {\n" + + " final Map content = new HashMap ();\n" + + " }\n" + + "}", res); + } + + + @Test + void removeGenericMethodDeclaration() { + String code = " | Foo> foo() {}"; + var res = filterGeneric(code); + assertEquals(" foo() {}", res); + } + + @Test + void removeGenericInitialisationNonAscii() { + String code = "class Clazz {\n" + + " public void test() {\n" + + " final バグ<カッコウ, カッコウ[]> カッコウ = new バグ<カッコウ, カッコウ[]>();\n" + + " }\n" + + "}"; + var res = filterGeneric(code); + assertEquals("class Clazz {\n" + + " public void test() {\n" + + " final バグ カッコウ = new バグ ();\n" + + " }\n" + + "}", res); + } + + @Test + void removeGenericShouldNotTouchComparison() { + String code = "class Clazz {\n" + + " public boolean test(int x) {\n" + + " int a = 0, b = 0;\n" + + " if (a < x && x > b) {\n" + + " return false;\n" + + " }\n" + + " return true;\n" + + " }\n" + + "}"; + var res = filterGeneric(code); + assertEquals(code, res); + } + + + @Test + void removeGenericShouldIgnoreChar() { + String code = "class Clazz {\n" + + " private static String trim(final String item) {\n" + + " return ('<' > 0) ? \"false\" : \"true\";\n" + + " }\n" + + "}"; + var res = filterGeneric(code); + assertEquals("class Clazz {\n" + + " private static String trim(final String item) {\n" + + " return ('<' > 0) ? \"false\" : \"true\";\n" + + " }\n" + + "}", res); + } + + @Test + void removeGenericShouldIgnoreString() { + String code = "class Clazz {\n" + + " private static String trim(final String item) {\n" + + " return (\"'<'\".length() > 0) ? \"false\" : \"true\";\n" + + " }\n" + + "}"; + var res = filterGeneric(code); + assertEquals("class Clazz {\n" + + " private static String trim(final String item) {\n" + + " return (\"'<'\".length() > 0) ? \"false\" : \"true\";\n" + + " }\n" + + "}", res); + } + + @Test + void removeGenericMultiline() { + String code = "public class FilePane extends BorderPane\n" + + "{\n" + + " private final TreeView tree;\n" + + " private Input input;\n" + + "\n" + + " public FilePane() {\n" + + " this.tree = new TreeView();\n" + + " Bus.subscribe(this);\n" + + " this.setCenter(this.tree);\n" + + " this.tree.setOnDragOver(this::lambda$new$0);\n" + + " this.tree.setOnMouseClicked(this::lambda$new$1);\n" + + " this.tree.setOnDragDropped(FilePane::lambda$new$2);\n" + + " this.tree.setShowRoot(false);\n" + + " this.tree.setCellFactory(this::lambda$new$3);\n" + + " Bus.subscribe(this);\n" + + " final TreeView tree = this.tree;\n" + + " Objects.requireNonNull(tree);\n" + + " Threads.runFx(tree::requestFocus);\n" + + " }\n" + + "}\n"; + var res = filterGeneric(code); + assertEquals("public class FilePane extends BorderPane\n" + + "{\n" + + " private final TreeView tree;\n" + + " private Input input;\n" + + "\n" + + " public FilePane() {\n" + + " this.tree = new TreeView ();\n" + + " Bus.subscribe(this);\n" + + " this.setCenter(this.tree);\n" + + " this.tree.setOnDragOver(this::lambda$new$0);\n" + + " this.tree.setOnMouseClicked(this::lambda$new$1);\n" + + " this.tree.setOnDragDropped(FilePane::lambda$new$2);\n" + + " this.tree.setShowRoot(false);\n" + + " this.tree.setCellFactory(this::lambda$new$3);\n" + + " Bus.subscribe(this);\n" + + " final TreeView tree = this.tree;\n" + + " Objects.requireNonNull(tree);\n" + + " Threads.runFx(tree::requestFocus);\n" + + " }\n" + + "}\n", res); + } + + @Test + void removeGenericClassDeclaration() { + String code = "public class FileTreeItem extends TreeItem implements Comparable {}"; + var res = filterGeneric(code); + assertEquals("public class FileTreeItem extends TreeItem implements Comparable {}", res); + } + + @Test + void removeGenericComplexClassDeclaration() { + String code = "public class FileTreeItem extends TreeItem implements Comparable {}"; + var res = filterGeneric(code); + assertEquals("public class FileTreeItem extends TreeItem implements Comparable {}", res); + } +}