Repository: pylint-dev/astroid

Issue:
`.arguments` property ignores keyword-only args, *args, and **kwargs
```python
>>> from astroid import extract_node
>>> node = extract_node("""def a(*args, b=None, c=None, **kwargs): ...""")
>>> node.args.arguments
[]
```

Expected to find all the arguments from the function signature.

The wanted data can be found here:

```python
>>> node.args.vararg
'args'
>>> node.args.kwarg
'kwargs'
>>> node.args.kwonlyargs
[<AssignName.b l.1 at 0x1048189b0>, <AssignName.c l.1 at 0x104818830>]
```

Discussed at https://github.com/pylint-dev/pylint/pull/7577#discussion_r989000829.

Notice that positional-only args are found for some reason 🤷 


Discussion:
Should the definition be changed as well? It states `args.arguments` returns required arguments, and AFAIK in the example none are required (I can call `a` without supplying any arguments).

I tried running the following:

```
>>> node = extract_node("""def a(kiwi, apple, *args, b=None, c=None, **kwargs): ...""")
>>> node.args.arguments
[<AssignName.kiwi l.1 at 0x7f5c55986b90>, <AssignName.apple l.1 at 0x7f5c55985a50>]
```

And it seems correct to me :thinking: 
https://github.com/pylint-dev/astroid/blob/fef38f2dd474b0dacd1dda3f15abbf61eb0e9a71/astroid/nodes/node_classes.py#L685-L688

The docstring seems to be correct?
Depends on how you parse the language. "positional and keyword" could describe the argument `kiwi` and exclude keyword-only arguments.

Essentially, the crux of this is whether we should
- leave the function as is, and audit everywhere that uses it (given that we keep finding bugs)
- change the function

@crazybolillo have you happened to sample the places that use this function to be able to offer a view on that? I'd be eager to hear it!
I think I got confused about the documentation :sob:. I was reading the docstring for `args` (`node.args.args` in the example):

https://github.com/pylint-dev/astroid/blob/d4f4452fe089f600bf9144ffdcd8e698816df3c2/astroid/nodes/node_classes.py#L554-L555

 but we are dealing with `arguments` (`node.args.arguments`). I will review the code further to see if I can come up with something



Patch:
diff --git a/astroid/arguments.py b/astroid/arguments.py
--- a/astroid/arguments.py
+++ b/astroid/arguments.py
@@ -181,7 +181,13 @@ def infer_argument(
 
         positional = self.positional_arguments[: len(funcnode.args.args)]
         vararg = self.positional_arguments[len(funcnode.args.args) :]
-        argindex = funcnode.args.find_argname(name)[0]
+
+        # preserving previous behavior, when vararg and kwarg were not included in find_argname results
+        if name in [funcnode.args.vararg, funcnode.args.kwarg]:
+            argindex = None
+        else:
+            argindex = funcnode.args.find_argname(name)[0]
+
         kwonlyargs = {arg.name for arg in funcnode.args.kwonlyargs}
         kwargs = {
             key: value
diff --git a/astroid/nodes/node_classes.py b/astroid/nodes/node_classes.py
--- a/astroid/nodes/node_classes.py
+++ b/astroid/nodes/node_classes.py
@@ -605,7 +605,9 @@ def _infer(
 DEPRECATED_ARGUMENT_DEFAULT = "DEPRECATED_ARGUMENT_DEFAULT"
 
 
-class Arguments(_base_nodes.AssignTypeNode):
+class Arguments(
+    _base_nodes.AssignTypeNode
+):  # pylint: disable=too-many-instance-attributes
     """Class representing an :class:`ast.arguments` node.
 
     An :class:`Arguments` node represents that arguments in a
@@ -704,7 +706,20 @@ class Arguments(_base_nodes.AssignTypeNode):
     kwargannotation: NodeNG | None
     """The type annotation for the variable length keyword arguments."""
 
-    def __init__(self, vararg: str | None, kwarg: str | None, parent: NodeNG) -> None:
+    vararg_node: AssignName | None
+    """The node for variable length arguments"""
+
+    kwarg_node: AssignName | None
+    """The node for variable keyword arguments"""
+
+    def __init__(
+        self,
+        vararg: str | None,
+        kwarg: str | None,
+        parent: NodeNG,
+        vararg_node: AssignName | None = None,
+        kwarg_node: AssignName | None = None,
+    ) -> None:
         """Almost all attributes can be None for living objects where introspection failed."""
         super().__init__(
             parent=parent,
@@ -720,6 +735,9 @@ def __init__(self, vararg: str | None, kwarg: str | None, parent: NodeNG) -> Non
         self.kwarg = kwarg
         """The name of the variable length keyword arguments."""
 
+        self.vararg_node = vararg_node
+        self.kwarg_node = kwarg_node
+
     # pylint: disable=too-many-arguments
     def postinit(
         self,
@@ -780,8 +798,21 @@ def fromlineno(self) -> int:
 
     @cached_property
     def arguments(self):
-        """Get all the arguments for this node, including positional only and positional and keyword"""
-        return list(itertools.chain((self.posonlyargs or ()), self.args or ()))
+        """Get all the arguments for this node. This includes:
+        * Positional only arguments
+        * Positional arguments
+        * Keyword arguments
+        * Variable arguments (.e.g *args)
+        * Variable keyword arguments (e.g **kwargs)
+        """
+        retval = list(itertools.chain((self.posonlyargs or ()), (self.args or ())))
+        if self.vararg_node:
+            retval.append(self.vararg_node)
+        retval += self.kwonlyargs or ()
+        if self.kwarg_node:
+            retval.append(self.kwarg_node)
+
+        return retval
 
     def format_args(self, *, skippable_names: set[str] | None = None) -> str:
         """Get the arguments formatted as string.
@@ -911,15 +942,20 @@ def default_value(self, argname):
         :raises NoDefault: If there is no default value defined for the
             given argument.
         """
-        args = self.arguments
+        args = [
+            arg for arg in self.arguments if arg.name not in [self.vararg, self.kwarg]
+        ]
+
+        index = _find_arg(argname, self.kwonlyargs)[0]
+        if index is not None and self.kw_defaults[index] is not None:
+            return self.kw_defaults[index]
+
         index = _find_arg(argname, args)[0]
         if index is not None:
-            idx = index - (len(args) - len(self.defaults))
+            idx = index - (len(args) - len(self.defaults) - len(self.kw_defaults))
             if idx >= 0:
                 return self.defaults[idx]
-        index = _find_arg(argname, self.kwonlyargs)[0]
-        if index is not None and self.kw_defaults[index] is not None:
-            return self.kw_defaults[index]
+
         raise NoDefault(func=self.parent, name=argname)
 
     def is_argument(self, name) -> bool:
@@ -934,11 +970,7 @@ def is_argument(self, name) -> bool:
             return True
         if name == self.kwarg:
             return True
-        return (
-            self.find_argname(name)[1] is not None
-            or self.kwonlyargs
-            and _find_arg(name, self.kwonlyargs)[1] is not None
-        )
+        return self.find_argname(name)[1] is not None
 
     def find_argname(self, argname, rec=DEPRECATED_ARGUMENT_DEFAULT):
         """Get the index and :class:`AssignName` node for given name.
@@ -956,7 +988,9 @@ def find_argname(self, argname, rec=DEPRECATED_ARGUMENT_DEFAULT):
                 stacklevel=2,
             )
         if self.arguments:
-            return _find_arg(argname, self.arguments)
+            index, argument = _find_arg(argname, self.arguments)
+            if argument:
+                return index, argument
         return None, None
 
     def get_children(self):
diff --git a/astroid/nodes/scoped_nodes/scoped_nodes.py b/astroid/nodes/scoped_nodes/scoped_nodes.py
--- a/astroid/nodes/scoped_nodes/scoped_nodes.py
+++ b/astroid/nodes/scoped_nodes/scoped_nodes.py
@@ -963,11 +963,7 @@ def argnames(self) -> list[str]:
             names = [elt.name for elt in self.args.arguments]
         else:
             names = []
-        if self.args.vararg:
-            names.append(self.args.vararg)
-        names += [elt.name for elt in self.args.kwonlyargs]
-        if self.args.kwarg:
-            names.append(self.args.kwarg)
+
         return names
 
     def infer_call_result(
@@ -1280,11 +1276,7 @@ def argnames(self) -> list[str]:
             names = [elt.name for elt in self.args.arguments]
         else:
             names = []
-        if self.args.vararg:
-            names.append(self.args.vararg)
-        names += [elt.name for elt in self.args.kwonlyargs]
-        if self.args.kwarg:
-            names.append(self.args.kwarg)
+
         return names
 
     def getattr(
diff --git a/astroid/protocols.py b/astroid/protocols.py
--- a/astroid/protocols.py
+++ b/astroid/protocols.py
@@ -352,14 +352,15 @@ def _arguments_infer_argname(
     # more
     from astroid import arguments  # pylint: disable=import-outside-toplevel
 
-    if not (self.arguments or self.vararg or self.kwarg):
+    if not self.arguments:
         yield util.Uninferable
         return
 
+    args = [arg for arg in self.arguments if arg.name not in [self.vararg, self.kwarg]]
     functype = self.parent.type
     # first argument of instance/class method
     if (
-        self.arguments
+        args
         and getattr(self.arguments[0], "name", None) == name
         and functype != "staticmethod"
     ):
@@ -388,7 +389,7 @@ def _arguments_infer_argname(
     if name == self.vararg:
         vararg = nodes.const_factory(())
         vararg.parent = self
-        if not self.arguments and self.parent.name == "__init__":
+        if not args and self.parent.name == "__init__":
             cls = self.parent.parent.scope()
             vararg.elts = [cls.instantiate_class()]
         yield vararg
diff --git a/astroid/rebuilder.py b/astroid/rebuilder.py
--- a/astroid/rebuilder.py
+++ b/astroid/rebuilder.py
@@ -21,6 +21,7 @@
 from astroid.const import IS_PYPY, PY38, PY39_PLUS, PY312_PLUS, Context
 from astroid.manager import AstroidManager
 from astroid.nodes import NodeNG
+from astroid.nodes.node_classes import AssignName
 from astroid.nodes.utils import Position
 from astroid.typing import InferenceResult
 
@@ -561,10 +562,33 @@ def visit_arguments(self, node: ast.arguments, parent: NodeNG) -> nodes.Argument
         """Visit an Arguments node by returning a fresh instance of it."""
         vararg: str | None = None
         kwarg: str | None = None
+        vararg_node = node.vararg
+        kwarg_node = node.kwarg
+
         newnode = nodes.Arguments(
             node.vararg.arg if node.vararg else None,
             node.kwarg.arg if node.kwarg else None,
             parent,
+            AssignName(
+                vararg_node.arg,
+                vararg_node.lineno,
+                vararg_node.col_offset,
+                parent,
+                end_lineno=vararg_node.end_lineno,
+                end_col_offset=vararg_node.end_col_offset,
+            )
+            if vararg_node
+            else None,
+            AssignName(
+                kwarg_node.arg,
+                kwarg_node.lineno,
+                kwarg_node.col_offset,
+                parent,
+                end_lineno=kwarg_node.end_lineno,
+                end_col_offset=kwarg_node.end_col_offset,
+            )
+            if kwarg_node
+            else None,
         )
         args = [self.visit(child, newnode) for child in node.args]
         defaults = [self.visit(child, newnode) for child in node.defaults]


Test patch:
diff --git a/tests/test_nodes.py b/tests/test_nodes.py
--- a/tests/test_nodes.py
+++ b/tests/test_nodes.py
@@ -22,6 +22,7 @@
     Uninferable,
     bases,
     builder,
+    extract_node,
     nodes,
     parse,
     test_utils,
@@ -1975,3 +1976,38 @@ def test_str_repr_no_warnings(node):
     test_node = node(**args)
     str(test_node)
     repr(test_node)
+
+
+def test_arguments_contains_all():
+    """Ensure Arguments.arguments actually returns all available arguments"""
+
+    def manually_get_args(arg_node) -> set:
+        names = set()
+        if arg_node.args.vararg:
+            names.add(arg_node.args.vararg)
+        if arg_node.args.kwarg:
+            names.add(arg_node.args.kwarg)
+
+        names.update([x.name for x in arg_node.args.args])
+        names.update([x.name for x in arg_node.args.kwonlyargs])
+
+        return names
+
+    node = extract_node("""def a(fruit: str, *args, b=None, c=None, **kwargs): ...""")
+    assert manually_get_args(node) == {x.name for x in node.args.arguments}
+
+    node = extract_node("""def a(mango: int, b="banana", c=None, **kwargs): ...""")
+    assert manually_get_args(node) == {x.name for x in node.args.arguments}
+
+    node = extract_node("""def a(self, num = 10, *args): ...""")
+    assert manually_get_args(node) == {x.name for x in node.args.arguments}
+
+
+def test_arguments_default_value():
+    node = extract_node(
+        "def fruit(eat='please', *, peel='no', trim='yes', **kwargs): ..."
+    )
+    assert node.args.default_value("eat").value == "please"
+
+    node = extract_node("def fruit(seeds, flavor='good', *, peel='maybe'): ...")
+    assert node.args.default_value("flavor").value == "good"