Repository: pylint-dev/pylint

Issue:
False positive for invalid-getnewargs-ex-returned
### Bug description

The error `invalid-getnewargs-ex-returned` is raised if the `tuple[tuple, dict]` is built from variables

```python
class A:
    def __init__(self, boo, far, *, hoo, haha):
        self._foo = boo
        self._bar = far
        self._hoo = hoo
        self._haha = haha

    def __getnewargs_ex__(self):
        args = (self._foo, self._bar)
        kwargs = {'hoo': self._hoo,
                  'haha': self._haha}
        return args, kwargs
```

### Command used

```shell
pylint pylint_bug.py
```

### Pylint output

```python
************* Module pylint_bug
pylint_bug.py:1:0: C0114: Missing module docstring (missing-module-docstring)
pylint_bug.py:3:0: C0115: Missing class docstring (missing-class-docstring)
pylint_bug.py:10:4: E0313: __getnewargs_ex__ does not return a tuple containing (tuple, dict) (invalid-getnewargs-ex-returned)
pylint_bug.py:3:0: R0903: Too few public methods (0/2) (too-few-public-methods)

------------------------------------------------------------------
Your code has been rated at 2.00/10 (previous run: 0.00/10, +2.00)
```

### Expected behavior

This code should not raise the error `invalid-getnewargs-ex-returned`

### Pylint version

```shell
pylint 3.3.4
astroid 3.3.8
Python 3.10.12 (main, Jan 17 2025, 14:35:34) [GCC 11.4.0]
```

### OS / Environment

Linux (Ubuntu)


Discussion:

Patch:
diff --git a/doc/whatsnew/fragments/10208.false_positive b/doc/whatsnew/fragments/10208.false_positive
new file mode 100644
index 0000000000..b2ab63045f
--- /dev/null
+++ b/doc/whatsnew/fragments/10208.false_positive
@@ -0,0 +1,3 @@
+Fix a false positive for ``invalid-getnewargs-ex-returned`` when the tuple or dict has been assigned to a name.
+
+Closes #10208
diff --git a/pylint/checkers/classes/special_methods_checker.py b/pylint/checkers/classes/special_methods_checker.py
index 025f285622..e1e19346dc 100644
--- a/pylint/checkers/classes/special_methods_checker.py
+++ b/pylint/checkers/classes/special_methods_checker.py
@@ -391,7 +391,7 @@ def _check_getnewargs_ex(
                 (inferred.elts[0], self._is_tuple),
                 (inferred.elts[1], self._is_dict),
             ):
-                if isinstance(arg, nodes.Call):
+                if isinstance(arg, (nodes.Call, nodes.Name)):
                     arg = safe_infer(arg)
 
                 if arg and not isinstance(arg, util.UninferableBase):


Test patch:
diff --git a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py
index efe6ba25fb..bb773b75bd 100644
--- a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py
+++ b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.py
@@ -30,6 +30,24 @@ class ThirdGoodGetNewArgsEx:
     """GetNewArgsEx through the metaclass."""
 
 
+class FourthGoodGetNewArgsEx:
+    """Test that `args` and `kwargs` (`Name` nodes) are inferred as tuples.
+
+    https://github.com/pylint-dev/pylint/issues/10208
+    """
+    def __init__(self, boo, far, *, hoo, haha):
+        self._foo = boo
+        self._bar = far
+        self._hoo = hoo
+        self._haha = haha
+
+    def __getnewargs_ex__(self):
+        args = (self._foo, self._bar)
+        kwargs = {'hoo': self._hoo,
+                  'haha': self._haha}
+        return args, kwargs
+
+
 class FirstBadGetNewArgsEx:
     """ __getnewargs_ex__ returns an integer """
 
diff --git a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.txt b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.txt
index b657c5f40b..f20a283621 100644
--- a/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.txt
+++ b/tests/functional/i/invalid/invalid_getnewargs/invalid_getnewargs_ex_returned.txt
@@ -1,6 +1,6 @@
-invalid-getnewargs-ex-returned:36:4:36:25:FirstBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
-invalid-getnewargs-ex-returned:43:4:43:25:SecondBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
-invalid-getnewargs-ex-returned:50:4:50:25:ThirdBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
-invalid-getnewargs-ex-returned:57:4:57:25:FourthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
-invalid-getnewargs-ex-returned:64:4:64:25:FifthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
-invalid-getnewargs-ex-returned:71:4:71:25:SixthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
+invalid-getnewargs-ex-returned:54:4:54:25:FirstBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
+invalid-getnewargs-ex-returned:61:4:61:25:SecondBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
+invalid-getnewargs-ex-returned:68:4:68:25:ThirdBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
+invalid-getnewargs-ex-returned:75:4:75:25:FourthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
+invalid-getnewargs-ex-returned:82:4:82:25:FifthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED
+invalid-getnewargs-ex-returned:89:4:89:25:SixthBadGetNewArgsEx.__getnewargs_ex__:__getnewargs_ex__ does not return a tuple containing (tuple, dict):UNDEFINED