mirror of
https://github.com/astral-sh/ruff.git
synced 2025-08-20 02:20:25 +00:00
Introduce ArgOrKeyword
to keep call parameter order (#7302)
## Motivation The `ast::Arguments` for call argument are split into positional arguments (args) and keywords arguments (keywords). We currently assume that call consists of first args and then keywords, which is generally the case, but not always: ```python f(*args, a=2, *args2, **kwargs) class A(*args, a=2, *args2, **kwargs): pass ``` The consequence is accidentally reordering arguments (https://github.com/astral-sh/ruff/pull/7268). ## Summary `Arguments::args_and_keywords` returns an iterator of an `ArgOrKeyword` enum that yields args and keywords in the correct order. I've fixed the obvious `args` and `keywords` usages, but there might be some cases with wrong assumptions remaining. ## Test Plan The generator got new test cases, otherwise the stacked PR (https://github.com/astral-sh/ruff/pull/7268) which uncovered this.
This commit is contained in:
parent
179128dc54
commit
56440ad835
8 changed files with 133 additions and 49 deletions
|
@ -1,5 +1,6 @@
|
|||
#![allow(clippy::derive_partial_eq_without_eq)]
|
||||
|
||||
use itertools::Itertools;
|
||||
use std::fmt;
|
||||
use std::fmt::Debug;
|
||||
use std::ops::Deref;
|
||||
|
@ -2177,6 +2178,34 @@ pub struct Arguments {
|
|||
pub keywords: Vec<Keyword>,
|
||||
}
|
||||
|
||||
/// An entry in the argument list of a function call.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub enum ArgOrKeyword<'a> {
|
||||
Arg(&'a Expr),
|
||||
Keyword(&'a Keyword),
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Expr> for ArgOrKeyword<'a> {
|
||||
fn from(arg: &'a Expr) -> Self {
|
||||
Self::Arg(arg)
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> From<&'a Keyword> for ArgOrKeyword<'a> {
|
||||
fn from(keyword: &'a Keyword) -> Self {
|
||||
Self::Keyword(keyword)
|
||||
}
|
||||
}
|
||||
|
||||
impl Ranged for ArgOrKeyword<'_> {
|
||||
fn range(&self) -> TextRange {
|
||||
match self {
|
||||
Self::Arg(arg) => arg.range(),
|
||||
Self::Keyword(keyword) => keyword.range(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Arguments {
|
||||
/// Return the number of positional and keyword arguments.
|
||||
pub fn len(&self) -> usize {
|
||||
|
@ -2212,6 +2241,46 @@ impl Arguments {
|
|||
.map(|keyword| &keyword.value)
|
||||
.or_else(|| self.find_positional(position))
|
||||
}
|
||||
|
||||
/// Return the positional and keyword arguments in the order of declaration.
|
||||
///
|
||||
/// Positional arguments are generally before keyword arguments, but star arguments are an
|
||||
/// exception:
|
||||
/// ```python
|
||||
/// class A(*args, a=2, *args2, **kwargs):
|
||||
/// pass
|
||||
///
|
||||
/// f(*args, a=2, *args2, **kwargs)
|
||||
/// ```
|
||||
/// where `*args` and `args2` are `args` while `a=1` and `kwargs` are `keywords`.
|
||||
///
|
||||
/// If you would just chain `args` and `keywords` the call would get reordered which we don't
|
||||
/// want. This function instead "merge sorts" them into the correct order.
|
||||
///
|
||||
/// Note that the order of evaluation is always first `args`, then `keywords`:
|
||||
/// ```python
|
||||
/// def f(*args, **kwargs):
|
||||
/// pass
|
||||
///
|
||||
/// def g(x):
|
||||
/// print(x)
|
||||
/// return x
|
||||
///
|
||||
///
|
||||
/// f(*g([1]), a=g(2), *g([3]), **g({"4": 5}))
|
||||
/// ```
|
||||
/// Output:
|
||||
/// ```text
|
||||
/// [1]
|
||||
/// [3]
|
||||
/// 2
|
||||
/// {'4': 5}
|
||||
/// ```
|
||||
pub fn arguments_source_order(&self) -> impl Iterator<Item = ArgOrKeyword<'_>> {
|
||||
let args = self.args.iter().map(ArgOrKeyword::Arg);
|
||||
let keywords = self.keywords.iter().map(ArgOrKeyword::Keyword);
|
||||
args.merge_by(keywords, |left, right| left.start() < right.start())
|
||||
}
|
||||
}
|
||||
|
||||
/// An AST node used to represent a sequence of type parameters.
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue