Bug567801 : Fix generic signature resolution in evaluation

When resolving generic signatures of variables available for
 evaluation, it was not considered all types of generic signatures.
This cause some of the generic signatures to resolved into Object
type when they can be clearly resolved to the correct expected type.

The fix will consider all generic cases including bounded and
wild-card scenarios and resolve the correct types for variables.


Change-Id: Ic40cec57b81654eeb7024b5d8d27dd7a1957290f
Signed-off-by: Gayan Perera <gayanper@gmail.com>
diff --git a/org.eclipse.jdt.debug.tests/java7/Bug567801.java b/org.eclipse.jdt.debug.tests/java7/Bug567801.java
new file mode 100644
index 0000000..9d1d02f
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/java7/Bug567801.java
@@ -0,0 +1,17 @@
+import java.io.IOException;
+import java.sql.SQLException;
+
+public class Bug567801 {
+	public static void main(String[] args) {
+		try {
+			if(args.length == 0) {
+				throw new SQLException();
+			} else {
+				throw new IOException();
+			}
+		} catch(SQLException | IOException e) {
+			e.printStackTrace();
+		}
+	}
+
+}
\ No newline at end of file
diff --git a/org.eclipse.jdt.debug.tests/java8/Bug567801.java b/org.eclipse.jdt.debug.tests/java8/Bug567801.java
new file mode 100644
index 0000000..dd317d7
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/java8/Bug567801.java
@@ -0,0 +1,31 @@
+import java.io.Closeable;
+import java.io.IOException;
+import java.io.Serializable;
+import java.sql.SQLException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.List;
+import java.util.stream.IntStream;
+
+public class Bug567801 {
+	public static void main(String[] args) {
+		testBody();
+	}
+	private static <T extends Closeable & Serializable> void testBody() {
+		List<Integer> numbers = new ArrayList<>();
+		List<List<Integer>> listOfNumberList = new ArrayList<>();
+		List<? extends Number> extendsList = Arrays.asList(10,20);
+		List<? super Integer> superList = Arrays.asList(10,20);
+		List<?> wildList = Arrays.asList(10,20);
+		List<T> intersectionList = Collections.emptyList();
+		List<long[]> parrayList = Arrays.asList(new long[] {100L});
+		List<String[]> arrayList = Arrays.asList(new String[] {"100"}, new String[] {"200"});
+		IntStream stream = IntStream.of(10);
+
+		numbers.add(11);
+		listOfNumberList.add(numbers);
+		
+		System.out.println(numbers);
+	}
+}
\ No newline at end of file
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
index a6f1483..4f3e8fd 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AbstractDebugTest.java
@@ -433,6 +433,7 @@
 	        	jp = createProject(ONE_SEVEN_PROJECT_NAME, JavaProjectHelper.TEST_1_7_SRC_DIR.toString(), JavaProjectHelper.JAVA_SE_1_7_EE_NAME, false);
 	    		cfgs.add(createLaunchConfiguration(jp, LiteralTests17.LITERAL_TYPE_NAME));
 				cfgs.add(createLaunchConfiguration(jp, "ThreadNameChange"));
+				cfgs.add(createLaunchConfiguration(jp, "Bug567801"));
 	    		loaded17 = true;
 	    		waitForBuild();
 	        }
@@ -484,6 +485,7 @@
 				cfgs.add(createLaunchConfiguration(jp, "AnonymousEvaluator"));
 				cfgs.add(createLaunchConfiguration(jp, "Bug564486"));
 				cfgs.add(createLaunchConfiguration(jp, "Bug564801"));
+				cfgs.add(createLaunchConfiguration(jp, "Bug567801"));
 	    		loaded18 = true;
 	    		waitForBuild();
 	        }
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
index dde4e60..2334837 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/AutomatedSuite.java
@@ -92,6 +92,7 @@
 import org.eclipse.jdt.debug.tests.core.WorkspaceSourceContainerTests;
 import org.eclipse.jdt.debug.tests.eval.GeneralEvalTests;
 import org.eclipse.jdt.debug.tests.eval.GenericsEvalTests;
+import org.eclipse.jdt.debug.tests.eval.LambdaVariableTest;
 import org.eclipse.jdt.debug.tests.eval.SyntheticVariableTests;
 import org.eclipse.jdt.debug.tests.launching.ClasspathShortenerTests;
 import org.eclipse.jdt.debug.tests.launching.ConfigurationEncodingTests;
@@ -327,6 +328,9 @@
 
 	//add the complete eval suite
 		addTest(new TestSuite(GeneralEvalTests.class));
+		if (JavaProjectHelper.isJava8Compatible()) {
+			addTest(new TestSuite(LambdaVariableTest.class));
+		}
 		//addTest(EvalTestSuite.suite());
 
 		// long classpath tests
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/GenericsEval17Test.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/GenericsEval17Test.java
new file mode 100644
index 0000000..041a240
--- /dev/null
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/GenericsEval17Test.java
@@ -0,0 +1,57 @@
+/*******************************************************************************
+ * Copyright (c) 2020 Gayan Perera 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:
+ *     Gayan Perera - initial API and implementation
+ *******************************************************************************/
+package org.eclipse.jdt.debug.tests.eval;
+
+import org.eclipse.debug.core.model.IValue;
+import org.eclipse.jdt.core.IJavaProject;
+import org.eclipse.jdt.debug.core.IJavaThread;
+import org.eclipse.jdt.debug.tests.AbstractDebugTest;
+
+public class GenericsEval17Test extends AbstractDebugTest {
+	private IJavaThread javaThread;
+
+	@Override
+	protected IJavaProject getProjectContext() {
+		return get17Project();
+	}
+
+	public GenericsEval17Test(String name) {
+		super(name);
+	}
+
+	public void testEvaluate_Bug567801_UnionType_ExpectValueType() throws Exception {
+		debugWithBreakpoint("Bug567801", 13);
+		String snippet = "e instanceof java.lang.Exception";
+		IValue value = doEval(javaThread, snippet);
+
+		assertNotNull("value is null", value);
+		assertEquals("value is not true", "true", value.getValueString());
+	}
+
+	private void debugWithBreakpoint(String testClass, int lineNumber) throws Exception {
+		createLineBreakpoint(lineNumber, testClass);
+		javaThread = launchToBreakpoint(testClass);
+		assertNotNull("The program did not suspend", javaThread);
+	}
+
+	@Override
+	protected void tearDown() throws Exception {
+		try {
+			terminateAndRemove(javaThread);
+		} finally {
+			super.tearDown();
+			removeAllBreakpoints();
+		}
+	}
+}
diff --git a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java
index 4ff72ea..53b5be3 100644
--- a/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java
+++ b/org.eclipse.jdt.debug.tests/tests/org/eclipse/jdt/debug/tests/eval/LambdaVariableTest.java
@@ -64,6 +64,87 @@
 		assertEquals("Actual value is not false", "false", value.toString());
 	}
 
+	public void testEvaluate_Bug567801_VariableWithTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "numbers.stream().anyMatch(a -> a >= 10)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_VariableWithNestedTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "listOfNumberList.stream().filter(l -> l.size() > 0).flatMap(l -> l.stream()).anyMatch(a -> a >= 10)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_VariableWithUpperBoundTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "extendsList.stream().anyMatch(a -> a.intValue() >= 10)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_VariableWithLowerBoundTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "superList.stream().anyMatch(a -> ((Integer)a).intValue() >= 10)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_VariableWithWildCardTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "wildList.stream().anyMatch(a -> ((Integer)a).intValue() >= 10)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_VariableWithIntersectionTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "intersectionList.stream().anyMatch(a -> a instanceof java.io.Closeable)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not false", "false", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_VariableWithPrimitiveArrayTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "parrayList.stream().anyMatch(a -> a.length > 0)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_VariableWithArrayTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "arrayList.stream().anyMatch(a -> a.length > 0)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
+	public void testEvaluate_Bug567801_PrimitiveTypeArgument_MustEvaluationWithCorrectType() throws Exception {
+		debugWithBreakpoint("Bug567801", 29);
+		String snippet = "stream.anyMatch(a -> a > 0)";
+		IValue value = doEval(javaThread, snippet);
+
+		assertEquals("wrong type : ", "boolean", value.getReferenceTypeName());
+		assertEquals("Actual value is not true", "true", value.getValueString());
+	}
+
 	private void debugWithBreakpoint(String testClass, int lineNumber) throws Exception {
 		createLineBreakpoint(lineNumber, testClass);
 		javaThread = launchToBreakpoint(testClass);
diff --git a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/RemoteEvaluatorBuilder.java b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/RemoteEvaluatorBuilder.java
index 0e4531e..149ff12 100644
--- a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/RemoteEvaluatorBuilder.java
+++ b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/RemoteEvaluatorBuilder.java
@@ -249,6 +249,18 @@
 			return localBindings.containsKey(binding);
 		}
 
+		private boolean isParentInLocalBinding(ASTNode parent) {
+			if (parent instanceof Name) {
+				// this will avoid unwanted upward traversals
+				if (isLocalBinding(((Name) parent).resolveBinding())) {
+					return true;
+				}
+				// traverse upstream to see if a parent is already handled
+				return isParentInLocalBinding(parent.getParent());
+			}
+			return false;
+		}
+
 		void addLocalBinding(IBinding binding, String name) {
 			localBindings.put(binding, name);
 		}
@@ -1494,7 +1506,10 @@
 		@Override
 		public boolean visit(SimpleName node) {
 			IBinding binding = node.resolveBinding();
-			if (!isLocalBinding(binding)) {
+			// when having code like arr.length the length is identified as a field variable. But since the arr is
+			// already pushed as a variable we don't need to handle length here. So if we have chained field access like
+			// obj.f1.f2 we will only push the obj as a variable.
+			if (!isLocalBinding(binding) && isParentInLocalBinding(node.getParent())) {
 				if (binding instanceof IVariableBinding) {
 					IVariableBinding vb = ((IVariableBinding) binding);
 					// For future optimization: Check for duplicates, so same value is only bound once
diff --git a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java
index b5be5c5..d9dc4ae 100644
--- a/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java
+++ b/org.eclipse.jdt.debug/eval/org/eclipse/jdt/internal/debug/eval/ast/engine/ASTEvaluationEngine.java
@@ -42,6 +42,7 @@
 import org.eclipse.debug.core.model.IVariable;
 import org.eclipse.jdt.core.IJavaProject;
 import org.eclipse.jdt.core.Signature;
+import org.eclipse.jdt.core.compiler.CharOperation;
 import org.eclipse.jdt.core.compiler.IProblem;
 import org.eclipse.jdt.core.dom.AST;
 import org.eclipse.jdt.core.dom.ASTParser;
@@ -80,6 +81,7 @@
 public class ASTEvaluationEngine implements IAstEvaluationEngine {
 	public static final String ANONYMOUS_VAR_PREFIX = "val$"; //$NON-NLS-1$
 	private static final int EVALUATION_DETAIL_BITMASK = DebugEvent.EVALUATION | DebugEvent.EVALUATION_IMPLICIT;
+	private static final String QN_OBJECT = "java.lang.Object"; //$NON-NLS-1$
 	private IJavaProject fProject;
 
 	private IJavaDebugTarget fDebugTarget;
@@ -403,19 +405,63 @@
 	}
 
 	private String getFixedUnresolvableGenericTypes(IJavaVariable variable) throws DebugException {
+		StringBuilder fixedSignature = new StringBuilder();
+		scanAndFixSignature(variable.getGenericSignature(), Signature.toString(variable.getSignature()), fixedSignature);
+		return fixedSignature.toString();
+	}
+
+	private void scanAndFixSignature(String genericSignature, String erasureSignature, StringBuilder fixedSignature) {
 		/*
 		 * This actually fix variables which are type of Generic Types which cannot be resolved to a type in the current content. For example variable
 		 * type like P_OUT in java.util.stream.ReferencePipeline.filter(Predicate<? super P_OUT>)
+		 *
+		 * and also generic signature such as Ljava/util/function/Predicate<+Ljava/util/List<Ljava/lang/Integer;>;>; Ljava/util/Comparator<-TT;>;
+		 * which will fail the properly resolved to the type.
 		 */
-
-		final String genericSignature = variable.getGenericSignature();
-		final String fqn = Signature.toString(genericSignature).replace('/', '.');
-		if (genericSignature.startsWith(String.valueOf(Signature.C_TYPE_VARIABLE))
-				|| Signature.getTypeArguments(genericSignature).length > 0) {
-			// resolve to the signature of the variable.
-			return Signature.toString(variable.getSignature()).replace('/', '.');
+		if (genericSignature.startsWith(String.valueOf(Signature.C_TYPE_VARIABLE)) ||
+				genericSignature.startsWith(String.valueOf(Signature.C_CAPTURE)) ||
+				genericSignature.startsWith(String.valueOf(Signature.C_STAR)) ||
+				genericSignature.startsWith(String.valueOf(Signature.C_SUPER)))
+		{
+			fixedSignature.append(toDotQualified(erasureSignature));
+			return;
 		}
-		return fqn;
+
+		if(genericSignature.startsWith(String.valueOf(Signature.C_EXTENDS))) {
+			fixedSignature.append(toDotQualified(Signature.toString(getUpperBoundTypeSignature(genericSignature))));
+			return;
+		}
+
+		// we have a proper type which might be parameterized so extract the type FQN
+		fixedSignature.append(toDotQualified(Signature.toString(Signature.getTypeErasure(genericSignature))));
+
+		String[] typeArguments = Signature.getTypeArguments(genericSignature);
+		if (typeArguments.length > 0) {
+			fixedSignature.append(Signature.C_GENERIC_START);
+			for (int i = 0; i < typeArguments.length; i++) {
+				if (i > 0) {
+					fixedSignature.append(',');
+				}
+				scanAndFixSignature(typeArguments[i], QN_OBJECT, fixedSignature);
+			}
+			fixedSignature.append(Signature.C_GENERIC_END);
+		}
+	}
+
+	private String toDotQualified(String fqn) {
+		return fqn.replace('/', '.');
+	}
+
+	private String getUpperBoundTypeSignature(String typeParamaterSignature) {
+		// +Ljava/util/List<Ljava/lang/Integer;>;
+		return String.valueOf(getBoudTypeParameterSignature(typeParamaterSignature.toCharArray(), Signature.C_EXTENDS));
+	}
+
+	private char[] getBoudTypeParameterSignature(char[] typeParamaterSignature, char boundType) {
+		if (typeParamaterSignature.length < 2 || typeParamaterSignature[0] != boundType) {
+			throw new IllegalArgumentException(Signature.toString(String.valueOf(typeParamaterSignature)));
+		}
+		return CharOperation.subarray(typeParamaterSignature, 1, typeParamaterSignature.length);
 	}
 
 	private CompilationUnit parseCompilationUnit(char[] source,
@@ -481,11 +527,11 @@
 			// Arrays with a base component type of a class or interface are
 			// treated
 			// as Object arrays and evaluated in Object.
-			String recTypeName = "java.lang.Object"; //$NON-NLS-1$
+			String recTypeName = QN_OBJECT;
 			String typeName = arrayType.getName();
 			if (componentType instanceof IJavaReferenceType) {
 				StringBuilder buf = new StringBuilder();
-				buf.append("java.lang.Object"); //$NON-NLS-1$
+				buf.append(QN_OBJECT);
 				for (int i = 0; i < dimension; i++) {
 					buf.append("[]"); //$NON-NLS-1$
 				}