| /******************************************************************************* |
| * 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.AbstractTagElement; |
| 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(AbstractTagElement node) { |
| registerCall(AbstractTagElement.class); |
| return false; |
| } |
| @SuppressWarnings("unused") // called reflectively |
| public void superVisit(AbstractTagElement node) { |
| super.visit(node); |
| } |
| @Override |
| public void endVisit(AbstractTagElement node) { |
| registerCall(AbstractTagElement.class); |
| } |
| @SuppressWarnings("unused") // called reflectively |
| public void superEndVisit(AbstractTagElement 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]); |
| } |
| } |