blob: f59e9c69c681613a63af1fc4fb7ae67f93509cdc [file] [log] [blame]
package org.checkerframework.framework.util;
import com.github.javaparser.JavaParser;
import com.github.javaparser.ParseProblemException;
import com.github.javaparser.ParseResult;
import com.github.javaparser.ParserConfiguration;
import com.github.javaparser.ast.CompilationUnit;
import com.github.javaparser.ast.Node;
import com.github.javaparser.ast.StubUnit;
import com.github.javaparser.ast.body.ClassOrInterfaceDeclaration;
import com.github.javaparser.ast.body.EnumDeclaration;
import com.github.javaparser.ast.body.TypeDeclaration;
import com.github.javaparser.ast.expr.AnnotationExpr;
import com.github.javaparser.ast.expr.BinaryExpr;
import com.github.javaparser.ast.expr.Expression;
import com.github.javaparser.ast.expr.StringLiteralExpr;
import com.github.javaparser.ast.visitor.VoidVisitorAdapter;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.InputStream;
import java.util.ArrayList;
import java.util.Optional;
import org.checkerframework.javacutil.BugInCF;
/**
* Utility methods for working with JavaParser. It is a replacement for StaticJavaParser that does
* not leak memory, and it provides some other methods.
*/
public class JavaParserUtil {
///
/// Replacements for StaticJavaParser
///
/**
* Parses the Java code contained in the {@code InputStream} and returns a {@code CompilationUnit}
* that represents it.
*
* <p>This is like {@code StaticJavaParser.parse}, but it does not lead to memory leaks because it
* creates a new instance of JavaParser each time it is invoked. Re-using {@code StaticJavaParser}
* causes memory problems because it retains too much memory.
*
* @param inputStream the Java source code
* @return CompilationUnit representing the Java source code
* @throws ParseProblemException if the source code has parser errors
*/
public static CompilationUnit parseCompilationUnit(InputStream inputStream) {
JavaParser javaParser = new JavaParser(new ParserConfiguration());
ParseResult<CompilationUnit> parseResult = javaParser.parse(inputStream);
if (parseResult.isSuccessful() && parseResult.getResult().isPresent()) {
return parseResult.getResult().get();
} else {
throw new ParseProblemException(parseResult.getProblems());
}
}
/**
* Parses the Java code contained in the {@code File} and returns a {@code CompilationUnit} that
* represents it.
*
* <p>This is like {@code StaticJavaParser.parse}, but it does not lead to memory leaks because it
* creates a new instance of JavaParser each time it is invoked. Re-using {@code StaticJavaParser}
* causes memory problems because it retains too much memory.
*
* @param file the Java source code
* @return CompilationUnit representing the Java source code
* @throws ParseProblemException if the source code has parser errors
* @throws FileNotFoundException if the file was not found
*/
public static CompilationUnit parseCompilationUnit(File file) throws FileNotFoundException {
JavaParser javaParser = new JavaParser(new ParserConfiguration());
ParseResult<CompilationUnit> parseResult = javaParser.parse(file);
if (parseResult.isSuccessful() && parseResult.getResult().isPresent()) {
return parseResult.getResult().get();
} else {
throw new ParseProblemException(parseResult.getProblems());
}
}
/**
* Parses the Java code contained in the {@code String} and returns a {@code CompilationUnit} that
* represents it.
*
* <p>This is like {@code StaticJavaParser.parse}, but it does not lead to memory leaks because it
* creates a new instance of JavaParser each time it is invoked. Re-using {@code StaticJavaParser}
* causes memory problems because it retains too much memory.
*
* @param javaSource the Java source code
* @return CompilationUnit representing the Java source code
* @throws ParseProblemException if the source code has parser errors
*/
public static CompilationUnit parseCompilationUnit(String javaSource) {
JavaParser javaParser = new JavaParser(new ParserConfiguration());
ParseResult<CompilationUnit> parseResult = javaParser.parse(javaSource);
if (parseResult.isSuccessful() && parseResult.getResult().isPresent()) {
return parseResult.getResult().get();
} else {
throw new ParseProblemException(parseResult.getProblems());
}
}
/**
* Parses the stub file contained in the {@code InputStream} and returns a {@code StubUnit} that
* represents it.
*
* <p>This is like {@code StaticJavaParser.parse}, but it does not lead to memory leaks because it
* creates a new instance of JavaParser each time it is invoked. Re-using {@code StaticJavaParser}
* causes memory problems because it retains too much memory.
*
* @param inputStream the stub file
* @return StubUnit representing the stub file
* @throws ParseProblemException if the source code has parser errors
*/
public static StubUnit parseStubUnit(InputStream inputStream) {
// The ParserConfiguration accumulates data each time parse is called, so create a new one each
// time. There's no method to set the ParserConfiguration used by a JavaParser, so a JavaParser
// has to be created each time.
ParserConfiguration configuration = new ParserConfiguration();
// Store the tokens so that errors have line and column numbers.
// configuration.setStoreTokens(false);
configuration.setLexicalPreservationEnabled(false);
configuration.setAttributeComments(false);
configuration.setDetectOriginalLineSeparator(false);
JavaParser javaParser = new JavaParser(configuration);
ParseResult<StubUnit> parseResult = javaParser.parseStubUnit(inputStream);
if (parseResult.isSuccessful() && parseResult.getResult().isPresent()) {
return parseResult.getResult().get();
} else {
throw new ParseProblemException(parseResult.getProblems());
}
}
/**
* Parses the {@code expression} and returns an {@code Expression} that represents it.
*
* <p>This is like {@code StaticJavaParser.parseExpression}, but it does not lead to memory leaks
* because it creates a new instance of JavaParser each time it is invoked. Re-using {@code
* StaticJavaParser} causes memory problems because it retains too much memory.
*
* @param expression the expression string
* @return the parsed expression
* @throws ParseProblemException if the expression has parser errors
*/
public static Expression parseExpression(String expression) {
// The ParserConfiguration accumulates data each time parse is called, so create a new one each
// time. There's no method to set the ParserConfiguration used by a JavaParser, so a JavaParser
// has to be created each time.
ParserConfiguration configuration = new ParserConfiguration();
configuration.setStoreTokens(false);
configuration.setLexicalPreservationEnabled(false);
configuration.setAttributeComments(false);
configuration.setDetectOriginalLineSeparator(false);
JavaParser javaParser = new JavaParser(configuration);
ParseResult<Expression> parseResult = javaParser.parseExpression(expression);
if (parseResult.isSuccessful() && parseResult.getResult().isPresent()) {
return parseResult.getResult().get();
} else {
throw new ParseProblemException(parseResult.getProblems());
}
}
///
/// Other methods
///
/**
* Given the compilation unit node for a source file, returns the top level type definition with
* the given name.
*
* @param root compilation unit to search
* @param name name of a top level type declaration in {@code root}
* @return a top level type declaration in {@code root} named {@code name}
*/
public static TypeDeclaration<?> getTypeDeclarationByName(CompilationUnit root, String name) {
Optional<ClassOrInterfaceDeclaration> classDecl = root.getClassByName(name);
if (classDecl.isPresent()) {
return classDecl.get();
}
Optional<ClassOrInterfaceDeclaration> interfaceDecl = root.getInterfaceByName(name);
if (interfaceDecl.isPresent()) {
return interfaceDecl.get();
}
Optional<EnumDeclaration> enumDecl = root.getEnumByName(name);
if (enumDecl.isPresent()) {
return enumDecl.get();
}
Optional<CompilationUnit.Storage> storage = root.getStorage();
if (storage.isPresent()) {
throw new BugInCF("Type " + name + " not found in " + storage.get().getPath());
} else {
throw new BugInCF("Type " + name + " not found in " + root);
}
}
/**
* Returns the fully qualified name of a type appearing in a given compilation unit.
*
* @param type a type declaration
* @param compilationUnit the compilation unit containing {@code type}
* @return the fully qualified name of {@code type} if {@code compilationUnit} contains a package
* declaration, or just the name of {@code type} otherwise
*/
public static String getFullyQualifiedName(
TypeDeclaration<?> type, CompilationUnit compilationUnit) {
if (compilationUnit.getPackageDeclaration().isPresent()) {
return compilationUnit.getPackageDeclaration().get().getNameAsString()
+ "."
+ type.getNameAsString();
} else {
return type.getNameAsString();
}
}
/**
* Side-effects {@code node} by removing all annotations from anywhere inside its subtree.
*
* @param node a JavaParser Node
*/
public static void clearAnnotations(Node node) {
node.accept(new ClearAnnotationsVisitor(), null);
}
/** A visitor that clears all annotations from a JavaParser AST. */
private static class ClearAnnotationsVisitor extends VoidVisitorWithDefaultAction {
@Override
public void defaultAction(Node node) {
for (Node child : new ArrayList<>(node.getChildNodes())) {
if (child instanceof AnnotationExpr) {
node.remove(child);
}
}
}
}
/**
* Side-effects node by combining any added String literals in node's subtree into their
* concatenation. For example, the expression {@code "a" + "b"} becomes {@code "ab"}. This occurs
* even if, when reading from left to right, the two string literals are not added directly. For
* example, the expression {@code 1 + "a" + "b"} parses as {@code (1 + "a") + "b"}}, but it is
* transformed into {@code 1 + "ab"}.
*
* <p>This is the same transformation performed by javac automatically. Javac seems to ignore
* string literals surrounded in parentheses, so this method does as well.
*
* @param node a JavaParser Node
*/
public static void concatenateAddedStringLiterals(Node node) {
node.accept(new StringLiteralConcatenateVisitor(), null);
}
/** Visitor that combines added String literals, see {@link #concatenateAddedStringLiterals}. */
public static class StringLiteralConcatenateVisitor extends VoidVisitorAdapter<Void> {
@Override
public void visit(BinaryExpr node, Void p) {
super.visit(node, p);
if (node.getOperator() == BinaryExpr.Operator.PLUS && node.getRight().isStringLiteralExpr()) {
String right = node.getRight().asStringLiteralExpr().asString();
if (node.getLeft().isStringLiteralExpr()) {
String left = node.getLeft().asStringLiteralExpr().asString();
node.replace(new StringLiteralExpr(left + right));
} else if (node.getLeft().isBinaryExpr()) {
BinaryExpr leftExpr = node.getLeft().asBinaryExpr();
if (leftExpr.getOperator() == BinaryExpr.Operator.PLUS
&& leftExpr.getRight().isStringLiteralExpr()) {
String left = leftExpr.getRight().asStringLiteralExpr().asString();
node.replace(
new BinaryExpr(
leftExpr.getLeft(),
new StringLiteralExpr(left + right),
BinaryExpr.Operator.PLUS));
}
}
}
}
}
}