Skip to content

Commit 0f208ce

Browse files
guillaume-dequennesonartech
authored andcommitted
SONARPY-2931 Rule S7488 Use non-blocking sleep functions in asynchronous code (#255)
GitOrigin-RevId: e10829139ff55caa50338486faef6d293942c416
1 parent 097a259 commit 0f208ce

9 files changed

Lines changed: 559 additions & 1 deletion

File tree

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -355,6 +355,7 @@ public Stream<Class<?>> getChecks() {
355355
SuperfluousCurlyBraceCheck.class,
356356
TempFileCreationCheck.class,
357357
ImplicitlySkippedTestCheck.class,
358+
TimeSleepInAsyncCheck.class,
358359
ToDoCommentCheck.class,
359360
TooManyLinesInFileCheck.class,
360361
TooManyLinesInFunctionCheck.class,
Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
/*
2+
* SonarQube Python Plugin
3+
* Copyright (C) 2011-2025 SonarSource SA
4+
* mailto:info AT sonarsource DOT com
5+
*
6+
* This program is free software; you can redistribute it and/or
7+
* modify it under the terms of the Sonar Source-Available License Version 1, as published by SonarSource SA.
8+
*
9+
* This program is distributed in the hope that it will be useful,
10+
* but WITHOUT ANY WARRANTY; without even the implied warranty of
11+
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.
12+
* See the Sonar Source-Available License for more details.
13+
*
14+
* You should have received a copy of the Sonar Source-Available License
15+
* along with this program; if not, see https://sonarsource.com/license/ssal/
16+
*/
17+
package org.sonar.python.checks;
18+
19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
22+
import org.sonar.check.Rule;
23+
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
24+
import org.sonar.plugins.python.api.SubscriptionContext;
25+
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
26+
import org.sonar.plugins.python.api.tree.AliasedName;
27+
import org.sonar.plugins.python.api.tree.CallExpression;
28+
import org.sonar.plugins.python.api.tree.Expression;
29+
import org.sonar.plugins.python.api.tree.ImportName;
30+
import org.sonar.plugins.python.api.tree.Name;
31+
import org.sonar.plugins.python.api.tree.Tree;
32+
import org.sonar.plugins.python.api.types.v2.TriBool;
33+
import org.sonar.python.quickfix.TextEditUtils;
34+
import org.sonar.python.tree.TreeUtils;
35+
import org.sonar.python.types.v2.TypeCheckBuilder;
36+
37+
@Rule(key = "S7488")
38+
public class TimeSleepInAsyncCheck extends PythonSubscriptionCheck {
39+
40+
private static final String MESSAGE = "Replace this call to time.sleep() with an asynchronous sleep function.";
41+
private static final String SECONDARY_MESSAGE = "This function is async.";
42+
private static final String SLEEP_QUICK_FIX = "Replace with %s.sleep()";
43+
44+
private TypeCheckBuilder isTimeSleepCall;
45+
private final Map<String, String> asyncLibraryAliases = new HashMap<>();
46+
47+
@Override
48+
public void initialize(Context context) {
49+
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::setupCheck);
50+
context.registerSyntaxNodeConsumer(Tree.Kind.IMPORT_NAME, this::checkImportName);
51+
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkTimeSleepInAsync);
52+
}
53+
54+
private void setupCheck(SubscriptionContext ctx) {
55+
isTimeSleepCall = ctx.typeChecker().typeCheckBuilder().isTypeWithName("time.sleep");
56+
asyncLibraryAliases.clear();
57+
}
58+
59+
private void checkImportName(SubscriptionContext ctx) {
60+
ImportName importName = (ImportName) ctx.syntaxNode();
61+
for (AliasedName module : importName.modules()) {
62+
List<Name> names = module.dottedName().names();
63+
if (names.size() > 1) {
64+
continue;
65+
}
66+
String moduleName = names.get(0).name();
67+
Name alias = module.alias();
68+
String moduleAlias = alias != null ? alias.name() : moduleName;
69+
if ("asyncio".equals(moduleName) || "trio".equals(moduleName) || "anyio".equals(moduleName)) {
70+
asyncLibraryAliases.put(moduleName, moduleAlias);
71+
}
72+
}
73+
}
74+
75+
private void checkTimeSleepInAsync(SubscriptionContext context) {
76+
CallExpression callExpression = (CallExpression) context.syntaxNode();
77+
Expression callee = callExpression.callee();
78+
if (isTimeSleepCall.check(callee.typeV2()) != TriBool.TRUE) {
79+
return;
80+
}
81+
TreeUtils.asyncTokenOfEnclosingFunction(callExpression)
82+
.ifPresent(asyncKeyword -> {
83+
var issue = context.addIssue(callee, MESSAGE).secondary(asyncKeyword, SECONDARY_MESSAGE);
84+
addQuickFixes(issue, callExpression);
85+
});
86+
}
87+
88+
private void addQuickFixes(PreciseIssue issue, CallExpression callExpression) {
89+
asyncLibraryAliases.forEach((library, alias) -> {
90+
String message = String.format(SLEEP_QUICK_FIX, alias);
91+
issue.addQuickFix(createQuickFix(callExpression, alias + ".sleep", message));
92+
});
93+
}
94+
95+
private static PythonQuickFix createQuickFix(CallExpression callExpression, String replacementFunction, String message) {
96+
return PythonQuickFix.newQuickFix(message)
97+
.addTextEdit(TextEditUtils.replace(callExpression.callee(), replacementFunction))
98+
.build();
99+
}
100+
}
Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
<p>This rule raises an issue when a synchronous <code>time.sleep()</code> call is used within an asynchronous function.</p>
2+
<h2>Why is this an issue?</h2>
3+
<p>Synchronous functions like <code>time.sleep()</code> halt the execution of the current thread. In an asynchronous context, which relies on a
4+
single-threaded event loop to manage multiple tasks concurrently, calling <code>time.sleep()</code> blocks this entire event loop. When the event loop
5+
is blocked, it cannot switch between tasks, process I/O events, or respond to other operations. Consequently, all other concurrent asynchronous tasks
6+
are paused until the <code>time.sleep()</code> call completes. This effectively negates the benefits of using <code>async/await</code> by turning a
7+
non-blocking operation (waiting) into a blocking one that freezes the application’s concurrency model.</p>
8+
<p>For instance, if an asynchronous web server uses <code>time.sleep()</code> in a request handler, it won’t be able to process any other incoming
9+
requests until the sleep call completes, leading to poor performance and responsiveness.</p>
10+
<p>The correct approach in asynchronous programming is to use non-blocking sleep functions provided by the specific asynchronous framework being used
11+
(e.g., <code>asyncio</code>, <code>Trio</code>, <code>AnyIO</code>). These functions give control back to the event loop, allowing it to run other
12+
tasks while the current task is "sleeping".</p>
13+
<h2>How to fix it in Asyncio</h2>
14+
<h3>Code examples</h3>
15+
<h4>Noncompliant code example</h4>
16+
<pre data-diff-id="1" data-diff-type="noncompliant">
17+
import time
18+
import asyncio
19+
20+
async def sleeping_function():
21+
time.sleep(1) # Noncompliant
22+
23+
asyncio.run(sleeping_function())
24+
</pre>
25+
<h4>Compliant solution</h4>
26+
<pre data-diff-id="1" data-diff-type="compliant">
27+
import asyncio
28+
29+
async def sleeping_function():
30+
await asyncio.sleep(1)
31+
32+
asyncio.run(sleeping_function())
33+
</pre>
34+
<h2>How to fix it in Trio</h2>
35+
<h3>Code examples</h3>
36+
<h4>Noncompliant code example</h4>
37+
<pre data-diff-id="2" data-diff-type="noncompliant">
38+
import time
39+
import trio
40+
41+
async def sleeping_function():
42+
time.sleep(1) # Noncompliant
43+
44+
trio.run(sleeping_function)
45+
</pre>
46+
<h4>Compliant solution</h4>
47+
<pre data-diff-id="2" data-diff-type="compliant">
48+
import trio
49+
50+
async def sleeping_function():
51+
await trio.sleep(1)
52+
53+
trio.run(sleeping_function)
54+
</pre>
55+
<h2>How to fix it in AnyIO</h2>
56+
<h3>Code examples</h3>
57+
<h4>Noncompliant code example</h4>
58+
<pre data-diff-id="3" data-diff-type="noncompliant">
59+
import time
60+
import anyio
61+
62+
async def sleeping_function():
63+
time.sleep(1) # Noncompliant
64+
65+
anyio.run(sleeping_function)
66+
</pre>
67+
<h4>Compliant solution</h4>
68+
<pre data-diff-id="3" data-diff-type="compliant">
69+
import anyio
70+
71+
async def sleeping_function():
72+
await anyio.sleep(1)
73+
74+
anyio.run(sleeping_function)
75+
</pre>
76+
<h2>Resources</h2>
77+
<h3>Documentation</h3>
78+
<ul>
79+
<li> Asyncio documentation - <a href="https://docs.python.org/3/library/asyncio-task.html#asyncio.sleep">Sleeping</a> </li>
80+
<li> Trio documentation - <a href="https://trio.readthedocs.io/en/stable/reference-core.html#trio.sleep">Time and clocks</a> </li>
81+
<li> AnyIO documentation - <a href="https://anyio.readthedocs.io/en/stable/api.html#anyio.sleep">Event loop</a> </li>
82+
</ul>
83+
Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,26 @@
1+
{
2+
"title": "Use non-blocking sleep functions in asynchronous code",
3+
"type": "BUG",
4+
"status": "ready",
5+
"remediation": {
6+
"func": "Constant\/Issue",
7+
"constantCost": "5min"
8+
},
9+
"tags": [
10+
"async",
11+
"asyncio",
12+
"trio",
13+
"anyio"
14+
],
15+
"defaultSeverity": "Major",
16+
"ruleSpecification": "RSPEC-7488",
17+
"sqKey": "S7488",
18+
"scope": "All",
19+
"quickfix": "infeasible",
20+
"code": {
21+
"impacts": {
22+
"RELIABILITY": "HIGH"
23+
},
24+
"attribute": "COMPLETE"
25+
}
26+
}

python-checks/src/main/resources/org/sonar/l10n/py/rules/python/Sonar_way_profile.json

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,7 @@
253253
"S6982",
254254
"S6983",
255255
"S6984",
256-
"S6985"
256+
"S6985",
257+
"S7488"
257258
]
258259
}

0 commit comments

Comments
 (0)