[red-knot] Consider all definitions after terminal statements unreachable (#15676)

`FlowSnapshot` now tracks a `reachable` bool, which indicates whether we
have encountered a terminal statement on that control flow path. When
merging flow states together, we skip any that have been marked
unreachable. This ensures that bindings that can only be reached through
unreachable paths are not considered visible.

## Test Plan

The new mdtests failed (with incorrect `reveal_type` results, and
spurious `possibly-unresolved-reference` errors) before adding the new
visibility constraints.

---------

Co-authored-by: Carl Meyer <carl@astral.sh>
This commit is contained in:
Douglas Creager 2025-01-29 14:06:57 -05:00 committed by GitHub
parent e1c9d10863
commit 15d886a502
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
4 changed files with 686 additions and 22 deletions

View file

@ -0,0 +1,640 @@
# Terminal statements
## Introduction
Terminal statements complicate a naive control-flow analysis.
As a simple example:
```py
def f(cond: bool) -> str:
if cond:
x = "test"
else:
raise ValueError
return x
def g(cond: bool):
if cond:
x = "test"
reveal_type(x) # revealed: Literal["test"]
else:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
raise ValueError
reveal_type(x) # revealed: Literal["test"]
```
In `f`, we should be able to determine that the `else` branch ends in a terminal statement, and that
the `return` statement can only be executed when the condition is true. We should therefore consider
the reference always bound, even though `x` is only bound in the true branch.
Similarly, in `g`, we should see that the assignment of the value `"terminal"` can never be seen by
the final `reveal_type`.
## `return`
A `return` statement is terminal; bindings that occur before it are not visible after it.
```py
def resolved_reference(cond: bool) -> str:
if cond:
x = "test"
else:
return "early"
return x # no possibly-unresolved-reference diagnostic!
def return_in_then_branch(cond: bool):
if cond:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
return
else:
x = "test"
reveal_type(x) # revealed: Literal["test"]
reveal_type(x) # revealed: Literal["test"]
def return_in_else_branch(cond: bool):
if cond:
x = "test"
reveal_type(x) # revealed: Literal["test"]
else:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
return
reveal_type(x) # revealed: Literal["test"]
def return_in_both_branches(cond: bool):
if cond:
x = "terminal1"
reveal_type(x) # revealed: Literal["terminal1"]
return
else:
x = "terminal2"
reveal_type(x) # revealed: Literal["terminal2"]
return
def return_in_try(cond: bool):
x = "before"
try:
if cond:
x = "test"
return
except:
# TODO: Literal["before"]
reveal_type(x) # revealed: Literal["before", "test"]
else:
reveal_type(x) # revealed: Literal["before"]
finally:
reveal_type(x) # revealed: Literal["before", "test"]
reveal_type(x) # revealed: Literal["before", "test"]
def return_in_nested_then_branch(cond1: bool, cond2: bool):
if cond1:
x = "test1"
reveal_type(x) # revealed: Literal["test1"]
else:
if cond2:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
return
else:
x = "test2"
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test1", "test2"]
def return_in_nested_else_branch(cond1: bool, cond2: bool):
if cond1:
x = "test1"
reveal_type(x) # revealed: Literal["test1"]
else:
if cond2:
x = "test2"
reveal_type(x) # revealed: Literal["test2"]
else:
x = "terminal"
reveal_type(x) # revealed: Literal["terminal"]
return
reveal_type(x) # revealed: Literal["test2"]
reveal_type(x) # revealed: Literal["test1", "test2"]
def return_in_both_nested_branches(cond1: bool, cond2: bool):
if cond1:
x = "test"
reveal_type(x) # revealed: Literal["test"]
else:
x = "terminal0"
if cond2:
x = "terminal1"
reveal_type(x) # revealed: Literal["terminal1"]
return
else:
x = "terminal2"
reveal_type(x) # revealed: Literal["terminal2"]
return
reveal_type(x) # revealed: Literal["test"]
```
## `continue`
A `continue` statement jumps back to the top of the innermost loop. This makes it terminal within
the loop body: definitions before it are not visible after it within the rest of the loop body. They
are likely visible after the loop body, since loops do not introduce new scopes. (Statically known
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
loop are not visible outside of it.)
TODO: We are not currently modeling the cyclic control flow for loops, pending fixpoint support in
Salsa. The false positives in this section are because of that, and not our terminal statement
support. See [ruff#14160](https://github.com/astral-sh/ruff/issues/14160) for more details.
```py
def resolved_reference(cond: bool) -> str:
while True:
if cond:
x = "test"
else:
continue
return x
def continue_in_then_branch(cond: bool, i: int):
x = "before"
for _ in range(i):
if cond:
x = "continue"
reveal_type(x) # revealed: Literal["continue"]
continue
else:
x = "loop"
reveal_type(x) # revealed: Literal["loop"]
reveal_type(x) # revealed: Literal["loop"]
# TODO: Should be Literal["before", "loop", "continue"]
reveal_type(x) # revealed: Literal["before", "loop"]
def continue_in_else_branch(cond: bool, i: int):
x = "before"
for _ in range(i):
if cond:
x = "loop"
reveal_type(x) # revealed: Literal["loop"]
else:
x = "continue"
reveal_type(x) # revealed: Literal["continue"]
continue
reveal_type(x) # revealed: Literal["loop"]
# TODO: Should be Literal["before", "loop", "continue"]
reveal_type(x) # revealed: Literal["before", "loop"]
def continue_in_both_branches(cond: bool, i: int):
x = "before"
for _ in range(i):
if cond:
x = "continue1"
reveal_type(x) # revealed: Literal["continue1"]
continue
else:
x = "continue2"
reveal_type(x) # revealed: Literal["continue2"]
continue
# TODO: Should be Literal["before", "continue1", "continue2"]
reveal_type(x) # revealed: Literal["before"]
def continue_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
x = "before"
for _ in range(i):
if cond1:
x = "loop1"
reveal_type(x) # revealed: Literal["loop1"]
else:
if cond2:
x = "continue"
reveal_type(x) # revealed: Literal["continue"]
continue
else:
x = "loop2"
reveal_type(x) # revealed: Literal["loop2"]
reveal_type(x) # revealed: Literal["loop2"]
reveal_type(x) # revealed: Literal["loop1", "loop2"]
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
def continue_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
x = "before"
for _ in range(i):
if cond1:
x = "loop1"
reveal_type(x) # revealed: Literal["loop1"]
else:
if cond2:
x = "loop2"
reveal_type(x) # revealed: Literal["loop2"]
else:
x = "continue"
reveal_type(x) # revealed: Literal["continue"]
continue
reveal_type(x) # revealed: Literal["loop2"]
reveal_type(x) # revealed: Literal["loop1", "loop2"]
# TODO: Should be Literal["before", "loop1", "loop2", "continue"]
reveal_type(x) # revealed: Literal["before", "loop1", "loop2"]
def continue_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
x = "before"
for _ in range(i):
if cond1:
x = "loop"
reveal_type(x) # revealed: Literal["loop"]
else:
if cond2:
x = "continue1"
reveal_type(x) # revealed: Literal["continue1"]
continue
else:
x = "continue2"
reveal_type(x) # revealed: Literal["continue2"]
continue
reveal_type(x) # revealed: Literal["loop"]
# TODO: Should be Literal["before", "loop", "continue1", "continue2"]
reveal_type(x) # revealed: Literal["before", "loop"]
```
## `break`
A `break` statement jumps to the end of the innermost loop. This makes it terminal within the loop
body: definitions before it are not visible after it within the rest of the loop body. They are
likely visible after the loop body, since loops do not introduce new scopes. (Statically known
infinite loops are one exception — if control never leaves the loop body, bindings inside of the
loop are not visible outside of it.)
```py
def resolved_reference(cond: bool) -> str:
while True:
if cond:
x = "test"
else:
break
return x
return x # error: [unresolved-reference]
def break_in_then_branch(cond: bool, i: int):
x = "before"
for _ in range(i):
if cond:
x = "break"
reveal_type(x) # revealed: Literal["break"]
break
else:
x = "loop"
reveal_type(x) # revealed: Literal["loop"]
reveal_type(x) # revealed: Literal["loop"]
reveal_type(x) # revealed: Literal["before", "break", "loop"]
def break_in_else_branch(cond: bool, i: int):
x = "before"
for _ in range(i):
if cond:
x = "loop"
reveal_type(x) # revealed: Literal["loop"]
else:
x = "break"
reveal_type(x) # revealed: Literal["break"]
break
reveal_type(x) # revealed: Literal["loop"]
reveal_type(x) # revealed: Literal["before", "loop", "break"]
def break_in_both_branches(cond: bool, i: int):
x = "before"
for _ in range(i):
if cond:
x = "break1"
reveal_type(x) # revealed: Literal["break1"]
break
else:
x = "break2"
reveal_type(x) # revealed: Literal["break2"]
break
reveal_type(x) # revealed: Literal["before", "break1", "break2"]
def break_in_nested_then_branch(cond1: bool, cond2: bool, i: int):
x = "before"
for _ in range(i):
if cond1:
x = "loop1"
reveal_type(x) # revealed: Literal["loop1"]
else:
if cond2:
x = "break"
reveal_type(x) # revealed: Literal["break"]
break
else:
x = "loop2"
reveal_type(x) # revealed: Literal["loop2"]
reveal_type(x) # revealed: Literal["loop2"]
reveal_type(x) # revealed: Literal["loop1", "loop2"]
reveal_type(x) # revealed: Literal["before", "loop1", "break", "loop2"]
def break_in_nested_else_branch(cond1: bool, cond2: bool, i: int):
x = "before"
for _ in range(i):
if cond1:
x = "loop1"
reveal_type(x) # revealed: Literal["loop1"]
else:
if cond2:
x = "loop2"
reveal_type(x) # revealed: Literal["loop2"]
else:
x = "break"
reveal_type(x) # revealed: Literal["break"]
break
reveal_type(x) # revealed: Literal["loop2"]
reveal_type(x) # revealed: Literal["loop1", "loop2"]
reveal_type(x) # revealed: Literal["before", "loop1", "loop2", "break"]
def break_in_both_nested_branches(cond1: bool, cond2: bool, i: int):
x = "before"
for _ in range(i):
if cond1:
x = "loop"
reveal_type(x) # revealed: Literal["loop"]
else:
if cond2:
x = "break1"
reveal_type(x) # revealed: Literal["break1"]
break
else:
x = "break2"
reveal_type(x) # revealed: Literal["break2"]
break
reveal_type(x) # revealed: Literal["loop"]
reveal_type(x) # revealed: Literal["before", "loop", "break1", "break2"]
```
## `raise`
A `raise` statement is terminal. If it occurs in a lexically containing `try` statement, it will
jump to one of the `except` clauses (if it matches the value being raised), or to the `else` clause
(if none match). Currently, we assume definitions from before the `raise` are visible in all
`except` and `else` clauses. (In the future, we might analyze the `except` clauses to see which ones
match the value being raised, and limit visibility to those clauses.) Definitions from before the
`raise` are not visible in any `else` clause, but are visible in `except` clauses or after the
containing `try` statement (since control flow may have passed through an `except`).
Currently we assume that an exception could be raised anywhere within a `try` block. We may want to
implement a more precise understanding of where exceptions (barring `KeyboardInterrupt` and
`MemoryError`) can and cannot actually be raised.
```py
def raise_in_then_branch(cond: bool):
x = "before"
try:
if cond:
x = "raise"
reveal_type(x) # revealed: Literal["raise"]
raise ValueError
else:
x = "else"
reveal_type(x) # revealed: Literal["else"]
reveal_type(x) # revealed: Literal["else"]
except ValueError:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise", "else"]
except:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise", "else"]
else:
reveal_type(x) # revealed: Literal["else"]
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise", "else"]
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise", "else"]
def raise_in_else_branch(cond: bool):
x = "before"
try:
if cond:
x = "else"
reveal_type(x) # revealed: Literal["else"]
else:
x = "raise"
reveal_type(x) # revealed: Literal["raise"]
raise ValueError
reveal_type(x) # revealed: Literal["else"]
except ValueError:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise"]
except:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise"]
else:
reveal_type(x) # revealed: Literal["else"]
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise"]
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise"]
def raise_in_both_branches(cond: bool):
x = "before"
try:
if cond:
x = "raise1"
reveal_type(x) # revealed: Literal["raise1"]
raise ValueError
else:
x = "raise2"
reveal_type(x) # revealed: Literal["raise2"]
raise ValueError
except ValueError:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
except:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
else:
x = "unreachable"
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "raise1", "raise2"]
def raise_in_nested_then_branch(cond1: bool, cond2: bool):
x = "before"
try:
if cond1:
x = "else1"
reveal_type(x) # revealed: Literal["else1"]
else:
if cond2:
x = "raise"
reveal_type(x) # revealed: Literal["raise"]
raise ValueError
else:
x = "else2"
reveal_type(x) # revealed: Literal["else2"]
reveal_type(x) # revealed: Literal["else2"]
reveal_type(x) # revealed: Literal["else1", "else2"]
except ValueError:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
except:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
else:
reveal_type(x) # revealed: Literal["else1", "else2"]
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "raise", "else2"]
def raise_in_nested_else_branch(cond1: bool, cond2: bool):
x = "before"
try:
if cond1:
x = "else1"
reveal_type(x) # revealed: Literal["else1"]
else:
if cond2:
x = "else2"
reveal_type(x) # revealed: Literal["else2"]
else:
x = "raise"
reveal_type(x) # revealed: Literal["raise"]
raise ValueError
reveal_type(x) # revealed: Literal["else2"]
reveal_type(x) # revealed: Literal["else1", "else2"]
except ValueError:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
except:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
else:
reveal_type(x) # revealed: Literal["else1", "else2"]
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else1", "else2", "raise"]
def raise_in_both_nested_branches(cond1: bool, cond2: bool):
x = "before"
try:
if cond1:
x = "else"
reveal_type(x) # revealed: Literal["else"]
else:
if cond2:
x = "raise1"
reveal_type(x) # revealed: Literal["raise1"]
raise ValueError
else:
x = "raise2"
reveal_type(x) # revealed: Literal["raise2"]
raise ValueError
reveal_type(x) # revealed: Literal["else"]
except ValueError:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
except:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
else:
reveal_type(x) # revealed: Literal["else"]
finally:
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
# Exceptions can occur anywhere, so "before" and "raise" are valid possibilities
reveal_type(x) # revealed: Literal["before", "else", "raise1", "raise2"]
```
## Terminal in `try` with `finally` clause
TODO: we don't yet model that a `break` or `continue` in a `try` block will jump to a `finally`
clause before it jumps to end/start of the loop.
```py
def f():
x = 1
while True:
try:
break
finally:
x = 2
# TODO: should be Literal[2]
reveal_type(x) # revealed: Literal[1]
```
## Nested functions
Free references inside of a function body refer to variables defined in the containing scope.
Function bodies are _lazy scopes_: at runtime, these references are not resolved immediately at the
point of the function definition. Instead, they are resolved _at the time of the call_, which means
that their values (and types) can be different for different invocations. For simplicity, we instead
resolve free references _at the end of the containing scope_. That means that in the examples below,
all of the `x` bindings should be visible to the `reveal_type`, regardless of where we place the
`return` statements.
TODO: These currently produce the wrong results, but not because of our terminal statement support.
See [ruff#15777](https://github.com/astral-sh/ruff/issues/15777) for more details.
```py
def top_level_return(cond1: bool, cond2: bool):
x = 1
def g():
# TODO eliminate Unknown
reveal_type(x) # revealed: Unknown | Literal[1, 2, 3]
if cond1:
if cond2:
x = 2
else:
x = 3
return
def return_from_if(cond1: bool, cond2: bool):
x = 1
def g():
# TODO: Literal[1, 2, 3]
reveal_type(x) # revealed: Unknown | Literal[1]
if cond1:
if cond2:
x = 2
else:
x = 3
return
def return_from_nested_if(cond1: bool, cond2: bool):
x = 1
def g():
# TODO: Literal[1, 2, 3]
reveal_type(x) # revealed: Unknown | Literal[1, 3]
if cond1:
if cond2:
x = 2
return
else:
x = 3
```
## Statically known terminal statements
Terminal statements do not yet interact correctly with statically known bounds. In this example, we
should see that the `return` statement is always executed, and therefore that the `"b"` assignment
is not visible to the `reveal_type`.
```py
def _(cond: bool):
x = "a"
if cond:
x = "b"
if True:
return
# TODO: Literal["a"]
reveal_type(x) # revealed: Literal["a", "b"]
```

View file

@ -368,6 +368,12 @@ impl<'db> SemanticIndexBuilder<'db> {
.record_visibility_constraint(VisibilityConstraint::VisibleIf(constraint))
}
/// Records that all remaining statements in the current block are unreachable, and therefore
/// not visible.
fn mark_unreachable(&mut self) {
self.current_use_def_map_mut().mark_unreachable();
}
/// Records a [`VisibilityConstraint::Ambiguous`] constraint.
fn record_ambiguous_visibility(&mut self) -> ScopedVisibilityConstraintId {
self.current_use_def_map_mut()
@ -1019,11 +1025,6 @@ where
}
self.visit_body(body);
}
ast::Stmt::Break(_) => {
if self.loop_state().is_inside() {
self.loop_break_states.push(self.flow_snapshot());
}
}
ast::Stmt::For(
for_stmt @ ast::StmtFor {
@ -1270,6 +1271,21 @@ where
// - https://github.com/astral-sh/ruff/pull/13633#discussion_r1788626702
self.visit_body(finalbody);
}
ast::Stmt::Raise(_) | ast::Stmt::Return(_) | ast::Stmt::Continue(_) => {
walk_stmt(self, stmt);
// Everything in the current block after a terminal statement is unreachable.
self.mark_unreachable();
}
ast::Stmt::Break(_) => {
if self.loop_state().is_inside() {
self.loop_break_states.push(self.flow_snapshot());
}
// Everything in the current block after a terminal statement is unreachable.
self.mark_unreachable();
}
_ => {
walk_stmt(self, stmt);
}

View file

@ -476,6 +476,7 @@ impl std::iter::FusedIterator for DeclarationsIterator<'_, '_> {}
pub(super) struct FlowSnapshot {
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
scope_start_visibility: ScopedVisibilityConstraintId,
reachable: bool,
}
#[derive(Debug)]
@ -503,6 +504,8 @@ pub(super) struct UseDefMapBuilder<'db> {
/// Currently live bindings and declarations for each symbol.
symbol_states: IndexVec<ScopedSymbolId, SymbolState>,
reachable: bool,
}
impl Default for UseDefMapBuilder<'_> {
@ -515,11 +518,16 @@ impl Default for UseDefMapBuilder<'_> {
bindings_by_use: IndexVec::new(),
definitions_by_definition: FxHashMap::default(),
symbol_states: IndexVec::new(),
reachable: true,
}
}
}
impl<'db> UseDefMapBuilder<'db> {
pub(super) fn mark_unreachable(&mut self) {
self.reachable = false;
}
pub(super) fn add_symbol(&mut self, symbol: ScopedSymbolId) {
let new_symbol = self
.symbol_states
@ -656,6 +664,7 @@ impl<'db> UseDefMapBuilder<'db> {
FlowSnapshot {
symbol_states: self.symbol_states.clone(),
scope_start_visibility: self.scope_start_visibility,
reachable: self.reachable,
}
}
@ -678,12 +687,25 @@ impl<'db> UseDefMapBuilder<'db> {
num_symbols,
SymbolState::undefined(self.scope_start_visibility),
);
self.reachable = snapshot.reachable;
}
/// Merge the given snapshot into the current state, reflecting that we might have taken either
/// path to get here. The new state for each symbol should include definitions from both the
/// prior state and the snapshot.
pub(super) fn merge(&mut self, snapshot: FlowSnapshot) {
// Unreachable snapshots should not be merged: If the current snapshot is unreachable, it
// should be completely overwritten by the snapshot we're merging in. If the other snapshot
// is unreachable, we should return without merging.
if !snapshot.reachable {
return;
}
if !self.reachable {
self.restore(snapshot);
return;
}
// We never remove symbols from `symbol_states` (it's an IndexVec, and the symbol
// IDs must line up), so the current number of known symbols must always be equal to or
// greater than the number of known symbols in a previously-taken snapshot.
@ -705,6 +727,9 @@ impl<'db> UseDefMapBuilder<'db> {
self.scope_start_visibility = self
.visibility_constraints
.add_or_constraint(self.scope_start_visibility, snapshot.scope_start_visibility);
// Both of the snapshots are reachable, so the merged result is too.
self.reachable = true;
}
pub(super) fn finish(mut self) -> UseDefMap<'db> {

View file

@ -26,25 +26,8 @@ const TOMLLIB_312_URL: &str = "https://raw.githubusercontent.com/python/cpython/
static EXPECTED_DIAGNOSTICS: &[&str] = &[
// We don't support `*` imports yet:
"error[lint:unresolved-import] /src/tomllib/_parser.py:7:29 Module `collections.abc` has no member `Iterable`",
// We don't support terminal statements in control flow yet:
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:66:18 Name `s` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:98:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:101:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:104:14 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:115:14 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:126:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:348:20 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:353:5 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:453:24 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:455:9 Name `nest` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:482:16 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:566:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:573:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:579:12 Name `char` used when possibly not defined",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:580:63 Name `char` used when possibly not defined",
// We don't handle intersections in `is_assignable_to` yet
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:626:46 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_datetime`; expected type `Match`",
"warning[lint:possibly-unresolved-reference] /src/tomllib/_parser.py:629:38 Name `datetime_obj` used when possibly not defined",
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:632:58 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_localtime`; expected type `Match`",
"error[lint:invalid-argument-type] /src/tomllib/_parser.py:639:52 Object of type `Unknown & ~AlwaysFalsy | @Todo & ~AlwaysFalsy` cannot be assigned to parameter 1 (`match`) of function `match_to_number`; expected type `Match`",
"warning[lint:unused-ignore-comment] /src/tomllib/_parser.py:682:31 Unused blanket `type: ignore` directive",