Skip to content

Commit 68ecb38

Browse files
sonar-nigel[bot]Vibe Bot
authored andcommitted
SONARPY-3030 Fix S6546 false positive for union types inside typing.Annotated (#1002)
Co-authored-by: Vibe Bot <vibe-bot@sonarsource.com> GitOrigin-RevId: cbb9c9f4c9648b2e541e997c467235569a1f7301
1 parent f21a39d commit 68ecb38

File tree

2 files changed

+19
-11
lines changed

2 files changed

+19
-11
lines changed

python-checks/src/main/java/org/sonar/python/checks/UnionTypeExpressionCheck.java

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -16,20 +16,23 @@
1616
*/
1717
package org.sonar.python.checks;
1818

19+
import java.util.function.Predicate;
1920
import org.sonar.check.Rule;
2021
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2122
import org.sonar.plugins.python.api.PythonVersionUtils;
2223
import org.sonar.plugins.python.api.SubscriptionContext;
2324
import org.sonar.plugins.python.api.tree.Expression;
2425
import org.sonar.plugins.python.api.tree.Tree;
2526
import org.sonar.plugins.python.api.tree.TypeAnnotation;
26-
import org.sonar.plugins.python.api.types.InferredType;
27-
import org.sonar.python.types.InferredTypes;
27+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatcher;
28+
import org.sonar.plugins.python.api.types.v2.matchers.TypeMatchers;
29+
import org.sonar.python.tree.TreeUtils;
2830

2931
@Rule(key = "S6546")
3032
public class UnionTypeExpressionCheck extends PythonSubscriptionCheck {
3133

3234
private static final String MESSAGE = "Use a union type expression for this type hint.";
35+
private static final TypeMatcher TYPING_UNION_MATCHER = TypeMatchers.isType("typing.Union");
3336

3437
@Override
3538
public void initialize(Context context) {
@@ -45,13 +48,9 @@ private static void checkTypeAnnotation(SubscriptionContext ctx) {
4548

4649
TypeAnnotation typeAnnotation = (TypeAnnotation) ctx.syntaxNode();
4750
Expression expression = typeAnnotation.expression();
48-
if (expression.is(Tree.Kind.BITWISE_OR)) {
49-
return;
50-
}
5151

52-
InferredType type = InferredTypes.fromTypeAnnotation(typeAnnotation);
53-
String fqn = InferredTypes.fullyQualifiedTypeName(type);
54-
if ("typing.Union".equals(fqn)) {
52+
Predicate<Tree> isUnionName = n -> n.is(Tree.Kind.NAME) && TYPING_UNION_MATCHER.isTrueFor((Expression) n, ctx);
53+
if (isUnionName.test(expression) || TreeUtils.hasDescendant(expression, isUnionName)) {
5554
ctx.addIssue(expression, MESSAGE);
5655
}
5756
}

python-checks/src/test/resources/checks/unionTypeExpression.py

Lines changed: 12 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import Union
1+
from typing import Annotated, Union
22
from typing import Union as u
33
import typing
44
import typing as t
@@ -40,8 +40,7 @@ def instance_method() -> Union[int, str]: # Noncompliant
4040
pass
4141

4242

43-
# There is no clear recommendation on this case, so we will just not handle them for the time being.
44-
def union_in_generic_type() -> list[Union[int, str]]: # FN
43+
def union_in_generic_type() -> list[Union[int, str]]: # Noncompliant
4544
pass
4645

4746
def ok(param: int | str) -> int | str:
@@ -66,3 +65,13 @@ def unknown_return_type() -> unknown:
6665

6766
def unknown_return_type_subscript() -> unknown[int, str]:
6867
pass
68+
69+
variable: Annotated[int | str, "metadata"]
70+
71+
top_level: Annotated[float | bytes, "info"]
72+
73+
def annotated_param(value: Annotated[int | str, "description"]) -> None:
74+
pass
75+
76+
def annotated_return() -> Annotated[int | str, "result"]:
77+
return 42

0 commit comments

Comments
 (0)