Repository: pylint-dev/pylint

Issue:
# Fix undefined-variable false positive with metaclass in nested class

## Description

Fixes #10823
Closes #10823

When an imported name is used as a `metaclass=` argument inside a nested class within a method, pylint incorrectly flags subsequent uses of that name at module level as `undefined-variable`.

### Reproducer

```python
import abc

class Test:
    def test1(self):
        class A(metaclass=abc.ABCMeta):
            pass

abc.ABCMeta  # E0602: Undefined variable 'abc' (undefined-variable)
```

### Root cause

`_check_classdef_metaclasses()` walks enclosing scopes to find the name used as a metaclass and collects `(scope_locals_dict, name)` tuples. Then `_check_metaclasses()` calls `scope_locals.pop(name, None)` — removing the name entirely from `to_consume` in the enclosing scope.

This was intended to prevent unused-import/unused-variable false positives: the metaclass usage should "count" as using the name. But `pop()` removes the name without moving it to the consumer's `consumed` dict. Later references to the same name can't find it in either `to_consume` or `consumed`, so pylint reports `undefined-variable`.

### Fix

Replace the raw `scope_locals.pop()` with `NamesConsumer.mark_as_consumed()`. This properly moves the name from `to_consume` to `consumed`, preserving both behaviors:

1. The import is not flagged as unused (moved out of `to_consume`)
2. Later references can find the name in `consumed` (no `undefined-variable`)

The function signatures change to pass the `NamesConsumer` and `found_nodes` needed by `mark_as_consumed()`.

### Test

Added `tests/functional/u/undefined/undefined_variable_metaclass_nested.py` — reproducer from the issue.

Implementation patch:
diff --git a/pylint/checkers/variables.py b/pylint/checkers/variables.py
index 5add0732c0..fbb7f69072 100644
--- a/pylint/checkers/variables.py
+++ b/pylint/checkers/variables.py
@@ -3387,27 +3387,29 @@ def _check_imports(self, not_consumed: Consumption) -> None:
 
     def _check_metaclasses(self, node: nodes.Module | nodes.FunctionDef) -> None:
         """Update consumption analysis for metaclasses."""
-        consumed: list[tuple[Consumption, str]] = []
+        consumed: list[tuple[NamesConsumer, str, list[nodes.NodeNG]]] = []
 
         for child_node in node.get_children():
             if isinstance(child_node, nodes.ClassDef):
                 consumed.extend(self._check_classdef_metaclasses(child_node, node))
 
-        # Pop the consumed items, in order to avoid having
-        # unused-import and unused-variable false positives
-        for scope_locals, name in consumed:
-            scope_locals.pop(name, None)
+        # Mark the consumed items properly so they move from to_consume
+        # to consumed, avoiding unused-import/unused-variable false positives
+        # while still allowing subsequent references to resolve.
+        for consumer, name, found_nodes in consumed:
+            if name in consumer.to_consume:
+                consumer.mark_as_consumed(name, found_nodes)
 
     def _check_classdef_metaclasses(
         self,
         klass: nodes.ClassDef,
         parent_node: nodes.Module | nodes.FunctionDef,
-    ) -> list[tuple[Consumption, str]]:
+    ) -> list[tuple[NamesConsumer, str, list[nodes.NodeNG]]]:
         if not klass._metaclass:
             # Skip if this class doesn't use explicitly a metaclass, but inherits it from ancestors
             return []
 
-        consumed: list[tuple[Consumption, str]] = []
+        consumed: list[tuple[NamesConsumer, str, list[nodes.NodeNG]]] = []
         metaclass = klass.metaclass()
         name = ""
         match klass._metaclass:
@@ -3433,7 +3435,7 @@ def _check_classdef_metaclasses(
                 found_nodes = scope_locals.get(name, [])
                 for found_node in found_nodes:
                     if found_node.lineno <= klass.lineno:
-                        consumed.append((scope_locals, name))
+                        consumed.append((to_consume, name, found_nodes))
                         found = True
                         break
             # Check parent scope


Test patch:
diff --git a/tests/functional/u/undefined/undefined_variable_metaclass_nested.py b/tests/functional/u/undefined/undefined_variable_metaclass_nested.py
new file mode 100644
index 0000000000..b1a29d0c8e
--- /dev/null
+++ b/tests/functional/u/undefined/undefined_variable_metaclass_nested.py
@@ -0,0 +1,20 @@
+"""Tests for undefined-variable false positive when a name is used as a metaclass
+in a nested class inside a method, and then used again at module level.
+
+https://github.com/pylint-dev/pylint/issues/10823
+"""
+# pylint: disable=too-few-public-methods,missing-class-docstring,missing-function-docstring
+# pylint: disable=unused-variable,pointless-statement
+
+import abc
+
+
+class Test:
+    def test1(self):
+        class A(metaclass=abc.ABCMeta):
+            pass
+
+
+# This should NOT trigger undefined-variable — abc was imported at module level
+# and consumed by the metaclass usage, but it should still be resolvable.
+abc.ABCMeta