1616 */
1717package org .sonar .python .checks ;
1818
19+ import static org .sonar .python .checks .hotspots .CommonValidationUtils .isEqualTo ;
20+
21+ import java .util .HashMap ;
22+ import java .util .List ;
1923import java .util .Map ;
24+ import java .util .Optional ;
25+
2026import org .sonar .check .Rule ;
2127import org .sonar .plugins .python .api .PythonSubscriptionCheck ;
2228import org .sonar .plugins .python .api .SubscriptionContext ;
29+ import org .sonar .plugins .python .api .quickfix .PythonQuickFix ;
30+ import org .sonar .plugins .python .api .tree .AliasedName ;
2331import org .sonar .plugins .python .api .tree .CallExpression ;
32+ import org .sonar .plugins .python .api .tree .ImportName ;
33+ import org .sonar .plugins .python .api .tree .Name ;
2434import org .sonar .plugins .python .api .tree .Tree ;
2535import org .sonar .plugins .python .api .tree .Tree .Kind ;
36+ import org .sonar .python .quickfix .TextEditUtils ;
2637import org .sonar .python .tree .TreeUtils ;
2738import org .sonar .python .types .v2 .TypeCheckMap ;
2839
29- import static org .sonar .python .checks .hotspots .CommonValidationUtils .isEqualTo ;
30-
3140@ Rule (key = "S7491" )
3241public class SleepZeroInAsyncCheck extends PythonSubscriptionCheck {
3342
3443 private static final String MESSAGE = "Use %s instead of %s." ;
3544 private static final String SECONDARY_MESSAGE = "This function is async." ;
36-
45+ private static final String QUICK_FIX_MESSAGE = "Replace with %s" ;
46+
3747 private TypeCheckMap <MessageHolder > asyncSleepFunctions ;
48+ private final Map <String , String > asyncLibraryAliases = new HashMap <>();
3849
3950 @ Override
4051 public void initialize (Context context ) {
41- context .registerSyntaxNodeConsumer (Kind .FILE_INPUT , this ::initializeTypeCheckMap );
52+ context .registerSyntaxNodeConsumer (Kind .FILE_INPUT , this ::initializeAnalysis );
53+ context .registerSyntaxNodeConsumer (Kind .IMPORT_NAME , this ::handleImportName );
4254 context .registerSyntaxNodeConsumer (Kind .CALL_EXPR , this ::checkCallExpr );
4355 }
4456
45- private void initializeTypeCheckMap (SubscriptionContext context ) {
57+ private void initializeAnalysis (SubscriptionContext context ) {
4658 asyncSleepFunctions = TypeCheckMap .ofEntries (
4759 Map .entry (context .typeChecker ().typeCheckBuilder ().isTypeWithFqn ("trio.sleep" ),
48- new MessageHolder ("trio.sleep" , "trio .lowlevel.checkpoint()" , "seconds" )),
60+ new MessageHolder ("trio.sleep" , "%s .lowlevel.checkpoint()" , "seconds" , "trio " )),
4961 Map .entry (context .typeChecker ().typeCheckBuilder ().isTypeOrInstanceWithName ("anyio.sleep" ),
50- new MessageHolder ("anyio.sleep" , "anyio.lowlevel.checkpoint()" , "delay" )));
62+ new MessageHolder ("anyio.sleep" , "%s.lowlevel.checkpoint()" , "delay" , "anyio" )));
63+
64+ asyncLibraryAliases .clear ();
65+ }
66+
67+ private void handleImportName (SubscriptionContext context ) {
68+ var importName = (ImportName ) context .syntaxNode ();
69+ importName .modules ().forEach (this ::trackModuleImport );
70+ }
71+
72+ private void trackModuleImport (AliasedName aliasedName ) {
73+ List <Name > names = aliasedName .dottedName ().names ();
74+ if (names .size () > 1 ) {
75+ return ;
76+ }
77+
78+ String moduleName = names .get (0 ).name ();
79+ if ("trio" .equals (moduleName ) || "anyio" .equals (moduleName )) {
80+ Name alias = aliasedName .alias ();
81+ String moduleAlias = alias != null ? alias .name () : moduleName ;
82+ asyncLibraryAliases .put (moduleName , moduleAlias );
83+ }
5184 }
5285
5386 private void checkCallExpr (SubscriptionContext context ) {
@@ -59,26 +92,46 @@ private void checkCallExpr(SubscriptionContext context) {
5992
6093 var callee = callExpr .callee ();
6194 asyncSleepFunctions .getOptionalForType (callee .typeV2 ()).ifPresent (
62- messageHolder -> messageHolder .handleCallExpr (context , callExpr , asyncKeyword ));
95+ messageHolder -> handleCallExpr (context , messageHolder , callExpr , asyncKeyword , asyncLibraryAliases ));
96+ }
97+
98+ private static void handleCallExpr (SubscriptionContext context , MessageHolder messageHolder , CallExpression callExpr , Tree asyncKeyword ,
99+ Map <String , String > asyncLibraryAliases ) {
100+ if (!isZero (callExpr , messageHolder .keywordArgumentName ())) {
101+ return ;
102+ }
103+
104+ var moduleAlias = asyncLibraryAliases .getOrDefault (messageHolder .libraryName (), messageHolder .libraryName ());
105+ var formattedReplacement = messageHolder .replacement ().formatted (moduleAlias );
106+
107+ var message = String .format (MESSAGE , formattedReplacement , messageHolder .fqn ());
108+ var issue = context .addIssue (callExpr , message );
109+ issue .secondary (asyncKeyword , SECONDARY_MESSAGE );
110+
111+ createQuickFix (messageHolder , callExpr , formattedReplacement , asyncLibraryAliases ).ifPresent (issue ::addQuickFix );
63112 }
64113
65- record MessageHolder (String fqn , String replacement , String keywordArgumentName ) {
66- public void handleCallExpr (SubscriptionContext ctx , CallExpression callExpr , Tree asyncKeyword ) {
67- if (!isZero (callExpr , keywordArgumentName )) {
68- return ;
69- }
70- var message = String .format (MESSAGE , replacement , fqn );
71- var issue = ctx .addIssue (callExpr , message );
72- issue .secondary (asyncKeyword , SECONDARY_MESSAGE );
114+ private static Optional <PythonQuickFix > createQuickFix (MessageHolder messageHolder , CallExpression callExpr , String formattedReplacement ,
115+ Map <String , String > asyncLibraryAliases ) {
116+ if (!asyncLibraryAliases .containsKey (messageHolder .libraryName ())) {
117+ return Optional .empty ();
73118 }
74119
75- private static boolean isZero (CallExpression callExpr , String keywordArgumentName ) {
76- var argument = TreeUtils .nthArgumentOrKeyword (0 , keywordArgumentName , callExpr .arguments ());
77- if (argument == null ) {
78- return false ;
79- }
80- return isEqualTo (argument .expression (), 0 );
120+ var quickFixMessage = String .format (QUICK_FIX_MESSAGE , formattedReplacement );
121+ var quickFix = PythonQuickFix .newQuickFix (quickFixMessage )
122+ .addTextEdit (TextEditUtils .replace (callExpr , formattedReplacement ))
123+ .build ();
124+ return Optional .of (quickFix );
125+ }
126+
127+ private static boolean isZero (CallExpression callExpr , String keywordArgumentName ) {
128+ var argument = TreeUtils .nthArgumentOrKeyword (0 , keywordArgumentName , callExpr .arguments ());
129+ if (argument == null ) {
130+ return false ;
81131 }
132+ return isEqualTo (argument .expression (), 0 );
133+ }
82134
135+ record MessageHolder (String fqn , String replacement , String keywordArgumentName , String libraryName ) {
83136 }
84137}
0 commit comments