Skip to content

Commit d5410ef

Browse files
authored
[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`.
1 parent 9db63fc commit d5410ef

File tree

6 files changed

+70
-44
lines changed

6 files changed

+70
-44
lines changed

crates/ruff_linter/src/checkers/ast/mod.rs

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -615,7 +615,8 @@ impl SemanticSyntaxContext for Checker<'_> {
615615
| SemanticSyntaxErrorKind::DuplicateMatchKey(_)
616616
| SemanticSyntaxErrorKind::DuplicateMatchClassAttribute(_)
617617
| SemanticSyntaxErrorKind::InvalidStarExpression
618-
| SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_) => {
618+
| SemanticSyntaxErrorKind::AsyncComprehensionOutsideAsyncFunction(_)
619+
| SemanticSyntaxErrorKind::DuplicateParameter(_) => {
619620
if self.settings.preview.is_enabled() {
620621
self.semantic_errors.borrow_mut().push(error);
621622
}

crates/ruff_python_parser/src/error.rs

Lines changed: 0 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -126,8 +126,6 @@ pub enum ParseErrorType {
126126
/// A default value was found for a `*` or `**` parameter.
127127
VarParameterWithDefault,
128128

129-
/// A duplicate parameter was found in a function definition or lambda expression.
130-
DuplicateParameter(String),
131129
/// A keyword argument was repeated.
132130
DuplicateKeywordArgumentError(String),
133131

@@ -285,9 +283,6 @@ impl std::fmt::Display for ParseErrorType {
285283
f.write_str("Invalid augmented assignment target")
286284
}
287285
ParseErrorType::InvalidDeleteTarget => f.write_str("Invalid delete target"),
288-
ParseErrorType::DuplicateParameter(arg_name) => {
289-
write!(f, "Duplicate parameter {arg_name:?}")
290-
}
291286
ParseErrorType::DuplicateKeywordArgumentError(arg_name) => {
292287
write!(f, "Duplicate keyword argument {arg_name:?}")
293288
}

crates/ruff_python_parser/src/parser/statement.rs

Lines changed: 0 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
use compact_str::CompactString;
22
use std::fmt::{Display, Write};
33

4-
use rustc_hash::{FxBuildHasher, FxHashSet};
5-
64
use ruff_python_ast::name::Name;
75
use ruff_python_ast::{
86
self as ast, ExceptHandler, Expr, ExprContext, IpyEscapeKind, Operator, PythonVersion, Stmt,
@@ -3339,10 +3337,6 @@ impl<'src> Parser<'src> {
33393337

33403338
parameters.range = self.node_range(start);
33413339

3342-
// test_err params_duplicate_names
3343-
// def foo(a, a=10, *a, a, a: str, **a): ...
3344-
self.validate_parameters(&parameters);
3345-
33463340
parameters
33473341
}
33483342

@@ -3630,25 +3624,6 @@ impl<'src> Parser<'src> {
36303624
}
36313625
}
36323626

3633-
/// Validate that the given parameters doesn't have any duplicate names.
3634-
///
3635-
/// Report errors for all the duplicate names found.
3636-
fn validate_parameters(&mut self, parameters: &ast::Parameters) {
3637-
let mut all_arg_names =
3638-
FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher);
3639-
3640-
for parameter in parameters {
3641-
let range = parameter.name().range();
3642-
let param_name = parameter.name().as_str();
3643-
if !all_arg_names.insert(param_name) {
3644-
self.add_error(
3645-
ParseErrorType::DuplicateParameter(param_name.to_string()),
3646-
range,
3647-
);
3648-
}
3649-
}
3650-
}
3651-
36523627
/// Classify the `match` soft keyword token.
36533628
///
36543629
/// # Panics

crates/ruff_python_parser/src/semantic_errors.rs

Lines changed: 57 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ use ruff_python_ast::{
1313
StmtImportFrom,
1414
};
1515
use ruff_text_size::{Ranged, TextRange, TextSize};
16-
use rustc_hash::FxHashSet;
16+
use rustc_hash::{FxBuildHasher, FxHashSet};
1717

1818
#[derive(Debug, Default)]
1919
pub struct SemanticSyntaxChecker {
@@ -74,8 +74,17 @@ impl SemanticSyntaxChecker {
7474
visitor.visit_pattern(&case.pattern);
7575
}
7676
}
77-
Stmt::FunctionDef(ast::StmtFunctionDef { type_params, .. })
78-
| Stmt::ClassDef(ast::StmtClassDef { type_params, .. })
77+
Stmt::FunctionDef(ast::StmtFunctionDef {
78+
type_params,
79+
parameters,
80+
..
81+
}) => {
82+
if let Some(type_params) = type_params {
83+
Self::duplicate_type_parameter_name(type_params, ctx);
84+
}
85+
Self::duplicate_parameter_name(parameters, ctx);
86+
}
87+
Stmt::ClassDef(ast::StmtClassDef { type_params, .. })
7988
| Stmt::TypeAlias(ast::StmtTypeAlias { type_params, .. }) => {
8089
if let Some(type_params) = type_params {
8190
Self::duplicate_type_parameter_name(type_params, ctx);
@@ -453,6 +462,32 @@ impl SemanticSyntaxChecker {
453462
}
454463
}
455464

465+
fn duplicate_parameter_name<Ctx: SemanticSyntaxContext>(
466+
parameters: &ast::Parameters,
467+
ctx: &Ctx,
468+
) {
469+
if parameters.len() < 2 {
470+
return;
471+
}
472+
473+
let mut all_arg_names =
474+
FxHashSet::with_capacity_and_hasher(parameters.len(), FxBuildHasher);
475+
476+
for parameter in parameters {
477+
let range = parameter.name().range();
478+
let param_name = parameter.name().as_str();
479+
if !all_arg_names.insert(param_name) {
480+
// test_err params_duplicate_names
481+
// def foo(a, a=10, *a, a, a: str, **a): ...
482+
Self::add_error(
483+
ctx,
484+
SemanticSyntaxErrorKind::DuplicateParameter(param_name.to_string()),
485+
range,
486+
);
487+
}
488+
}
489+
}
490+
456491
fn irrefutable_match_case<Ctx: SemanticSyntaxContext>(stmt: &ast::StmtMatch, ctx: &Ctx) {
457492
// test_ok irrefutable_case_pattern_at_end
458493
// match x:
@@ -646,6 +681,12 @@ impl SemanticSyntaxChecker {
646681
Self::yield_outside_function(ctx, expr, YieldOutsideFunctionKind::Await);
647682
Self::await_outside_async_function(ctx, expr, AwaitOutsideAsyncFunctionKind::Await);
648683
}
684+
Expr::Lambda(ast::ExprLambda {
685+
parameters: Some(parameters),
686+
..
687+
}) => {
688+
Self::duplicate_parameter_name(parameters, ctx);
689+
}
649690
_ => {}
650691
}
651692
}
@@ -889,6 +930,9 @@ impl Display for SemanticSyntaxError {
889930
SemanticSyntaxErrorKind::AwaitOutsideAsyncFunction(kind) => {
890931
write!(f, "{kind} outside of an asynchronous function")
891932
}
933+
SemanticSyntaxErrorKind::DuplicateParameter(name) => {
934+
write!(f, r#"Duplicate parameter "{name}""#)
935+
}
892936
}
893937
}
894938
}
@@ -1200,6 +1244,16 @@ pub enum SemanticSyntaxErrorKind {
12001244
/// async with x: ... # error
12011245
/// ```
12021246
AwaitOutsideAsyncFunction(AwaitOutsideAsyncFunctionKind),
1247+
1248+
/// Represents a duplicate parameter name in a function or lambda expression.
1249+
///
1250+
/// ## Examples
1251+
///
1252+
/// ```python
1253+
/// def f(x, x): ...
1254+
/// lambda x, x: ...
1255+
/// ```
1256+
DuplicateParameter(String),
12031257
}
12041258

12051259
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]

crates/ruff_python_parser/tests/snapshots/invalid_syntax@expressions__lambda_duplicate_parameters.py.snap

Lines changed: 10 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,16 @@ Module(
284284
```
285285
## Errors
286286

287+
|
288+
7 | lambda a, *a: 1
289+
8 |
290+
9 | lambda a, *, **a: 1
291+
| ^^^ Syntax Error: Expected one or more keyword parameter after '*' separator
292+
|
293+
294+
295+
## Semantic Syntax Errors
296+
287297
|
288298
1 | lambda a, a: 1
289299
| ^ Syntax Error: Duplicate parameter "a"
@@ -322,14 +332,6 @@ Module(
322332
|
323333

324334

325-
|
326-
7 | lambda a, *a: 1
327-
8 |
328-
9 | lambda a, *, **a: 1
329-
| ^^^ Syntax Error: Expected one or more keyword parameter after '*' separator
330-
|
331-
332-
333335
|
334336
7 | lambda a, *a: 1
335337
8 |

crates/ruff_python_parser/tests/snapshots/invalid_syntax@params_duplicate_names.py.snap

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,6 @@
11
---
22
source: crates/ruff_python_parser/tests/fixtures.rs
33
input_file: crates/ruff_python_parser/resources/inline/err/params_duplicate_names.py
4-
snapshot_kind: text
54
---
65
## AST
76

@@ -132,7 +131,7 @@ Module(
132131
},
133132
)
134133
```
135-
## Errors
134+
## Semantic Syntax Errors
136135

137136
|
138137
1 | def foo(a, a=10, *a, a, a: str, **a): ...

0 commit comments

Comments
 (0)