/* * SonarQube Java * Copyright (C) 2012-2016 SonarSource SA * mailto:contact AT sonarsource DOT com * * This program is free software; you can redistribute it and/or * modify it under the terms of the GNU Lesser General Public * License as published by the Free Software Foundation; either * version 3 of the License, or (at your option) any later version. * * This program is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU * Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with this program; if not, write to the Free Software Foundation, * Inc., 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301, USA. */ package org.sonar.java.resolve; import com.google.common.collect.Lists; import com.google.common.collect.Sets; import org.assertj.core.api.Fail; import org.junit.Before; import org.junit.Test; import org.sonar.java.ast.parser.JavaParser; import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.ClassTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; import org.sonar.plugins.java.api.tree.Tree; import org.sonar.plugins.java.api.tree.VariableTree; import java.io.File; import java.nio.charset.StandardCharsets; import java.util.Arrays; import java.util.HashSet; import java.util.List; import java.util.Set; import static org.assertj.core.api.Assertions.assertThat; public class LeastUpperBoundTest { private LeastUpperBound leastUpperBound; private JavaType intType; private JavaType longType; @Before public void setUp() { ParametrizedTypeCache parametrizedTypeCache = new ParametrizedTypeCache(); Symbols symbols = new Symbols(new BytecodeCompleter(Lists.<File>newArrayList(), parametrizedTypeCache)); TypeSubstitutionSolver typeSubstitutionSolver = new TypeSubstitutionSolver(parametrizedTypeCache, symbols); intType = symbols.intType; longType = symbols.longType; leastUpperBound = new LeastUpperBound(typeSubstitutionSolver, parametrizedTypeCache, symbols); } private JavaType leastUpperBound(Type... types) { return (JavaType) leastUpperBound.leastUpperBound(Sets.newLinkedHashSet(Lists.newArrayList(types))); } @Test public void lub_of_one_element_is_itself() { CompilationUnitTree cut = treeOf("class A<T> { A<String> var; }"); ClassTree classA = (ClassTree) cut.types().get(0); Type varType = ((VariableTree) classA.members().get(0)).type().symbolType(); Type a = classA.symbol().type(); assertThat(leastUpperBound(a)).isSameAs(a); assertThat(leastUpperBound(varType)).isSameAs(varType); } @Test public void lub_of_primitives() throws Exception { assertThat(leastUpperBound.leastUpperBound(Sets.newHashSet(intType, intType))).isSameAs(intType); assertThat(leastUpperBound.leastUpperBound(Sets.newHashSet(intType, longType)).isUnknown()).isTrue(); } @Test public void lub_should_fail_if_no_type_provided() { try { leastUpperBound.leastUpperBound(Sets.<Type>newHashSet()); Fail.fail("should have failed"); } catch (Exception e) { assertThat(e).isInstanceOf(IllegalArgumentException.class); } } @Test public void lub_with_shared_supertypes1() { List<Type> typesFromInput = declaredTypes( "class A extends Exception {}", "class B extends Exception {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type lub = leastUpperBound(a, b); assertThat(lub.is("java.lang.Exception")).isTrue(); } @Test public void lub_with_shared_supertypes2() { List<Type> typesFromInput = declaredTypes( "import java.io.Serializable;", "class A extends Exception {}", "class B extends Throwable {}", "class C implements Serializable {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type c = typesFromInput.get(2); Type lub = leastUpperBound(a, b, c); assertThat(lub.is("java.io.Serializable")).isTrue(); } @Test public void lub_with_shared_supertypes3() { List<Type> typesFromInput = declaredTypes( "class A extends Exception {}", "class B {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type lub = leastUpperBound(a, b); assertThat(lub.is("java.lang.Object")).isTrue(); } @Test public void lub_with_hierarchy_of_supertypes1() { List<Type> typesFromInput = declaredTypes( "class A extends Exception {}", "class B extends A {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type lub = leastUpperBound(a, b); assertThat(lub).isSameAs(a); lub = leastUpperBound(b, a); assertThat(lub).isSameAs(a); } @Test public void lub_with_hierarchy_of_supertypes2() { List<Type> typesFromInput = declaredTypes( "class A extends Exception {}", "class B extends Throwable {}", "class C extends B {}"); Type a = typesFromInput.get(0); Type c = typesFromInput.get(2); Type lub = leastUpperBound(a, c); assertThat(lub.is("java.lang.Throwable")).isTrue(); } @Test public void lub_approximation_inheritance_and_multiple_bounds() { List<Type> typesFromInput = declaredTypes( "class A implements I1, I2 {}", "class B implements I2, I1 {}", "interface I1 {}", "interface I2 {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type lub = leastUpperBound(a, b); Type i1 = typesFromInput.get(2); // should be <I1 & I2>, not only i1 (first interface of first type analyzed) assertThat(lub).isSameAs(i1); } @Test public void lub_approximation_with_complexe_inheritance() { List<Type> typesFromInput = declaredTypes( "class A extends Exception implements I1, I2 {}", "class B extends Exception implements I2, I1 {}", "interface I1 {}", "interface I2 {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type lub = leastUpperBound(a, b); // should be <Exception & I1 & I2> assertThat(lub.is("java.lang.Exception")).isTrue(); } @Test public void lub_select_best_return_first_classes_then_interfaces_ordered_alphabetically() { List<Type> typesFromInput = declaredTypes( "class A {}", "class B {}", "interface I1 {}", "interface I2 {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type i1 = typesFromInput.get(2); Type i2 = typesFromInput.get(3); Type best = LeastUpperBound.best(Lists.newArrayList(i1, a, b, i2)); assertThat(best.is("A")).isTrue(); best = LeastUpperBound.best(Lists.newArrayList(i2, i1)); assertThat(best.is("I1")).isTrue(); } @Test public void lub_with_unknown_inheritance() { List<Type> typesFromInput = declaredTypes( "class A extends Exception {}", "class B extends UnknownException {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type lub = leastUpperBound(a, b); assertThat(lub.isUnknown()).isTrue(); } @Test public void lub_of_generics() { List<Type> typesFromInput = declaredTypes( "class A extends Exception {}", "class B extends Exception implements I1<Exception> {}", "interface I1<T> {}"); Type a = typesFromInput.get(0); Type b = typesFromInput.get(1); Type lub = leastUpperBound(a, b); assertThat(lub).isSameAs(a.symbol().superClass()); typesFromInput = declaredTypes( "class A<T> extends java.util.List<T> {}", "class B extends A<String> {}"); a = typesFromInput.get(0); b = typesFromInput.get(1); lub = leastUpperBound(a, b); assertThat(lub).isSameAs(a); // FIXME : should be the other way around but we don't care about type parameter in lub for now. assertThat(lub).isSameAs(b.symbol().superClass().erasure()); assertThat(lub).isNotSameAs(b.symbol().superClass()); } @Test public void lub_of_generics_with_raw_type() { List<Type> typesFromInput = declaredTypes( "class Parent<X> {}", "class Child<Y> extends Parent<Y> {}", "class ChildString extends Child<String> {}", "class ChildRaw extends Child {}"); Type ChildString = typesFromInput.get(2); Type ChildRaw = typesFromInput.get(3); JavaType lub = leastUpperBound(ChildString, ChildRaw); assertThat(lub.isTagged(JavaType.PARAMETERIZED)).isFalse(); assertThat(lub.is("Child")).isTrue(); } @Test public void lub_of_generics_without_loop() { List<Type> typesFromInput = declaredTypes( "class Parent<X1, X2> {}", "class Child<Y1, Y2> extends Parent<Y1, Y2> {}", "class GrandChild<Z1, Z2> extends Child<Z1, Z2> {}", "class A {}", "class B extends A {}", "class C extends A {}", "class D extends C {}", "class ChildBA extends Child<B, A> {}", "class ChildCA extends Child<C, A> {}", "class GrandChildDA extends GrandChild<D, D> {}"); Type childBA = typesFromInput.get(7); Type childCA = typesFromInput.get(8); Type grandChildDD = typesFromInput.get(9); JavaType lub = leastUpperBound(childBA, childCA, grandChildDD); assertThat(lub.isTagged(JavaType.PARAMETERIZED)).isTrue(); ParametrizedTypeJavaType ptt = (ParametrizedTypeJavaType) lub; assertThat(ptt.rawType.is("Child")).isTrue(); JavaType substitution = ptt.substitution(ptt.typeParameters().get(0)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.EXTENDS); assertThat(((WildCardType) substitution).bound.is("A")).isTrue(); substitution = ptt.substitution(ptt.typeParameters().get(1)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.EXTENDS); assertThat(((WildCardType) substitution).bound.is("A")).isTrue(); } @Test public void lub_of_generics_with_multiple_typeArgs_and_wildcards() { List<Type> typesFromInput = declaredTypes( "class Parent<X1, X2, X3, X4, X5, X6> {}", "class A {}", "class B extends A {}", "class C extends A {}", "class D extends C {}", // -------------- Parent< X1 ----- , X2 ------- , X3 ------- , X4 ----- , X5 ------- , X6> "class P1 extends Parent< ? super B, ? extends B, ? extends A, A, ? super A , ? super B > {}", "class P2 extends Parent< ? super C, ? extends C, A, ? super A, ? extends A, ? extends C > {}", "class P3 extends Parent< ? super D, ? extends D, A, A, A , ? > {}"); Type p1 = typesFromInput.get(5); Type p2 = typesFromInput.get(6); Type p3 = typesFromInput.get(7); JavaType lub = leastUpperBound(p1, p2, p3); assertThat(lub.isTagged(JavaType.PARAMETERIZED)).isTrue(); ParametrizedTypeJavaType ptt = (ParametrizedTypeJavaType) lub; assertThat(ptt.rawType.is("Parent")).isTrue(); JavaType substitution; // X1 substitution = ptt.substitution(ptt.typeParameters().get(0)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.SUPER); // FIXME SONARJAVA-1632 - should be B & C & D assertThat(((WildCardType) substitution).bound.is("B")).isTrue(); // X2 substitution = ptt.substitution(ptt.typeParameters().get(1)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.EXTENDS); assertThat(((WildCardType) substitution).bound.is("A")).isTrue(); // X3 substitution = ptt.substitution(ptt.typeParameters().get(2)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.EXTENDS); assertThat(((WildCardType) substitution).bound.is("A")).isTrue(); // X4 substitution = ptt.substitution(ptt.typeParameters().get(3)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.SUPER); assertThat(((WildCardType) substitution).bound.is("A")).isTrue(); // X5 substitution = ptt.substitution(ptt.typeParameters().get(4)); assertThat(substitution.isTagged(JavaType.CLASS)).isTrue(); assertThat(substitution.is("A")).isTrue(); // X6 substitution = ptt.substitution(ptt.typeParameters().get(5)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.UNBOUNDED); } @Test public void lub_of_generics_without_loop2() { List<Type> typesFromInput = declaredTypes( "class Parent<X> {}", "class Child<Y> extends Parent<Y> {}", "class Other<Z> {}", "class A {}", "class ChildP extends Parent<Other<? extends A>> {}", "class ChildC extends Child<Other<? extends A>> {}"); Type ChildP = typesFromInput.get(4); Type childC = typesFromInput.get(5); JavaType lub = leastUpperBound(ChildP, childC); assertThat(lub.isTagged(JavaType.PARAMETERIZED)).isTrue(); ParametrizedTypeJavaType ptt = (ParametrizedTypeJavaType) lub; assertThat(ptt.rawType.is("Parent")).isTrue(); JavaType substitution = ptt.substitution(ptt.typeParameters().get(0)); assertThat(substitution.isTagged(JavaType.PARAMETERIZED)).isTrue(); ptt = (ParametrizedTypeJavaType) substitution; assertThat(ptt.rawType.is("Other")).isTrue(); substitution = ptt.substitution(ptt.typeParameters().get(0)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.EXTENDS); assertThat(((WildCardType) substitution).bound.is("A")).isTrue(); } @Test public void lub_of_generics_infinite_types() { List<Type> typesFromInput = declaredTypes( "class Parent<X> {}", "class Child<Y> extends Parent<Y> {}", "class ChildInteger extends Child<Integer> {}", "class ChildString extends Child<String> {}"); Type childInteger = typesFromInput.get(2); Type childString = typesFromInput.get(3); JavaType lub = leastUpperBound(childInteger, childString); assertThat(lub.isTagged(JavaType.PARAMETERIZED)).isTrue(); ParametrizedTypeJavaType ptt = (ParametrizedTypeJavaType) lub; assertThat(ptt.rawType.is("Child")).isTrue(); JavaType substitution = ptt.substitution(ptt.typeParameters().get(0)); assertThat(substitution.isTagged(JavaType.WILDCARD)).isTrue(); assertThat(((WildCardType) substitution).boundType).isEqualTo(WildCardType.BoundType.EXTENDS); assertThat(((WildCardType) substitution).bound.isSubtypeOf("java.lang.Comparable")).isTrue(); } @Test public void supertypes() { Type arrayList = declaredTypes("class MyArrayList extends java.util.ArrayList<String> {}").get(0); Set<Type> supertypes = leastUpperBound.supertypes((JavaType) arrayList); assertContains(supertypes, "MyArrayList", // from MyArrayList "java.util.ArrayList<java.lang.String>", // from ArrayList "java.util.AbstractList<java.lang.String>", "java.util.RandomAccess", "java.lang.Cloneable", "java.io.Serializable", // from ArrayList and AbstractList "java.util.List<java.lang.String>", // from AbstractList "java.util.AbstractCollection<java.lang.String>", // from List and AbstractCollection "java.util.Collection<java.lang.String>", // from AbstractCollection "java.lang.Object", // from Collection "java.lang.Iterable<java.lang.String>"); } private static void assertContains(Set<Type> supertypes, String... fullyQualifiedNames) { Set<String> toCheck = new HashSet<>(Arrays.asList(fullyQualifiedNames)); assertThat(supertypes.stream().allMatch(t -> match((JavaType) t, toCheck, fullyQualifiedNames))).isTrue(); assertThat(toCheck).isEmpty(); } private static boolean match(JavaType type, Set<String> toCheck, String... fullyQualifiedNames) { for (String fullyQualifiedName : fullyQualifiedNames) { String newFullyQualifiedName = fullyQualifiedName; String typeArgFullyQualifiedName = null; int param = fullyQualifiedName.indexOf('<'); if (param > 0) { newFullyQualifiedName = fullyQualifiedName.substring(0, param); typeArgFullyQualifiedName = fullyQualifiedName.substring(param + 1, fullyQualifiedName.length() - 1); } if (type.is(newFullyQualifiedName)) { if (typeArgFullyQualifiedName != null) { assertThat(type.isTagged(JavaType.PARAMETERIZED)).isTrue(); ParametrizedTypeJavaType ptt = (ParametrizedTypeJavaType) type; assertThat(ptt.typeSubstitution.substitutedTypes().get(0).is(typeArgFullyQualifiedName)).isTrue(); } toCheck.remove(fullyQualifiedName); return true; } } return false; } private static List<Type> declaredTypes(String... lines) { CompilationUnitTree tree = treeOf(lines); List<Type> results = Lists.newLinkedList(); for (Tree classTree : tree.types()) { Type type = ((ClassTree) classTree).symbol().type(); results.add(type); } return results; } private static CompilationUnitTree treeOf(String... lines) { StringBuilder builder = new StringBuilder(); for (String line : lines) { builder.append(line).append(System.lineSeparator()); } CompilationUnitTree cut = (CompilationUnitTree) JavaParser.createParser(StandardCharsets.UTF_8).parse(builder.toString()); SemanticModel.createFor(cut, Lists.newArrayList(new File("target/test-classes"), new File("target/classes"))); return cut; } }