Skip to content

Commit c078bc1

Browse files
ghislainpiotsonartech
authored andcommitted
SONARPY-2972 Implement quickfix for S7486 (#300)
GitOrigin-RevId: 27785a239c32d84d5368a1a17d8445df3cccaae5
1 parent 571f0de commit c078bc1

File tree

3 files changed

+154
-2
lines changed

3 files changed

+154
-2
lines changed

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

Lines changed: 48 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -16,44 +16,74 @@
1616
*/
1717
package org.sonar.python.checks;
1818

19+
import java.util.HashMap;
20+
import java.util.List;
21+
import java.util.Map;
1922
import java.util.Optional;
23+
2024
import org.sonar.check.Rule;
2125
import org.sonar.plugins.python.api.PythonSubscriptionCheck;
2226
import org.sonar.plugins.python.api.SubscriptionContext;
27+
import org.sonar.plugins.python.api.quickfix.PythonQuickFix;
28+
import org.sonar.plugins.python.api.tree.AliasedName;
2329
import org.sonar.plugins.python.api.tree.CallExpression;
2430
import org.sonar.plugins.python.api.tree.Expression;
31+
import org.sonar.plugins.python.api.tree.ImportName;
32+
import org.sonar.plugins.python.api.tree.Name;
2533
import org.sonar.plugins.python.api.tree.RegularArgument;
2634
import org.sonar.plugins.python.api.tree.Token;
2735
import org.sonar.plugins.python.api.tree.Tree;
2836
import org.sonar.plugins.python.api.types.v2.TriBool;
2937
import org.sonar.python.checks.hotspots.CommonValidationUtils;
38+
import org.sonar.python.quickfix.TextEditUtils;
3039
import org.sonar.python.tree.TreeUtils;
3140
import org.sonar.python.types.v2.TypeCheckBuilder;
3241

3342
@Rule(key = "S7486")
3443
public class AsyncLongSleepCheck extends PythonSubscriptionCheck {
3544

3645
private static final String MESSAGE = "Replace this call with \"%s.sleep_forever()\" as the sleep duration exceeds 24 hours.";
46+
private static final String SECONDARY_MESSAGE = "This function is async.";
47+
private static final String QUICK_FIX_MESSAGE = "Replace with %s";
3748
private static final int SECONDS_IN_DAY = 86400;
3849

3950
private TypeCheckBuilder isTrioSleepCall;
4051
private TypeCheckBuilder isAnyioSleepCall;
52+
private final Map<String, String> asyncLibraryAliases = new HashMap<>();
4153

4254
@Override
4355
public void initialize(Context context) {
4456
context.registerSyntaxNodeConsumer(Tree.Kind.FILE_INPUT, this::setupCheck);
57+
context.registerSyntaxNodeConsumer(Tree.Kind.IMPORT_NAME, this::checkImportName);
4558
context.registerSyntaxNodeConsumer(Tree.Kind.CALL_EXPR, this::checkAsyncLongSleep);
4659
}
4760

4861
private void setupCheck(SubscriptionContext ctx) {
4962
isTrioSleepCall = ctx.typeChecker().typeCheckBuilder().isTypeWithFqn("trio.sleep");
5063
isAnyioSleepCall = ctx.typeChecker().typeCheckBuilder().isTypeWithName("anyio.sleep");
64+
asyncLibraryAliases.clear();
65+
}
66+
67+
private void checkImportName(SubscriptionContext ctx) {
68+
ImportName importName = (ImportName) ctx.syntaxNode();
69+
for (AliasedName module : importName.modules()) {
70+
List<Name> names = module.dottedName().names();
71+
if (names.size() > 1) {
72+
continue;
73+
}
74+
String moduleName = names.get(0).name();
75+
Name aliasName = module.alias();
76+
String alias = aliasName != null ? aliasName.name() : moduleName;
77+
if ("trio".equals(moduleName) || "anyio".equals(moduleName)) {
78+
asyncLibraryAliases.put(moduleName, alias);
79+
}
80+
}
5181
}
5282

5383
private void checkAsyncLongSleep(SubscriptionContext context) {
5484
CallExpression callExpression = (CallExpression) context.syntaxNode();
5585
Expression callee = callExpression.callee();
56-
86+
5787
String libraryName;
5888
if (isTrioSleepCall.check(callee.typeV2()) == TriBool.TRUE) {
5989
libraryName = "trio";
@@ -74,11 +104,27 @@ private void checkAsyncLongSleep(SubscriptionContext context) {
74104
}
75105
if (CommonValidationUtils.isMoreThan(durationExpr, SECONDS_IN_DAY)) {
76106
String message = String.format(MESSAGE, libraryName);
77-
context.addIssue(callExpression, message).secondary(asyncToken, "This function is async.");
107+
var issue = context.addIssue(callExpression, message)
108+
.secondary(asyncToken, SECONDARY_MESSAGE);
109+
createQuickFix(libraryName, callExpression)
110+
.ifPresent(issue::addQuickFix);
78111
}
79112
}
80113

81114
private static Optional<Expression> extractDurationExpression(CallExpression callExpression, String paramName) {
82115
return Optional.ofNullable(TreeUtils.nthArgumentOrKeyword(0, paramName, callExpression.arguments())).map(RegularArgument::expression);
83116
}
117+
118+
private Optional<PythonQuickFix> createQuickFix(String libraryName, CallExpression callExpression) {
119+
String alias = asyncLibraryAliases.get(libraryName);
120+
if (alias == null) {
121+
return Optional.empty();
122+
}
123+
String replacement = alias + ".sleep_forever()";
124+
String quickFixMsg = String.format(QUICK_FIX_MESSAGE, replacement);
125+
return Optional.of(
126+
PythonQuickFix.newQuickFix(quickFixMsg)
127+
.addTextEdit(TextEditUtils.replace(callExpression, replacement))
128+
.build());
129+
}
84130
}

python-checks/src/test/java/org/sonar/python/checks/AsyncLongSleepCheckTest.java

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

19+
import java.util.stream.Stream;
20+
1921
import org.junit.jupiter.api.Test;
22+
import org.junit.jupiter.params.ParameterizedTest;
23+
import org.junit.jupiter.params.provider.Arguments;
24+
import org.junit.jupiter.params.provider.MethodSource;
25+
import org.sonar.python.checks.quickfix.PythonQuickFixVerifier;
2026
import org.sonar.python.checks.utils.PythonCheckVerifier;
2127

2228
class AsyncLongSleepCheckTest {
@@ -26,4 +32,103 @@ void test() {
2632
PythonCheckVerifier.verify("src/test/resources/checks/asyncLongSleep.py", new AsyncLongSleepCheck());
2733
}
2834

35+
@ParameterizedTest
36+
@MethodSource("quickFixTestCases")
37+
void quickFixTest(String testName, String before, String after, String expectedMessage) {
38+
var check = new AsyncLongSleepCheck();
39+
PythonQuickFixVerifier.verify(check, before, after);
40+
PythonQuickFixVerifier.verifyQuickFixMessages(check, before, expectedMessage);
41+
}
42+
43+
static Stream<Arguments> quickFixTestCases() {
44+
return Stream.of(
45+
Arguments.of("trio literal", """
46+
import trio
47+
48+
async def f():
49+
await trio.sleep(86401)
50+
""", """
51+
import trio
52+
53+
async def f():
54+
await trio.sleep_forever()
55+
""", "Replace with trio.sleep_forever()"),
56+
Arguments.of("anyio literal", """
57+
import anyio
58+
59+
async def f():
60+
await anyio.sleep(86401)
61+
""", """
62+
import anyio
63+
64+
async def f():
65+
await anyio.sleep_forever()
66+
""", "Replace with anyio.sleep_forever()"),
67+
Arguments.of("trio alias", """
68+
import trio as t
69+
70+
async def f():
71+
await t.sleep(86401)
72+
""", """
73+
import trio as t
74+
75+
async def f():
76+
await t.sleep_forever()
77+
""", "Replace with t.sleep_forever()"),
78+
Arguments.of("anyio alias", """
79+
import anyio as a
80+
81+
async def f():
82+
await a.sleep(86401)
83+
""", """
84+
import anyio as a
85+
86+
async def f():
87+
await a.sleep_forever()
88+
""", "Replace with a.sleep_forever()"));
89+
}
90+
91+
@ParameterizedTest
92+
@MethodSource("noQuickFixTestCases")
93+
void noQuickFixTest(String testName, String before) {
94+
var check = new AsyncLongSleepCheck();
95+
PythonQuickFixVerifier.verifyNoQuickFixes(check, before);
96+
}
97+
98+
static Stream<Arguments> noQuickFixTestCases() {
99+
return Stream.of(
100+
Arguments.of("submodule import", """
101+
import trio.sleep
102+
async def f():
103+
await trio.sleep(86401)
104+
"""),
105+
Arguments.of("from import", """
106+
from anyio import sleep
107+
async def f():
108+
await sleep(86401)
109+
"""));
110+
}
111+
112+
@Test
113+
void multipleLibrariesImportedTest() {
114+
var check = new AsyncLongSleepCheck();
115+
var before = """
116+
import trio as t
117+
import anyio as a
118+
119+
async def f():
120+
await t.sleep(86401)
121+
""";
122+
var after = """
123+
import trio as t
124+
import anyio as a
125+
126+
async def f():
127+
await t.sleep_forever()
128+
""";
129+
PythonQuickFixVerifier.verify(check, before, after);
130+
PythonQuickFixVerifier.verifyQuickFixMessages(check, before,
131+
"Replace with t.sleep_forever()");
132+
}
133+
29134
}

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

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
import anyio
33
import trio as t
44
import anyio as a
5+
import something_else
56

67
async def long_sleep_trio():
78
await trio.sleep(86401) # Noncompliant

0 commit comments

Comments
 (0)