refactor icon indicating copy to clipboard operation
refactor copied to clipboard

Decorators are not covered when replacing decorated definitions

Open isidentical opened this issue 3 years ago • 2 comments

Problem

Replace() action depends on the positions that are present in the original AST node. But for definitions like FunctionDef (and all other decorated ones), the lineno points to the introducer keyword (like def in FunctionDef) instead of the first decorator. So when we are replacing the whole function, we leave decorators behind which causes a lot of problems. We should account for this in Replace (and others where we care about the position).

Example Source Code

import ast
from refactor import Rule, Replace, run, common

class MakeFunctionsAsync(Rule):
    def match(self, node: ast.AST) -> Replace:
        assert isinstance(node, ast.FunctionDef)

        new_node = common.clone(node)
        new_node.__class__ = ast.AsyncFunctionDef
        return Replace(node, new_node)


if __name__ == "__main__":
    run([MakeFunctionsAsync])
def foo():
    pass


@deco
def bar():
    pass

isidentical avatar Aug 16 '22 20:08 isidentical

We need this feature as well, given we have some manual while loop based retry code can be converted to a retry decorator

wizpresso-steve-cy-fan avatar Oct 19 '22 06:10 wizpresso-steve-cy-fan

Adding some observation (bear in mind I may not fully understand the issue). When comparing the original FunctionDef to the updated AsyncFunctionDef they seem to have the same decorator_list:

FunctionDef(
    name='test_xxx',
    args=arguments(
        posonlyargs=[],
        args=[
            arg(arg='self'),
            arg(arg='mocked_api')],
        kwonlyargs=[],
        kw_defaults=[],
        defaults=[]),
    body=[
    ...
    decorator_list=[
        Call(
            func=Name(id='aioresponses', ctx=Load()),
            args=[],
            keywords=[])])

and

AsyncFunctionDef(
    name='test_xxx',
    args=arguments(
        posonlyargs=[],
        args=[
            arg(arg='self'),
            arg(arg='mocked_api')],
        kwonlyargs=[],
        kw_defaults=[],
        defaults=[]),
    body=[
    ...
    decorator_list=[
        Call(
            func=Name(id='aioresponses', ctx=Load()),
            args=[],
            keywords=[])])

But

@@ -181,72 +180,61 @@
         return data
 
     @aioresponses()
-    def test_xxx(self, mocked_api):
+    @aioresponses()
+    async def test_xxx(self, mocked_api):

Based on the diff, the decorator would be duplicated

MementoRC avatar Dec 02 '22 00:12 MementoRC