Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion crates/ruff_linter/src/checkers/ast/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -549,12 +549,14 @@ impl SemanticSyntaxContext for Checker<'_> {
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_)
if self.settings.preview.is_enabled() =>
{
self.semantic_errors.borrow_mut().push(error);
}
SemanticSyntaxErrorKind::ReboundComprehensionVariable
| SemanticSyntaxErrorKind::DuplicateTypeParameter => {}
| SemanticSyntaxErrorKind::DuplicateTypeParameter
| SemanticSyntaxErrorKind::MultipleCaseAssignment(_) => {}
}
}
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
match 2:
case x as x: ... # MatchAs
case [y, z, y]: ... # MatchSequence
case [y, z, *y]: ... # MatchSequence
case [y, y, y]: ... # MatchSequence multiple
case {1: x, 2: x}: ... # MatchMapping duplicate pattern
case {1: x, **x}: ... # MatchMapping duplicate in **rest
case Class(x, x): ... # MatchClass positional
case Class(x=1, x=2): ... # MatchClass keyword
case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
match 2:
case Class(x) | [x] | x: ...
125 changes: 122 additions & 3 deletions crates/ruff_python_parser/src/semantic_errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ use std::fmt::Display;
use ruff_python_ast::{
self as ast,
visitor::{walk_expr, Visitor},
Expr, PythonVersion, Stmt, StmtExpr, StmtImportFrom,
Expr, Pattern, PythonVersion, Stmt, StmtExpr, StmtImportFrom,
};
use ruff_text_size::{Ranged, TextRange};

Expand Down Expand Up @@ -61,6 +61,7 @@ impl SemanticSyntaxChecker {
}

Self::duplicate_type_parameter_name(stmt, ctx);
Self::multiple_case_assignment(stmt, ctx);
}

fn duplicate_type_parameter_name<Ctx: SemanticSyntaxContext>(stmt: &ast::Stmt, ctx: &Ctx) {
Expand Down Expand Up @@ -109,6 +110,21 @@ impl SemanticSyntaxChecker {
}
}

fn multiple_case_assignment<Ctx: SemanticSyntaxContext>(stmt: &Stmt, ctx: &Ctx) {
let Stmt::Match(ast::StmtMatch { cases, .. }) = stmt else {
return;
};

for case in cases {
let mut visitor = MultipleCaseAssignmentVisitor {
names: Vec::new(),
ctx,
};
visitor.visit_pattern(&case.pattern);
visitor.emit_errors();
}
}

pub fn visit_stmt<Ctx: SemanticSyntaxContext>(&mut self, stmt: &ast::Stmt, ctx: &Ctx) {
// update internal state
match stmt {
Expand Down Expand Up @@ -209,7 +225,7 @@ pub struct SemanticSyntaxError {

impl Display for SemanticSyntaxError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.kind {
match &self.kind {
SemanticSyntaxErrorKind::LateFutureImport => {
f.write_str("__future__ imports must be at the top of the file")
}
Expand All @@ -219,11 +235,14 @@ impl Display for SemanticSyntaxError {
SemanticSyntaxErrorKind::DuplicateTypeParameter => {
f.write_str("duplicate type parameter")
}
SemanticSyntaxErrorKind::MultipleCaseAssignment(name) => {
write!(f, "multiple assignments to name `{name}` in pattern")
}
}
}
}

#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum SemanticSyntaxErrorKind {
/// Represents the use of a `__future__` import after the beginning of a file.
///
Expand Down Expand Up @@ -265,6 +284,18 @@ pub enum SemanticSyntaxErrorKind {
/// class C[T, T]: ...
/// ```
DuplicateTypeParameter,

/// Represents a duplicate binding in a `case` pattern of a `match` statement.
///
/// ## Examples
///
/// ```python
/// match x:
/// case [x, y, x]: ...
/// case x as x: ...
/// case Class(x=1, x=2): ...
/// ```
MultipleCaseAssignment(ast::name::Name),
}

/// Searches for the first named expression (`x := y`) rebinding one of the `iteration_variables` in
Expand All @@ -291,6 +322,94 @@ impl Visitor<'_> for ReboundComprehensionVisitor<'_> {
}
}

struct MultipleCaseAssignmentVisitor<'a, Ctx> {
names: Vec<&'a ast::Identifier>,
ctx: &'a Ctx,
}

impl<'a, Ctx: SemanticSyntaxContext> MultipleCaseAssignmentVisitor<'a, Ctx> {
/// Emit [`SemanticSyntaxError`]s for every duplicate variable assignment.
fn emit_errors(mut self) {
self.names.sort_by_key(|name| &name.id);
for (n1, n2) in self.names.iter().zip(self.names.iter().skip(1)) {
if n1.id == n2.id {
SemanticSyntaxChecker::add_error(
self.ctx,
SemanticSyntaxErrorKind::MultipleCaseAssignment(n2.id.clone()),
n2.range,
);
}
}
}

fn visit_pattern(&mut self, pattern: &'a Pattern) {
// test_err multiple_assignment_in_case_pattern
// match 2:
// case x as x: ... # MatchAs
// case [y, z, y]: ... # MatchSequence
// case [y, z, *y]: ... # MatchSequence
// case [y, y, y]: ... # MatchSequence multiple
// case {1: x, 2: x}: ... # MatchMapping duplicate pattern
// case {1: x, **x}: ... # MatchMapping duplicate in **rest
// case Class(x, x): ... # MatchClass positional
// case Class(x=1, x=2): ... # MatchClass keyword
// case [x] | {1: x} | Class(x=1, x=2): ... # MatchOr

// test_ok multiple_assignment_in_case_pattern
// match 2:
// case Class(x) | [x] | x: ...
match pattern {
Pattern::MatchValue(_) | Pattern::MatchSingleton(_) => {}
Pattern::MatchStar(ast::PatternMatchStar { name, .. }) => {
if let Some(name) = name {
self.names.push(name);
}
}
Pattern::MatchSequence(ast::PatternMatchSequence { patterns, .. }) => {
for pattern in patterns {
self.visit_pattern(pattern);
}
}
Pattern::MatchMapping(ast::PatternMatchMapping { patterns, rest, .. }) => {
for pattern in patterns {
self.visit_pattern(pattern);
}
if let Some(rest) = rest {
self.names.push(rest);
}
}
Pattern::MatchClass(ast::PatternMatchClass { arguments, .. }) => {
for pattern in &arguments.patterns {
self.visit_pattern(pattern);
}
for keyword in &arguments.keywords {
self.names.push(&keyword.attr);
self.visit_pattern(&keyword.pattern);
}
}
Pattern::MatchAs(ast::PatternMatchAs { pattern, name, .. }) => {
if let Some(pattern) = pattern {
self.visit_pattern(pattern);
}
if let Some(name) = name {
self.names.push(name);
}
}
Pattern::MatchOr(ast::PatternMatchOr { patterns, .. }) => {
// each of these patterns should be visited separately
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think it would be useful to provide the reason for why should this be done. From what I understand, I think it's because they're separate patterns and checking for duplicate names are isolated to a single pattern.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I expanded the comment and also moved the relevant test_ok case down.

I also switched to the hash set implementation we discussed yesterday and cleaned up some of the docs and code locations.

for pattern in patterns {
let mut visitor = Self {
names: Vec::new(),
ctx: self.ctx,
};
visitor.visit_pattern(pattern);
visitor.emit_errors();
}
}
}
}
}

pub trait SemanticSyntaxContext {
/// Returns `true` if a module's docstring boundary has been passed.
fn seen_docstring_boundary(&self) -> bool;
Expand Down
Loading
Loading