Skip to content

Commit 393d5ba

Browse files
guillaume-dequennesonartech
authored andcommitted
SONARPY-2939 Fix FP on S7503 when implementing protocol methods (#290)
GitOrigin-RevId: dca9fffc4f550a473a00b0718a72434775b22063
1 parent 30389b5 commit 393d5ba

2 files changed

Lines changed: 102 additions & 20 deletions

File tree

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

Lines changed: 33 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -31,37 +31,48 @@
3131
import org.sonar.plugins.python.api.tree.WithStatement;
3232
import org.sonar.plugins.python.api.tree.YieldExpression;
3333
import org.sonar.plugins.python.api.tree.YieldStatement;
34+
import org.sonar.plugins.python.api.types.v2.ClassType;
35+
import org.sonar.plugins.python.api.types.v2.FunctionType;
3436
import org.sonar.python.checks.utils.CheckUtils;
3537

3638
@Rule(key = "S7503")
3739
public class AsyncFunctionNotAsyncCheck extends PythonSubscriptionCheck {
38-
40+
3941
private static final String MESSAGE = "Use asynchronous features in this function or remove the `async` keyword.";
40-
42+
4143
@Override
4244
public void initialize(Context context) {
4345
context.registerSyntaxNodeConsumer(Tree.Kind.FUNCDEF, AsyncFunctionNotAsyncCheck::checkAsyncFunction);
4446
}
45-
47+
4648
private static void checkAsyncFunction(SubscriptionContext ctx) {
4749
FunctionDef functionDef = (FunctionDef) ctx.syntaxNode();
48-
50+
4951
Token asyncKeyword = functionDef.asyncKeyword();
5052
if (asyncKeyword == null || isException(functionDef)) {
5153
return;
5254
}
5355
AsyncFeatureVisitor visitor = new AsyncFeatureVisitor();
5456
functionDef.body().accept(visitor);
55-
57+
5658
if (!visitor.hasAsyncFeature()) {
5759
ctx.addIssue(functionDef.name(), MESSAGE).secondary(asyncKeyword, "This function is async.");
5860
}
5961
}
6062

6163
private static boolean isException(FunctionDef functionDef) {
62-
return CheckUtils.isAbstract(functionDef) || isEmptyFunction(functionDef.body());
64+
return CheckUtils.isAbstract(functionDef) ||
65+
isEmptyFunction(functionDef.body()) ||
66+
isDunderMethod(functionDef) ||
67+
!functionDef.decorators().isEmpty() ||
68+
mightBeOverridingMethod(functionDef);
6369
}
64-
70+
71+
private static boolean isDunderMethod(FunctionDef functionDef) {
72+
String methodName = functionDef.name().name();
73+
return methodName.startsWith("__");
74+
}
75+
6576
private static boolean isEmptyFunction(StatementList body) {
6677
for (Statement statement : body.statements()) {
6778
if (!CheckUtils.isEmptyStatement(statement)) {
@@ -70,20 +81,25 @@ private static boolean isEmptyFunction(StatementList body) {
7081
}
7182
return true;
7283
}
73-
84+
85+
private static boolean mightBeOverridingMethod(FunctionDef functionDef) {
86+
FunctionType functionType = (FunctionType) functionDef.name().typeV2();
87+
return functionType.owner() instanceof ClassType classType && (classType.hasUnresolvedHierarchy() || classType.inheritedMember(functionType.name()).isPresent());
88+
}
89+
7490
private static class AsyncFeatureVisitor extends BaseTreeVisitor {
75-
91+
7692
private boolean asyncFeatureFound = false;
77-
93+
7894
public boolean hasAsyncFeature() {
7995
return asyncFeatureFound;
8096
}
81-
97+
8298
@Override
8399
public void visitAwaitExpression(AwaitExpression awaitExpression) {
84100
asyncFeatureFound = true;
85101
}
86-
102+
87103
@Override
88104
public void visitForStatement(ForStatement forStatement) {
89105
if (forStatement.isAsync()) {
@@ -94,7 +110,7 @@ public void visitForStatement(ForStatement forStatement) {
94110
super.visitForStatement(forStatement);
95111
}
96112
}
97-
113+
98114
@Override
99115
public void visitWithStatement(WithStatement withStatement) {
100116
if (withStatement.isAsync()) {
@@ -104,22 +120,22 @@ public void visitWithStatement(WithStatement withStatement) {
104120
super.visitWithStatement(withStatement);
105121
}
106122
}
107-
123+
108124
@Override
109125
public void visitYieldStatement(YieldStatement yieldStatement) {
110126
asyncFeatureFound = true;
111127
}
112-
128+
113129
@Override
114130
public void visitYieldExpression(YieldExpression yieldExpression) {
115131
asyncFeatureFound = true;
116132
}
117-
133+
118134
@Override
119135
public void visitFunctionDef(FunctionDef functionDef) {
120136
// Skip nested functions
121137
}
122-
138+
123139
@Override
124140
protected void scan(@Nullable Tree tree) {
125141
// Stop scanning if we've already found an async feature

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

Lines changed: 69 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ async def async_method_with_await(self): # Compliant
8484
return await self.some_coroutine()
8585

8686
@classmethod
87-
async def async_classmethod_without_await(cls): # Noncompliant
87+
async def async_classmethod_without_await(cls): # Avoid FPs with decorators
8888
return cls.some_value
8989

9090
async def async_method_with_inner_function(self):
@@ -101,9 +101,75 @@ async def abstract_async_method_2(self): # Compliant
101101
raise NotImplementedError("This is an abstract method")
102102

103103
@abc.other
104-
async def other_decorator_1(self): # Noncompliant
104+
async def other_decorator_1(self): # Avoid FPs with decorators
105105
raise NotImplementedError("...")
106106

107107
@unknown()
108-
async def other_decorator_1(self): # Noncompliant
108+
async def other_decorator_1(self): # Avoid FPs with decorators
109109
raise NotImplementedError("...")
110+
111+
# Async protocol methods - should be compliant even without await
112+
class AsyncContextManager:
113+
async def __aenter__(self):
114+
return self
115+
116+
async def __aexit__(self, exc_type, exc_val, exc_tb):
117+
pass
118+
119+
async def __unknown_dunder__(self):
120+
# Avoid risk of FPs
121+
pass
122+
123+
class AsyncIterator:
124+
async def __aiter__(self):
125+
return self
126+
127+
async def __anext__(self):
128+
if self.should_stop():
129+
raise StopAsyncIteration
130+
return self.value
131+
132+
class AsyncResource:
133+
async def __aclose__(self):
134+
print("Releasing resources")
135+
136+
class AsyncAwaitableObject:
137+
async def __await__(self):
138+
yield "something"
139+
140+
async def regular_method_without_await(self): # Noncompliant
141+
print("This is not a protocol method")
142+
143+
# FastAPI route examples
144+
from fastapi import FastAPI, APIRouter
145+
146+
app = FastAPI()
147+
router = APIRouter()
148+
149+
@app.get("/items/{item_id}")
150+
async def read_item(item_id: int): # Compliant - FastAPI route
151+
return {"item_id": item_id}
152+
153+
@app.post("/users/")
154+
async def create_user(user_data: dict): # Compliant - FastAPI route
155+
# No await, but this is still valid for FastAPI routes
156+
return {"user_id": 123, "data": user_data}
157+
158+
@router.put("/items/{item_id}")
159+
async def update_item(item_id: int, item: dict): # Compliant - FastAPI route via router
160+
return {"item_id": item_id, "item": item}
161+
162+
@app.delete("/items/{item_id}")
163+
async def delete_item(item_id: int): # Compliant - FastAPI route
164+
# No await, but this is still valid for FastAPI routes
165+
return {"deleted": True}
166+
167+
168+
class MyClass:
169+
async def my_method(self):
170+
await something()
171+
172+
class MyOtherClass(MyClass):
173+
async def my_method(self):
174+
# No issue on overriding methods
175+
do_something()

0 commit comments

Comments
 (0)