[syntax-errors] Make duplicate parameter names a semantic error (#17131)

Status
--

This is a pretty minor change, but it was breaking a red-knot mdtest
until #17463 landed. Now this should close #11934 as the last syntax
error being tracked there!

Summary
--

Moves `Parser::validate_parameters` to
`SemanticSyntaxChecker::duplicate_parameter_name`.

Test Plan
--

Existing tests, with `## Errors` replaced with `## Semantic Syntax
Errors`.
This commit is contained in:
Brent Westbrook 2025-04-23 15:45:51 -04:00 committed by GitHub
parent 9db63fc58c
commit d5410ef9fe
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 70 additions and 44 deletions

View file

@ -615,7 +615,8 @@ impl SemanticSyntaxContext for Checker<'_> {
| SemanticSyntaxErrorKind::DuplicateMatchKey(_) | SemanticSyntaxErrorKind::DuplicateMatchKey(_)
| SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_) | SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_)
| SemanticSyntaxErrorKind::InvalidStarExpression | SemanticSyntaxErrorKind::InvalidStarExpression
| SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) => { | SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_)
| SemanticSyntaxErrorKind::DuplicateParameter(_) => {
if self.settings.preview.is_enabled() { if self.settings.preview.is_enabled() {
self.semantic_errors.borrow_mut().push(error); self.semantic_errors.borrow_mut().push(error);
} }

View file

@ -126,8 +126,6 @@ pub enum ParseErrorType {
/// A default value was found for a `*` or `**` parameter. /// A default value was found for a `*` or `**` parameter.
VarParameterWithDefault, VarParameterWithDefault,
/// A duplicate parameter was found in a function definition or lambda expression.
DuplicateParameter(String),
/// A keyword argument was repeated. /// A keyword argument was repeated.
DuplicateKeywordArgumentError(String), DuplicateKeywordArgumentError(String),
@ -285,9 +283,6 @@ impl std::fmt::Display for ParseErrorType {
f.write_str("Invalid augmented assignment target") f.write_str("Invalid augmented assignment target")
} }
ParseErrorType::InvalidDeleteTarget => f.write_str("Invalid delete target"), ParseErrorType::InvalidDeleteTarget => f.write_str("Invalid delete target"),
ParseErrorType::DuplicateParameter(arg_name) => {
write!(f, "Duplicate parameter {arg_name:?}")
}
ParseErrorType::DuplicateKeywordArgumentError(arg_name) => { ParseErrorType::DuplicateKeywordArgumentError(arg_name) => {
write!(f, "Duplicate keyword argument {arg_name:?}") write!(f, "Duplicate keyword argument {arg_name:?}")
} }

View file

@ -1,8 +1,6 @@
use compact_str::CompactString; use compact_str::CompactString;
use std::fmt::{Display, Write}; use std::fmt::{Display, Write};
use rustc_hash::{FxBuildHasher, FxHashSet};
use ruff_python_ast::name::Name; use ruff_python_ast::name::Name;
use ruff_python_ast::{ use ruff_python_ast::{
self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt, self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt,
@ -3339,10 +3337,6 @@ impl<'src> Parser<'src> {
parameters.range = self.node_range(start); parameters.range = self.node_range(start);
// test_err params_duplicate_names
// def foo(a, a=10, *a, a, a: str, **a): ...
self.validate_parameters(&parameters);
parameters parameters
} }
@ -3630,25 +3624,6 @@ impl<'src> Parser<'src> {
} }
} }
/// Validate that the given parameters doesn't have any duplicate names.
///
/// Report errors for all the duplicate names found.
fn validate_parameters(&mut self, parameters: &ast::Parameters) {
let mut all_arg_names =
FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher);
for parameter in parameters {
let range = parameter.name().range();
let param_name = parameter.name().as_str();
if !all_arg_names.insert(param_name) {
self.add_error(
ParseErrorType::DuplicateParameter(param_name.to_string()),
range,
);
}
}
}
/// Classify the `match` soft keyword token. /// Classify the `match` soft keyword token.
/// ///
/// # Panics /// # Panics

View file

@ -13,7 +13,7 @@ use ruff_python_ast::{
StmtImportFrom, StmtImportFrom,
}; };
use ruff_text_size::{Ranged, TextRange, TextSize}; use ruff_text_size::{Ranged, TextRange, TextSize};
use rustc_hash::FxHashSet; use rustc_hash::{FxBuildHasher, FxHashSet};
#[derive(Debug, Default)] #[derive(Debug, Default)]
pub struct SemanticSyntaxChecker { pub struct SemanticSyntaxChecker {
@ -74,8 +74,17 @@ impl SemanticSyntaxChecker {
visitor.visit_pattern(&case.pattern); visitor.visit_pattern(&case.pattern);
} }
} }
Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. }) Stmt::FunctionDef(ast::StmtFunctionDef {
| Stmt::ClassDef(ast::StmtClassDef { type_params, .. }) type_params,
parameters,
..
}) => {
if let Some(type_params) = type_params {
Self::duplicate_type_parameter_name(type_params, ctx);
}
Self::duplicate_parameter_name(parameters, ctx);
}
Stmt::ClassDef(ast::StmtClassDef { type_params, .. })
| Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => { | Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => {
if let Some(type_params) = type_params { if let Some(type_params) = type_params {
Self::duplicate_type_parameter_name(type_params, ctx); Self::duplicate_type_parameter_name(type_params, ctx);
@ -453,6 +462,32 @@ impl SemanticSyntaxChecker {
} }
} }
fn duplicate_parameter_name<Ctx: SemanticSyntaxContext>(
parameters: &ast::Parameters,
ctx: &Ctx,
) {
if parameters.len() < 2 {
return;
}
let mut all_arg_names =
FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher);
for parameter in parameters {
let range = parameter.name().range();
let param_name = parameter.name().as_str();
if !all_arg_names.insert(param_name) {
// test_err params_duplicate_names
// def foo(a, a=10, *a, a, a: str, **a): ...
Self::add_error(
ctx,
SemanticSyntaxErrorKind::DuplicateParameter(param_name.to_string()),
range,
);
}
}
}
fn irrefutable_match_case<Ctx: SemanticSyntaxContext>(stmt: &ast::StmtMatch, ctx: &Ctx) { fn irrefutable_match_case<Ctx: SemanticSyntaxContext>(stmt: &ast::StmtMatch, ctx: &Ctx) {
// test_ok irrefutable_case_pattern_at_end // test_ok irrefutable_case_pattern_at_end
// match x: // match x:
@ -646,6 +681,12 @@ impl SemanticSyntaxChecker {
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await); Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await); Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await);
} }
Expr::Lambda(ast::ExprLambda {
parameters: Some(parameters),
..
}) => {
Self::duplicate_parameter_name(parameters, ctx);
}
_ => {} _ => {}
} }
} }
@ -889,6 +930,9 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => { SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => {
write!(f, "{kind} outside of an asynchronous function") write!(f, "{kind} outside of an asynchronous function")
} }
SemanticSyntaxErrorKind::DuplicateParameter(name) => {
write!(f, r#"Duplicate parameter "{name}""#)
}
} }
} }
} }
@ -1200,6 +1244,16 @@ pub enum SemanticSyntaxErrorKind {
/// async with x: ... # error /// async with x: ... # error
/// ``` /// ```
AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind), AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind),
/// Represents a duplicate parameter name in a function or lambda expression.
///
/// ## Examples
///
/// ```python
/// def f(x, x): ...
/// lambda x, x: ...
/// ```
DuplicateParameter(String),
} }
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

View file

@ -284,44 +284,6 @@ Module(
``` ```
## Errors ## Errors
|
1 | lambda a, a: 1
| ^ Syntax Error: Duplicate parameter "a"
2 |
3 | lambda a, *, a: 1
|
|
1 | lambda a, a: 1
2 |
3 | lambda a, *, a: 1
| ^ Syntax Error: Duplicate parameter "a"
4 |
5 | lambda a, a=20: 1
|
|
3 | lambda a, *, a: 1
4 |
5 | lambda a, a=20: 1
| ^ Syntax Error: Duplicate parameter "a"
6 |
7 | lambda a, *a: 1
|
|
5 | lambda a, a=20: 1
6 |
7 | lambda a, *a: 1
| ^ Syntax Error: Duplicate parameter "a"
8 |
9 | lambda a, *, **a: 1
|
| |
7 | lambda a, *a: 1 7 | lambda a, *a: 1
8 | 8 |
@ -330,6 +292,46 @@ Module(
| |
## Semantic Syntax Errors
|
1 | lambda a, a: 1
| ^ Syntax Error: Duplicate parameter "a"
2 |
3 | lambda a, *, a: 1
|
|
1 | lambda a, a: 1
2 |
3 | lambda a, *, a: 1
| ^ Syntax Error: Duplicate parameter "a"
4 |
5 | lambda a, a=20: 1
|
|
3 | lambda a, *, a: 1
4 |
5 | lambda a, a=20: 1
| ^ Syntax Error: Duplicate parameter "a"
6 |
7 | lambda a, *a: 1
|
|
5 | lambda a, a=20: 1
6 |
7 | lambda a, *a: 1
| ^ Syntax Error: Duplicate parameter "a"
8 |
9 | lambda a, *, **a: 1
|
| |
7 | lambda a, *a: 1 7 | lambda a, *a: 1
8 | 8 |

View file

@ -1,7 +1,6 @@
--- ---
source: crates/ruff_python_parser/tests/fixtures.rs source: crates/ruff_python_parser/tests/fixtures.rs
input_file: crates/ruff_python_parser/resources/inline/err/params_duplicate_names.py input_file: crates/ruff_python_parser/resources/inline/err/params_duplicate_names.py
snapshot_kind: text
--- ---
## AST ## AST
@ -132,7 +131,7 @@ Module(
}, },
) )
``` ```
## Errors ## Semantic Syntax Errors
| |
1 | def foo(a, a=10, *a, a, a: str, **a): ... 1 | def foo(a, a=10, *a, a, a: str, **a): ...