Compare commits

...

5 Commits

Author SHA1 Message Date
David Peter
f3d12c4aac [ty] Allow specialization of generic classes with TypeVar instances 2025-11-21 14:29:09 +01:00
Andrew Gallant
1af318534a [ty] Add support for relative import completions
We already supported `from .. import <CURSOR>`, but we didn't support
`from ..<CURSOR>`. This adds support for that.
2025-11-21 08:01:02 -05:00
Andrew Gallant
553e568624 [ty] Refactor detection of import statements for completions
This commit essentially does away of all our old heuristic and piecemeal
code for detecting different kinds of import statements. Instead, we
offer one single state machine that does everything. This on its own
fixes a few bugs. For example, `import collections.abc, unico<CURSOR>`
would previously offer global scope completions instead of module
completions.

For the most part though, this commit is a refactoring that preserves
parity. In the next commit, we'll add support for completions on
relative imports.
2025-11-21 08:01:02 -05:00
Andrew Gallant
cdef3f5ab8 [ty] Use dedicated collector for completions
This is a small refactor that helps centralize the
logic for how we gather, convert and possibly filter
completions.

Some of this logic was spread out before, which
motivated this refactor. Moreover, as part of other
refactoring, I found myself chaffing against the
lack of this abstraction.
2025-11-21 08:01:02 -05:00
Alex Waygood
6178822427 [ty] Attach subdiagnostics to unresolved-import errors for relative imports as well as absolute imports (#21554) 2025-11-21 12:40:53 +00:00
11 changed files with 981 additions and 581 deletions

View File

@@ -8,13 +8,12 @@ use crate::symbols::{QueryPattern, SymbolInfo, symbols_for_file_global_only};
///
/// Returns symbols from all files in the workspace and dependencies, filtered
/// by the query.
pub fn all_symbols<'db>(db: &'db dyn Db, query: &str) -> Vec<AllSymbolInfo<'db>> {
pub fn all_symbols<'db>(db: &'db dyn Db, query: &QueryPattern) -> Vec<AllSymbolInfo<'db>> {
// If the query is empty, return immediately to avoid expensive file scanning
if query.is_empty() {
if query.will_match_everything() {
return Vec::new();
}
let query = QueryPattern::new(query);
let results = std::sync::Mutex::new(Vec::new());
{
let modules = all_modules(db);
@@ -144,7 +143,7 @@ ABCDEFGHIJKLMNOP = 'https://api.example.com'
impl CursorTest {
fn all_symbols(&self, query: &str) -> String {
let symbols = all_symbols(&self.db, query);
let symbols = all_symbols(&self.db, &QueryPattern::new(query));
if symbols.is_empty() {
return "No symbols found".to_string();

File diff suppressed because it is too large Load Diff

View File

@@ -67,6 +67,16 @@ impl QueryPattern {
symbol_name.contains(&self.original)
}
}
/// Returns true when it is known that this pattern will return `true` for
/// all inputs given to `QueryPattern::is_match_symbol_name`.
///
/// This will never return `true` incorrectly, but it may return `false`
/// incorrectly. That is, it's possible that this query will match all
/// inputs but this still returns `false`.
pub fn will_match_everything(&self) -> bool {
self.re.is_none()
}
}
impl From<&str> for QueryPattern {

View File

@@ -389,9 +389,8 @@ ListOrTupleLegacy = Union[list[T], tuple[T, ...]]
MyCallable = Callable[P, T]
AnnotatedType = Annotated[T, "tag"]
# TODO: Consider displaying this as `<class 'list[T]'>`, … instead? (and similar for some others below)
reveal_type(MyList) # revealed: <class 'list[typing.TypeVar]'>
reveal_type(MyDict) # revealed: <class 'dict[typing.TypeVar, typing.TypeVar]'>
reveal_type(MyList) # revealed: <class 'list[T]'>
reveal_type(MyDict) # revealed: <class 'dict[T, U]'>
reveal_type(MyType) # revealed: GenericAlias
reveal_type(IntAndType) # revealed: <class 'tuple[int, typing.TypeVar]'>
reveal_type(Pair) # revealed: <class 'tuple[typing.TypeVar, typing.TypeVar]'>
@@ -496,9 +495,9 @@ def _(
my_callable: MyCallable,
):
# TODO: Should be `list[Unknown]`
reveal_type(my_list) # revealed: list[typing.TypeVar]
reveal_type(my_list) # revealed: list[T]
# TODO: Should be `dict[Unknown, Unknown]`
reveal_type(my_dict) # revealed: dict[typing.TypeVar, typing.TypeVar]
reveal_type(my_dict) # revealed: dict[T, U]
# TODO: Should be `(...) -> Unknown`
reveal_type(my_callable) # revealed: (...) -> typing.TypeVar
```
@@ -523,7 +522,7 @@ reveal_mro(Derived1)
GenericBaseAlias = GenericBase[T]
# TODO: No error here
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[typing.TypeVar]'>` with no `__class_getitem__` method"
# error: [non-subscriptable] "Cannot subscript object of type `<class 'GenericBase[T]'>` with no `__class_getitem__` method"
class Derived2(GenericBaseAlias[int]):
pass
```

View File

@@ -28,6 +28,10 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist.foo.ba
2 |
3 | stat = add(10, 15)
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
```

View File

@@ -28,6 +28,10 @@ error[unresolved-import]: Cannot resolve imported module `.does_not_exist`
2 |
3 | stat = add(10, 15)
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
```

View File

@@ -40,6 +40,10 @@ error[unresolved-import]: Cannot resolve imported module `....foo`
2 |
3 | stat = add(10, 15)
|
info: Searched in the following paths during module resolution:
info: 1. /src (first-party code)
info: 2. vendored://stdlib (stdlib typeshed stubs vendored by ty)
info: make sure your Python environment is properly configured: https://docs.astral.sh/ty/modules/#python-environment
info: rule `unresolved-import` is enabled by default
```

View File

@@ -296,7 +296,7 @@ impl ModuleName {
}
/// Computes the absolute module name from the LHS components of `from LHS import RHS`
pub(crate) fn from_identifier_parts(
pub fn from_identifier_parts(
db: &dyn Db,
importing_file: File,
module: Option<&str>,

View File

@@ -91,11 +91,7 @@ impl<'db> SemanticModel<'db> {
}
/// Returns completions for symbols available in a `from module import <CURSOR>` context.
pub fn from_import_completions(
&self,
import: &ast::StmtImportFrom,
_name: Option<usize>,
) -> Vec<Completion<'db>> {
pub fn from_import_completions(&self, import: &ast::StmtImportFrom) -> Vec<Completion<'db>> {
let module_name = match ModuleName::from_import_statement(self.db, self.file, import) {
Ok(module_name) => module_name,
Err(err) => {
@@ -110,69 +106,8 @@ impl<'db> SemanticModel<'db> {
self.module_completions(&module_name)
}
/// Returns completions only for submodules for the module
/// identified by `name` in `import`.
///
/// For example, `import re, os.<CURSOR>, zlib`.
pub fn import_submodule_completions(
&self,
import: &ast::StmtImport,
name: usize,
) -> Vec<Completion<'db>> {
let module_ident = &import.names[name].name;
let Some((parent_ident, _)) = module_ident.rsplit_once('.') else {
return vec![];
};
let module_name =
match ModuleName::from_identifier_parts(self.db, self.file, Some(parent_ident), 0) {
Ok(module_name) => module_name,
Err(err) => {
tracing::debug!(
"Could not extract module name from `{module:?}`: {err:?}",
module = module_ident,
);
return vec![];
}
};
self.import_submodule_completions_for_name(&module_name)
}
/// Returns completions only for submodules for the module
/// used in a `from module import attribute` statement.
///
/// For example, `from os.<CURSOR>`.
pub fn from_import_submodule_completions(
&self,
import: &ast::StmtImportFrom,
) -> Vec<Completion<'db>> {
let level = import.level;
let Some(module_ident) = import.module.as_deref() else {
return vec![];
};
let Some((parent_ident, _)) = module_ident.rsplit_once('.') else {
return vec![];
};
let module_name = match ModuleName::from_identifier_parts(
self.db,
self.file,
Some(parent_ident),
level,
) {
Ok(module_name) => module_name,
Err(err) => {
tracing::debug!(
"Could not extract module name from `{module:?}` with level {level}: {err:?}",
module = import.module,
level = import.level,
);
return vec![];
}
};
self.import_submodule_completions_for_name(&module_name)
}
/// Returns submodule-only completions for the given module.
fn import_submodule_completions_for_name(
pub fn import_submodule_completions_for_name(
&self,
module_name: &ModuleName,
) -> Vec<Completion<'db>> {

View File

@@ -7197,6 +7197,8 @@ impl<'db> Type<'db> {
instance.apply_type_mapping_impl(db, type_mapping, tcx, visitor)
},
Type::NewTypeInstance(_) if matches!(type_mapping, TypeMapping::BindLegacyTypevars(_)) => self,
Type::NewTypeInstance(newtype) => visitor.visit(self, || {
Type::NewTypeInstance(newtype.map_base_class_type(db, |class_type| {
class_type.apply_type_mapping_impl(db, type_mapping, tcx, visitor)
@@ -7275,6 +7277,8 @@ impl<'db> Type<'db> {
// TODO(jelle): Materialize should be handled differently, since TypeIs is invariant
Type::TypeIs(type_is) => type_is.with_type(db, type_is.return_type(db).apply_type_mapping(db, type_mapping, tcx)),
Type::TypeAlias(_) if matches!(type_mapping, TypeMapping::BindLegacyTypevars(_)) => self,
Type::TypeAlias(alias) => {
// Do not call `value_type` here. `value_type` does the specialization internally, so `apply_type_mapping` is performed without `visitor` inheritance.
// In the case of recursive type aliases, this leads to infinite recursion.

View File

@@ -101,14 +101,14 @@ use crate::types::typed_dict::{
};
use crate::types::visitor::any_over_type;
use crate::types::{
CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType, DataclassParams,
DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
BindingContext, CallDunderError, CallableBinding, CallableType, ClassLiteral, ClassType,
DataclassParams, DynamicType, InternedType, IntersectionBuilder, IntersectionType, KnownClass,
KnownInstanceType, LintDiagnosticGuard, MemberLookupPolicy, MetaclassCandidate,
PEP695TypeAliasType, Parameter, ParameterForm, Parameters, SpecialFormType, SubclassOfType,
TrackedConstraintSet, Truthiness, Type, TypeAliasType, TypeAndQualifiers, TypeContext,
TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation, TypeVarIdentity,
TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder, UnionType,
UnionTypeInstance, binding_type, todo_type,
TypeMapping, TypeQualifiers, TypeVarBoundOrConstraintsEvaluation, TypeVarDefaultEvaluation,
TypeVarIdentity, TypeVarInstance, TypeVarKind, TypeVarVariance, TypedDictType, UnionBuilder,
UnionType, UnionTypeInstance, binding_type, todo_type,
};
use crate::types::{ClassBase, add_inferred_python_version_hint_to_diagnostic};
use crate::unpack::{EvaluationMode, UnpackPosition};
@@ -5803,6 +5803,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
".".repeat(level as usize),
module.unwrap_or_default()
));
if level == 0 {
if let Some(module_name) = module.and_then(ModuleName::new) {
let program = Program::get(self.db());
@@ -5831,39 +5832,39 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
}
}
}
// Add search paths information to the diagnostic
// Use the same search paths function that is used in actual module resolution
let verbose = self.db().verbose();
let search_paths = search_paths(self.db(), ModuleResolveMode::StubsAllowed);
diagnostic.info(format_args!(
"Searched in the following paths during module resolution:"
));
let mut search_paths = search_paths.enumerate();
while let Some((index, path)) = search_paths.next() {
if index > 4 && !verbose {
let more = search_paths.count() + 1;
diagnostic.info(format_args!(
" ... and {more} more paths. Run with `-v` to see all paths."
));
break;
}
diagnostic.info(format_args!(
" {}. {} ({})",
index + 1,
path,
path.describe_kind()
));
}
diagnostic.info(
"make sure your Python environment is properly configured: \
https://docs.astral.sh/ty/modules/#python-environment",
);
}
// Add search paths information to the diagnostic
// Use the same search paths function that is used in actual module resolution
let verbose = self.db().verbose();
let search_paths = search_paths(self.db(), ModuleResolveMode::StubsAllowed);
diagnostic.info(format_args!(
"Searched in the following paths during module resolution:"
));
let mut search_paths = search_paths.enumerate();
while let Some((index, path)) = search_paths.next() {
if index > 4 && !verbose {
let more = search_paths.count() + 1;
diagnostic.info(format_args!(
" ... and {more} more paths. Run with `-v` to see all paths."
));
break;
}
diagnostic.info(format_args!(
" {}. {} ({})",
index + 1,
path,
path.describe_kind()
));
}
diagnostic.info(
"make sure your Python environment is properly configured: \
https://docs.astral.sh/ty/modules/#python-environment",
);
}
fn infer_import_definition(
@@ -11045,6 +11046,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
value_ty,
generic_context,
specialize,
true,
)
}
@@ -11069,6 +11071,7 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
value_ty,
generic_context,
specialize,
false,
)
}
@@ -11078,12 +11081,30 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
value_ty: Type<'db>,
generic_context: GenericContext<'db>,
specialize: impl FnOnce(&[Option<Type<'db>>]) -> Type<'db>,
bind_legacy_typevars: bool,
) -> Type<'db> {
let slice_node = subscript.slice.as_ref();
let db = self.db();
let do_bind_legacy_typevars = |ty: Type<'db>| {
if bind_legacy_typevars {
ty.apply_type_mapping(
db,
&TypeMapping::BindLegacyTypevars(BindingContext::Synthetic),
TypeContext::default(),
)
} else {
ty
}
};
let call_argument_types = match slice_node {
ast::Expr::Tuple(tuple) => {
let arguments = CallArguments::positional(
tuple.elts.iter().map(|elt| self.infer_type_expression(elt)),
tuple
.elts
.iter()
.map(|elt| do_bind_legacy_typevars(self.infer_type_expression(elt))),
);
self.store_expression_type(
slice_node,
@@ -11091,8 +11112,11 @@ impl<'db, 'ast> TypeInferenceBuilder<'db, 'ast> {
);
arguments
}
_ => CallArguments::positional([self.infer_type_expression(slice_node)]),
_ => CallArguments::positional([do_bind_legacy_typevars(
self.infer_type_expression(slice_node),
)]),
};
let binding = Binding::single(value_ty, generic_context.signature(self.db()));
let bindings = match Bindings::from(binding)
.match_parameters(self.db(), &call_argument_types)