diff --git a/java-frontend/src/main/java/org/sonar/java/model/JParser.java b/java-frontend/src/main/java/org/sonar/java/model/JParser.java index 777d928b86..1cf6f17618 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JParser.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JParser.java @@ -2620,15 +2620,23 @@ private JavaTree.AnnotatedTypeTree convertSimpleType(SimpleType e) { private JavaTree.UnionTypeTreeImpl convertUnionType(UnionType e) { QualifiedIdentifierListTreeImpl alternatives = QualifiedIdentifierListTreeImpl.emptyList(); + ITypeBinding[] alternativeBindings = new ITypeBinding[e.types().size()]; for (int i = 0; i < e.types().size(); i++) { Type o = (Type) e.types().get(i); - alternatives.add(convertType(o)); + TypeTree typeTree = convertType(o); + alternatives.add(typeTree); + if (typeTree instanceof AbstractTypedTree typedTree && typedTree.typeBinding != null) { + alternativeBindings[i] = typedTree.typeBinding; + } else { + alternativeBindings[i] = o.resolveBinding(); + } if (i < e.types().size() - 1) { alternatives.separators().add(firstTokenAfter(o, TerminalToken.TokenNameOR)); } } JavaTree.UnionTypeTreeImpl t = new JavaTree.UnionTypeTreeImpl(alternatives); t.typeBinding = e.resolveBinding(); + t.alternativeBindings = alternativeBindings; return t; } diff --git a/java-frontend/src/main/java/org/sonar/java/model/JSema.java b/java-frontend/src/main/java/org/sonar/java/model/JSema.java index 6a2f1ffd52..5a7e7d4d1a 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JSema.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JSema.java @@ -50,6 +50,7 @@ public final class JSema implements Sema { private final Map staticInitializerBlockSymbols = new HashMap<>(); private final Map annotations = new HashMap<>(); private final Map nameToTypeCache = new HashMap<>(); + final Map unionTypeAlternatives = new HashMap<>(); JSema(AST ast) { this.ast = ast; diff --git a/java-frontend/src/main/java/org/sonar/java/model/JType.java b/java-frontend/src/main/java/org/sonar/java/model/JType.java index 18e8cf72ab..0f0a15c858 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JType.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JType.java @@ -210,34 +210,57 @@ public String fullyQualifiedName() { return fullyQualifiedName; } - private static String fullyQualifiedName(ITypeBinding typeBinding) { - String qualifiedName; + private String fullyQualifiedName(ITypeBinding typeBinding) { + String qualifiedName = baseQualifiedName(typeBinding); + + if (typeBinding.isIntersectionType()) { + return buildIntersectionTypeName(typeBinding, qualifiedName); + } else if (isUnionType()) { + return buildUnionTypeName(typeBinding, qualifiedName); + } + return qualifiedName; + } + + private String baseQualifiedName(ITypeBinding typeBinding) { if (typeBinding.isNullType()) { - qualifiedName = ""; + return ""; } else if (typeBinding.isPrimitive()) { - qualifiedName = typeBinding.getName(); + return typeBinding.getName(); } else if (typeBinding.isArray()) { - qualifiedName = fullyQualifiedName(typeBinding.getComponentType()) + "[]"; + return fullyQualifiedName(typeBinding.getComponentType()) + "[]"; } else if (typeBinding.isCapture()) { - qualifiedName = "!capture!"; + return "!capture!"; } else if (typeBinding.isTypeVariable()) { - qualifiedName = typeBinding.getName(); + return typeBinding.getName(); } else { - qualifiedName = typeBinding.getBinaryName(); - if (qualifiedName == null) { + String binaryName = typeBinding.getBinaryName(); + if (binaryName == null) { // e.g. anonymous class in unreachable code - qualifiedName = typeBinding.getKey(); + return typeBinding.getKey(); } + return binaryName; } - if (typeBinding.isIntersectionType()) { - TreeSet intersectionTypes = new TreeSet<>(); - intersectionTypes.add(qualifiedName); - for (ITypeBinding typeBound : typeBinding.getTypeBounds()) { - intersectionTypes.add(fullyQualifiedName(typeBound)); - } - qualifiedName = String.join(" & ", intersectionTypes); + } + + private String buildIntersectionTypeName(ITypeBinding typeBinding, String baseName) { + TreeSet intersectionTypes = new TreeSet<>(); + intersectionTypes.add(baseName); + for (ITypeBinding typeBound : typeBinding.getTypeBounds()) { + intersectionTypes.add(fullyQualifiedName(typeBound)); } - return qualifiedName; + return String.join(" & ", intersectionTypes); + } + + private String buildUnionTypeName(ITypeBinding typeBinding, String baseName) { + ITypeBinding[] alternatives = sema.unionTypeAlternatives.get(typeBinding); + if (alternatives == null) { + return baseName; + } + TreeSet unionTypes = new TreeSet<>(); + for (ITypeBinding alternative : alternatives) { + unionTypes.add(fullyQualifiedName(alternative)); + } + return String.join(" | ", unionTypes); } @Override @@ -258,6 +281,24 @@ public Type[] getIntersectionTypes() { return types; } + @Override + public boolean isUnionType() { + return sema.unionTypeAlternatives.containsKey(typeBinding); + } + + @Override + public Type[] getUnionTypes() { + ITypeBinding[] alternatives = sema.unionTypeAlternatives.get(typeBinding); + if (alternatives == null) { + return new Type[] { this }; + } + Type[] types = new Type[alternatives.length]; + for (int i = 0; i < alternatives.length; i++) { + types[i] = sema.type(alternatives[i]); + } + return types; + } + /** * @see JSymbol#name() */ diff --git a/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java b/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java index 895bc18860..95d2cf83c5 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java +++ b/java-frontend/src/main/java/org/sonar/java/model/JavaTree.java @@ -38,6 +38,7 @@ import org.sonar.java.model.expression.AssessableExpressionTree; import org.sonar.java.model.expression.TypeArgumentListTreeImpl; import org.sonar.plugins.java.api.semantic.Symbol; +import org.sonar.plugins.java.api.semantic.Type; import org.sonar.plugins.java.api.tree.AnnotationTree; import org.sonar.plugins.java.api.tree.ArrayTypeTree; import org.sonar.plugins.java.api.tree.CompilationUnitTree; @@ -476,6 +477,7 @@ public List children() { public static class UnionTypeTreeImpl extends AbstractTypedTree implements UnionTypeTree { private final ListTree typeAlternatives; + ITypeBinding[] alternativeBindings; public UnionTypeTreeImpl(QualifiedIdentifierListTreeImpl typeAlternatives) { this.typeAlternatives = Objects.requireNonNull(typeAlternatives); @@ -505,6 +507,14 @@ public List children() { public List annotations() { return Collections.emptyList(); } + + @Override + public Type symbolType() { + if (typeBinding != null && alternativeBindings != null) { + root.sema.unionTypeAlternatives.put(typeBinding, alternativeBindings); + } + return super.symbolType(); + } } public static class NotImplementedTreeImpl extends AssessableExpressionTree { diff --git a/java-frontend/src/main/java/org/sonar/java/model/Symbols.java b/java-frontend/src/main/java/org/sonar/java/model/Symbols.java index 7c89cf26b5..f65373a6d2 100644 --- a/java-frontend/src/main/java/org/sonar/java/model/Symbols.java +++ b/java-frontend/src/main/java/org/sonar/java/model/Symbols.java @@ -469,5 +469,15 @@ public boolean isIntersectionType() { public Type[] getIntersectionTypes() { return new Type[] { this }; } + + @Override + public boolean isUnionType() { + return false; + } + + @Override + public Type[] getUnionTypes() { + return new Type[] { this }; + } } } diff --git a/java-frontend/src/main/java/org/sonar/plugins/java/api/semantic/Type.java b/java-frontend/src/main/java/org/sonar/plugins/java/api/semantic/Type.java index 014200e911..23bbc214ff 100644 --- a/java-frontend/src/main/java/org/sonar/plugins/java/api/semantic/Type.java +++ b/java-frontend/src/main/java/org/sonar/plugins/java/api/semantic/Type.java @@ -299,4 +299,21 @@ interface ArrayType extends Type { */ Type[] getIntersectionTypes(); + /** + * Check if this type is a union type. For example, return true for the type of 'e' in the following code: + *
+   *   try { } catch (IOException | SQLException e) { }
+   *
+ */ + boolean isUnionType(); + + /** + * This method returns more than one type when {@link #isUnionType()} is true. For example, it + * returns {@code ["java.io.IOException", "java.sql.SQLException"] } for the type of 'e' in the following code: + *
+   *   try { } catch (IOException | SQLException e) { }
+   *
+ */ + Type[] getUnionTypes(); + } diff --git a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java index 3ef832296b..e367f66f6e 100644 --- a/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java +++ b/java-frontend/src/test/java/org/sonar/java/model/JParserSemanticTest.java @@ -1497,11 +1497,69 @@ void type_union() { Type symbolType = t.symbolType(); assertThat(symbolType).isNotNull(); assertThat(symbolType.isUnknown()).isFalse(); - // "fullyQualifiedName()" should be unique for each different type, like for example "java.lang.MatchException | java.lang.NumberFormatException" - // this will be fixed by SONARJAVA-5718 - assertThat(symbolType.fullyQualifiedName()).isEqualTo("java.lang.RuntimeException"); - assertThat(symbolType.getIntersectionTypes()).extracting(Type::fullyQualifiedName) - .containsExactly("java.lang.RuntimeException"); + // "fullyQualifiedName()" should be unique for each different type + assertThat(symbolType.fullyQualifiedName()).isEqualTo("java.lang.MatchException | java.lang.NumberFormatException"); + assertThat(symbolType.isUnionType()).isTrue(); + assertThat(symbolType.getUnionTypes()).extracting(Type::fullyQualifiedName) + .containsExactly("java.lang.MatchException", "java.lang.NumberFormatException"); + } + + @Test + void type_union_with_three_alternatives() { + CompilationUnitTree cu = test("class C { void m() { try { } catch (MatchException | NumberFormatException | ArithmeticException v) { } } }"); + ClassTree c = (ClassTree) cu.types().get(0); + MethodTree m = (MethodTree) c.members().get(0); + TryStatementTree s = (TryStatementTree) m.block().body().get(0); + VariableTreeImpl v = (VariableTreeImpl) s.catches().get(0).parameter(); + AbstractTypedTree t = (AbstractTypedTree) v.type(); + Type symbolType = t.symbolType(); + assertThat(symbolType.isUnionType()).isTrue(); + // FQN should be sorted alphabetically + assertThat(symbolType.fullyQualifiedName()).isEqualTo("java.lang.ArithmeticException | java.lang.MatchException | java.lang.NumberFormatException"); + assertThat(symbolType.getUnionTypes()).hasSize(3); + // getUnionTypes() returns types in source order, not sorted + assertThat(symbolType.getUnionTypes()).extracting(Type::fullyQualifiedName) + .containsExactly("java.lang.MatchException", "java.lang.NumberFormatException", "java.lang.ArithmeticException"); + } + + @Test + void type_union_ordering() { + // Test that union types are sorted alphabetically in FQN + CompilationUnitTree cu = test("class C { void m() { try { } catch (NumberFormatException | MatchException v) { } } }"); + ClassTree c = (ClassTree) cu.types().get(0); + MethodTree m = (MethodTree) c.members().get(0); + TryStatementTree s = (TryStatementTree) m.block().body().get(0); + VariableTreeImpl v = (VariableTreeImpl) s.catches().get(0).parameter(); + AbstractTypedTree t = (AbstractTypedTree) v.type(); + Type symbolType = t.symbolType(); + // Should be sorted: MatchException comes before NumberFormatException + assertThat(symbolType.fullyQualifiedName()).isEqualTo("java.lang.MatchException | java.lang.NumberFormatException"); + } + + @Test + void non_union_type_returns_false_for_isUnionType() { + CompilationUnitTree cu = test("class C { void m() { try { } catch (NumberFormatException v) { } } }"); + ClassTree c = (ClassTree) cu.types().get(0); + MethodTree m = (MethodTree) c.members().get(0); + TryStatementTree s = (TryStatementTree) m.block().body().get(0); + VariableTreeImpl v = (VariableTreeImpl) s.catches().get(0).parameter(); + AbstractTypedTree t = (AbstractTypedTree) v.type(); + Type symbolType = t.symbolType(); + assertThat(symbolType.isUnionType()).isFalse(); + assertThat(symbolType.getUnionTypes()).hasSize(1); + assertThat(symbolType.getUnionTypes()[0]).isEqualTo(symbolType); + } + + @Test + void primitive_type_is_not_union() { + CompilationUnitTree cu = test("class C { void m(int x) { } }"); + ClassTree c = (ClassTree) cu.types().get(0); + MethodTree m = (MethodTree) c.members().get(0); + VariableTreeImpl v = (VariableTreeImpl) m.parameters().get(0); + Type symbolType = v.symbol().type(); + assertThat(symbolType.isUnionType()).isFalse(); + assertThat(symbolType.getUnionTypes()).hasSize(1); + assertThat(symbolType.getUnionTypes()[0]).isEqualTo(symbolType); } @Test