blob: cc63bec5a625b50d323097bec8de0944d6ba8fdc [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2000, 2021 IBM Corporation and others.
*
* This program and the accompanying materials
* are made available under the terms of the Eclipse Public License 2.0
* which accompanies this distribution, and is available at
* https://www.eclipse.org/legal/epl-2.0/
*
* SPDX-License-Identifier: EPL-2.0
*
* Contributors:
* IBM Corporation - initial API and implementation
*******************************************************************************/
package org.eclipse.jdt.ui.tests.core;
import static org.junit.Assert.assertEquals;
import static org.junit.Assert.assertNull;
import static org.junit.Assert.fail;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.HashSet;
import java.util.Iterator;
import java.util.Set;
import org.junit.Test;
import org.eclipse.core.runtime.Assert;
import org.eclipse.jdt.core.dom.ASTNode;
import org.eclipse.jdt.core.dom.ASTVisitor;
import org.eclipse.jdt.core.dom.AbstractTypeDeclaration;
import org.eclipse.jdt.core.dom.AnnotatableType;
import org.eclipse.jdt.core.dom.Annotation;
import org.eclipse.jdt.core.dom.BodyDeclaration;
import org.eclipse.jdt.core.dom.Comment;
import org.eclipse.jdt.core.dom.Expression;
import org.eclipse.jdt.core.dom.MethodReference;
import org.eclipse.jdt.core.dom.ModuleDirective;
import org.eclipse.jdt.core.dom.ModulePackageAccess;
import org.eclipse.jdt.core.dom.Name;
import org.eclipse.jdt.core.dom.Pattern;
import org.eclipse.jdt.core.dom.Statement;
import org.eclipse.jdt.core.dom.Type;
import org.eclipse.jdt.core.dom.VariableDeclaration;
import org.eclipse.jdt.internal.corext.dom.HierarchicalASTVisitor;
@SuppressWarnings("javadoc")
public class HierarchicalASTVisitorTest {
private static class TestHierarchicalASTVisitor extends HierarchicalASTVisitor {
//---- BEGIN <REGION TO BE UPDATED IN RESPONSE TO ASTNode HIERARCHY CHANGES> ---------------------
/* ******************************************************************************
* Whereas the other parts of this test should be relatively static,
* this portion of the file must be maintained in response to
* changes that occur in the ASTNode hierarchy (and, thus, the ASTVisitor).
* Such changes would include addition or removal of node types to or from
* the hierarchy, or changes in the superclass/subclass relationships
* among node classes. Such changes necessitate, also, changes in the
* HierarchicalASTVisitor itself, whose structure and behaviour this test
* verifies.
*
* The changes that must be made to this file in response to such changes in
* the ASTNode hierarchy are localized here and limited to maintenance of the
* following set of visit(XX node), superVisit(XX node), endVisit(XX node),
* and superEndVisit(XX node) implementations.
* There should be one such quadruple for each non-leaf ASTNode descendant class,
* including ASTNode itself.
*
* *****************************************************************************/
/* Here, for each non-leaf ASTNode descendant class, the visit(XX) method
* is overridden to call registerCall(XX.class), and a void superVisit
* (XX node) method is provided, which simply calls super.visit(XX).
* Accordingly for endVisit(XX) and superEndVisit(XX).
*/
@Override
public boolean visit(ASTNode node) {
registerCall(ASTNode.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(ASTNode node) {
super.visit(node);
}
@Override
public void endVisit(ASTNode node) {
registerCall(ASTNode.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(ASTNode node) {
super.visit(node);
}
@Override
public boolean visit(BodyDeclaration node) {
registerCall(BodyDeclaration.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(BodyDeclaration node) {
super.visit(node);
}
@Override
public void endVisit(BodyDeclaration node) {
registerCall(BodyDeclaration.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(BodyDeclaration node) {
super.visit(node);
}
@Override
public boolean visit(AbstractTypeDeclaration node) {
registerCall(AbstractTypeDeclaration.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(AbstractTypeDeclaration node) {
super.visit(node);
}
@Override
public void endVisit(AbstractTypeDeclaration node) {
registerCall(AbstractTypeDeclaration.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(AbstractTypeDeclaration node) {
super.visit(node);
}
@Override
public boolean visit(Comment node) {
registerCall(Comment.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(Comment node) {
super.visit(node);
}
@Override
public void endVisit(Comment node) {
registerCall(Comment.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(Comment node) {
super.visit(node);
}
@Override
public boolean visit(Expression node) {
registerCall(Expression.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(Expression node) {
super.visit(node);
}
@Override
public void endVisit(Expression node) {
registerCall(Expression.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(Expression node) {
super.visit(node);
}
@Override
public boolean visit(Annotation node) {
registerCall(Annotation.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(Annotation node) {
super.visit(node);
}
@Override
public void endVisit(Annotation node) {
registerCall(Annotation.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(Annotation node) {
super.visit(node);
}
@Override
public boolean visit(MethodReference node) {
registerCall(MethodReference.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(MethodReference node) {
super.visit(node);
}
@Override
public void endVisit(MethodReference node) {
registerCall(MethodReference.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(MethodReference node) {
super.visit(node);
}
@Override
public boolean visit(Name node) {
registerCall(Name.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(Name node) {
super.visit(node);
}
@Override
public void endVisit(Name node) {
registerCall(Name.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(Name node) {
super.visit(node);
}
@Override
public boolean visit(ModuleDirective node) {
registerCall(ModuleDirective.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(ModuleDirective node) {
super.visit(node);
}
@Override
public void endVisit(ModuleDirective node) {
registerCall(ModuleDirective.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(ModuleDirective node) {
super.visit(node);
}
@Override
public boolean visit(ModulePackageAccess node) {
registerCall(ModulePackageAccess.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(ModulePackageAccess node) {
super.visit(node);
}
@Override
public void endVisit(ModulePackageAccess node) {
registerCall(ModulePackageAccess.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(ModulePackageAccess node) {
super.visit(node);
}
@Override
public boolean visit(Pattern node) {
registerCall(Pattern.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(Pattern node) {
super.visit(node);
}
@Override
public void endVisit(Pattern node) {
registerCall(Pattern.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(Pattern node) {
super.visit(node);
}
@Override
public boolean visit(Statement node) {
registerCall(Statement.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(Statement node) {
super.visit(node);
}
@Override
public void endVisit(Statement node) {
registerCall(Statement.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(Statement node) {
super.visit(node);
}
@Override
public boolean visit(Type node) {
registerCall(Type.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(Type node) {
super.visit(node);
}
@Override
public void endVisit(Type node) {
registerCall(Type.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(Type node) {
super.visit(node);
}
@Override
public boolean visit(AnnotatableType node) {
registerCall(AnnotatableType.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(AnnotatableType node) {
super.visit(node);
}
@Override
public void endVisit(AnnotatableType node) {
registerCall(AnnotatableType.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(AnnotatableType node) {
super.visit(node);
}
@Override
public boolean visit(VariableDeclaration node) {
registerCall(VariableDeclaration.class);
return false;
}
@SuppressWarnings("unused") // called reflectively
public void superVisit(VariableDeclaration node) {
super.visit(node);
}
@Override
public void endVisit(VariableDeclaration node) {
registerCall(VariableDeclaration.class);
}
@SuppressWarnings("unused") // called reflectively
public void superEndVisit(VariableDeclaration node) {
super.visit(node);
}
//---- END <REGION TO BE UPDATED IN RESPONSE TO ASTNode HIERARCHY CHANGES> ----------------------
/**
* Verifies that the visit(XX) method in HierarchicalASTVisitor calls
* visit(YY), where XX is the name of <code>clazz</code> and YY is the
* name of the superclass of clazz.
*
* <code>clazz</code> must be a <b>proper</b> descendant of ASTNode (<code>clazz</code> is not ASTNode).
*/
private static void checkMethodCallsSuperclassMethod(Class<? extends ASTNode> clazz, boolean isLeaf, boolean isEndVisit) {
Assert.isTrue(ASTNode.class.isAssignableFrom(clazz));
Assert.isTrue(!ASTNode.class.equals(clazz));
TestHierarchicalASTVisitor visitor= new TestHierarchicalASTVisitor();
visitor._checkMethodCallsSuperclassMethod(clazz, isLeaf, isEndVisit);
}
/**
* This class must have certain methods corresponding to the
* ASTNode descendant class <code>clazz</code>.
* This method reflectively verifies that they are present.
*/
private static void checkRequiredMethodsForNonLeaf(Class<? extends ASTNode> clazz, boolean isEndVisit) {
Assert.isTrue(ASTNode.class.isAssignableFrom(clazz));
try {
TestHierarchicalASTVisitor.class.getDeclaredMethod(getVisitMethodName(isEndVisit), clazz);
} catch (NoSuchMethodException e) {
fail("Test must be updated since TestHierarchicalASTVisitor (declared within test class), is missing a method corresponding to non-leaf node class '" + getSimpleName(clazz) + "'");
}
try {
TestHierarchicalASTVisitor.class.getDeclaredMethod(getSuperVisitName(isEndVisit), clazz);
} catch (NoSuchMethodException e) {
fail("Test must be updated since TestHierarchicalASTVisitor (declared within test class), is missing a method corresponding to non-leaf node class '" + getSimpleName(clazz) + "'");
}
}
private Class<? extends ASTNode> fNodeClassForCalledMethod= null;
private void _checkMethodCallsSuperclassMethod(Class<? extends ASTNode> clazz, boolean isLeaf, boolean isEndVisit) {
/* Invoke a method which will result in the execution of
* HierarchicalASTVisitor's implementation of visit(XX), where
* XX is the name of clazz.
*
* If clazz is a leaf, we can invoke visit(XX) directly.
* Otherwise, we must invoke superVisit(XX), (in this class)
* which calls super.visit(XX), since visit(XX) is overridden in
* this class.
*
* The parameter passed to visit(XX) or superVisit(XX)
* is null. If there ever was any requirement that the
* parameter to visit(XX) methods, be non-null, we would simply have
* to reflectively instantiate an appropriately typed node.
*/
try {
Method method= TestHierarchicalASTVisitor.class.getMethod(isLeaf ? getVisitMethodName(isEndVisit) : "superVisit", clazz);
method.invoke(this, new Object[] { null });
} catch (NoSuchMethodException | IllegalAccessException | InvocationTargetException e) {
/* NoSuchMethodException should have already been discovered by
* hasRequiredMethodsForNonLeaf(..)
*/
e.printStackTrace();
Assert.isTrue(false);
}
/*
* Verify that the method invokation caused
* a call to visit(YY), where YY is the name of the superclass of
* clazz. (Also, verify that no other visit(ZZ) method was called).
*/
checkSuperclassMethodCalled(clazz, isEndVisit);
}
private void checkSuperclassMethodCalled(Class<? extends ASTNode> clazz, boolean isEndVisit) {
Assert.isNotNull(clazz.getSuperclass());
/*
* This class' implementations of the visit(YY) methods (for non-
* leaf YY) cause fNodeClassForCalledMethod to be set to YY.class.
* Such an implementation will be the one executed when a visit(XX)
* implementation in HierarchicalASTVisitor calls the visit(YY)
* method corresponding to XX's superclass, YY. We check here that
* fNodeClassForCalledMethod was set to the superclass of clazz.
*/
assertEquals(getSuperMethodNotCalledMessageFor(clazz, isEndVisit), clazz.getSuperclass(), fNodeClassForCalledMethod);
}
private String getSuperMethodNotCalledMessageFor(Class<? extends ASTNode> clazz, boolean isEndVisit) {
return getMethodSignatureFor(clazz, isEndVisit) + " in HierarchicalASTVisitor should call " + getMethodSignatureFor(clazz.getSuperclass(), isEndVisit) + ", the visitor method for its superclass.";
}
private void registerCall(Class<? extends ASTNode> nodeClassForMethod) {
assertNull("The invocation of a visit(XX) method in HierarchicalASTVisitor has caused " +
"more than one other visit(XX) method to be called. Every visit(XX) method in " +
"HierarchicalASTVisitor, except visit(ASTNode), should simply call visit(YY), " +
"where YY is the superclass of XX.", fNodeClassForCalledMethod);
fNodeClassForCalledMethod= nodeClassForMethod;
}
}
private Set<Class<? extends ASTNode>> fLeaves;
@Test
public void test() {
fLeaves= getLeafASTNodeDescendants();
Set<Class<? extends ASTNode>> allASTNodeDescendants= computeAllDescendantsFromLeaves(fLeaves.iterator(), ASTNode.class);
checkAllMethodsForHierarchyExist(allASTNodeDescendants.iterator(), false);
checkAllMethodsForHierarchyExist(allASTNodeDescendants.iterator(), true);
checkMethodsCallSuperclassMethod(allASTNodeDescendants.iterator(), false);
checkMethodsCallSuperclassMethod(allASTNodeDescendants.iterator(), true);
}
private boolean isLeaf(Class<? extends ASTNode> clazz) {
return fLeaves.contains(clazz);
}
/**
* For both HierarchicalASTVisitor and a subsequent part of this test to be correct,
* HierarchicalASTVisitor and TestHierarchicalASTVisitor must declare certain methods,
* each one corresponding to a class in the ASTNode hierarchy. Specifically,
* HierarchicalASTVisitor must declare a method corresponding to each class in the hierarchy,
* whereas TestHierarchicalASTVisitor must declare a pair of methods for each non-leaf
* class in the ASTNode hierarchy.
*
* This method verifies that these required methods exist, and suggests the updates
* that are needed to properly maintain the set of methods.
*/
private void checkAllMethodsForHierarchyExist(Iterator<Class<? extends ASTNode>> hierarchyClasses, boolean isEndVisit) {
while (hierarchyClasses.hasNext()) {
Class<? extends ASTNode> descendant= hierarchyClasses.next();
checkHierarchicalASTVisitorMethodExistsFor(descendant, isEndVisit);
if (!isLeaf(descendant))
TestHierarchicalASTVisitor.checkRequiredMethodsForNonLeaf(descendant, isEndVisit);
}
}
/**
* All visit(XX) implementations in HierarchicalASTVisitor, each
* corresponding to a class XX, must call visit(YY), where YY is the
* superclass of YY, unless XX is ASTNode. This method verifies this using
* reflection and a contrived subclass of HierarchicalASTVisitor,
* TestHierarchicalASTVisitor.
*/
private void checkMethodsCallSuperclassMethod(Iterator<Class<? extends ASTNode>> hierarchyClasses, boolean isEndVisit) {
while (hierarchyClasses.hasNext()) {
Class<? extends ASTNode> descendant= hierarchyClasses.next();
if (!ASTNode.class.equals(descendant))
TestHierarchicalASTVisitor.checkMethodCallsSuperclassMethod(descendant, isLeaf(descendant), isEndVisit);
}
}
private void checkHierarchicalASTVisitorMethodExistsFor(Class<? extends ASTNode> nodeClass, boolean isEndVisit) {
Assert.isTrue(ASTNode.class.isAssignableFrom(nodeClass));
try {
HierarchicalASTVisitor.class.getDeclaredMethod(getVisitMethodName(isEndVisit), nodeClass);
} catch (NoSuchMethodException e) {
String signature= getVisitMethodName(isEndVisit) + "(" + getSimpleName(nodeClass) + ")";
fail("HierarchicalASTVisitor must be updated to reflect a change in the ASTNode hierarchy. No method " + signature + " was found in HierarchicalASTVisitor.");
}
}
private static String getVisitMethodName(boolean isEndVisit) {
return isEndVisit ? "endVisit" : "visit";
}
private static String getSuperVisitName(boolean isEndVisit) {
return isEndVisit ? "superEndVisit" : "superVisit";
}
private static String getSimpleName(Class<?> clazz) {
String qualified= clazz.getName();
return qualified.substring(qualified.lastIndexOf('.') + 1);
}
private static String getMethodSignatureFor(Class<?> clazz, boolean isEndVisit) {
return getVisitMethodName(isEndVisit) + "(" + getSimpleName(clazz) + ")";
}
/**
* Finds the set of all descendants of <code>root</code> which are not proper descendants
* of a class in the sequence <code>leaves</code>. This will include <code>root</code>
* and all the elements of <code>leaves</code>.
*/
private static Set<Class<? extends ASTNode>> computeAllDescendantsFromLeaves(Iterator<Class<? extends ASTNode>> leaves, Class<? extends ASTNode> root) {
Set<Class<? extends ASTNode>> all= new HashSet<>();
while (leaves.hasNext()) {
Class<? extends ASTNode> leaf= leaves.next();
addAllAncestorsInclusive(leaf, root, all);
}
return all;
}
private static void addAllAncestorsInclusive(Class<? extends ASTNode> from, Class<? extends ASTNode> to, Set<Class<? extends ASTNode>> set) {
Assert.isTrue(to.isAssignableFrom(from));
Assert.isTrue(!from.isInterface());
Assert.isTrue(!to.isInterface());
Class<?> ancestor= from;
while (!ancestor.equals(to)) {
@SuppressWarnings("unchecked")
Class<? extends ASTNode> nodeType= (Class<? extends ASTNode>) ancestor;
set.add(nodeType);
ancestor= ancestor.getSuperclass();
if (ancestor == null) {
Assert.isTrue(false);
/* not expected, given assertions passed above */
}
}
set.add(to);
}
/**
* Returns all the leaf node classes (classes with no subclasses) in the
* ASTNode . Since every non-leaf ASTNode descendant (incl. ASTNode)
* is abstract, the set of leaf ASTNode descendants is the set of concrete
* ASTNode descendants.
*
* If ASTVisitor is maintained, this set will be the set of classes for which
* ASTVisitor has visit(..) methods. We use this property to compute the set,
* which means that we are as up-to-date as ASTVisitor (to be more
* "up-to-date" would be to require something that HierarchicalASTVisitor would,
* semantically, not be able to provide anyway!).
*/
private static Set<Class<? extends ASTNode>> getLeafASTNodeDescendants() {
Set<Class<? extends ASTNode>> result= new HashSet<>();
for (Method method : ASTVisitor.class.getMethods()) {
if (isVisitMethod(method)) {
@SuppressWarnings("unchecked")
Class<? extends ASTNode> nodeType= (Class<? extends ASTNode>) method.getParameterTypes()[0];
result.add(nodeType);
}
}
return result;
}
private static boolean isVisitMethod(Method method) {
if (!"visit".equals(method.getName()))
return false;
Class<?>[] parameters= method.getParameterTypes();
return parameters.length == 1 && ASTNode.class.isAssignableFrom(parameters[0]);
}
}