[syntax-errors] Detect duplicate keys in match mapping patterns (#17129)

Summary
--

Detects duplicate literals in `match` mapping keys.

This PR also adds a `source` method to `SemanticSyntaxContext` to
display the duplicated key in the error message by slicing out its
range.

Test Plan
--

New inline tests.
This commit is contained in:
Brent Westbrook 2025-04-03 10:22:37 -04:00 committed by GitHub
parent ca0cce3f9c
commit 6e2b8f9696
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
7 changed files with 1298 additions and 6 deletions

View file

@ -89,7 +89,7 @@ fn test_valid_syntax(input_path: &Path) {
let parsed = parsed.try_into_module().expect("Parsed with Mode::Module");
let mut visitor = SemanticSyntaxCheckerVisitor::new(
TestContext::default().with_python_version(options.target_version()),
TestContext::new(&source).with_python_version(options.target_version()),
);
for stmt in parsed.suite() {
@ -185,7 +185,7 @@ fn test_invalid_syntax(input_path: &Path) {
let parsed = parsed.try_into_module().expect("Parsed with Mode::Module");
let mut visitor = SemanticSyntaxCheckerVisitor::new(
TestContext::default().with_python_version(options.target_version()),
TestContext::new(&source).with_python_version(options.target_version()),
);
for stmt in parsed.suite() {
@ -462,13 +462,22 @@ impl<'ast> SourceOrderVisitor<'ast> for ValidateAstVisitor<'ast> {
}
}
#[derive(Debug, Default)]
struct TestContext {
#[derive(Debug)]
struct TestContext<'a> {
diagnostics: RefCell<Vec<SemanticSyntaxError>>,
python_version: PythonVersion,
source: &'a str,
}
impl TestContext {
impl<'a> TestContext<'a> {
fn new(source: &'a str) -> Self {
Self {
diagnostics: RefCell::default(),
python_version: PythonVersion::default(),
source,
}
}
#[must_use]
fn with_python_version(mut self, python_version: PythonVersion) -> Self {
self.python_version = python_version;
@ -476,7 +485,7 @@ impl TestContext {
}
}
impl SemanticSyntaxContext for TestContext {
impl SemanticSyntaxContext for TestContext<'_> {
fn seen_docstring_boundary(&self) -> bool {
false
}
@ -489,6 +498,10 @@ impl SemanticSyntaxContext for TestContext {
self.diagnostics.borrow_mut().push(error);
}
fn source(&self) -> &str {
self.source
}
fn global(&self, _name: &str) -> Option<TextRange> {
None
}