diff --git a/crates/ty_python_core/src/place.rs b/crates/ty_python_core/src/place.rs index d5306bb7e6201..9606325a23c0e 100644 --- a/crates/ty_python_core/src/place.rs +++ b/crates/ty_python_core/src/place.rs @@ -9,6 +9,7 @@ use crate::{Db, PossiblyNarrowedPlaces}; use ruff_db::parsed::ParsedModuleRef; use ruff_index::IndexVec; use ruff_python_ast as ast; +use ruff_python_ast::name::Name; use smallvec::SmallVec; use std::hash::Hash; use std::iter::FusedIterator; @@ -45,11 +46,16 @@ pub enum PlaceExpr { } impl PlaceExpr { + /// Create a symbol place from a name, without requiring an AST occurrence. + pub fn from_name(name: Name) -> Self { + Self::Symbol(Symbol::new(name)) + } + /// Create a new `PlaceExpr` from a name. /// /// This always returns a `PlaceExpr::Symbol` with empty flags and `name`. pub fn from_expr_name(name: &ast::ExprName) -> Self { - PlaceExpr::Symbol(Symbol::new(name.id.clone())) + Self::from_name(name.id.clone()) } /// Tries to create a `PlaceExpr` from an expression. diff --git a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md index 4f1db6a01e7bd..53e26c581dade 100644 --- a/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md +++ b/crates/ty_python_semantic/resources/mdtest/annotations/invalid.md @@ -1240,7 +1240,7 @@ python-version = "3.12" class C: list = 42 - # TODO: `visible_ancestor_scopes` skips the class through nested annotation scopes, + # TODO: The builtin lookup skips the class through nested annotation scopes, # so we incorrectly offer a fix that resolves `list` to `C.list`. type Alias[T] = [int] # snapshot: invalid-type-form ``` @@ -1262,6 +1262,220 @@ help: Replace with `list[...]` note: This is an unsafe fix and may change runtime behavior ``` +#### Collection literal fixes with aliases to builtins + +An import or assignment that binds a collection name to the corresponding builtin still permits the +fix. + +```py +import builtins +from builtins import list + +items: [int] # snapshot: invalid-type-form + +tuple = builtins.tuple +pair: (int, str) # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:4:8 + | +4 | items: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +help: Replace with `list[...]` + | +3 | + - items: [int] # snapshot: invalid-type-form +4 + items: list[int] # snapshot: invalid-type-form +5 | + | +note: This is an unsafe fix and may change runtime behavior + + +error[invalid-type-form]: Tuple literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:7:7 + | +7 | pair: (int, str) # snapshot: invalid-type-form + | ^^^^^^^^^^ Did you mean `tuple[int, str]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +help: Replace with `tuple[...]` + | +6 | tuple = builtins.tuple + - pair: (int, str) # snapshot: invalid-type-form +7 + pair: tuple[int, str] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Collection literal fixes with unreachable shadowing + +An assignment in an unreachable module-level branch does not shadow the builtin. + +```py +if False: + list = 42 + +items: [int] # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:4:8 + | +4 | items: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +help: Replace with `list[...]` + | +3 | + - items: [int] # snapshot: invalid-type-form +4 + items: list[int] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Collection literal fixes with global declarations + +A `global` declaration skips enclosing function bindings and resolves to the builtin imported at +module scope. + +```py +from builtins import list + +def outer(): + list = 42 + + def inner(): + global list + items: [int] # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:8:16 + | +8 | items: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +help: Replace with `list[...]` + | +7 | global list + - items: [int] # snapshot: invalid-type-form +8 + items: list[int] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Collection literal fixes with nonlocal declarations + +Following `nonlocal` declarations through nested functions can resolve a name to an alias of the +builtin. + +```py +def outer(): + from builtins import set + + def middle(): + nonlocal set + + def inner(): + nonlocal set + items: {int} # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: Set literals are not allowed in type expressions + --> src/mdtest_snippet.py:9:20 + | +9 | items: {int} # snapshot: invalid-type-form + | ^^^^^ Did you mean `set[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +help: Replace with `set[...]` + | +8 | nonlocal set + - items: {int} # snapshot: invalid-type-form +9 + items: set[int] # snapshot: invalid-type-form + | +note: This is an unsafe fix and may change runtime behavior +``` + +#### Collection literal fixes with ambiguous bindings + +A name that might refer to a different class is not a suitable replacement, even if one of its +possible values is the expected builtin. + +```py +import builtins + +def check(flag: bool): + list = builtins.list if flag else builtins.set + + def inner(): + items: [int] # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:7:16 + | +7 | items: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +``` + +#### Collection literal fixes with unbound local names + +An unreachable local assignment still makes the name local to the function. It prevents lookup from +falling back to the builtin. + +```py +def check(): + if False: + list = 42 + items: [int] # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:4:12 + | +4 | items: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +``` + +#### Collection literal fixes before a local alias is bound + +An assignment of the builtin to a local name does not make that name available earlier in the +function. + +```py +import builtins + +def check(): + items: [int] # snapshot: invalid-type-form + list = builtins.list +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:4:12 + | +4 | items: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +``` + #### Collection literal fixes with project-level builtin overrides A project-level `__builtins__.pyi` can replace `list` while leaving the standard `set` builtin @@ -1304,6 +1518,31 @@ note: This is an unsafe fix and may change runtime behavior list: object ``` +#### Collection literal fixes with project-level replacement classes + +A replacement class in `__builtins__.pyi` is not the standard collection class, even when it has the +same name. + +```py +items: [int] # snapshot: invalid-type-form +``` + +```snapshot +error[invalid-type-form]: List literals are not allowed in this context in a type expression + --> src/mdtest_snippet.py:1:8 + | +1 | items: [int] # snapshot: invalid-type-form + | ^^^^^ Did you mean `list[int]`? +info: See the following page for a reference on valid type expressions: +info: https://typing.python.org/en/latest/spec/annotations.html#type-and-annotation-expressions +``` + +`__builtins__.pyi`: + +```pyi +class list: ... +``` + #### Collection literal fixes are omitted in string annotations Collection literals parsed from quoted annotations do not have source ranges that can be rewritten diff --git a/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md index 11107d7eeadb9..df488946f2840 100644 --- a/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md +++ b/crates/ty_python_semantic/resources/mdtest/diagnostics/unresolved_reference.md @@ -79,6 +79,31 @@ error[unresolved-reference]: Name `List` used when not defined | ^^^^ Did you mean `list`? ``` +### Builtin replacement imported explicitly + +Importing `list` from `builtins` still permits replacing the unresolved `List` with `list`. + +```py +from builtins import list + +items: List[int] # snapshot: unresolved-reference +``` + +```snapshot +error[unresolved-reference]: Name `List` used when not defined + --> src/mdtest_snippet.py:3:8 + | +3 | items: List[int] # snapshot: unresolved-reference + | ^^^^ Did you mean `list`? +help: Replace with `list` + | +2 | + - items: List[int] # snapshot: unresolved-reference +3 + items: list[int] # snapshot: unresolved-reference + | +note: This is an unsafe fix and may change runtime behavior +``` + ### Info not present before Python 3.9 diff --git a/crates/ty_python_semantic/src/place_load.rs b/crates/ty_python_semantic/src/place_load.rs index 8d9cc2e3b31d9..c4f117b205833 100644 --- a/crates/ty_python_semantic/src/place_load.rs +++ b/crates/ty_python_semantic/src/place_load.rs @@ -94,7 +94,15 @@ use ty_python_core::{ ProgramFile, SemanticIndex, }; -use crate::Db; +use crate::place::{ + ConsideredDefinitions, Definedness, Place, PlaceAndQualifiers, RequiresExplicitReExport, + builtins_module_scope, class_body_implicit_symbol, explicit_global_symbol, + implicit_builtins_symbol, module_type_implicit_global_symbol, place_by_id, place_from_bindings, + place_from_bindings_with_reachability_cache, +}; +use crate::reachability::ReachabilityEvaluationCache; +use crate::types::original_class_type; +use crate::{Db, ProgramEnvironment}; /// Returns an iterator over the steps that resolve a value for a place load. pub(crate) fn resolve_place_load<'db, 'ast>( @@ -137,12 +145,15 @@ pub(crate) enum PlaceLoadMode<'ast> { /// /// For example, `Model` in `item: Model` can resolve to a class defined later in the scope. Deferred, - /// Resolve reachable bindings in a parsed string annotation. + /// Resolve all reachable bindings without an indexed expression occurrence. /// /// A caller uses this mode for a name such as `Model` after parsing `item: "Model"`. The /// parsed expression is not part of the original semantic index, so it may not have its own /// place-table entry. - StringAnnotation, + /// + /// Autofixes also use this mode when introducing a name that does not occur in the source. + /// Callers must account for bindings that might not have executed at the insertion point. + Untracked, } /// Exposes an iterator over the steps that resolve the value for a place load. @@ -686,9 +697,72 @@ pub(crate) struct PlaceLoadSource<'db> { role: PlaceLoadSourceRole, } -impl PlaceLoadSource<'_> { +impl<'db> PlaceLoadSource<'db> { + /// Infer this source's value before applying constraints from earlier resolution steps. + pub(crate) fn infer_type( + self, + db: &'db dyn Db, + env: &ProgramEnvironment<'db>, + scope: ScopeId<'db>, + reachability_cache: Option<&ReachabilityEvaluationCache<'db>>, + ) -> PlaceAndQualifiers<'db> { + let is_class_body_global_fallback = self.is_class_body_global_fallback(); + match self.kind { + PlaceLoadSourceKind::Bindings(bindings) => { + let mut place = if let Some(cache) = reachability_cache { + place_from_bindings_with_reachability_cache(db, env, bindings, cache) + } else { + place_from_bindings(db, env, bindings) + } + .place; + + // Compatibility policy: ty historically treats a possibly-bound module snapshot + // reached through a class-body global fallback as definitely bound. At runtime, + // an unbound snapshot would continue to builtins or produce a name error. + if is_class_body_global_fallback && let Place::Defined(defined) = place { + place = Place::Defined(defined.with_definedness(Definedness::AlwaysDefined)); + } + + place.into() + } + PlaceLoadSourceKind::DefinitionsFromOwningScope { scope, id } => place_by_id( + db, + scope, + id, + RequiresExplicitReExport::No, + ConsideredDefinitions::AllReachable, + ), + PlaceLoadSourceKind::Implicit(implicit) => match implicit { + ImplicitPlaceLoad::DunderClass(definition) => original_class_type(db, definition) + .map(|class| Place::bound(class).into()) + .unwrap_or_else(|| Place::Undefined.into()), + ImplicitPlaceLoad::ClassBodySymbol(name) => { + let implicit = class_body_implicit_symbol(db, env, &name); + if implicit.place.is_definitely_bound() { + implicit + } else { + Place::Undefined.into() + } + } + ImplicitPlaceLoad::ExplicitGlobalSymbol { file, name } => { + explicit_global_symbol(db, file, &name) + } + ImplicitPlaceLoad::ModuleImplicitGlobal { file, name } => { + module_type_implicit_global_symbol(db, file, &name) + } + ImplicitPlaceLoad::Builtin(name) => { + if Some(scope) == builtins_module_scope(db, env) { + Place::Undefined.into() + } else { + implicit_builtins_symbol(db, env, &name) + } + } + }, + } + } + /// Returns whether this source is the module fallback for a class-local name. - pub(crate) fn is_class_body_global_fallback(&self) -> bool { + fn is_class_body_global_fallback(&self) -> bool { self.role == PlaceLoadSourceRole::ClassBodyGlobalFallback } @@ -932,12 +1006,12 @@ impl<'db> PlaceLoadResolutionContext<'db, '_> { Some((scope, ConstraintKey::UseId(use_id))), )) } - PlaceLoadMode::Deferred | PlaceLoadMode::StringAnnotation => { + PlaceLoadMode::Deferred | PlaceLoadMode::Untracked => { let source = table .place_id(place_expr) .map(|id| PlaceLoadSourceKind::Bindings(use_def.reachable_bindings(id))); assert!( - source.is_some() || matches!(self.mode, PlaceLoadMode::StringAnnotation), + source.is_some() || matches!(self.mode, PlaceLoadMode::Untracked), "Expected the place table to create a place for every valid PlaceExpr node" ); source.map(|source| (source, None)) @@ -957,7 +1031,7 @@ impl<'db> PlaceLoadResolutionContext<'db, '_> { table .parents(place_expr) .filter_map(|prefix_id| match self.mode { - PlaceLoadMode::Deferred | PlaceLoadMode::StringAnnotation => { + PlaceLoadMode::Deferred | PlaceLoadMode::Untracked => { Some(PlaceExprPrefixLoad::AllReachable(prefix_id)) } PlaceLoadMode::AtExpression(mut prefix_expr_ref) => { diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index 82bb7393728fb..ca7d788a41d5e 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -16,7 +16,10 @@ use ty_module_resolver::{ use crate::Db; use crate::place::implicit_globals::all_implicit_module_globals; -use crate::place::{builtins_module_scope, implicit_builtins_symbol_scope}; +use crate::place::{Definedness, Place, PlaceAndQualifiers, known_module_symbol}; +use crate::place_load::{ + PlaceLoadMode, PlaceLoadResolutionStep, PlaceLoadSourceKind, resolve_place_load, +}; use crate::types::ide_support::{ImportAliasResolution, definition_for_name}; use crate::types::list_members::{all_members, all_reachable_members}; use crate::types::{ @@ -24,6 +27,7 @@ use crate::types::{ infer_complete_scope_types, inferred_declaration, }; use ty_python_core::definition::{Definition, DefinitionKind}; +use ty_python_core::place::PlaceExpr; use ty_python_core::place_table; use ty_python_core::scope::{FileScopeId, Scope}; use ty_python_core::semantic_index; @@ -90,37 +94,57 @@ impl<'db> SemanticModel<'db> { line_index(self.db, self.file()) } - /// Returns whether `name` refers to a standard builtin in the scope containing `node`. - /// - /// This method uses a simplified implementation of name resolution: any binding or declaration - /// in a visible scope shadows the builtin, even if it does not reach `node`. As a result, it - /// can return `false` when the builtin is actually available. That is acceptable when deciding - /// whether to offer an autofix: we can safely omit the fix in edge cases where resolving the - /// name precisely would require more complex analysis. + /// Returns whether a name introduced by an autofix resolves to the standard builtin class. /// - /// Definitions in a project-level `__builtins__.pyi` also shadow standard builtins. + /// Use the same name resolution and binding inference as an ordinary load, including aliases + /// and project-level builtin overrides. The introduced name has no recorded use-site state, + /// so consider all reachable bindings in the scope containing `node`. pub(crate) fn definitely_has_builtin_binding( &self, name: &str, node: ast::AnyNodeRef<'_>, ) -> bool { - let index = semantic_index(self.db, self.program_file()); - let Some(scope) = self.scope(node) else { + let db = self.db; + let env = self.program_environment(); + let Some(builtin) = known_module_symbol(db, &env, KnownModule::Builtins, name) + .ignore_possibly_undefined() + .and_then(Type::as_class_literal) + else { return false; }; - - if index.visible_ancestor_scopes(scope).any(|(scope, _)| { - index - .place_table(scope) - .symbol_by_name(name) - .is_some_and(|symbol| symbol.is_bound() || symbol.is_declared()) - }) { + let Some(scope) = self.scope(node) else { return false; + }; + let scope = scope.to_scope_id(db, self.program_file()); + let index = semantic_index(db, self.program_file()); + let mut resolution = resolve_place_load( + db, + index, + scope, + PlaceExpr::from_name(Name::new(name)), + PlaceLoadMode::Untracked, + ); + let mut place = PlaceAndQualifiers::from(Place::Undefined); + while let Some(PlaceLoadResolutionStep::Source(source)) = resolution.next() { + place = place.or_fall_back_to(db, &env, || { + let is_local_source = matches!(source.kind, PlaceLoadSourceKind::Bindings(_)); + let mut inferred = source.infer_type(db, &env, scope, None); + // Reachable local bindings can occur after the insertion point. They cannot + // establish that the introduced name is already bound there. + if is_local_source && let Place::Defined(defined) = inferred.place { + inferred.place = + Place::Defined(defined.with_definedness(Definedness::PossiblyUndefined)); + } + inferred + }); + if place.place.is_definitely_bound() { + return place + .ignore_possibly_undefined() + .and_then(Type::as_class_literal) + == Some(builtin); + } } - - let env = self.program_environment(); - implicit_builtins_symbol_scope(self.db, &env, name) - .is_some_and(|scope| Some(scope) == builtins_module_scope(self.db, &env)) + false } /// Returns a map from symbol name to that symbol's diff --git a/crates/ty_python_semantic/src/types.rs b/crates/ty_python_semantic/src/types.rs index 9cfdf94f125b4..381807b70602e 100644 --- a/crates/ty_python_semantic/src/types.rs +++ b/crates/ty_python_semantic/src/types.rs @@ -38,6 +38,7 @@ pub(crate) use self::infer::{ InferredDeclaration, TypeContext, infer_complete_scope_types, infer_deferred_types, infer_definition_types, infer_expression_type, infer_expression_types, infer_same_file_expression_type, infer_scope_types, is_discarded_dict_key_assignment, + original_class_type, }; pub(crate) use self::iteration::extract_fixed_length_iterable_element_types; pub use self::known_instance::KnownInstanceType; diff --git a/crates/ty_python_semantic/src/types/infer/builder.rs b/crates/ty_python_semantic/src/types/infer/builder.rs index 32fb8732d2e4e..f055fd8f4c456 100644 --- a/crates/ty_python_semantic/src/types/infer/builder.rs +++ b/crates/ty_python_semantic/src/types/infer/builder.rs @@ -35,16 +35,14 @@ use super::{ }; use crate::diagnostic::format_enumeration; use crate::place::{ - ConsideredDefinitions, DefinedPlace, Definedness, LookupError, Place, PlaceAndQualifiers, - RequiresExplicitReExport, TypeOrigin, builtins_module_scope, class_body_implicit_symbol, - explicit_global_symbol, implicit_builtins_symbol, loop_header_reachability, - module_type_implicit_global_declaration, module_type_implicit_global_symbol, place_by_id, - place_from_bindings_with_reachability_cache, place_from_declarations_with_reachability_cache, - typing_extensions_symbol, + DefinedPlace, Definedness, LookupError, Place, PlaceAndQualifiers, TypeOrigin, + loop_header_reachability, module_type_implicit_global_declaration, + module_type_implicit_global_symbol, place_from_bindings_with_reachability_cache, + place_from_declarations_with_reachability_cache, typing_extensions_symbol, }; use crate::place_load::{ - ImplicitPlaceLoad, PlaceExprPrefixLoad, PlaceExprPrefixLoads, PlaceLoadFailure, PlaceLoadMode, - PlaceLoadResolutionStep, PlaceLoadSource, PlaceLoadSourceKind, resolve_place_load, + PlaceExprPrefixLoad, PlaceExprPrefixLoads, PlaceLoadFailure, PlaceLoadMode, + PlaceLoadResolutionStep, PlaceLoadSource, resolve_place_load, }; use crate::reachability::{ReachabilityEvaluationCache, evaluate_reachability_with_cache}; use crate::types::add_inferred_python_version_hint_to_diagnostic; @@ -9987,7 +9985,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> (PlaceAndQualifiers<'db>, Vec<(FileScopeId, ConstraintKey)>) { let env = self.program_environment(); let mode = if self.is_deferred() && self.in_string_annotation() { - PlaceLoadMode::StringAnnotation + PlaceLoadMode::Untracked } else if self.is_deferred() { PlaceLoadMode::Deferred } else { @@ -10063,63 +10061,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> { ) -> PlaceAndQualifiers<'db> { let db = self.db(); let env = self.program_environment(); - let is_class_body_global_fallback = source.is_class_body_global_fallback(); - - let place = match source.kind { - PlaceLoadSourceKind::Bindings(bindings) => { - let mut place = place_from_bindings_with_reachability_cache( - db, - env, - bindings, - self.reachability_cache(), - ) - .place; - - // Compatibility policy: ty historically treats a possibly-bound module snapshot - // reached through a class-body global fallback as definitely bound. At runtime, - // an unbound snapshot would continue to builtins or produce a name error. - if is_class_body_global_fallback && let Place::Defined(defined) = place { - place = Place::Defined(defined.with_definedness(Definedness::AlwaysDefined)); - } - - place.into() - } - PlaceLoadSourceKind::DefinitionsFromOwningScope { scope, id } => place_by_id( - db, - scope, - id, - RequiresExplicitReExport::No, - ConsideredDefinitions::AllReachable, - ), - PlaceLoadSourceKind::Implicit(implicit) => match implicit { - ImplicitPlaceLoad::DunderClass(definition) => original_class_type(db, definition) - .map_or_else( - || Place::Undefined.into(), - |class| Place::bound(class).into(), - ), - ImplicitPlaceLoad::ClassBodySymbol(name) => { - let implicit = class_body_implicit_symbol(db, env, &name); - if implicit.place.is_definitely_bound() { - implicit - } else { - Place::Undefined.into() - } - } - ImplicitPlaceLoad::ExplicitGlobalSymbol { file, name } => { - explicit_global_symbol(db, file, &name) - } - ImplicitPlaceLoad::ModuleImplicitGlobal { file, name } => { - module_type_implicit_global_symbol(db, file, &name) - } - ImplicitPlaceLoad::Builtin(name) => { - if Some(self.scope()) == builtins_module_scope(db, env) { - Place::Undefined.into() - } else { - implicit_builtins_symbol(db, env, &name) - } - } - }, - }; + let place = source.infer_type(db, env, self.scope(), Some(self.reachability_cache())); if narrowing_constraints.is_empty() { place diff --git a/crates/ty_python_semantic/src/types/infer/tests.rs b/crates/ty_python_semantic/src/types/infer/tests.rs index 97f466c39dac3..468b3fdd99d76 100644 --- a/crates/ty_python_semantic/src/types/infer/tests.rs +++ b/crates/ty_python_semantic/src/types/infer/tests.rs @@ -520,6 +520,49 @@ fn simple_assignment_does_not_enter_salsa_cycle() { assert_eq!(cycles, Vec::::new()); } +#[test] +fn builtin_autofixes_do_not_reenter_scope_inference() -> anyhow::Result<()> { + let mut db = setup_db(); + db.write_dedented( + "src/a.py", + " + from builtins import list, NotImplementedError + + items: [int] + other: List[int] + + def check(): + raise NotImplemented + ", + )?; + + let file = system_path_to_file(&db, "src/a.py")?; + let diagnostics = check_types(&db, program_file(&db, file)); + assert_eq!(diagnostics.len(), 3); + assert!( + diagnostics + .iter() + .all(|diagnostic| diagnostic.fix().is_some()) + ); + + let events = db.take_salsa_events(); + let cycles = salsa::attach(&db, || { + events + .into_iter() + .filter_map(|event| { + if let salsa::EventKind::WillIterateCycle { database_key, .. } = event.kind { + Some(format!("{database_key:?}")) + } else { + None + } + }) + .filter(|key| key.contains("infer_scope_types_impl")) + .collect::>() + }); + assert_eq!(cycles, Vec::::new()); + Ok(()) +} + /// Test that a symbol known to be unbound in a scope does not still trigger cycle-causing /// reachability-constraint checks in that scope. #[test]