Repository: browniebroke/django-codemod

Issue:
Local variable shadowing import from outer scope
**Describe the bug**

In case something is being renamed by a codemodder in the current file, and if a local variable shadows this name from the outer scope, the shadowed variable gets renamed as well.

The code to change should avoid these kind of shadowing, but ideally we should not be tricked by it.

**To Reproduce**

```python
from django.conf.urls import url
from django.urls import reverse_lazy


class MyModelAdmin(admin.ModelAdmin):
    def get_urls(self):
        return [
            url(r'^some-route/$', some_view, name=self.store_value_view_name),
        ] + super().get_urls()

    def get_result_url(self):
        url = reverse_lazy('some-url-name')
        return url
```

When running `URLTransformer` on this code, the variable `url` inside `get_result_url` gets incorrectly renamed to `re_path`.

**Additional context**
Add any other context about the problem here.



Discussion:
More problematic, the file also conatined a form class with an attribute called `url`:

```python
class MyFilter(BaseFilter):
    url = reverse_lazy('some-url-name')
```

The `BaseFilter` class is expecting this `url` attribute to be set in its subclasses, this name isn't shadowing the import.

Resolving patch:
diff --git a/django_codemod/visitors/base.py b/django_codemod/visitors/base.py
--- a/django_codemod/visitors/base.py
+++ b/django_codemod/visitors/base.py
@@ -7,6 +7,7 @@
     BaseExpression,
     BaseSmallStatement,
     Call,
+    CSTNode,
     ImportAlias,
     ImportFrom,
     ImportStar,
@@ -18,6 +19,7 @@
 from libcst import matchers as m
 from libcst.codemod import ContextAwareTransformer
 from libcst.codemod.visitors import AddImportsVisitor
+from libcst.metadata import Scope, ScopeProvider
 
 
 class BaseDjCodemodTransformer(ContextAwareTransformer, ABC):
@@ -93,6 +95,7 @@ def leave_ImportFrom(
             return super().leave_ImportFrom(original_node, updated_node)
         # This is a match
         new_names = list(self.gen_new_imported_names(updated_node.names))
+        self.save_import_scope(original_node)
         if not new_names:
             # Nothing left in the import statement: remove it
             return RemoveFromParent()
@@ -113,6 +116,22 @@ def gen_new_imported_names(
                     continue
             yield import_alias
 
+    def resolve_scope(self, node: CSTNode) -> Scope:
+        scopes_map = self.context.wrapper.resolve(ScopeProvider)
+        return scopes_map[node]
+
+    def save_import_scope(self, import_from: ImportFrom) -> None:
+        scope = self.resolve_scope(import_from)
+        self.context.scratch[self.ctx_key_import_scope] = scope
+
+    @property
+    def import_scope(self) -> Optional[Scope]:
+        return self.context.scratch.get(self.ctx_key_import_scope, None)
+
+    @property
+    def ctx_key_import_scope(self) -> str:
+        return f"{self.rename_from}-import_scope"
+
     @staticmethod
     def tidy_new_imported_names(
         new_names: Sequence[ImportAlias],
@@ -153,6 +172,7 @@ def leave_Name(self, original_node: Name, updated_node: Name) -> BaseExpression:
             self.is_imported_with_old_name
             and not self.entity_is_name_called
             and m.matches(updated_node, m.Name(value=self.old_name))
+            and self.resolve_scope(original_node) == self.import_scope
         ):
             return updated_node.with_changes(value=self.new_name)
         return super().leave_Name(original_node, updated_node)


Test patch:
diff --git a/tests/visitors/test_base.py b/tests/visitors/test_base.py
--- a/tests/visitors/test_base.py
+++ b/tests/visitors/test_base.py
@@ -167,6 +167,28 @@ def test_lambda_no_value(self) -> None:
         """
         self.assertCodemod(before, after)
 
+    def test_name_from_outer_scope(self) -> None:
+        """When import from outer scope has same name as function variable."""
+        before = """
+            from django.dummy.module import func
+
+            result = func()
+
+            def something():
+                func = get_func()
+                return func
+        """
+        after = """
+            from django.dummy.module import better_func
+
+            result = better_func()
+
+            def something():
+                func = get_func()
+                return func
+        """
+        self.assertCodemod(before, after)
+
 
 class OtherModuleFuncRenameTransformer(BaseFuncRenameTransformer):
     """Transformer with different module."""