Skip to content

Commit ac00be6

Browse files
SONARPY-900 S5886 should not report on async functions returning AsyncGenerator
1 parent 394c313 commit ac00be6

File tree

2 files changed

+27
-3
lines changed

2 files changed

+27
-3
lines changed

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

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -44,6 +44,7 @@ public class FunctionReturnTypeCheck extends PythonSubscriptionCheck {
4444

4545
private static final String MESSAGE = "Return a value of type \"%s\" instead of \"%s\" or update function \"%s\" type hint.";
4646
private static final List<String> ITERABLE_TYPES = Arrays.asList("typing.Generator", "typing.Iterator", "typing.Iterable");
47+
private static final List<String> ASYNC_ITERABLE_TYPES = Arrays.asList("typing.AsyncGenerator", "typing.AsyncIterator", "typing.AsyncIterable");
4748

4849
@Override
4950
public void initialize(Context context) {
@@ -68,14 +69,24 @@ private static void raiseIssues(SubscriptionContext ctx, FunctionDef functionDef
6869
String functionName = functionDef.name().name();
6970
String returnTypeName = InferredTypes.typeName(declaredReturnType);
7071
if (!returnTypeVisitor.yieldStatements.isEmpty()) {
72+
boolean isAsyncFunction = functionDef.asyncKeyword() != null;
73+
String recommendedSuperType = isAsyncFunction ? "typing.AsyncGenerator" : "typing.Generator";
7174
// Here we should probably use an equivalent of "canBeOrExtend" (accepting uncertainty) instead of "mustBeOrExtend"
72-
if (ITERABLE_TYPES.stream().anyMatch(declaredReturnType::mustBeOrExtend)) {
75+
if (ITERABLE_TYPES.stream().anyMatch(declaredReturnType::mustBeOrExtend) || ASYNC_ITERABLE_TYPES.stream().anyMatch(declaredReturnType::mustBeOrExtend)) {
76+
if (isMixedUpAnnotation(isAsyncFunction, declaredReturnType)) {
77+
returnTypeVisitor.yieldStatements
78+
.forEach(y -> {
79+
PreciseIssue issue =
80+
ctx.addIssue(y, String.format("Annotate function \"%s\" with \"%s\" or one of its supertypes.", functionName, recommendedSuperType));
81+
addSecondaries(issue, functionDef);
82+
});
83+
}
7384
return;
7485
}
7586
returnTypeVisitor.yieldStatements
7687
.forEach(y -> {
7788
PreciseIssue issue =
78-
ctx.addIssue(y, String.format("Remove this yield statement or annotate function \"%s\" with \"typing.Generator\" or one of its supertypes.", functionName));
89+
ctx.addIssue(y, String.format("Remove this yield statement or annotate function \"%s\" with \"%s\" or one of its supertypes.", functionName, recommendedSuperType));
7990
addSecondaries(issue, functionDef);
8091
});
8192
}
@@ -92,6 +103,10 @@ private static void raiseIssues(SubscriptionContext ctx, FunctionDef functionDef
92103
});
93104
}
94105

106+
private static boolean isMixedUpAnnotation(boolean isAsyncFunction, InferredType declaredReturnType) {
107+
return isAsyncFunction ? ITERABLE_TYPES.stream().anyMatch(declaredReturnType::mustBeOrExtend) : ASYNC_ITERABLE_TYPES.stream().anyMatch(declaredReturnType::mustBeOrExtend);
108+
}
109+
95110
private static void addSecondaries(PreciseIssue issue, FunctionDef functionDef) {
96111
issue.secondary(functionDef.name(), "Function definition.");
97112
TypeAnnotation returnTypeAnnotation = functionDef.returnTypeAnnotation();

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

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
from typing import List, SupportsFloat, Set, Dict, NoReturn, Text, Generator, Tuple, Union, AnyStr, Iterator, Iterable, Callable, Optional, TypedDict
1+
from typing import List, SupportsFloat, Set, Dict, NoReturn, Text, Generator, Tuple, Union, AnyStr, Iterator, Iterable, Callable, Optional, TypedDict, AsyncIterator, AsyncGenerator
22
import numpy as np
33

44
def builtins():
@@ -313,3 +313,12 @@ def my_dict() -> MyCustomDict:
313313
users = {1,2,3}
314314
messages = {1,2,3}
315315
return dict(user_ids=users, message_ids=messages)
316+
317+
async def no_issue_for_async_iterators() -> AsyncIterator[int]:
318+
yield 1
319+
320+
async def no_issue_for_async_generators() -> AsyncGenerator[int]:
321+
yield 1
322+
323+
async def async_function_returning_iterator() -> Iterator[int]:
324+
yield 1 # Noncompliant {{Annotate function "async_function_returning_iterator" with "typing.AsyncGenerator" or one of its supertypes.}}

0 commit comments

Comments
 (0)