blob: a8c271495e7969177b54f925413659d1ef963f0b [file] [log] [blame]
package org.checkerframework.framework.ajava;
import com.sun.source.tree.AnnotatedTypeTree;
import com.sun.source.tree.AnnotationTree;
import com.sun.source.tree.ClassTree;
import com.sun.source.tree.DoWhileLoopTree;
import com.sun.source.tree.ExpressionStatementTree;
import com.sun.source.tree.ExpressionTree;
import com.sun.source.tree.ForLoopTree;
import com.sun.source.tree.IdentifierTree;
import com.sun.source.tree.IfTree;
import com.sun.source.tree.ImportTree;
import com.sun.source.tree.LambdaExpressionTree;
import com.sun.source.tree.MemberSelectTree;
import com.sun.source.tree.MethodInvocationTree;
import com.sun.source.tree.MethodTree;
import com.sun.source.tree.ModifiersTree;
import com.sun.source.tree.NewArrayTree;
import com.sun.source.tree.NewClassTree;
import com.sun.source.tree.StatementTree;
import com.sun.source.tree.SwitchTree;
import com.sun.source.tree.SynchronizedTree;
import com.sun.source.tree.Tree;
import com.sun.source.tree.Tree.Kind;
import com.sun.source.tree.VariableTree;
import com.sun.source.tree.WhileLoopTree;
import com.sun.tools.javac.tree.JCTree.JCExpression;
import com.sun.tools.javac.util.Position;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import org.checkerframework.checker.nullness.qual.Nullable;
/**
* After this visitor visits a tree, {@link #getTrees} returns all the trees that should match with
* some JavaParser node. Some trees shouldn't be matched with a JavaParser node because there isn't
* a corresponding JavaParser node. These trees are excluded.
*
* <p>The primary purpose is to test the {@link JointJavacJavaParserVisitor} class when the
* -AajavaChecks flag is used. That class traverses a javac tree and JavaParser AST simultaneously,
* so the trees this class stores can be used to test if the entirety of the javac tree was visited.
*/
public class ExpectedTreesVisitor extends TreeScannerWithDefaults {
/** The set of trees that should be matched to a JavaParser node when visiting both. */
private Set<Tree> trees = new HashSet<>();
/**
* Returns the visited trees that should match to some JavaParser node.
*
* @return the visited trees that should match to some JavaParser node
*/
public Set<Tree> getTrees() {
return trees;
}
/**
* Records that {@code tree} should have a corresponding JavaParser node.
*
* @param tree the tree to record
*/
@Override
public void defaultAction(Tree tree) {
trees.add(tree);
}
@Override
public Void visitAnnotation(AnnotationTree tree, Void p) {
// Skip annotations because ajava files are not required to have the same annotations as
// their corresponding java files.
return null;
}
@Override
public Void visitClass(ClassTree tree, Void p) {
defaultAction(tree);
visit(tree.getModifiers(), p);
visit(tree.getTypeParameters(), p);
visit(tree.getExtendsClause(), p);
visit(tree.getImplementsClause(), p);
if (tree.getKind() == Kind.ENUM) {
// Enum constants expand to a VariableTree like
// public static final MY_ENUM_CONSTANT = new MyEnum(args ...)
// The constructor invocation in the initializer has no corresponding JavaParser node,
// so this removes those invocations. This doesn't remove any trees that should be
// matched to a JavaParser node, because it's illegal to explicitly construct an
// instance of an enum.
for (Tree member : tree.getMembers()) {
member.accept(this, p);
if (member.getKind() != Kind.VARIABLE) {
continue;
}
VariableTree variable = (VariableTree) member;
ExpressionTree initializer = variable.getInitializer();
if (initializer == null || initializer.getKind() != Kind.NEW_CLASS) {
continue;
}
NewClassTree constructor = (NewClassTree) initializer;
if (constructor.getIdentifier().getKind() != Kind.IDENTIFIER) {
continue;
}
IdentifierTree name = (IdentifierTree) constructor.getIdentifier();
if (name.getName().contentEquals(tree.getSimpleName())) {
trees.remove(variable.getType());
trees.remove(constructor);
trees.remove(constructor.getIdentifier());
}
}
} else {
visit(tree.getMembers(), p);
}
return null;
}
@Override
public Void visitExpressionStatement(ExpressionStatementTree tree, Void p) {
// Javac inserts calls to super() at the start of constructors with no this or super call.
// These don't have matching JavaParser nodes.
if (JointJavacJavaParserVisitor.isDefaultSuperConstructorCall(tree)) {
return null;
}
// Whereas synthetic constructors should be skipped, regular super() and this() should still
// be added. JavaParser has no expression statement surrounding these, so remove the
// expression statement itself.
Void result = super.visitExpressionStatement(tree, p);
if (tree.getExpression().getKind() == Kind.METHOD_INVOCATION) {
MethodInvocationTree invocation = (MethodInvocationTree) tree.getExpression();
if (invocation.getMethodSelect().getKind() == Kind.IDENTIFIER) {
IdentifierTree identifier = (IdentifierTree) invocation.getMethodSelect();
if (identifier.getName().contentEquals("this")
|| identifier.getName().contentEquals("super")) {
trees.remove(tree);
trees.remove(identifier);
}
}
}
return result;
}
@Override
public Void visitForLoop(ForLoopTree tree, Void p) {
// Javac nests a for loop's updates in expression statements but JavaParser stores the
// statements directly, so remove the expression statements.
Void result = super.visitForLoop(tree, p);
for (StatementTree initializer : tree.getInitializer()) {
trees.remove(initializer);
}
for (ExpressionStatementTree update : tree.getUpdate()) {
trees.remove(update);
}
return result;
}
@Override
public Void visitSwitch(SwitchTree tree, Void p) {
super.visitSwitch(tree, p);
// javac surrounds switch expression in a ParenthesizedTree but JavaParser does not.
trees.remove(tree.getExpression());
return null;
}
@Override
public Void visitSynchronized(SynchronizedTree tree, Void p) {
super.visitSynchronized(tree, p);
// javac surrounds synchronized expressions in a ParenthesizedTree but JavaParser does not.
trees.remove(tree.getExpression());
return null;
}
@Override
public Void visitIf(IfTree tree, Void p) {
// In an if statement, javac stores the condition as a parenthesized expression, which has no
// corresponding JavaParserNode, so remove the parenthesized expression, but not its child.
Void result = super.visitIf(tree, p);
trees.remove(tree.getCondition());
return result;
}
@Override
public Void visitImport(ImportTree tree, Void p) {
// Javac stores an import like a.* as a member select, but JavaParser just stores "a", so
// don't add the member select in that case.
if (tree.getQualifiedIdentifier().getKind() == Kind.MEMBER_SELECT) {
MemberSelectTree memberSelect = (MemberSelectTree) tree.getQualifiedIdentifier();
if (memberSelect.getIdentifier().contentEquals("*")) {
memberSelect.getExpression().accept(this, p);
return null;
}
}
return super.visitImport(tree, p);
}
@Override
public Void visitMethod(MethodTree tree, Void p) {
// Synthetic default constructors don't have matching JavaParser nodes. Conservatively skip
// nullary (no-argument) constructor calls, even if they may not be synthetic.
if (JointJavacJavaParserVisitor.isNoArgumentConstructor(tree)) {
return null;
}
Void result = super.visitMethod(tree, p);
// A varargs parameter like String... is converted to String[], where the array type doesn't
// have a corresponding JavaParser node. Conservatively skip the array type (but not the
// component type) if it's the last argument.
if (!tree.getParameters().isEmpty()) {
VariableTree last = tree.getParameters().get(tree.getParameters().size() - 1);
if (last.getType().getKind() == Kind.ARRAY_TYPE) {
trees.remove(last.getType());
}
if (last.getType().getKind() == Kind.ANNOTATED_TYPE) {
AnnotatedTypeTree annotatedType = (AnnotatedTypeTree) last.getType();
if (annotatedType.getUnderlyingType().getKind() == Kind.ARRAY_TYPE) {
trees.remove(annotatedType);
trees.remove(annotatedType.getUnderlyingType());
}
}
}
return result;
}
@Override
public Void visitMethodInvocation(MethodInvocationTree tree, Void p) {
Void result = super.visitMethodInvocation(tree, p);
// In a method invocation like myObject.myMethod(), the method invocation stores
// myObject.myMethod as its own MemberSelectTree which has no corresponding JavaParserNode.
if (tree.getMethodSelect().getKind() == Kind.MEMBER_SELECT) {
trees.remove(tree.getMethodSelect());
}
return result;
}
@Override
public Void visitModifiers(ModifiersTree tree, Void p) {
// Don't add ModifierTrees or children because they have no corresponding JavaParser node.
return null;
}
@Override
public Void visitNewArray(NewArrayTree tree, Void p) {
// Skip array initialization because it's not implemented yet.
return null;
}
@Override
public Void visitNewClass(NewClassTree tree, Void p) {
defaultAction(tree);
if (tree.getEnclosingExpression() != null) {
tree.getEnclosingExpression().accept(this, p);
}
tree.getIdentifier().accept(this, p);
for (Tree typeArgument : tree.getTypeArguments()) {
typeArgument.accept(this, p);
}
for (Tree arg : tree.getTypeArguments()) {
arg.accept(this, p);
}
if (tree.getClassBody() == null) {
return null;
}
// Anonymous class bodies require special handling. There isn't a corresponding JavaParser
// node, and synthetic constructors must be skipped.
ClassTree body = tree.getClassBody();
visit(body.getModifiers(), p);
visit(body.getTypeParameters(), p);
visit(body.getImplementsClause(), p);
for (Tree member : body.getMembers()) {
// Constructors cannot be declared in an anonymous class, so don't add them.
if (member.getKind() == Kind.METHOD) {
MethodTree methodTree = (MethodTree) member;
if (methodTree.getName().contentEquals("<init>")) {
continue;
}
}
member.accept(this, p);
}
return null;
}
@Override
public Void visitLambdaExpression(LambdaExpressionTree tree, Void p) {
for (VariableTree parameter : tree.getParameters()) {
// Programmers may omit parameter types for lambda expressions. When not specified,
// javac infers them but JavaParser uses UnknownType. Conservatively, don't add
// parameter types for lambda expressions.
visit(parameter.getModifiers(), p);
visit(parameter.getNameExpression(), p);
assert parameter.getInitializer() == null;
}
visit(tree.getBody(), p);
return null;
}
@Override
public Void visitWhileLoop(WhileLoopTree tree, Void p) {
super.visitWhileLoop(tree, p);
// javac surrounds while loop conditions in a ParenthesizedTree but JavaParser does not.
trees.remove(tree.getCondition());
return null;
}
@Override
public Void visitDoWhileLoop(DoWhileLoopTree tree, Void p) {
super.visitDoWhileLoop(tree, p);
// javac surrounds while loop conditions in a ParenthesizedTree but JavaParser does not.
trees.remove(tree.getCondition());
return null;
}
@Override
public Void visitVariable(VariableTree tree, Void p) {
// Javac expands the keyword "var" in a variable declaration to its inferred type.
// JavaParser has a special "var" construct, so they won't match. If a javac type was generated
// this way, then it won't have a position in source code so in that case we don't add it.
if (((JCExpression) tree.getType()).pos == Position.NOPOS) {
return null;
}
return super.visitVariable(tree, p);
}
/**
* Calls the correct visit method for {@code tree} if {@code tree} is non-null.
*
* @param tree the tree to visit
* @param p secondary parameter to the visitor
*/
private void visit(@Nullable Tree tree, Void p) {
if (tree != null) {
tree.accept(this, p);
}
}
/**
* If {@code trees} is non-null, visits each non-null tree in {@code trees} in order.
*
* @param trees the list of trees to visit
* @param p secondary parameter to the visitor
*/
private void visit(@Nullable List<? extends @Nullable Tree> trees, Void p) {
if (trees == null) {
return;
}
for (Tree tree : trees) {
visit(tree, p);
}
}
}