Repository: SpoonLabs/gumtree-spoon-ast-diff
Pull request #198 (2021-09-22 08:55:57)

feat: introduce a collector node for thrown exceptions
The changes group exceptions after the `throws` keyword into one root node - `THROWN_TYPES`. This is inspired by the approach followed in https://github.com/SpoonLabs/gumtree-spoon-ast-diff/issues/181. Further, this root node has been wrapped in `CtVirtualElement` so that the problem analogous to https://github.com/SpoonLabs/gumtree-spoon-ast-diff/issues/192 can also be dealt with.

https://github.com/SpoonLabs/gumtree-spoon-ast-diff/issues/183: This node cannot be parsed using `TreeScanner` only because it deals with creating root nodes (wrapping child nodes in `CtVirtualElement`) which can only be done using `NodeCreator`.

algomaster99: @monperrus ping for reviews.
algomaster99: The tests failed. Let me check what went wrong.
algomaster99: @monperrus now you can drop your reviews.

Patch:
diff --git a/src/main/java/gumtree/spoon/builder/NodeCreator.java b/src/main/java/gumtree/spoon/builder/NodeCreator.java
index e7a770e..589ee33 100644
--- a/src/main/java/gumtree/spoon/builder/NodeCreator.java
+++ b/src/main/java/gumtree/spoon/builder/NodeCreator.java
@@ -135,13 +135,19 @@ public class NodeCreator extends CtInheritanceScanner {
 			builder.addSiblingNode(returnType);
 		}
 
-		for (CtTypeReference thrown : e.getThrownTypes()) {
-			ITree thrownType = builder.createNode("THROWS", thrown.getQualifiedName());
-			thrownType.setMetadata(SpoonGumTreeBuilder.SPOON_OBJECT, thrown);
-			type.putMetadata(SpoonGumTreeBuilder.GUMTREE_NODE, thrownType);
-			builder.addSiblingNode(thrownType);
+		if (!e.getThrownTypes().isEmpty()) {
+			ITree thrownTypeRoot = builder.createNode("THROWN_TYPES", "");
+			String virtualNodeDescription = "ThrownTypes_" + e.getSimpleName();
+			thrownTypeRoot.setMetadata(SpoonGumTreeBuilder.SPOON_OBJECT, new CtVirtualElement(virtualNodeDescription, e, e.getThrownTypes(), CtRole.THROWN));
+
+			for (CtTypeReference<? extends Throwable> thrownType : e.getThrownTypes()) {
+				ITree thrownNode = builder.createNode("THROWN", thrownType.getQualifiedName());
+				thrownNode.setMetadata(SpoonGumTreeBuilder.SPOON_OBJECT, thrownType);
+				thrownType.putMetadata(SpoonGumTreeBuilder.GUMTREE_NODE, thrownNode);
+				thrownTypeRoot.addChild(thrownNode);
+			}
+			builder.addSiblingNode(thrownTypeRoot);
 		}
-
 		super.visitCtMethod(e);
 	}
 }


Test patch:
diff --git a/src/test/java/gumtree/spoon/AstComparatorTest.java b/src/test/java/gumtree/spoon/AstComparatorTest.java
index 31a8081..b667e23 100644
--- a/src/test/java/gumtree/spoon/AstComparatorTest.java
+++ b/src/test/java/gumtree/spoon/AstComparatorTest.java
@@ -1526,7 +1526,7 @@ public class AstComparatorTest {
 		result.debugInformation();
 
 		assertEquals(1, actions.size());
-		assertTrue(result.containsOperation(OperationKind.Insert, "THROWS", "java.lang.Exception"));
+		assertTrue(result.containsOperation(OperationKind.Insert, "THROWN_TYPES"));
 	}
 
 	@Test
diff --git a/src/test/java/gumtree/spoon/diff/DiffTest.java b/src/test/java/gumtree/spoon/diff/DiffTest.java
index 0cf07cd..1a3df68 100644
--- a/src/test/java/gumtree/spoon/diff/DiffTest.java
+++ b/src/test/java/gumtree/spoon/diff/DiffTest.java
@@ -1,5 +1,6 @@
 package gumtree.spoon.diff;
 
+import static org.junit.Assert.assertNotNull;
 import static org.junit.Assert.assertArrayEquals;
 import static org.junit.Assert.assertEquals;
 import static org.junit.Assert.assertTrue;
@@ -301,4 +302,24 @@ public class DiffTest {
 		CtVirtualElement superInterfaceRoot = (CtVirtualElement) diff.getRootOperations().get(0).getSrcNode();
 		assertArrayEquals(new String[]{"A", "B"}, superInterfaceRoot.getChildren().stream().map(Object::toString).toArray());
 	}
+
+	@Test
+	public void test_diffOfThrownTypes_insertionOfRootNodeOfThrowable() throws Exception {
+		File left = new File("src/test/resources/examples/thrownTypes/left.java");
+		File right = new File("src/test/resources/examples/thrownTypes/right.java");
+
+		Diff diff = new AstComparator().compare(left, right);
+
+		// assert that only the root of throwables is inserted
+		assertEquals(1, diff.getRootOperations().size());
+		assertTrue(diff.containsOperation(OperationKind.Insert, "THROWN_TYPES"));
+
+		// verify children of the inserted root node
+		CtVirtualElement thrownTypeRoot = (CtVirtualElement) diff.getRootOperations().get(0).getSrcNode();
+		assertNotNull(thrownTypeRoot);
+		assertArrayEquals(new String[] {
+				"java.lang.ClassNotFoundException",
+				"java.lang.ClassCastException"
+		}, thrownTypeRoot.getChildren().stream().map(Object::toString).toArray());
+	}
 }
diff --git a/src/test/resources/examples/thrownTypes/left.java b/src/test/resources/examples/thrownTypes/left.java
new file mode 100644
index 0000000..efba197
--- /dev/null
+++ b/src/test/resources/examples/thrownTypes/left.java
@@ -0,0 +1,3 @@
+class ThrownType {
+    public static void main(String[] args) { }
+}
diff --git a/src/test/resources/examples/thrownTypes/right.java b/src/test/resources/examples/thrownTypes/right.java
new file mode 100644
index 0000000..e64e4b0
--- /dev/null
+++ b/src/test/resources/examples/thrownTypes/right.java
@@ -0,0 +1,3 @@
+class ThrownType {
+    public static void main(String[] args) throws ClassNotFoundException, ClassCastException { }
+}