From 339167d372546226229e74648c464221f0a57b3b Mon Sep 17 00:00:00 2001 From: David Salvisberg Date: Wed, 8 Jan 2025 13:09:06 +0100 Subject: [PATCH] [`flake8-type-checking`] Apply `TC008` more eagerly in `TYPE_CHECKING` blocks and disapply it in stubs (#15180) Co-authored-by: Alex Waygood --- .../TC008_typing_execution_context.py | 31 +++ crates/ruff_linter/src/checkers/ast/mod.rs | 3 +- .../src/rules/flake8_type_checking/mod.rs | 1 + .../rules/type_alias_quotes.rs | 61 ++++- ...ias_TC008_typing_execution_context.py.snap | 254 ++++++++++++++++++ .../pyupgrade/rules/quoted_annotation.rs | 6 + .../ruff/rules/missing_fstring_syntax.rs | 3 +- crates/ruff_python_semantic/src/model.rs | 48 +++- 8 files changed, 387 insertions(+), 20 deletions(-) create mode 100644 crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py create mode 100644 crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap diff --git a/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py new file mode 100644 index 0000000000000..86e7ec4a4a3b8 --- /dev/null +++ b/crates/ruff_linter/resources/test/fixtures/flake8_type_checking/TC008_typing_execution_context.py @@ -0,0 +1,31 @@ +from __future__ import annotations + +from typing import TypeAlias, TYPE_CHECKING + +from foo import Foo + +if TYPE_CHECKING: + from typing import Dict + + OptStr: TypeAlias = str | None + Bar: TypeAlias = Foo[int] + + a: TypeAlias = 'int' # TC008 + b: TypeAlias = 'Dict' # TC008 + c: TypeAlias = 'Foo' # TC008 + d: TypeAlias = 'Foo[str]' # TC008 + e: TypeAlias = 'Foo.bar' # TC008 + f: TypeAlias = 'Foo | None' # TC008 + g: TypeAlias = 'OptStr' # TC008 + h: TypeAlias = 'Bar' # TC008 + i: TypeAlias = Foo['str'] # TC008 + j: TypeAlias = 'Baz' # OK (this would be treated as use before define) + k: TypeAlias = 'k | None' # False negative in type checking block + l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + m: TypeAlias = ('int' # TC008 + | None) + n: TypeAlias = ('int' # TC008 (fix removes comment currently) + ' | None') + + + class Baz: ... diff --git a/crates/ruff_linter/src/checkers/ast/mod.rs b/crates/ruff_linter/src/checkers/ast/mod.rs index 138125851612b..d6a35b6a0696e 100644 --- a/crates/ruff_linter/src/checkers/ast/mod.rs +++ b/crates/ruff_linter/src/checkers/ast/mod.rs @@ -2352,7 +2352,8 @@ impl<'a> Checker<'a> { let parsed_expr = parsed_annotation.expression(); self.visit_expr(parsed_expr); if self.semantic.in_type_alias_value() { - if self.enabled(Rule::QuotedTypeAlias) { + // stub files are covered by PYI020 + if !self.source_type.is_stub() && self.enabled(Rule::QuotedTypeAlias) { flake8_type_checking::rules::quoted_type_alias( self, parsed_expr, diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs index b38b03fcf8aa5..3535901c90b34 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/mod.rs @@ -67,6 +67,7 @@ mod tests { // so we want to make sure their fixes are not going around in circles. #[test_case(Rule::UnquotedTypeAlias, Path::new("TC007.py"))] #[test_case(Rule::QuotedTypeAlias, Path::new("TC008.py"))] + #[test_case(Rule::QuotedTypeAlias, Path::new("TC008_typing_execution_context.py"))] fn type_alias_rules(rule_code: Rule, path: &Path) -> Result<()> { let snapshot = format!("{}_{}", rule_code.as_ref(), path.to_string_lossy()); let diagnostics = test_path( diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs index 47411228fdcc9..e2f3c7a04e6c7 100644 --- a/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs +++ b/crates/ruff_linter/src/rules/flake8_type_checking/rules/type_alias_quotes.rs @@ -4,7 +4,7 @@ use ruff_diagnostics::{AlwaysFixableViolation, Diagnostic, Edit, Fix, FixAvailab use ruff_macros::{derive_message_formats, ViolationMetadata}; use ruff_python_ast as ast; use ruff_python_ast::{Expr, Stmt}; -use ruff_python_semantic::{Binding, SemanticModel}; +use ruff_python_semantic::{Binding, SemanticModel, TypingOnlyBindingsStatus}; use ruff_python_stdlib::typing::{is_pep_593_generic_type, is_standard_library_literal}; use ruff_text_size::Ranged; @@ -68,14 +68,20 @@ impl Violation for UnquotedTypeAlias { /// /// ## Why is this bad? /// Unnecessary string forward references can lead to additional overhead -/// in runtime libraries making use of type hints, as well as lead to bad +/// in runtime libraries making use of type hints. They can also have bad /// interactions with other runtime uses like [PEP 604] type unions. /// -/// For explicit type aliases the quotes are only considered redundant -/// if the type expression contains no subscripts or attribute accesses -/// this is because of stubs packages. Some types will only be subscriptable -/// at type checking time, similarly there may be some module-level -/// attributes like type aliases that are only available in the stubs. +/// PEP-613 type aliases are only flagged by the rule if Ruff can have high +/// confidence that the quotes are unnecessary. Specifically, any PEP-613 +/// type alias where the type expression on the right-hand side contains +/// subscripts or attribute accesses will not be flagged. This is because +/// type aliases can reference types that are, for example, generic in stub +/// files but not at runtime. That can mean that a type checker expects the +/// referenced type to be subscripted with type arguments despite the fact +/// that doing so would fail at runtime if the type alias value was not +/// quoted. Similarly, a type alias might need to reference a module-level +/// attribute that exists in a stub file but not at runtime, meaning that +/// the type alias value would need to be quoted to avoid a runtime error. /// /// ## Example /// Given: @@ -101,6 +107,15 @@ impl Violation for UnquotedTypeAlias { /// ## Fix safety /// This rule's fix is marked as safe, unless the type annotation contains comments. /// +/// ## See also +/// This rule only applies to type aliases in non-stub files. For removing quotes in other +/// contexts or in stub files, see: +/// +/// - [`quoted-annotation-in-stub`](quoted-annotation-in-stub.md): A rule that +/// removes all quoted annotations from stub files +/// - [`quoted-annotation`](quoted-annotation.md): A rule that removes unnecessary quotes +/// from *annotations* in runtime files. +/// /// ## References /// - [PEP 613 – Explicit Type Aliases](https://peps.python.org/pep-0613/) /// - [PEP 695: Generic Type Alias](https://peps.python.org/pep-0695/#generic-type-alias) @@ -219,7 +234,11 @@ fn collect_typing_references<'a>( let Some(binding_id) = checker.semantic().resolve_name(name) else { return; }; - if checker.semantic().simulate_runtime_load(name).is_some() { + if checker + .semantic() + .simulate_runtime_load(name, TypingOnlyBindingsStatus::Disallowed) + .is_some() + { return; } @@ -291,11 +310,22 @@ fn quotes_are_unremovable(semantic: &SemanticModel, expr: &Expr) -> bool { ctx: ExprContext::Load, .. }) => quotes_are_unremovable(semantic, value), - // for subscripts and attributes we don't know whether it's safe - // to do at runtime, since the operation may only be available at - // type checking time. E.g. stubs only generics. Or stubs only - // type aliases. - Expr::Subscript(_) | Expr::Attribute(_) => true, + Expr::Subscript(ast::ExprSubscript { value, slice, .. }) => { + // for subscripts we don't know whether it's safe to do at runtime + // since the operation may only be available at type checking time. + // E.g. stubs only generics. + if !semantic.in_type_checking_block() { + return true; + } + quotes_are_unremovable(semantic, value) || quotes_are_unremovable(semantic, slice) + } + Expr::Attribute(ast::ExprAttribute { value, .. }) => { + // for attributes we also don't know whether it's safe + if !semantic.in_type_checking_block() { + return true; + } + quotes_are_unremovable(semantic, value) + } Expr::List(ast::ExprList { elts, .. }) | Expr::Tuple(ast::ExprTuple { elts, .. }) => { for elt in elts { if quotes_are_unremovable(semantic, elt) { @@ -305,7 +335,10 @@ fn quotes_are_unremovable(semantic: &SemanticModel, expr: &Expr) -> bool { false } Expr::Name(name) => { - semantic.resolve_name(name).is_some() && semantic.simulate_runtime_load(name).is_none() + semantic.resolve_name(name).is_some() + && semantic + .simulate_runtime_load(name, semantic.in_type_checking_block().into()) + .is_none() } _ => false, } diff --git a/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap new file mode 100644 index 0000000000000..0da908ea8ef87 --- /dev/null +++ b/crates/ruff_linter/src/rules/flake8_type_checking/snapshots/ruff_linter__rules__flake8_type_checking__tests__quoted-type-alias_TC008_typing_execution_context.py.snap @@ -0,0 +1,254 @@ +--- +source: crates/ruff_linter/src/rules/flake8_type_checking/mod.rs +--- +TC008_typing_execution_context.py:13:20: TC008 [*] Remove quotes from type alias + | +11 | Bar: TypeAlias = Foo[int] +12 | +13 | a: TypeAlias = 'int' # TC008 + | ^^^^^ TC008 +14 | b: TypeAlias = 'Dict' # TC008 +15 | c: TypeAlias = 'Foo' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +10 10 | OptStr: TypeAlias = str | None +11 11 | Bar: TypeAlias = Foo[int] +12 12 | +13 |- a: TypeAlias = 'int' # TC008 + 13 |+ a: TypeAlias = int # TC008 +14 14 | b: TypeAlias = 'Dict' # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 + +TC008_typing_execution_context.py:14:20: TC008 [*] Remove quotes from type alias + | +13 | a: TypeAlias = 'int' # TC008 +14 | b: TypeAlias = 'Dict' # TC008 + | ^^^^^^ TC008 +15 | c: TypeAlias = 'Foo' # TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +11 11 | Bar: TypeAlias = Foo[int] +12 12 | +13 13 | a: TypeAlias = 'int' # TC008 +14 |- b: TypeAlias = 'Dict' # TC008 + 14 |+ b: TypeAlias = Dict # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 + +TC008_typing_execution_context.py:15:20: TC008 [*] Remove quotes from type alias + | +13 | a: TypeAlias = 'int' # TC008 +14 | b: TypeAlias = 'Dict' # TC008 +15 | c: TypeAlias = 'Foo' # TC008 + | ^^^^^ TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +12 12 | +13 13 | a: TypeAlias = 'int' # TC008 +14 14 | b: TypeAlias = 'Dict' # TC008 +15 |- c: TypeAlias = 'Foo' # TC008 + 15 |+ c: TypeAlias = Foo # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 + +TC008_typing_execution_context.py:16:20: TC008 [*] Remove quotes from type alias + | +14 | b: TypeAlias = 'Dict' # TC008 +15 | c: TypeAlias = 'Foo' # TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 + | ^^^^^^^^^^ TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +13 13 | a: TypeAlias = 'int' # TC008 +14 14 | b: TypeAlias = 'Dict' # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 |- d: TypeAlias = 'Foo[str]' # TC008 + 16 |+ d: TypeAlias = Foo[str] # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 + +TC008_typing_execution_context.py:17:20: TC008 [*] Remove quotes from type alias + | +15 | c: TypeAlias = 'Foo' # TC008 +16 | d: TypeAlias = 'Foo[str]' # TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 + | ^^^^^^^^^ TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 +19 | g: TypeAlias = 'OptStr' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +14 14 | b: TypeAlias = 'Dict' # TC008 +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 |- e: TypeAlias = 'Foo.bar' # TC008 + 17 |+ e: TypeAlias = Foo.bar # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 + +TC008_typing_execution_context.py:18:20: TC008 [*] Remove quotes from type alias + | +16 | d: TypeAlias = 'Foo[str]' # TC008 +17 | e: TypeAlias = 'Foo.bar' # TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 + | ^^^^^^^^^^^^ TC008 +19 | g: TypeAlias = 'OptStr' # TC008 +20 | h: TypeAlias = 'Bar' # TC008 + | + = help: Remove quotes + +ℹ Safe fix +15 15 | c: TypeAlias = 'Foo' # TC008 +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 |- f: TypeAlias = 'Foo | None' # TC008 + 18 |+ f: TypeAlias = Foo | None # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 + +TC008_typing_execution_context.py:19:20: TC008 [*] Remove quotes from type alias + | +17 | e: TypeAlias = 'Foo.bar' # TC008 +18 | f: TypeAlias = 'Foo | None' # TC008 +19 | g: TypeAlias = 'OptStr' # TC008 + | ^^^^^^^^ TC008 +20 | h: TypeAlias = 'Bar' # TC008 +21 | i: TypeAlias = Foo['str'] # TC008 + | + = help: Remove quotes + +ℹ Safe fix +16 16 | d: TypeAlias = 'Foo[str]' # TC008 +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 |- g: TypeAlias = 'OptStr' # TC008 + 19 |+ g: TypeAlias = OptStr # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) + +TC008_typing_execution_context.py:20:20: TC008 [*] Remove quotes from type alias + | +18 | f: TypeAlias = 'Foo | None' # TC008 +19 | g: TypeAlias = 'OptStr' # TC008 +20 | h: TypeAlias = 'Bar' # TC008 + | ^^^^^ TC008 +21 | i: TypeAlias = Foo['str'] # TC008 +22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) + | + = help: Remove quotes + +ℹ Safe fix +17 17 | e: TypeAlias = 'Foo.bar' # TC008 +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 |- h: TypeAlias = 'Bar' # TC008 + 20 |+ h: TypeAlias = Bar # TC008 +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block + +TC008_typing_execution_context.py:21:24: TC008 [*] Remove quotes from type alias + | +19 | g: TypeAlias = 'OptStr' # TC008 +20 | h: TypeAlias = 'Bar' # TC008 +21 | i: TypeAlias = Foo['str'] # TC008 + | ^^^^^ TC008 +22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) +23 | k: TypeAlias = 'k | None' # False negative in type checking block + | + = help: Remove quotes + +ℹ Safe fix +18 18 | f: TypeAlias = 'Foo | None' # TC008 +19 19 | g: TypeAlias = 'OptStr' # TC008 +20 20 | h: TypeAlias = 'Bar' # TC008 +21 |- i: TypeAlias = Foo['str'] # TC008 + 21 |+ i: TypeAlias = Foo[str] # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + +TC008_typing_execution_context.py:24:20: TC008 [*] Remove quotes from type alias + | +22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) +23 | k: TypeAlias = 'k | None' # False negative in type checking block +24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + | ^^^^^ TC008 +25 | m: TypeAlias = ('int' # TC008 +26 | | None) + | + = help: Remove quotes + +ℹ Safe fix +21 21 | i: TypeAlias = Foo['str'] # TC008 +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block +24 |- l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) + 24 |+ l: TypeAlias = int | None # TC008 (because TC010 is not enabled) +25 25 | m: TypeAlias = ('int' # TC008 +26 26 | | None) +27 27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) + +TC008_typing_execution_context.py:25:21: TC008 [*] Remove quotes from type alias + | +23 | k: TypeAlias = 'k | None' # False negative in type checking block +24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 | m: TypeAlias = ('int' # TC008 + | ^^^^^ TC008 +26 | | None) +27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) + | + = help: Remove quotes + +ℹ Safe fix +22 22 | j: TypeAlias = 'Baz' # OK (this would be treated as use before define) +23 23 | k: TypeAlias = 'k | None' # False negative in type checking block +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 |- m: TypeAlias = ('int' # TC008 + 25 |+ m: TypeAlias = (int # TC008 +26 26 | | None) +27 27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) +28 28 | ' | None') + +TC008_typing_execution_context.py:27:21: TC008 [*] Remove quotes from type alias + | +25 | m: TypeAlias = ('int' # TC008 +26 | | None) +27 | n: TypeAlias = ('int' # TC008 (fix removes comment currently) + | _____________________^ +28 | | ' | None') + | |_________________^ TC008 + | + = help: Remove quotes + +ℹ Unsafe fix +24 24 | l: TypeAlias = 'int' | None # TC008 (because TC010 is not enabled) +25 25 | m: TypeAlias = ('int' # TC008 +26 26 | | None) +27 |- n: TypeAlias = ('int' # TC008 (fix removes comment currently) +28 |- ' | None') + 27 |+ n: TypeAlias = (int | None) +29 28 | +30 29 | +31 30 | class Baz: ... diff --git a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs index 7ee7cee03a919..19a35c06b297c 100644 --- a/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs +++ b/crates/ruff_linter/src/rules/pyupgrade/rules/quoted_annotation.rs @@ -54,6 +54,12 @@ use crate::checkers::ast::Checker; /// bar: Bar /// ``` /// +/// ## See also +/// - [`quoted-annotation-in-stub`](quoted-annotation-in-stub.md): A rule that +/// removes all quoted annotations from stub files +/// - [`quoted-type-alias`](quoted-type-alias.md): A rule that removes unnecessary quotes +/// from type aliases. +/// /// ## References /// - [PEP 563 – Postponed Evaluation of Annotations](https://peps.python.org/pep-0563/) /// - [Python documentation: `__future__`](https://docs.python.org/3/library/__future__.html#module-__future__) diff --git a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs index ca19c06346d23..186b9636e38a0 100644 --- a/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs +++ b/crates/ruff_linter/src/rules/ruff/rules/missing_fstring_syntax.rs @@ -7,7 +7,7 @@ use ruff_python_ast as ast; use ruff_python_literal::format::FormatSpec; use ruff_python_parser::parse_expression; use ruff_python_semantic::analyze::logging::is_logger_candidate; -use ruff_python_semantic::{Modules, SemanticModel}; +use ruff_python_semantic::{Modules, SemanticModel, TypingOnlyBindingsStatus}; use ruff_text_size::{Ranged, TextRange}; use crate::checkers::ast::Checker; @@ -216,6 +216,7 @@ fn should_be_fstring( id, literal.range(), semantic.scope_id, + TypingOnlyBindingsStatus::Disallowed, ) .map_or(true, |id| semantic.binding(id).kind.is_builtin()) { diff --git a/crates/ruff_python_semantic/src/model.rs b/crates/ruff_python_semantic/src/model.rs index 9bfdee6c8f858..b8c13a22ca733 100644 --- a/crates/ruff_python_semantic/src/model.rs +++ b/crates/ruff_python_semantic/src/model.rs @@ -710,8 +710,17 @@ impl<'a> SemanticModel<'a> { /// /// References from within an [`ast::Comprehension`] can produce incorrect /// results when referring to a [`BindingKind::NamedExprAssignment`]. - pub fn simulate_runtime_load(&self, name: &ast::ExprName) -> Option { - self.simulate_runtime_load_at_location_in_scope(name.id.as_str(), name.range, self.scope_id) + pub fn simulate_runtime_load( + &self, + name: &ast::ExprName, + typing_only_bindings_status: TypingOnlyBindingsStatus, + ) -> Option { + self.simulate_runtime_load_at_location_in_scope( + name.id.as_str(), + name.range, + self.scope_id, + typing_only_bindings_status, + ) } /// Simulates a runtime load of the given symbol. @@ -743,6 +752,7 @@ impl<'a> SemanticModel<'a> { symbol: &str, symbol_range: TextRange, scope_id: ScopeId, + typing_only_bindings_status: TypingOnlyBindingsStatus, ) -> Option { let mut seen_function = false; let mut class_variables_visible = true; @@ -785,7 +795,9 @@ impl<'a> SemanticModel<'a> { // runtime binding with a source-order inaccurate one for shadowed_id in scope.shadowed_bindings(binding_id) { let binding = &self.bindings[shadowed_id]; - if binding.context.is_typing() { + if typing_only_bindings_status.is_disallowed() + && binding.context.is_typing() + { continue; } if let BindingKind::Annotation @@ -820,7 +832,9 @@ impl<'a> SemanticModel<'a> { _ => binding_id, }; - if self.bindings[candidate_id].context.is_typing() { + if typing_only_bindings_status.is_disallowed() + && self.bindings[candidate_id].context.is_typing() + { continue; } @@ -2058,6 +2072,32 @@ impl ShadowedBinding { } } +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum TypingOnlyBindingsStatus { + Allowed, + Disallowed, +} + +impl TypingOnlyBindingsStatus { + pub const fn is_allowed(self) -> bool { + matches!(self, TypingOnlyBindingsStatus::Allowed) + } + + pub const fn is_disallowed(self) -> bool { + matches!(self, TypingOnlyBindingsStatus::Disallowed) + } +} + +impl From for TypingOnlyBindingsStatus { + fn from(value: bool) -> Self { + if value { + TypingOnlyBindingsStatus::Allowed + } else { + TypingOnlyBindingsStatus::Disallowed + } + } +} + bitflags! { /// A select list of Python modules that the semantic model can explicitly track. #[derive(Debug)]