mirror of
https://github.com/astral-sh/ruff.git
synced 2025-09-29 05:14:52 +00:00
[ty] Add a new abstraction for adding imports to a module
This is somewhat inspired by a similar abstraction in `ruff_linter`. The main idea is to create an importer once for a module that you want to add imports to. And then call `import` to generate an edit for each symbol you want to add. I haven't done any performance profiling here yet. I don't know if it will be a bottleneck. In particular, I do expect `Importer::import` (but not `Importer::new`) to get called many times for a single completion request when auto-import is enabled. Particularly in projects with a lot of unimported symbols. Because I don't know the perf impact, I didn't do any premature optimization here. But there are surely some low hanging fruit if this does prove to be a problem. New tests make up a big portion of the diff here. I tried to think of a bunch of different cases, although I'm sure there are more.
This commit is contained in:
parent
bcc8d6910b
commit
b6a29592e7
3 changed files with 1812 additions and 32 deletions
1716
crates/ty_ide/src/importer.rs
Normal file
1716
crates/ty_ide/src/importer.rs
Normal file
File diff suppressed because it is too large
Load diff
|
@ -14,6 +14,7 @@ mod goto_definition;
|
||||||
mod goto_references;
|
mod goto_references;
|
||||||
mod goto_type_definition;
|
mod goto_type_definition;
|
||||||
mod hover;
|
mod hover;
|
||||||
|
mod importer;
|
||||||
mod inlay_hints;
|
mod inlay_hints;
|
||||||
mod markup;
|
mod markup;
|
||||||
mod references;
|
mod references;
|
||||||
|
@ -294,7 +295,10 @@ mod tests {
|
||||||
use ruff_db::Db;
|
use ruff_db::Db;
|
||||||
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
|
use ruff_db::diagnostic::{Diagnostic, DiagnosticFormat, DisplayDiagnosticConfig};
|
||||||
use ruff_db::files::{File, FileRootKind, system_path_to_file};
|
use ruff_db::files::{File, FileRootKind, system_path_to_file};
|
||||||
|
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
||||||
|
use ruff_db::source::{SourceText, source_text};
|
||||||
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
use ruff_db::system::{DbWithWritableSystem, SystemPath, SystemPathBuf};
|
||||||
|
use ruff_python_codegen::Stylist;
|
||||||
use ruff_python_trivia::textwrap::dedent;
|
use ruff_python_trivia::textwrap::dedent;
|
||||||
use ruff_text_size::TextSize;
|
use ruff_text_size::TextSize;
|
||||||
use ty_project::ProjectMetadata;
|
use ty_project::ProjectMetadata;
|
||||||
|
@ -350,11 +354,17 @@ mod tests {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// The file and offset into that file containing
|
/// The file and offset into that file where a `<CURSOR>` marker
|
||||||
/// a `<CURSOR>` marker.
|
/// is located.
|
||||||
|
///
|
||||||
|
/// (Along with other information about that file, such as the
|
||||||
|
/// parsed AST.)
|
||||||
pub(super) struct Cursor {
|
pub(super) struct Cursor {
|
||||||
pub(super) file: File,
|
pub(super) file: File,
|
||||||
pub(super) offset: TextSize,
|
pub(super) offset: TextSize,
|
||||||
|
pub(super) parsed: ParsedModuleRef,
|
||||||
|
pub(super) source: SourceText,
|
||||||
|
pub(super) stylist: Stylist<'static>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Default)]
|
#[derive(Default)]
|
||||||
|
@ -371,8 +381,20 @@ mod tests {
|
||||||
SystemPathBuf::from("/"),
|
SystemPathBuf::from("/"),
|
||||||
));
|
));
|
||||||
|
|
||||||
let mut cursor: Option<Cursor> = None;
|
let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")])
|
||||||
|
.to_search_paths(db.system(), db.vendored())
|
||||||
|
.expect("Valid search path settings");
|
||||||
|
|
||||||
|
Program::from_settings(
|
||||||
|
&db,
|
||||||
|
ProgramSettings {
|
||||||
|
python_version: PythonVersionWithSource::default(),
|
||||||
|
python_platform: PythonPlatform::default(),
|
||||||
|
search_paths,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
let mut cursor: Option<Cursor> = None;
|
||||||
for &Source {
|
for &Source {
|
||||||
ref path,
|
ref path,
|
||||||
ref contents,
|
ref contents,
|
||||||
|
@ -405,25 +427,22 @@ mod tests {
|
||||||
cursor.is_none(),
|
cursor.is_none(),
|
||||||
"found more than one source that contains `<CURSOR>`"
|
"found more than one source that contains `<CURSOR>`"
|
||||||
);
|
);
|
||||||
cursor = Some(Cursor { file, offset });
|
let source = source_text(&db, file);
|
||||||
|
let parsed = parsed_module(&db, file).load(&db);
|
||||||
|
let stylist =
|
||||||
|
Stylist::from_tokens(parsed.tokens(), source.as_str()).into_owned();
|
||||||
|
cursor = Some(Cursor {
|
||||||
|
file,
|
||||||
|
offset,
|
||||||
|
parsed,
|
||||||
|
source,
|
||||||
|
stylist,
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
let search_paths = SearchPathSettings::new(vec![SystemPathBuf::from("/")])
|
|
||||||
.to_search_paths(db.system(), db.vendored())
|
|
||||||
.expect("Valid search path settings");
|
|
||||||
|
|
||||||
Program::from_settings(
|
|
||||||
&db,
|
|
||||||
ProgramSettings {
|
|
||||||
python_version: PythonVersionWithSource::default(),
|
|
||||||
python_platform: PythonPlatform::default(),
|
|
||||||
search_paths,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
let mut insta_settings = insta::Settings::clone_current();
|
let mut insta_settings = insta::Settings::clone_current();
|
||||||
insta_settings.add_filter(r#"\\(\w\w|\s|\.|")"#, "/$1");
|
insta_settings.add_filter(r#"\\(\w\w|\.|")"#, "/$1");
|
||||||
// Filter out TODO types because they are different between debug and release builds.
|
// Filter out TODO types because they are different between debug and release builds.
|
||||||
insta_settings.add_filter(r"@Todo\(.+\)", "@Todo");
|
insta_settings.add_filter(r"@Todo\(.+\)", "@Todo");
|
||||||
|
|
||||||
|
|
|
@ -3,6 +3,7 @@ use ruff_db::source::line_index;
|
||||||
use ruff_python_ast as ast;
|
use ruff_python_ast as ast;
|
||||||
use ruff_python_ast::{Expr, ExprRef, HasNodeIndex, name::Name};
|
use ruff_python_ast::{Expr, ExprRef, HasNodeIndex, name::Name};
|
||||||
use ruff_source_file::LineIndex;
|
use ruff_source_file::LineIndex;
|
||||||
|
use rustc_hash::FxHashMap;
|
||||||
|
|
||||||
use crate::Db;
|
use crate::Db;
|
||||||
use crate::module_name::ModuleName;
|
use crate::module_name::ModuleName;
|
||||||
|
@ -37,6 +38,37 @@ impl<'db> SemanticModel<'db> {
|
||||||
line_index(self.db, self.file)
|
line_index(self.db, self.file)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns a map from symbol name to that symbol's
|
||||||
|
/// type and definition site (if available).
|
||||||
|
///
|
||||||
|
/// The symbols are the symbols in scope at the given
|
||||||
|
/// AST node.
|
||||||
|
pub fn members_in_scope_at(
|
||||||
|
&self,
|
||||||
|
node: ast::AnyNodeRef<'_>,
|
||||||
|
) -> FxHashMap<Name, MemberDefinition<'db>> {
|
||||||
|
let index = semantic_index(self.db, self.file);
|
||||||
|
let mut members = FxHashMap::default();
|
||||||
|
let Some(file_scope) = self.scope(node) else {
|
||||||
|
return members;
|
||||||
|
};
|
||||||
|
|
||||||
|
for (file_scope, _) in index.ancestor_scopes(file_scope) {
|
||||||
|
for memberdef in
|
||||||
|
all_declarations_and_bindings(self.db, file_scope.to_scope_id(self.db, self.file))
|
||||||
|
{
|
||||||
|
members.insert(
|
||||||
|
memberdef.member.name,
|
||||||
|
MemberDefinition {
|
||||||
|
ty: memberdef.member.ty,
|
||||||
|
definition: memberdef.definition,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
members
|
||||||
|
}
|
||||||
|
|
||||||
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module<'_>> {
|
pub fn resolve_module(&self, module_name: &ModuleName) -> Option<Module<'_>> {
|
||||||
resolve_module(self.db, module_name)
|
resolve_module(self.db, module_name)
|
||||||
}
|
}
|
||||||
|
@ -212,20 +244,7 @@ impl<'db> SemanticModel<'db> {
|
||||||
pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec<Completion<'db>> {
|
pub fn scoped_completions(&self, node: ast::AnyNodeRef<'_>) -> Vec<Completion<'db>> {
|
||||||
let index = semantic_index(self.db, self.file);
|
let index = semantic_index(self.db, self.file);
|
||||||
|
|
||||||
// TODO: We currently use `try_expression_scope_id` here as a hotfix for [1].
|
let Some(file_scope) = self.scope(node) else {
|
||||||
// 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 => Some(FileScopeId::global()),
|
|
||||||
Some(expr) => index.try_expression_scope_id(&expr),
|
|
||||||
},
|
|
||||||
}) else {
|
|
||||||
return vec![];
|
return vec![];
|
||||||
};
|
};
|
||||||
let mut completions = vec![];
|
let mut completions = vec![];
|
||||||
|
@ -244,6 +263,32 @@ impl<'db> SemanticModel<'db> {
|
||||||
completions.extend(self.module_completions(&builtins));
|
completions.extend(self.module_completions(&builtins));
|
||||||
completions
|
completions
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn scope(&self, node: ast::AnyNodeRef<'_>) -> Option<FileScopeId> {
|
||||||
|
let index = semantic_index(self.db, self.file);
|
||||||
|
|
||||||
|
// 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
|
||||||
|
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 => Some(FileScopeId::global()),
|
||||||
|
Some(expr) => index.try_expression_scope_id(&expr),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The type and definition (if available) of a symbol.
|
||||||
|
#[derive(Clone, Debug)]
|
||||||
|
pub struct MemberDefinition<'db> {
|
||||||
|
pub ty: Type<'db>,
|
||||||
|
pub definition: Option<Definition<'db>>,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// A classification of symbol names.
|
/// A classification of symbol names.
|
||||||
|
|
Loading…
Add table
Add a link
Reference in a new issue