diff --git a/_typos.toml b/_typos.toml index 402e1755337bb..24406f10bba16 100644 --- a/_typos.toml +++ b/_typos.toml @@ -4,6 +4,10 @@ extend-exclude = [ "crates/ty_vendored/vendor/**/*", "**/resources/**/*", "**/snapshots/**/*", + # Completion tests tend to have a lot of incomplete + # words naturally. It's annoying to have to make all + # of them actually words. So just ignore typos here. + "crates/ty_ide/src/completion.rs", ] [default.extend-words] diff --git a/crates/ty_ide/src/completion.rs b/crates/ty_ide/src/completion.rs index ab8b6e7cf0c84..cfbd998d31495 100644 --- a/crates/ty_ide/src/completion.rs +++ b/crates/ty_ide/src/completion.rs @@ -861,6 +861,162 @@ print(f\"{some "); } + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_function_identifier1() { + let test = cursor_test( + "\ +def m +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_function_identifier2() { + let test = cursor_test( + "\ +def m(): pass +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn fscope_id_missing_function_identifier3() { + let test = cursor_test( + "\ +def m(): pass + +", + ); + + assert_snapshot!(test.completions(), @r" + m + "); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_class_identifier1() { + let test = cursor_test( + "\ +class M +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_type_alias1() { + let test = cursor_test( + "\ +Fo = float +", + ); + + assert_snapshot!(test.completions(), @r" + Fo + float + "); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_import1() { + let test = cursor_test( + "\ +import fo +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_import2() { + let test = cursor_test( + "\ +import foo as ba +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_from_import1() { + let test = cursor_test( + "\ +from fo import wat +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_from_import2() { + let test = cursor_test( + "\ +from foo import wa +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_from_import3() { + let test = cursor_test( + "\ +from foo import wat as ba +", + ); + + assert_snapshot!(test.completions(), @""); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_try_except1() { + let test = cursor_test( + "\ +try: + pass +except Type: + pass +", + ); + + assert_snapshot!(test.completions(), @r" + Type + "); + } + + // Ref: https://github.com/astral-sh/ty/issues/572 + #[test] + fn scope_id_missing_global1() { + let test = cursor_test( + "\ +def _(): + global fo +", + ); + + assert_snapshot!(test.completions(), @""); + } + impl CursorTest { fn completions(&self) -> String { let completions = completion(&self.db, self.file, self.cursor_offset); diff --git a/crates/ty_python_semantic/src/semantic_index.rs b/crates/ty_python_semantic/src/semantic_index.rs index 6d57a8515c879..c3a2f19418eba 100644 --- a/crates/ty_python_semantic/src/semantic_index.rs +++ b/crates/ty_python_semantic/src/semantic_index.rs @@ -259,6 +259,14 @@ impl<'db> SemanticIndex<'db> { self.scopes_by_expression[&expression.into()] } + /// Returns the ID of the `expression`'s enclosing scope. + pub(crate) fn try_expression_scope_id( + &self, + expression: impl Into, + ) -> Option { + self.scopes_by_expression.get(&expression.into()).copied() + } + /// Returns the [`Scope`] of the `expression`'s enclosing scope. #[allow(unused)] #[track_caller] diff --git a/crates/ty_python_semantic/src/semantic_model.rs b/crates/ty_python_semantic/src/semantic_model.rs index e3e7b15cf8613..c112fc1ba3b7e 100644 --- a/crates/ty_python_semantic/src/semantic_model.rs +++ b/crates/ty_python_semantic/src/semantic_model.rs @@ -47,15 +47,22 @@ impl<'db> SemanticModel<'db> { /// scope of this model's `File` are returned. pub fn completions(&self, node: ast::AnyNodeRef<'_>) -> Vec { let index = semantic_index(self.db, self.file); - let file_scope = match node { - ast::AnyNodeRef::Identifier(identifier) => index.expression_scope_id(identifier), + + // TODO: We currently use `try_expression_scope_id` here as a hotfix for [1]. + // Revert this to use `expression_scope_id` once a proper fix is in place. + // + // [1] https://github.com/astral-sh/ty/issues/572 + let Some(file_scope) = (match node { + ast::AnyNodeRef::Identifier(identifier) => index.try_expression_scope_id(identifier), node => match node.as_expr_ref() { // If we couldn't identify a specific // expression that we're in, then just // fall back to the global scope. - None => FileScopeId::global(), - Some(expr) => index.expression_scope_id(expr), + None => Some(FileScopeId::global()), + Some(expr) => index.try_expression_scope_id(expr), }, + }) else { + return vec![]; }; let mut symbols = vec![]; for (file_scope, _) in index.ancestor_scopes(file_scope) {