Skip to content

Commit

Permalink
fix: parser removing more than generics
Browse files Browse the repository at this point in the history
  • Loading branch information
Jydett committed Nov 30, 2023
1 parent 0c37490 commit a516a06
Show file tree
Hide file tree
Showing 2 changed files with 283 additions and 12 deletions.
103 changes: 91 additions & 12 deletions recaf-core/src/main/java/me/coley/recaf/parse/JavaParserHelper.java
Original file line number Diff line number Diff line change
@@ -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;
Expand All @@ -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));
}

Expand Down Expand Up @@ -82,12 +86,20 @@ public ParseResult<CompilationUnit> 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.
Expand All @@ -104,14 +116,15 @@ public ParseResult<CompilationUnit> 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).
* <br>
* Examples of valid matches:
* <pre>
* <T>
* <List<Set<String>>>
* <T extends EntityLivingBase<X> | Foo>
* </pre>
* 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.
*
Expand All @@ -121,14 +134,80 @@ public ParseResult<CompilationUnit> 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);
}

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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<String, byte[]> content = new HashMap<String, byte[]>();\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 = "<T extends EntityLivingBase<X> | 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<String> tree;\n"
+ " private Input input;\n"
+ "\n"
+ " public FilePane() {\n"
+ " this.tree = new TreeView<String>();\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<String> 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<String> implements Comparable<String> {}";
var res = filterGeneric(code);
assertEquals("public class FileTreeItem extends TreeItem implements Comparable {}", res);
}

@Test
void removeGenericComplexClassDeclaration() {
String code = "public class FileTreeItem<T super X> extends TreeItem<T> implements Comparable<T> {}";
var res = filterGeneric(code);
assertEquals("public class FileTreeItem extends TreeItem implements Comparable {}", res);
}
}

0 comments on commit a516a06

Please sign in to comment.