From 2b398e940f29d15c226338391c2f9aef59c01bf6 Mon Sep 17 00:00:00 2001 From: ghislainpiot Date: Thu, 5 Jun 2025 14:59:25 +0000 Subject: [PATCH 1/3] Create rule S7524: `async for` should be used with asynchronous iterators in `async` functions --- rules/S7524/metadata.json | 2 + rules/S7524/python/metadata.json | 25 +++++++ rules/S7524/python/rule.adoc | 112 +++++++++++++++++++++++++++++++ 3 files changed, 139 insertions(+) create mode 100644 rules/S7524/metadata.json create mode 100644 rules/S7524/python/metadata.json create mode 100644 rules/S7524/python/rule.adoc diff --git a/rules/S7524/metadata.json b/rules/S7524/metadata.json new file mode 100644 index 00000000000..2c63c085104 --- /dev/null +++ b/rules/S7524/metadata.json @@ -0,0 +1,2 @@ +{ +} diff --git a/rules/S7524/python/metadata.json b/rules/S7524/python/metadata.json new file mode 100644 index 00000000000..dd2a5d09e5d --- /dev/null +++ b/rules/S7524/python/metadata.json @@ -0,0 +1,25 @@ +{ + "title": "`async for` should be used with asynchronous iterators in `async` functions", + "type": "BUG", + "status": "ready", + "remediation": { + "func": "Constant/Issue", + "constantCost": "5min" + }, + "tags": [ + "async" + ], + "defaultSeverity": "Major", + "ruleSpecification": "RSPEC-7524", + "sqKey": "S7524", + "scope": "All", + "defaultQualityProfiles": ["Sonar way"], + "quickfix": "covered", + "code": { + "impacts": { + "MAINTAINABILITY": "LOW", + "RELIABILITY": "LOW" + }, + "attribute": "CONVENTIONAL" + } +} diff --git a/rules/S7524/python/rule.adoc b/rules/S7524/python/rule.adoc new file mode 100644 index 00000000000..faaa0ef6754 --- /dev/null +++ b/rules/S7524/python/rule.adoc @@ -0,0 +1,112 @@ +This rule raises an issue when a synchronous `for` loop is used with an iterator implementing both synchronous and asynchronous iteration protocols. + +== Why is this an issue? + +Some iterators implement both synchronous and asynchronous iteration protocols, meaning they provide both: +- `+__iter__+`/`+__next__+` methods for synchronous iteration +- `+__aiter__+`/`+__anext__+` methods for asynchronous iteration + +When working with such iterators inside `async` functions, a standard `for` loop bypasses their asynchronous capabilities. This can lead to missed opportunities for concurrent execution and inconsistent async/await patterns within your asynchronous code. + +Using `async for` ensures that the asynchronous capabilities of the iterator are utilized, maintaining consistency with the asynchronous programming model and potentially allowing for better performance through concurrent operations. + +=== What is the potential impact? + +Using a standard `for` loop with these iterators in async functions can lead to: + +* **Missed Concurrency**: The asynchronous iteration protocol may allow for concurrent operations or non-blocking I/O that are bypassed when using synchronous iteration. +* **Inconsistent Async Patterns**: Mixing synchronous iteration with asynchronous code makes the codebase less consistent and harder to reason about. +* **Performance Degradation**: Potential performance benefits from asynchronous iteration (such as yielding control to the event loop during I/O operations) are lost. +* **Code Clarity Issues**: Other developers may be confused about why an object with async capabilities is being used synchronously in an async context. + +== How to fix it + +Use the `async for` statement when iterating over such iterators within `async` functions. This ensures that the asynchronous iteration protocol is used, maintaining consistency with the async programming model. + +=== Code examples + +==== Noncompliant code example + +[source,python,diff-id=1,diff-type=noncompliant] +---- +import asyncio + +class DualProtocolIterator: + def __init__(self): + ... + + # Synchronous iteration protocol + def __iter__(self): + ... + + def __next__(self): + ... + + # Asynchronous iteration protocol + def __aiter__(self): + ... + + async def __anext__(self): + ... + +async def process_data(): + iterator = DualProtocolIterator() + + for item in iterator: # Noncompliant + ... +---- + +==== Compliant solution + +[source,python,diff-id=1,diff-type=compliant] +---- +import asyncio + +class DualProtocolIterator: + def __init__(self): + ... + + # Synchronous iteration protocol + def __iter__(self): + ... + + def __next__(self): + ... + + # Asynchronous iteration protocol + def __aiter__(self): + ... + + async def __anext__(self): + ... + +async def process_data(): + iterator = DualProtocolIterator() + + async for item in iterator: # Compliant + ... +---- + +ifdef::env-github,rspecator-view[] + +== Implementation Specification +(visible only on this page) + +=== Message + +Use 'async for' to iterate over dual-protocol iterators in 'async' functions. +Quickfix should be considered for implementation + +=== Highlighting + +* Primary location: The `for` keyword of the loop when used with a dual-protocol iterator inside an `async` function. +* Secondary locations: The `async` keyword of the enclosing function +endif::env-github,rspecator-view[] + +== Resources + +=== Documentation + +* Python Documentation - https://docs.python.org/3/reference/compound_stmts.html#the-async-for-statement[The async for statement] +* Python Documentation - https://docs.python.org/3/reference/datamodel.html#the-iterator-protocol[The iterator protocol] +* Python Documentation - https://docs.python.org/3/reference/datamodel.html#asynchronous-iterators[Asynchronous Iterators] From 8e6efa9ba85435caeb6ee1ac8c0eb0a17b83c0bd Mon Sep 17 00:00:00 2001 From: Ghislain Piot Date: Tue, 10 Jun 2025 12:20:57 +0200 Subject: [PATCH 2/3] Fix after review --- rules/S7524/python/metadata.json | 4 +-- rules/S7524/python/rule.adoc | 52 +++++++++++--------------------- 2 files changed, 19 insertions(+), 37 deletions(-) diff --git a/rules/S7524/python/metadata.json b/rules/S7524/python/metadata.json index dd2a5d09e5d..bbbe5437af9 100644 --- a/rules/S7524/python/metadata.json +++ b/rules/S7524/python/metadata.json @@ -1,6 +1,6 @@ { - "title": "`async for` should be used with asynchronous iterators in `async` functions", - "type": "BUG", + "title": "\"async for\" should be used for asynchronous iterators", + "type": "CODE_SMELL", "status": "ready", "remediation": { "func": "Constant/Issue", diff --git a/rules/S7524/python/rule.adoc b/rules/S7524/python/rule.adoc index faaa0ef6754..4c5792bad24 100644 --- a/rules/S7524/python/rule.adoc +++ b/rules/S7524/python/rule.adoc @@ -1,27 +1,19 @@ -This rule raises an issue when a synchronous `for` loop is used with an iterator implementing both synchronous and asynchronous iteration protocols. +This rule raises an issue when a synchronous `for` loop is used with an iterator implementing the asynchronous iteration protocol. == Why is this an issue? -Some iterators implement both synchronous and asynchronous iteration protocols, meaning they provide both: -- `+__iter__+`/`+__next__+` methods for synchronous iteration -- `+__aiter__+`/`+__anext__+` methods for asynchronous iteration - -When working with such iterators inside `async` functions, a standard `for` loop bypasses their asynchronous capabilities. This can lead to missed opportunities for concurrent execution and inconsistent async/await patterns within your asynchronous code. - -Using `async for` ensures that the asynchronous capabilities of the iterator are utilized, maintaining consistency with the asynchronous programming model and potentially allowing for better performance through concurrent operations. +When working inside an `async` function, you should use `async for` to iterate over asynchronous iterators. An asynchronous iterator is an object that provides `+__aiter__+` and `+__anext__+` methods and should be used instead of a synchronous `for` loop. === What is the potential impact? -Using a standard `for` loop with these iterators in async functions can lead to: +Not following the proper async pattern can lead to: -* **Missed Concurrency**: The asynchronous iteration protocol may allow for concurrent operations or non-blocking I/O that are bypassed when using synchronous iteration. -* **Inconsistent Async Patterns**: Mixing synchronous iteration with asynchronous code makes the codebase less consistent and harder to reason about. -* **Performance Degradation**: Potential performance benefits from asynchronous iteration (such as yielding control to the event loop during I/O operations) are lost. -* **Code Clarity Issues**: Other developers may be confused about why an object with async capabilities is being used synchronously in an async context. +* **Incorrect Logic**: It shows a misunderstanding of the asynchronous nature of the data source. +* **Inconsistent async usage**: Mixing synchronous and asynchronous iteration patterns makes code harder to read and maintain. == How to fix it -Use the `async for` statement when iterating over such iterators within `async` functions. This ensures that the asynchronous iteration protocol is used, maintaining consistency with the async programming model. +Use the `async for` statement to iterate over asynchronous iterators inside an `async` function. === Code examples @@ -29,30 +21,24 @@ Use the `async for` statement when iterating over such iterators within `async` [source,python,diff-id=1,diff-type=noncompliant] ---- -import asyncio - -class DualProtocolIterator: +class MyIterator: def __init__(self): ... - # Synchronous iteration protocol def __iter__(self): ... def __next__(self): ... - # Asynchronous iteration protocol def __aiter__(self): ... async def __anext__(self): ... -async def process_data(): - iterator = DualProtocolIterator() - - for item in iterator: # Noncompliant +async def main(): + for _ in MyIterator(): # Noncompliant ... ---- @@ -60,30 +46,24 @@ async def process_data(): [source,python,diff-id=1,diff-type=compliant] ---- -import asyncio - -class DualProtocolIterator: +class MyIterator: def __init__(self): ... - # Synchronous iteration protocol def __iter__(self): ... def __next__(self): ... - # Asynchronous iteration protocol def __aiter__(self): ... async def __anext__(self): ... -async def process_data(): - iterator = DualProtocolIterator() - - async for item in iterator: # Compliant +async def main(): + async for _ in MyIterator(): # Compliant ... ---- @@ -94,13 +74,15 @@ ifdef::env-github,rspecator-view[] === Message -Use 'async for' to iterate over dual-protocol iterators in 'async' functions. -Quickfix should be considered for implementation +Use "async for" for asynchronous iterators + +Consider implementing quickfix to replace the `for` keyword with `async for`. === Highlighting -* Primary location: The `for` keyword of the loop when used with a dual-protocol iterator inside an `async` function. +* Primary location: The `for` keyword of the loop. * Secondary locations: The `async` keyword of the enclosing function +* Secondary locations: The `+__aiter__+` and `+__anext__+` methods if we can get the location endif::env-github,rspecator-view[] == Resources From 66ac55671ae1aab6c5e3e31364d41d22f5a4023b Mon Sep 17 00:00:00 2001 From: Ghislain Piot Date: Fri, 13 Jun 2025 16:12:43 +0200 Subject: [PATCH 3/3] Update metadata.json --- rules/S7524/python/metadata.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rules/S7524/python/metadata.json b/rules/S7524/python/metadata.json index bbbe5437af9..2d1053825e4 100644 --- a/rules/S7524/python/metadata.json +++ b/rules/S7524/python/metadata.json @@ -9,7 +9,7 @@ "tags": [ "async" ], - "defaultSeverity": "Major", + "defaultSeverity": "Minor", "ruleSpecification": "RSPEC-7524", "sqKey": "S7524", "scope": "All",