mirror of https://github.com/astral-sh/ruff
6116 lines
181 KiB
Rust
6116 lines
181 KiB
Rust
use std::cmp::Ordering;
|
|
|
|
use ruff_db::files::File;
|
|
use ruff_db::parsed::{ParsedModuleRef, parsed_module};
|
|
use ruff_db::source::source_text;
|
|
use ruff_diagnostics::Edit;
|
|
use ruff_python_ast::name::Name;
|
|
use ruff_python_ast::token::{Token, TokenAt, TokenKind, Tokens};
|
|
use ruff_python_ast::{self as ast, AnyNodeRef};
|
|
use ruff_python_codegen::Stylist;
|
|
use ruff_text_size::{Ranged, TextLen, TextRange, TextSize};
|
|
use ty_python_semantic::types::UnionType;
|
|
use ty_python_semantic::{
|
|
Completion as SemanticCompletion, KnownModule, ModuleName, NameKind, SemanticModel,
|
|
types::{CycleDetector, KnownClass, Type},
|
|
};
|
|
|
|
use crate::docstring::Docstring;
|
|
use crate::find_node::covering_node;
|
|
use crate::goto::Definitions;
|
|
use crate::importer::{ImportRequest, Importer};
|
|
use crate::symbols::QueryPattern;
|
|
use crate::{Db, all_symbols};
|
|
|
|
/// A collection of completions built up from various sources.
|
|
#[derive(Clone)]
|
|
struct Completions<'db> {
|
|
db: &'db dyn Db,
|
|
items: Vec<Completion<'db>>,
|
|
query: QueryPattern,
|
|
}
|
|
|
|
impl<'db> Completions<'db> {
|
|
/// Create a new empty collection of completions.
|
|
///
|
|
/// The given typed text should correspond to what we believe
|
|
/// the user has typed as part of the next symbol they are writing.
|
|
/// This collection will treat it as a query when present, and only
|
|
/// add completions that match it.
|
|
fn fuzzy(db: &'db dyn Db, typed: Option<&str>) -> Completions<'db> {
|
|
let query = typed
|
|
.map(QueryPattern::fuzzy)
|
|
.unwrap_or_else(QueryPattern::matches_all_symbols);
|
|
Completions {
|
|
db,
|
|
items: vec![],
|
|
query,
|
|
}
|
|
}
|
|
|
|
fn exactly(db: &'db dyn Db, symbol: &str) -> Completions<'db> {
|
|
let query = QueryPattern::exactly(symbol);
|
|
Completions {
|
|
db,
|
|
items: vec![],
|
|
query,
|
|
}
|
|
}
|
|
|
|
/// Convert this collection into a simple
|
|
/// sequence of completions.
|
|
fn into_completions(mut self) -> Vec<Completion<'db>> {
|
|
self.items.sort_by(compare_suggestions);
|
|
self.items
|
|
.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
|
|
self.items
|
|
}
|
|
|
|
fn into_imports(mut self) -> Vec<ImportEdit> {
|
|
self.items.sort_by(compare_suggestions);
|
|
self.items
|
|
.dedup_by(|c1, c2| (&c1.name, c1.module_name) == (&c2.name, c2.module_name));
|
|
self.items
|
|
.into_iter()
|
|
.filter_map(|item| {
|
|
Some(ImportEdit {
|
|
label: format!("import {}.{}", item.module_name?, item.name),
|
|
edit: item.import?,
|
|
})
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Attempts to adds the given completion to this collection.
|
|
///
|
|
/// When added, `true` is returned.
|
|
///
|
|
/// This might not add the completion for a variety of reasons.
|
|
/// For example, if the symbol name does not match this collection's
|
|
/// query.
|
|
fn try_add(&mut self, completion: Completion<'db>) -> bool {
|
|
if !self.query.is_match_symbol_name(completion.name.as_str()) {
|
|
return false;
|
|
}
|
|
self.force_add(completion);
|
|
true
|
|
}
|
|
|
|
/// Attempts to adds the given semantic completion to this collection.
|
|
///
|
|
/// When added, `true` is returned.
|
|
fn try_add_semantic(&mut self, completion: SemanticCompletion<'db>) -> bool {
|
|
self.try_add(Completion::from_semantic_completion(self.db, completion))
|
|
}
|
|
|
|
/// Always adds the given completion to this collection.
|
|
fn force_add(&mut self, completion: Completion<'db>) {
|
|
self.items.push(completion);
|
|
}
|
|
|
|
/// Tags completions with whether they are known to be usable in
|
|
/// a `raise` context.
|
|
///
|
|
/// It's possible that some completions are usable in a `raise`
|
|
/// but aren't marked by this method. That is, false negatives are
|
|
/// possible but false positives are not.
|
|
fn tag_raisable(&mut self) {
|
|
let raisable_type = UnionType::from_elements(
|
|
self.db,
|
|
[
|
|
KnownClass::BaseException.to_subclass_of(self.db),
|
|
KnownClass::BaseException.to_instance(self.db),
|
|
],
|
|
);
|
|
for item in &mut self.items {
|
|
let Some(ty) = item.ty else { continue };
|
|
item.is_definitively_raisable = ty.is_assignable_to(self.db, raisable_type);
|
|
}
|
|
}
|
|
|
|
/// Removes any completion that doesn't satisfy the given predicate.
|
|
fn retain(&mut self, predicate: impl FnMut(&Completion<'_>) -> bool) {
|
|
self.items.retain(predicate);
|
|
}
|
|
}
|
|
|
|
impl<'db> Extend<SemanticCompletion<'db>> for Completions<'db> {
|
|
fn extend<T>(&mut self, it: T)
|
|
where
|
|
T: IntoIterator<Item = SemanticCompletion<'db>>,
|
|
{
|
|
for c in it {
|
|
self.try_add_semantic(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'db> Extend<Completion<'db>> for Completions<'db> {
|
|
fn extend<T>(&mut self, it: T)
|
|
where
|
|
T: IntoIterator<Item = Completion<'db>>,
|
|
{
|
|
for c in it {
|
|
self.try_add(c);
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct Completion<'db> {
|
|
/// The label shown to the user for this suggestion.
|
|
pub name: Name,
|
|
/// The text that should be inserted at the cursor
|
|
/// when the completion is selected.
|
|
///
|
|
/// When this is not set, `name` is used.
|
|
pub insert: Option<Box<str>>,
|
|
/// The type of this completion, if available.
|
|
///
|
|
/// Generally speaking, this is always available
|
|
/// *unless* this was a completion corresponding to
|
|
/// an unimported symbol. In that case, computing the
|
|
/// type of all such symbols could be quite expensive.
|
|
pub ty: Option<Type<'db>>,
|
|
/// The "kind" of this completion.
|
|
///
|
|
/// When this is set, it takes priority over any kind
|
|
/// inferred from `ty`.
|
|
///
|
|
/// Usually this is set when `ty` is `None`, since it
|
|
/// may be cheaper to compute at scale (e.g., for
|
|
/// unimported symbol completions).
|
|
///
|
|
/// Callers should use [`Completion::kind`] to get the
|
|
/// kind, which will take type information into account
|
|
/// if this kind is not present.
|
|
pub kind: Option<CompletionKind>,
|
|
/// The name of the module that this completion comes from.
|
|
///
|
|
/// This is generally only present when this is a completion
|
|
/// suggestion for an unimported symbol.
|
|
pub module_name: Option<&'db ModuleName>,
|
|
/// An import statement to insert (or ensure is already
|
|
/// present) when this completion is selected.
|
|
pub import: Option<Edit>,
|
|
/// Whether this suggestion came from builtins or not.
|
|
///
|
|
/// At time of writing (2025-06-26), this information
|
|
/// doesn't make it into the LSP response. Instead, we
|
|
/// use it mainly in tests so that we can write less
|
|
/// noisy tests.
|
|
pub builtin: bool,
|
|
/// Whether this item only exists for type checking purposes and
|
|
/// will be missing at runtime
|
|
pub is_type_check_only: bool,
|
|
/// Whether this item can definitively be used in a `raise` context.
|
|
///
|
|
/// Note that this may not always be computed. (i.e., Only computed
|
|
/// when we are in a `raise` context.) And also note that if this
|
|
/// is `true`, then it's definitively usable in `raise`, but if
|
|
/// it's `false`, it _may_ still be usable in `raise`.
|
|
pub is_definitively_raisable: bool,
|
|
/// The documentation associated with this item, if
|
|
/// available.
|
|
pub documentation: Option<Docstring>,
|
|
}
|
|
|
|
impl<'db> Completion<'db> {
|
|
fn from_semantic_completion(
|
|
db: &'db dyn Db,
|
|
semantic: SemanticCompletion<'db>,
|
|
) -> Completion<'db> {
|
|
let definition = semantic.ty.and_then(|ty| Definitions::from_ty(db, ty));
|
|
let documentation = definition.and_then(|def| def.docstring(db));
|
|
let is_type_check_only = semantic.is_type_check_only(db);
|
|
Completion {
|
|
name: semantic.name,
|
|
insert: None,
|
|
ty: semantic.ty,
|
|
kind: None,
|
|
module_name: None,
|
|
import: None,
|
|
builtin: semantic.builtin,
|
|
is_type_check_only,
|
|
is_definitively_raisable: false,
|
|
documentation,
|
|
}
|
|
}
|
|
|
|
/// Returns the "kind" of this completion.
|
|
///
|
|
/// This is meant to be a very general classification of this completion.
|
|
/// Typically, this is communicated from the LSP server to a client, and
|
|
/// the client uses this information to help improve the UX (perhaps by
|
|
/// assigning an icon of some kind to the completion).
|
|
pub fn kind(&self, db: &'db dyn Db) -> Option<CompletionKind> {
|
|
type CompletionKindVisitor<'db> =
|
|
CycleDetector<CompletionKind, Type<'db>, Option<CompletionKind>>;
|
|
|
|
fn imp<'db>(
|
|
db: &'db dyn Db,
|
|
ty: Type<'db>,
|
|
visitor: &CompletionKindVisitor<'db>,
|
|
) -> Option<CompletionKind> {
|
|
Some(match ty {
|
|
Type::FunctionLiteral(_)
|
|
| Type::DataclassDecorator(_)
|
|
| Type::WrapperDescriptor(_)
|
|
| Type::DataclassTransformer(_)
|
|
| Type::Callable(_) => CompletionKind::Function,
|
|
Type::BoundMethod(_) | Type::KnownBoundMethod(_) => CompletionKind::Method,
|
|
Type::ModuleLiteral(_) => CompletionKind::Module,
|
|
Type::ClassLiteral(_) | Type::GenericAlias(_) | Type::SubclassOf(_) => {
|
|
CompletionKind::Class
|
|
}
|
|
// This is a little weird for "struct." I'm mostly interpreting
|
|
// "struct" here as a more general "object." ---AG
|
|
Type::NominalInstance(_)
|
|
| Type::PropertyInstance(_)
|
|
| Type::BoundSuper(_)
|
|
| Type::TypedDict(_)
|
|
| Type::NewTypeInstance(_) => CompletionKind::Struct,
|
|
Type::IntLiteral(_)
|
|
| Type::BooleanLiteral(_)
|
|
| Type::TypeIs(_)
|
|
| Type::StringLiteral(_)
|
|
| Type::LiteralString
|
|
| Type::BytesLiteral(_) => CompletionKind::Value,
|
|
Type::EnumLiteral(_) => CompletionKind::Enum,
|
|
Type::ProtocolInstance(_) => CompletionKind::Interface,
|
|
Type::TypeVar(_) => CompletionKind::TypeParameter,
|
|
Type::Union(union) => union
|
|
.elements(db)
|
|
.iter()
|
|
.find_map(|&ty| imp(db, ty, visitor))?,
|
|
Type::Intersection(intersection) => intersection
|
|
.iter_positive(db)
|
|
.find_map(|ty| imp(db, ty, visitor))?,
|
|
Type::Dynamic(_)
|
|
| Type::Never
|
|
| Type::SpecialForm(_)
|
|
| Type::KnownInstance(_)
|
|
| Type::AlwaysTruthy
|
|
| Type::AlwaysFalsy => return None,
|
|
Type::TypeAlias(alias) => {
|
|
visitor.visit(ty, || imp(db, alias.value_type(db), visitor))?
|
|
}
|
|
})
|
|
}
|
|
self.kind.or_else(|| {
|
|
self.ty
|
|
.and_then(|ty| imp(db, ty, &CompletionKindVisitor::default()))
|
|
})
|
|
}
|
|
|
|
fn keyword(name: &str) -> Self {
|
|
Completion {
|
|
name: name.into(),
|
|
insert: None,
|
|
ty: None,
|
|
kind: Some(CompletionKind::Keyword),
|
|
module_name: None,
|
|
import: None,
|
|
builtin: false,
|
|
is_type_check_only: false,
|
|
is_definitively_raisable: false,
|
|
documentation: None,
|
|
}
|
|
}
|
|
|
|
fn value_keyword(name: &str, ty: Type<'db>) -> Completion<'db> {
|
|
Completion {
|
|
name: name.into(),
|
|
insert: None,
|
|
ty: Some(ty),
|
|
kind: Some(CompletionKind::Keyword),
|
|
module_name: None,
|
|
import: None,
|
|
builtin: true,
|
|
is_type_check_only: false,
|
|
is_definitively_raisable: false,
|
|
documentation: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The "kind" of a completion.
|
|
///
|
|
/// This is taken directly from the LSP completion specification:
|
|
/// <https://microsoft.github.io/language-server-protocol/specifications/lsp/3.17/specification/#completionItemKind>
|
|
///
|
|
/// The idea here is that [`Completion::kind`] defines the mapping to this from
|
|
/// `Type` (and possibly other information), which might be interesting and
|
|
/// contentious. Then the outer edges map this to the LSP types, which is
|
|
/// expected to be mundane and boring.
|
|
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
|
pub enum CompletionKind {
|
|
Text,
|
|
Method,
|
|
Function,
|
|
Constructor,
|
|
Field,
|
|
Variable,
|
|
Class,
|
|
Interface,
|
|
Module,
|
|
Property,
|
|
Unit,
|
|
Value,
|
|
Enum,
|
|
Keyword,
|
|
Snippet,
|
|
Color,
|
|
File,
|
|
Reference,
|
|
Folder,
|
|
EnumMember,
|
|
Constant,
|
|
Struct,
|
|
Event,
|
|
Operator,
|
|
TypeParameter,
|
|
}
|
|
|
|
#[derive(Clone, Debug, Default)]
|
|
pub struct CompletionSettings {
|
|
pub auto_import: bool,
|
|
}
|
|
|
|
pub fn completion<'db>(
|
|
db: &'db dyn Db,
|
|
settings: &CompletionSettings,
|
|
file: File,
|
|
offset: TextSize,
|
|
) -> Vec<Completion<'db>> {
|
|
let parsed = parsed_module(db, file).load(db);
|
|
let tokens = tokens_start_before(parsed.tokens(), offset);
|
|
let typed = find_typed_text(db, file, &parsed, offset);
|
|
|
|
if is_in_no_completions_place(db, file, &parsed, offset, tokens, typed.as_deref()) {
|
|
return vec![];
|
|
}
|
|
|
|
let mut completions = Completions::fuzzy(db, typed.as_deref());
|
|
|
|
if let Some(import) = ImportStatement::detect(db, file, &parsed, tokens, typed.as_deref()) {
|
|
import.add_completions(db, file, &mut completions);
|
|
} else {
|
|
let Some(target_token) = CompletionTargetTokens::find(&parsed, offset) else {
|
|
return vec![];
|
|
};
|
|
let Some(target) = target_token.ast(&parsed, offset) else {
|
|
return vec![];
|
|
};
|
|
|
|
let model = SemanticModel::new(db, file);
|
|
let (semantic_completions, scoped) = match target {
|
|
CompletionTargetAst::ObjectDot { expr } => (model.attribute_completions(expr), None),
|
|
CompletionTargetAst::Scoped(scoped) => {
|
|
(model.scoped_completions(scoped.node), Some(scoped))
|
|
}
|
|
};
|
|
|
|
completions.extend(semantic_completions);
|
|
if scoped.is_some() {
|
|
add_keyword_completions(db, &mut completions);
|
|
}
|
|
if settings.auto_import {
|
|
if let Some(scoped) = scoped {
|
|
add_unimported_completions(
|
|
db,
|
|
file,
|
|
&parsed,
|
|
scoped,
|
|
|module_name: &ModuleName, symbol: &str| {
|
|
ImportRequest::import_from(module_name.as_str(), symbol)
|
|
},
|
|
&mut completions,
|
|
);
|
|
}
|
|
}
|
|
}
|
|
|
|
if is_raising_exception(tokens) {
|
|
completions.tag_raisable();
|
|
|
|
// As a special case, and because it's a common footgun, we
|
|
// specifically disallow `NotImplemented` in this context.
|
|
// `NotImplementedError` should be used instead. So if we can
|
|
// definitively detect `NotImplemented`, then we can safely
|
|
// omit it from suggestions.
|
|
completions.retain(|item| {
|
|
let Some(ty) = item.ty else { return true };
|
|
!ty.is_notimplemented(db)
|
|
});
|
|
}
|
|
|
|
completions.into_completions()
|
|
}
|
|
|
|
pub(crate) struct ImportEdit {
|
|
pub label: String,
|
|
pub edit: Edit,
|
|
}
|
|
|
|
pub(crate) fn missing_imports(
|
|
db: &dyn Db,
|
|
file: File,
|
|
parsed: &ParsedModuleRef,
|
|
symbol: &str,
|
|
node: AnyNodeRef,
|
|
) -> Vec<ImportEdit> {
|
|
let mut completions = Completions::exactly(db, symbol);
|
|
let scoped = ScopedTarget { node };
|
|
add_unimported_completions(
|
|
db,
|
|
file,
|
|
parsed,
|
|
scoped,
|
|
|module_name: &ModuleName, symbol: &str| {
|
|
ImportRequest::import_from(module_name.as_str(), symbol).force()
|
|
},
|
|
&mut completions,
|
|
);
|
|
|
|
completions.into_imports()
|
|
}
|
|
|
|
/// Adds completions derived from keywords.
|
|
///
|
|
/// This should generally only be used when offering "scoped" completions.
|
|
/// This will include keywords corresponding to Python values (like `None`)
|
|
/// and general language keywords (like `raise`).
|
|
fn add_keyword_completions<'db>(db: &'db dyn Db, completions: &mut Completions<'db>) {
|
|
let keyword_values = [
|
|
("None", Type::none(db)),
|
|
("True", Type::BooleanLiteral(true)),
|
|
("False", Type::BooleanLiteral(false)),
|
|
];
|
|
for (name, ty) in keyword_values {
|
|
completions.try_add(Completion::value_keyword(name, ty));
|
|
}
|
|
|
|
// Note that we specifically omit the `type` keyword here, since
|
|
// it will be included via `builtins`. This does make its sorting
|
|
// priority slighty different than other keywords, but it's not
|
|
// clear (to me, AG) if that's an issue or not. Since the builtin
|
|
// completion has an actual type associated with it, we use that
|
|
// instead of a keyword completion.
|
|
let keywords = [
|
|
"and", "as", "assert", "async", "await", "break", "class", "continue", "def", "del",
|
|
"elif", "else", "except", "finally", "for", "from", "global", "if", "import", "in", "is",
|
|
"lambda", "nonlocal", "not", "or", "pass", "raise", "return", "try", "while", "with",
|
|
"yield", "case", "match",
|
|
];
|
|
for name in keywords {
|
|
completions.try_add(Completion::keyword(name));
|
|
}
|
|
}
|
|
|
|
/// Adds completions not in scope.
|
|
///
|
|
/// `scoped` should be information about the identified scope
|
|
/// in which the cursor is currently placed.
|
|
///
|
|
/// The completions returned will auto-insert import statements
|
|
/// when selected into `File`.
|
|
fn add_unimported_completions<'db>(
|
|
db: &'db dyn Db,
|
|
file: File,
|
|
parsed: &ParsedModuleRef,
|
|
scoped: ScopedTarget<'_>,
|
|
create_import_request: impl for<'a> Fn(&'a ModuleName, &'a str) -> ImportRequest<'a>,
|
|
completions: &mut Completions<'db>,
|
|
) {
|
|
// This is redundant since `all_symbols` will also bail
|
|
// when the query can match everything. But we bail here
|
|
// to avoid building an `Importer` and other plausibly
|
|
// costly work when we know we won't use it.
|
|
if completions.query.will_match_everything() {
|
|
return;
|
|
}
|
|
|
|
let source = source_text(db, file);
|
|
let stylist = Stylist::from_tokens(parsed.tokens(), source.as_str());
|
|
let importer = Importer::new(db, &stylist, file, source.as_str(), parsed);
|
|
let members = importer.members_in_scope_at(scoped.node, scoped.node.start());
|
|
|
|
for symbol in all_symbols(db, file, &completions.query) {
|
|
if symbol.module.file(db) == Some(file) || symbol.module.is_known(db, KnownModule::Builtins)
|
|
{
|
|
continue;
|
|
}
|
|
|
|
let request = create_import_request(symbol.module.name(db), &symbol.symbol.name);
|
|
// FIXME: `all_symbols` doesn't account for wildcard imports.
|
|
// Since we're looking at every module, this is probably
|
|
// "fine," but it might mean that we import a symbol from the
|
|
// "wrong" module.
|
|
let import_action = importer.import(request, &members);
|
|
// N.B. We use `add` here because `all_symbols` already
|
|
// takes our query into account.
|
|
completions.force_add(Completion {
|
|
name: ast::name::Name::new(&symbol.symbol.name),
|
|
insert: Some(import_action.symbol_text().into()),
|
|
ty: None,
|
|
kind: symbol.symbol.kind.to_completion_kind(),
|
|
module_name: Some(symbol.module.name(db)),
|
|
import: import_action.import().cloned(),
|
|
builtin: false,
|
|
// TODO: `is_type_check_only` requires inferring the type of the symbol
|
|
is_type_check_only: false,
|
|
is_definitively_raisable: false,
|
|
documentation: None,
|
|
});
|
|
}
|
|
}
|
|
|
|
/// The kind of tokens identified under the cursor.
|
|
#[derive(Debug)]
|
|
enum CompletionTargetTokens<'t> {
|
|
/// A `object.attribute` token form was found, where
|
|
/// `attribute` may be empty.
|
|
///
|
|
/// This requires a name token followed by a dot token.
|
|
///
|
|
/// This is "possibly" an `object.attribute` because
|
|
/// the object token may not correspond to an object
|
|
/// or it may correspond to *part* of an object.
|
|
/// This is resolved when we try to find an overlapping
|
|
/// AST `ExprAttribute` node later. If we couldn't, then
|
|
/// this is probably not an `object.attribute`.
|
|
PossibleObjectDot {
|
|
/// The token preceding the dot.
|
|
object: &'t Token,
|
|
/// The token, if non-empty, following the dot.
|
|
///
|
|
/// For right now, this is only used to determine which
|
|
/// module in an `import` statement to return submodule
|
|
/// completions for. But we could use it for other things,
|
|
/// like only returning completions that start with a prefix
|
|
/// corresponding to this token.
|
|
#[expect(dead_code)]
|
|
attribute: Option<&'t Token>,
|
|
},
|
|
/// A token was found under the cursor, but it didn't
|
|
/// match any of our anticipated token patterns.
|
|
Generic { token: &'t Token },
|
|
/// No token was found. We generally treat this like
|
|
/// `Generic` (i.e., offer scope based completions).
|
|
Unknown,
|
|
}
|
|
|
|
impl<'t> CompletionTargetTokens<'t> {
|
|
/// Look for the best matching token pattern at the given offset.
|
|
fn find(parsed: &ParsedModuleRef, offset: TextSize) -> Option<CompletionTargetTokens<'_>> {
|
|
static OBJECT_DOT_EMPTY: [TokenKind; 1] = [TokenKind::Dot];
|
|
static OBJECT_DOT_NON_EMPTY: [TokenKind; 2] = [TokenKind::Dot, TokenKind::Name];
|
|
|
|
let offset = match parsed.tokens().at_offset(offset) {
|
|
TokenAt::None => return Some(CompletionTargetTokens::Unknown),
|
|
TokenAt::Single(tok) => tok.end(),
|
|
TokenAt::Between(_, tok) => tok.start(),
|
|
};
|
|
let before = tokens_start_before(parsed.tokens(), offset);
|
|
Some(
|
|
// Our strategy when it comes to `object.attribute` here is
|
|
// to look for the `.` and then take the token immediately
|
|
// preceding it. Later, we look for an `ExprAttribute` AST
|
|
// node that overlaps (even partially) with this token. And
|
|
// that's the object we try to complete attributes for.
|
|
if let Some([_dot]) = token_suffix_by_kinds(before, OBJECT_DOT_EMPTY) {
|
|
let object = before[..before.len() - 1].last()?;
|
|
CompletionTargetTokens::PossibleObjectDot {
|
|
object,
|
|
attribute: None,
|
|
}
|
|
} else if let Some([_dot, attribute]) =
|
|
token_suffix_by_kinds(before, OBJECT_DOT_NON_EMPTY)
|
|
{
|
|
let object = before[..before.len() - 2].last()?;
|
|
CompletionTargetTokens::PossibleObjectDot {
|
|
object,
|
|
attribute: Some(attribute),
|
|
}
|
|
} else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Float]) {
|
|
// If we're writing a `float`, then we should
|
|
// specifically not offer completions. This wouldn't
|
|
// normally be an issue, but if completions are
|
|
// automatically triggered by a `.` (which is what we
|
|
// request as an LSP server), then we can get here
|
|
// in the course of just writing a decimal number.
|
|
return None;
|
|
} else if let Some([_]) = token_suffix_by_kinds(before, [TokenKind::Ellipsis]) {
|
|
// Similarly as above. If we've just typed an ellipsis,
|
|
// then we shouldn't show completions. Note that
|
|
// this doesn't prevent `....<CURSOR>` from showing
|
|
// completions (which would be the attributes available
|
|
// on an `ellipsis` object).
|
|
return None;
|
|
} else {
|
|
let Some(last) = before.last() else {
|
|
return Some(CompletionTargetTokens::Unknown);
|
|
};
|
|
CompletionTargetTokens::Generic { token: last }
|
|
},
|
|
)
|
|
}
|
|
|
|
/// Returns a corresponding AST node for these tokens.
|
|
///
|
|
/// `offset` should be the offset of the cursor.
|
|
///
|
|
/// If no plausible AST node could be found, then `None` is returned.
|
|
fn ast(
|
|
&self,
|
|
parsed: &'t ParsedModuleRef,
|
|
offset: TextSize,
|
|
) -> Option<CompletionTargetAst<'t>> {
|
|
match *self {
|
|
CompletionTargetTokens::PossibleObjectDot { object, .. } => {
|
|
let covering_node = covering_node(parsed.syntax().into(), object.range())
|
|
.find_last(|node| {
|
|
// We require that the end of the node range not
|
|
// exceed the cursor offset. This avoids selecting
|
|
// a node "too high" in the AST in cases where
|
|
// completions are requested in the middle of an
|
|
// expression. e.g., `foo.<CURSOR>.bar`.
|
|
if node.is_expr_attribute() {
|
|
return node.range().end() <= offset;
|
|
}
|
|
// For import statements though, they can't be
|
|
// nested, so we don't care as much about the
|
|
// cursor being strictly after the statement.
|
|
// And indeed, sometimes it won't be! e.g.,
|
|
//
|
|
// import re, os.p<CURSOR>, zlib
|
|
//
|
|
// So just return once we find an import.
|
|
node.is_stmt_import() || node.is_stmt_import_from()
|
|
})
|
|
.ok()?;
|
|
match covering_node.node() {
|
|
ast::AnyNodeRef::ExprAttribute(expr) => {
|
|
Some(CompletionTargetAst::ObjectDot { expr })
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
CompletionTargetTokens::Generic { token } => {
|
|
let node = covering_node(parsed.syntax().into(), token.range()).node();
|
|
Some(CompletionTargetAst::Scoped(ScopedTarget { node }))
|
|
}
|
|
CompletionTargetTokens::Unknown => {
|
|
let range = TextRange::empty(offset);
|
|
let covering_node = covering_node(parsed.syntax().into(), range);
|
|
Some(CompletionTargetAst::Scoped(ScopedTarget {
|
|
node: covering_node.node(),
|
|
}))
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The AST node patterns that we support identifying under the cursor.
|
|
#[derive(Debug)]
|
|
enum CompletionTargetAst<'t> {
|
|
/// A `object.attribute` scenario, where we want to
|
|
/// list attributes on `object` for completions.
|
|
ObjectDot { expr: &'t ast::ExprAttribute },
|
|
/// A scoped scenario, where we want to list all items available in
|
|
/// the most narrow scope containing the giving AST node.
|
|
Scoped(ScopedTarget<'t>),
|
|
}
|
|
|
|
#[derive(Clone, Copy, Debug)]
|
|
struct ScopedTarget<'t> {
|
|
/// The node with the smallest range that fully covers
|
|
/// the token under the cursor.
|
|
node: ast::AnyNodeRef<'t>,
|
|
}
|
|
|
|
/// A representation of the completion context for a possibly incomplete import
|
|
/// statement.
|
|
#[derive(Clone, Debug)]
|
|
enum ImportStatement<'a> {
|
|
FromImport(FromImport<'a>),
|
|
Import(Import<'a>),
|
|
Incomplete(IncompleteImport),
|
|
}
|
|
|
|
/// A representation of the completion context for a possibly incomplete
|
|
/// `from ... import ...` statement.
|
|
#[derive(Clone, Debug)]
|
|
struct FromImport<'a> {
|
|
ast: &'a ast::StmtImportFrom,
|
|
kind: FromImportKind,
|
|
}
|
|
|
|
/// The kind of completions to offer for a `from import` statement.
|
|
///
|
|
/// This is either something like `from col<CURSOR>`, where we should
|
|
/// offer module completions, or `from collections.<CURSOR>`, where
|
|
/// we should offer submodule completions or
|
|
/// `from collections import default<CURSOR>` where we should offer
|
|
/// submodule/attribute completions.
|
|
#[derive(Clone, Debug)]
|
|
enum FromImportKind {
|
|
Module,
|
|
Submodule {
|
|
parent: ModuleName,
|
|
},
|
|
Relative {
|
|
parent: ModuleName,
|
|
/// When `true`, an `import` keyword is allowed next.
|
|
/// For example, `from ...<CURSOR>` should offer `import`
|
|
/// but also submodule completions.
|
|
import_keyword_allowed: bool,
|
|
},
|
|
Attribute,
|
|
}
|
|
|
|
/// A representation of the completion context for a possibly incomplete
|
|
/// `import ...` statement.
|
|
#[derive(Clone, Debug)]
|
|
struct Import<'a> {
|
|
#[expect(dead_code)]
|
|
ast: &'a ast::StmtImport,
|
|
kind: ImportKind,
|
|
}
|
|
|
|
/// The kind of completions to offer for an `import` statement.
|
|
///
|
|
/// This is either something like `import col<CURSOR>`, where we should
|
|
/// offer module completions, or `import collections.<CURSOR>`, where
|
|
/// we should offer submodule completions.
|
|
#[derive(Clone, Debug)]
|
|
enum ImportKind {
|
|
Module,
|
|
Submodule { parent: ModuleName },
|
|
}
|
|
|
|
/// Occurs when we detect that an import statement
|
|
/// is likely incomplete by virtue of a missing or
|
|
/// in-progress `as` or `import` keyword.
|
|
#[derive(Clone, Debug)]
|
|
enum IncompleteImport {
|
|
As,
|
|
Import,
|
|
}
|
|
|
|
impl<'a> ImportStatement<'a> {
|
|
/// The number of tokens we're willing to consume backwards from
|
|
/// the cursor's position until we give up looking for an import
|
|
/// statement. The state machine below has lots of opportunities
|
|
/// to bail way earlier than this, but if there's, e.g., a long
|
|
/// list of name tokens for something that isn't an import, then we
|
|
/// could end up doing a lot of wasted work here. Probably humans
|
|
/// aren't often working with single import statements over 1,000
|
|
/// tokens long.
|
|
///
|
|
/// The other thing to consider here is that, by the time we get to
|
|
/// this point, ty has already done some work proportional to the
|
|
/// length of `tokens` anyway. The unit of work we do below is very
|
|
/// small.
|
|
const LIMIT: usize = 1_000;
|
|
|
|
/// Attempts to detect an import statement in reverse starting at
|
|
/// the end of `tokens`. That is, `tokens` should correspond to the
|
|
/// sequence of tokens up to the end user's cursor. `typed` should
|
|
/// correspond to the text the user has typed, which is usually,
|
|
/// but not always, the text corresponding to the last token in
|
|
/// `tokens`.
|
|
fn detect(
|
|
db: &'a dyn Db,
|
|
file: File,
|
|
parsed: &'a ParsedModuleRef,
|
|
tokens: &'a [Token],
|
|
typed: Option<&str>,
|
|
) -> Option<ImportStatement<'a>> {
|
|
use TokenKind as TK;
|
|
|
|
// This state machine moves backwards through the token stream,
|
|
// starting at where the user's cursor is and ending when
|
|
// either a `from` token is found, or a token that cannot
|
|
// possibly appear in an import statement at a particular
|
|
// position is found.
|
|
//
|
|
// To understand this state machine, it's recommended to become
|
|
// familiar with the grammar for Python import statements:
|
|
// https://docs.python.org/3/reference/grammar.html
|
|
|
|
/// The current state of the parser below.
|
|
#[derive(Clone, Copy, Debug)]
|
|
enum S {
|
|
/// Our initial state.
|
|
Start,
|
|
/// We just saw an `import` token.
|
|
Import,
|
|
/// We just saw a first "name" token. That is,
|
|
/// a name-like token that appears just before
|
|
/// the end user's cursor.
|
|
///
|
|
/// This isn't just limited to `TokenKind::Name`.
|
|
/// This also includes keywords and things like
|
|
/// "unknown" tokens that can stand in for names
|
|
/// at times.
|
|
FirstName,
|
|
/// A name seen immediately after the first name. This
|
|
/// indicates we have an incomplete import statement like
|
|
/// `import foo a<CURSOR>` or `from foo imp<CURSOR>`. But
|
|
/// we mush on.
|
|
AdjacentName,
|
|
|
|
/// A state where we expect to see the start of or
|
|
/// continuation of a list of names following `import`.
|
|
/// In the [grammar], this is either `dotted_as_names`
|
|
/// or `import_from_as_names`.
|
|
///
|
|
/// [grammar]: https://docs.python.org/3/reference/grammar.html
|
|
NameList,
|
|
/// Occurs after seeing a name-like token at the end
|
|
/// of a name list. This could be an alias, a dotted
|
|
/// name or a non-dotted name.
|
|
NameListNameOrAlias,
|
|
/// Occurs when we've seen an `as` in a list of names.
|
|
As,
|
|
/// Occurs when we see a name-like token after an `as`
|
|
/// keyword.
|
|
AsName,
|
|
|
|
/// Occurs when we see a `.` between name-like tokens
|
|
/// after an `as` keyword. This implies we must parse
|
|
/// a `from` statement, since an `as` in a `from` can
|
|
/// never alias a dotted name.
|
|
AsDottedNameDot,
|
|
/// Occurs when we see a name-like token after a
|
|
/// `.name as`.
|
|
AsDottedName,
|
|
/// Occurs when we see a comma right before `a.b as foo`.
|
|
AsDottedNameComma,
|
|
/// Occurs before `, a.b as foo`. In this state, we can
|
|
/// see either a non-dotted alias or a dotted name.
|
|
AsDottedNameOrAlias,
|
|
/// Occurs before `bar, a.b as foo`. In this state, we can
|
|
/// see a `.`, `as`, or `import`.
|
|
AsDottedNameOrAliasName,
|
|
|
|
/// Occurs when we've seen a dot right before the cursor
|
|
/// or after the first name-like token. That is, `.name`.
|
|
/// This could be from `import module.name` or `from ..name
|
|
/// import blah`.
|
|
InitialDot,
|
|
/// Occurs when we see `foo.bar<CURSOR>`. When we enter
|
|
/// this state, it means we must be in an `import`
|
|
/// statement, since `from foo.bar` is always invalid.
|
|
InitialDotName,
|
|
/// Occurs when we see `.foo.bar<CURSOR>`. This lets us
|
|
/// continue consuming a dotted name.
|
|
InitialDottedName,
|
|
|
|
// When the states below occur, we are locked into
|
|
// recognizing a `from ... import ...` statement.
|
|
/// Occurs when we've seen an ellipsis right before the
|
|
/// cursor or after the first name-like token. That is,
|
|
/// `...name`. This must be from a
|
|
/// `from ...name import blah` statement.
|
|
FromEllipsisName,
|
|
/// A state for consuming `.` and `...` in a `from` import
|
|
/// statement. We enter this after seeing a `.` or a `...`
|
|
/// right after an `import` statement or a `...` right
|
|
/// before the end user's cursor. Either way, we have to
|
|
/// consume only dots at this point until we find a `from`
|
|
/// token.
|
|
FromDots,
|
|
/// Occurs when we've seen an `import` followed by a name-like
|
|
/// token. i.e., `from name import` or `from ...name import`.
|
|
FromDottedName,
|
|
/// Occurs when we've seen an `import` followed by a
|
|
/// name-like token with a dot. i.e., `from .name import`
|
|
/// or `from ..name import`.
|
|
FromDottedNameDot,
|
|
/// A `*` was just seen, which must mean the import is of
|
|
/// the form `from module import *`.
|
|
FromStar,
|
|
/// A left parenthesis was just seen.
|
|
FromLpar,
|
|
|
|
// Below are terminal states. Once we reach one
|
|
// of these, the state machine ends.
|
|
/// We just saw a `from` token. We never have any
|
|
/// outgoing transitions from this.
|
|
From,
|
|
/// This is like `import`, but used in a context
|
|
/// where we know we're in an import statement and
|
|
/// specifically *not* a `from ... import ...`
|
|
/// statement.
|
|
ImportFinal,
|
|
}
|
|
|
|
let mut state = S::Start;
|
|
// The token immediate before (or at) the cursor.
|
|
let last = tokens.last()?;
|
|
// A token corresponding to `import`, if found.
|
|
let mut import: Option<&Token> = None;
|
|
// A token corresponding to `from`, if found.
|
|
let mut from: Option<&Token> = None;
|
|
// Whether an initial dot was found right before the cursor,
|
|
// or right before the name at the cursor.
|
|
let mut initial_dot = false;
|
|
// An incomplete import statement was found.
|
|
// Usually either `from foo imp<CURSOR>`
|
|
// or `import foo a<CURSOR>`.
|
|
let mut incomplete_as_or_import = false;
|
|
for token in tokens.iter().rev().take(Self::LIMIT) {
|
|
if token.kind().is_trivia() {
|
|
continue;
|
|
}
|
|
state = match (state, token.kind()) {
|
|
// These cases handle our "initial" condition.
|
|
// Basically, this is what detects how to drop us into
|
|
// the full state machine below for parsing any kind of
|
|
// import statement. There are also some cases we try
|
|
// to detect here that indicate the user is probably
|
|
// typing an `import` or `as` token. In effect, we
|
|
// try to pluck off the initial name-like token that
|
|
// represents where the cursor likely is. And then
|
|
// we move on to try and detect the type of import
|
|
// statement that we're dealing with.
|
|
// (S::Start, TK::Newline) => S::Start,
|
|
(S::Start, TK::Star) => S::FromStar,
|
|
(S::Start, TK::Name) if typed.is_none() => S::AdjacentName,
|
|
(S::Start, TK::Name) => S::FirstName,
|
|
(S::Start | S::FirstName | S::AdjacentName, TK::Import) => S::Import,
|
|
(S::Start | S::FirstName | S::AdjacentName, TK::Lpar) => S::FromLpar,
|
|
(S::Start | S::FirstName | S::AdjacentName, TK::Comma) => S::NameList,
|
|
(S::Start | S::FirstName | S::AdjacentName, TK::Dot) => S::InitialDot,
|
|
(S::Start | S::FirstName | S::AdjacentName, TK::Ellipsis) => S::FromEllipsisName,
|
|
(S::Start | S::FirstName, TK::As) => S::As,
|
|
(S::Start | S::AdjacentName, TK::From) => S::From,
|
|
(S::FirstName, TK::From) => S::From,
|
|
(S::FirstName, TK::Name) => S::AdjacentName,
|
|
|
|
// This handles the case where we see `.name`. Here,
|
|
// we could be in `from .name`, `from ..name`, `from
|
|
// ...name`, `from foo.name`, `import foo.name`,
|
|
// `import bar, foo.name` and so on.
|
|
(S::InitialDot, TK::Dot | TK::Ellipsis) => S::FromDots,
|
|
(S::InitialDot, TK::Name) => S::InitialDotName,
|
|
(S::InitialDot, TK::From) => S::From,
|
|
(S::InitialDotName, TK::Dot) => S::InitialDottedName,
|
|
(S::InitialDotName, TK::Ellipsis) => S::FromDots,
|
|
(S::InitialDotName, TK::As) => S::AsDottedNameDot,
|
|
(S::InitialDotName, TK::Comma) => S::AsDottedNameOrAlias,
|
|
(S::InitialDotName, TK::Import) => S::ImportFinal,
|
|
(S::InitialDotName, TK::From) => S::From,
|
|
(S::InitialDottedName, TK::Dot | TK::Ellipsis) => S::FromDots,
|
|
(S::InitialDottedName, TK::Name) => S::InitialDotName,
|
|
(S::InitialDottedName, TK::From) => S::From,
|
|
|
|
// This state machine parses `dotted_as_names` or
|
|
// `import_from_as_names`. It has a carve out for when
|
|
// it finds a dot, which indicates it must parse only
|
|
// `dotted_as_names`.
|
|
(S::NameList, TK::Name | TK::Unknown) => S::NameListNameOrAlias,
|
|
(S::NameList, TK::Lpar) => S::FromLpar,
|
|
(S::NameListNameOrAlias, TK::As) => S::As,
|
|
(S::NameListNameOrAlias, TK::Comma) => S::NameList,
|
|
(S::NameListNameOrAlias, TK::Import) => S::Import,
|
|
(S::NameListNameOrAlias, TK::Lpar) => S::FromLpar,
|
|
(S::NameListNameOrAlias, TK::Unknown) => S::NameListNameOrAlias,
|
|
// This pops us out of generic name-list parsing
|
|
// and puts us firmly into `dotted_as_names` in
|
|
// the grammar.
|
|
(S::NameListNameOrAlias, TK::Dot) => S::AsDottedNameDot,
|
|
|
|
// This identifies aliasing via `as`. The main trick
|
|
// here is that if we see a `.`, then we move to a
|
|
// different set of states since we know we must be in
|
|
// an `import` statement. Without a `.` though, we
|
|
// could be in an `import` or a `from`. For example,
|
|
// `import numpy as np` or
|
|
// `from collections import defaultdict as dd`.
|
|
(S::As, TK::Name) => S::AsName,
|
|
(S::AsName, TK::Dot) => S::AsDottedNameDot,
|
|
(S::AsName, TK::Import) => S::Import,
|
|
(S::AsName, TK::Comma) => S::NameList,
|
|
|
|
// This is the mini state machine for handling
|
|
// `dotted_as_names`. We enter it when we see
|
|
// `foo.bar as baz`. We therefore know this must
|
|
// be an `import` statement and not a `from import`
|
|
// statement.
|
|
(S::AsDottedName, TK::Dot) => S::AsDottedNameDot,
|
|
(S::AsDottedName, TK::Comma) => S::AsDottedNameComma,
|
|
(S::AsDottedName, TK::Import) => S::ImportFinal,
|
|
(S::AsDottedNameDot, TK::Name) => S::AsDottedName,
|
|
(S::AsDottedNameComma, TK::Name) => S::AsDottedNameOrAlias,
|
|
(S::AsDottedNameOrAlias, TK::Name) => S::AsDottedNameOrAliasName,
|
|
(S::AsDottedNameOrAlias, TK::Dot) => S::AsDottedNameDot,
|
|
(S::AsDottedNameOrAliasName, TK::Dot | TK::As) => S::AsDottedNameDot,
|
|
(S::AsDottedNameOrAliasName, TK::Import) => S::ImportFinal,
|
|
|
|
// A `*` and `(` immediately constrains what we're allowed to see.
|
|
// We can jump right to expecting an `import` keyword.
|
|
(S::FromStar | S::FromLpar, TK::Import) => S::Import,
|
|
|
|
// The transitions below handle everything from `from`
|
|
// to `import`. Basically, once we see an `import`
|
|
// token or otherwise know we're parsing the module
|
|
// section of a `from` import statement, we end up in
|
|
// one of the transitions below.
|
|
(S::Import, TK::Dot | TK::Ellipsis) => S::FromDots,
|
|
(S::Import, TK::Name | TK::Unknown) => S::FromDottedName,
|
|
(S::FromDottedName, TK::Dot) => S::FromDottedNameDot,
|
|
(S::FromDottedName, TK::Ellipsis) => S::FromDots,
|
|
(S::FromDottedNameDot, TK::Name) => S::FromDottedName,
|
|
(S::FromDottedNameDot, TK::Dot | TK::Ellipsis) => S::FromDots,
|
|
(S::FromEllipsisName | S::FromDots, TK::Dot | TK::Ellipsis) => S::FromDots,
|
|
(
|
|
S::FromEllipsisName | S::FromDots | S::FromDottedName | S::FromDottedNameDot,
|
|
TK::From,
|
|
) => S::From,
|
|
|
|
_ => break,
|
|
};
|
|
// If we transition into a few different special
|
|
// states, we record the token.
|
|
match state {
|
|
S::Import | S::ImportFinal => {
|
|
import = Some(token);
|
|
}
|
|
S::From => {
|
|
from = Some(token);
|
|
}
|
|
S::AdjacentName => {
|
|
// We've seen two adjacent name-like tokens
|
|
// right before the cursor. At this point,
|
|
// we continue on to try to recognize a nearly
|
|
// valid import statement, and to figure out
|
|
// what kinds of completions we should offer
|
|
// (if any).
|
|
incomplete_as_or_import = true;
|
|
}
|
|
S::InitialDot | S::FromEllipsisName => {
|
|
initial_dot = true;
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
|
|
// Now find a possibly dotted name up to where the current
|
|
// cursor is. This could be an item inside a module, a module
|
|
// name, a submodule name or even a relative module. The
|
|
// point is that it is the thing that the end user is trying
|
|
// to complete.
|
|
let source = source_text(db, file);
|
|
let mut to_complete = String::new();
|
|
let end = last.range().end();
|
|
let mut start = end;
|
|
for token in tokens.iter().rev().take(Self::LIMIT) {
|
|
match token.kind() {
|
|
TK::Name | TK::Dot | TK::Ellipsis => {
|
|
start = token.range().start();
|
|
}
|
|
_ => break,
|
|
}
|
|
}
|
|
to_complete.push_str(&source[TextRange::new(start, end)]);
|
|
|
|
// If the typed text corresponds precisely to a keyword,
|
|
// then as a special case, consider it "incomplete" for that
|
|
// keyword. This occurs when the cursor is immediately at the
|
|
// end of `import` or `as`, e.g., `import<CURSOR>`. So we
|
|
// should provide it as a completion so that the end user can
|
|
// confirm it as-is. We special case this because a complete
|
|
// `import` or `as` gets special recognition as a special token
|
|
// kind, and it isn't worth complicating the state machine
|
|
// above to account for this.
|
|
//
|
|
// We also handle the more general "incomplete" cases here too.
|
|
// Basically, `incomplete_as_or_import` is set to `true` when
|
|
// we detect an "adjacent" name in an import statement. Some
|
|
// examples:
|
|
//
|
|
// from foo <CURSOR>
|
|
// from foo imp<CURSOR>
|
|
// from foo import bar <CURSOR>
|
|
// from foo import bar a<CURSOR>
|
|
// import foo <CURSOR>
|
|
// import foo a<CURSOR>
|
|
//
|
|
// Since there is a very limited number of cases, we can
|
|
// suggest `import` when an `import` token isn't present. And
|
|
// `as` when an `import` token *is* present. Notably, `as` can
|
|
// only appear after an `import` keyword!
|
|
if typed == Some("import") || (incomplete_as_or_import && import.is_none()) {
|
|
return Some(ImportStatement::Incomplete(IncompleteImport::Import));
|
|
} else if typed == Some("as") || (incomplete_as_or_import && import.is_some()) {
|
|
return Some(ImportStatement::Incomplete(IncompleteImport::As));
|
|
}
|
|
match (from, import) {
|
|
(None, None) => None,
|
|
(None, Some(import)) => {
|
|
let ast = find_ast_for_import(parsed, import)?;
|
|
// If we found a dot near the cursor, then this
|
|
// must be a request for submodule completions.
|
|
let kind = if initial_dot {
|
|
let (parent, _) = to_complete.rsplit_once('.')?;
|
|
let module_name = ModuleName::new(parent)?;
|
|
ImportKind::Submodule {
|
|
parent: module_name,
|
|
}
|
|
} else {
|
|
ImportKind::Module
|
|
};
|
|
Some(ImportStatement::Import(Import { ast, kind }))
|
|
}
|
|
(Some(from), import) => {
|
|
let ast = find_ast_for_from_import(parsed, from)?;
|
|
// If we saw an `import` keyword, then that means the
|
|
// cursor must be *after* the `import`. And thus we
|
|
// only ever need to offer completions for importable
|
|
// elements from the module being imported.
|
|
let kind = if import.is_some() {
|
|
FromImportKind::Attribute
|
|
} else if !initial_dot {
|
|
FromImportKind::Module
|
|
} else {
|
|
let to_complete_without_leading_dots = to_complete.trim_start_matches('.');
|
|
|
|
// When there aren't any leading dots to trim, then we
|
|
// have a regular absolute import. Otherwise, it's relative.
|
|
if to_complete == to_complete_without_leading_dots {
|
|
let (parent, _) = to_complete.rsplit_once('.')?;
|
|
let parent = ModuleName::new(parent)?;
|
|
FromImportKind::Submodule { parent }
|
|
} else {
|
|
let all_dots = to_complete.chars().all(|c| c == '.');
|
|
// We should suggest `import` in `from ...<CURSOR>`
|
|
// and `from ...imp<CURSOR>`.
|
|
let import_keyword_allowed =
|
|
all_dots || !to_complete_without_leading_dots.contains('.');
|
|
let parent = if all_dots {
|
|
ModuleName::from_import_statement(db, file, ast).ok()?
|
|
} else {
|
|
// We know `to_complete` is not all dots.
|
|
// But that it starts with a dot.
|
|
// So we must have one of `..foo`, `..foo.`
|
|
// or `..foo.bar`. We drop the leading dots,
|
|
// since those are captured by `ast.level`.
|
|
// From there, we can treat it like a normal
|
|
// module name. We want to list submodule
|
|
// completions, so we pop off the last element
|
|
// if there are any remaining dots.
|
|
let parent = to_complete_without_leading_dots
|
|
.rsplit_once('.')
|
|
.map(|(parent, _)| parent);
|
|
ModuleName::from_identifier_parts(db, file, parent, ast.level).ok()?
|
|
};
|
|
FromImportKind::Relative {
|
|
parent,
|
|
import_keyword_allowed,
|
|
}
|
|
}
|
|
};
|
|
Some(ImportStatement::FromImport(FromImport { ast, kind }))
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Add completions, if any and if appropriate, based on the detected
|
|
/// import statement.
|
|
fn add_completions<'db>(
|
|
&self,
|
|
db: &'db dyn Db,
|
|
file: File,
|
|
completions: &mut Completions<'db>,
|
|
) {
|
|
let model = SemanticModel::new(db, file);
|
|
match *self {
|
|
ImportStatement::Import(Import { ref kind, .. }) => match *kind {
|
|
ImportKind::Module => {
|
|
completions.extend(model.import_completions());
|
|
}
|
|
ImportKind::Submodule { ref parent } => {
|
|
completions.extend(model.import_submodule_completions_for_name(parent));
|
|
}
|
|
},
|
|
ImportStatement::FromImport(FromImport { ast, ref kind }) => match *kind {
|
|
FromImportKind::Module => {
|
|
completions.extend(model.import_completions());
|
|
}
|
|
FromImportKind::Submodule { ref parent } => {
|
|
completions.extend(model.import_submodule_completions_for_name(parent));
|
|
}
|
|
FromImportKind::Relative {
|
|
ref parent,
|
|
import_keyword_allowed,
|
|
} => {
|
|
completions.extend(model.import_submodule_completions_for_name(parent));
|
|
if import_keyword_allowed {
|
|
completions.try_add(Completion::keyword("import"));
|
|
}
|
|
}
|
|
FromImportKind::Attribute => {
|
|
completions.extend(model.from_import_completions(ast));
|
|
}
|
|
},
|
|
ImportStatement::Incomplete(IncompleteImport::As) => {
|
|
completions.try_add(Completion::keyword("as"));
|
|
}
|
|
ImportStatement::Incomplete(IncompleteImport::Import) => {
|
|
completions.try_add(Completion::keyword("import"));
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Finds the AST node, if available, corresponding to the given `from`
|
|
/// token.
|
|
///
|
|
/// This always returns `None` when the `token` is not a `from` token.
|
|
fn find_ast_for_from_import<'p>(
|
|
parsed: &'p ParsedModuleRef,
|
|
token: &Token,
|
|
) -> Option<&'p ast::StmtImportFrom> {
|
|
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
|
.find_first(|node| node.is_stmt_import_from())
|
|
.ok()?;
|
|
let ast::AnyNodeRef::StmtImportFrom(from_import) = covering_node.node() else {
|
|
return None;
|
|
};
|
|
Some(from_import)
|
|
}
|
|
|
|
/// Finds the AST node, if available, corresponding to the given `import`
|
|
/// token.
|
|
///
|
|
/// This always returns `None` when the `token` is not a `import` token.
|
|
fn find_ast_for_import<'p>(
|
|
parsed: &'p ParsedModuleRef,
|
|
token: &Token,
|
|
) -> Option<&'p ast::StmtImport> {
|
|
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
|
.find_first(|node| node.is_stmt_import())
|
|
.ok()?;
|
|
let ast::AnyNodeRef::StmtImport(import) = covering_node.node() else {
|
|
return None;
|
|
};
|
|
Some(import)
|
|
}
|
|
|
|
/// Returns a slice of tokens that all start before the given
|
|
/// [`TextSize`] offset.
|
|
///
|
|
/// If the given offset is between two tokens, the returned slice will end just
|
|
/// before the following token. In other words, if the offset is between the
|
|
/// end of previous token and start of next token, the returned slice will end
|
|
/// just before the next token.
|
|
///
|
|
/// Unlike `Tokens::before`, this never panics. If `offset` is within a token's
|
|
/// range (including if it's at the very beginning), then that token will be
|
|
/// included in the slice returned.
|
|
fn tokens_start_before(tokens: &Tokens, offset: TextSize) -> &[Token] {
|
|
let partition_point = tokens.partition_point(|token| token.start() < offset);
|
|
|
|
&tokens[..partition_point]
|
|
}
|
|
|
|
/// Returns a suffix of `tokens` corresponding to the `kinds` given.
|
|
///
|
|
/// If a suffix of `tokens` with the given `kinds` could not be found,
|
|
/// then `None` is returned.
|
|
///
|
|
/// This is useful for matching specific patterns of token sequences
|
|
/// in order to identify what kind of completions we should offer.
|
|
fn token_suffix_by_kinds<const N: usize>(
|
|
tokens: &[Token],
|
|
kinds: [TokenKind; N],
|
|
) -> Option<[&Token; N]> {
|
|
if kinds.len() > tokens.len() {
|
|
return None;
|
|
}
|
|
for (token, expected_kind) in tokens.iter().rev().zip(kinds.iter().rev()) {
|
|
if &token.kind() != expected_kind {
|
|
return None;
|
|
}
|
|
}
|
|
Some(std::array::from_fn(|i| {
|
|
&tokens[tokens.len() - (kinds.len() - i)]
|
|
}))
|
|
}
|
|
|
|
/// Looks for the text typed immediately before the cursor offset
|
|
/// given.
|
|
///
|
|
/// If there isn't any typed text or it could not otherwise be found,
|
|
/// then `None` is returned.
|
|
///
|
|
/// When `Some` is returned, the string is guaranteed to be non-empty.
|
|
fn find_typed_text(
|
|
db: &dyn Db,
|
|
file: File,
|
|
parsed: &ParsedModuleRef,
|
|
offset: TextSize,
|
|
) -> Option<String> {
|
|
let source = source_text(db, file);
|
|
let tokens = tokens_start_before(parsed.tokens(), offset);
|
|
let last = tokens.last()?;
|
|
// It's odd to include `TokenKind::Import` here, but it
|
|
// indicates that the user has typed `import`. This is
|
|
// useful to know in some contexts. And this applies also
|
|
// to the other keywords.
|
|
if !matches!(last.kind(), TokenKind::Name) && !last.kind().is_keyword() {
|
|
return None;
|
|
}
|
|
// This one's weird, but if the cursor is beyond
|
|
// what is in the closest `Name` token, then it's
|
|
// likely we can't infer anything about what has
|
|
// been typed. This likely means there is whitespace
|
|
// or something that isn't represented in the token
|
|
// stream. So just give up.
|
|
if last.end() < offset || last.range().is_empty() {
|
|
return None;
|
|
}
|
|
let range = TextRange::new(last.start(), offset);
|
|
Some(source[range].to_string())
|
|
}
|
|
|
|
/// Whether the last token is in a place where we should not provide completions.
|
|
fn is_in_no_completions_place(
|
|
db: &dyn Db,
|
|
file: File,
|
|
parsed: &ParsedModuleRef,
|
|
offset: TextSize,
|
|
tokens: &[Token],
|
|
typed: Option<&str>,
|
|
) -> bool {
|
|
is_in_comment(tokens)
|
|
|| is_in_string(tokens)
|
|
|| is_in_definition_place(db, file, parsed, offset, tokens, typed)
|
|
}
|
|
|
|
/// Whether the last token is within a comment or not.
|
|
fn is_in_comment(tokens: &[Token]) -> bool {
|
|
tokens.last().is_some_and(|t| t.kind().is_comment())
|
|
}
|
|
|
|
/// Whether the last token is positioned within a string token (regular, f-string, t-string, etc).
|
|
///
|
|
/// Note that this will return `false` when the last token is positioned within an
|
|
/// interpolation block in an f-string or a t-string.
|
|
fn is_in_string(tokens: &[Token]) -> bool {
|
|
tokens.last().is_some_and(|t| {
|
|
matches!(
|
|
t.kind(),
|
|
TokenKind::String | TokenKind::FStringMiddle | TokenKind::TStringMiddle
|
|
)
|
|
})
|
|
}
|
|
|
|
/// Returns true when the tokens indicate that the definition of a new
|
|
/// name is being introduced at the end.
|
|
fn is_in_definition_place(
|
|
db: &dyn Db,
|
|
file: File,
|
|
parsed: &ParsedModuleRef,
|
|
offset: TextSize,
|
|
tokens: &[Token],
|
|
typed: Option<&str>,
|
|
) -> bool {
|
|
fn is_definition_token(token: &Token) -> bool {
|
|
matches!(
|
|
token.kind(),
|
|
TokenKind::Def | TokenKind::Class | TokenKind::Type | TokenKind::As | TokenKind::For
|
|
)
|
|
}
|
|
|
|
let is_definition_keyword = |token: &Token| {
|
|
if is_definition_token(token) {
|
|
true
|
|
} else if token.kind() == TokenKind::Name {
|
|
let source = source_text(db, file);
|
|
&source[token.range()] == "type"
|
|
} else {
|
|
false
|
|
}
|
|
};
|
|
if match tokens {
|
|
[.., penultimate, _] if typed.is_some() => is_definition_keyword(penultimate),
|
|
[.., last] if typed.is_none() => is_definition_keyword(last),
|
|
_ => false,
|
|
} {
|
|
return true;
|
|
}
|
|
// Analyze the AST if token matching is insufficient
|
|
// to determine if we're inside a name definition.
|
|
is_in_variable_binding(parsed, offset, typed)
|
|
}
|
|
|
|
/// Returns true when the cursor sits on a binding statement.
|
|
/// E.g. naming a parameter, type parameter, or `for` <name>).
|
|
fn is_in_variable_binding(parsed: &ParsedModuleRef, offset: TextSize, typed: Option<&str>) -> bool {
|
|
let range = if let Some(typed) = typed {
|
|
let start = offset.saturating_sub(typed.text_len());
|
|
TextRange::new(start, offset)
|
|
} else {
|
|
TextRange::empty(offset)
|
|
};
|
|
|
|
let covering = covering_node(parsed.syntax().into(), range);
|
|
covering.ancestors().any(|node| match node {
|
|
ast::AnyNodeRef::Parameter(param) => param.name.range.contains_range(range),
|
|
ast::AnyNodeRef::TypeParamTypeVar(type_param) => {
|
|
type_param.name.range.contains_range(range)
|
|
}
|
|
ast::AnyNodeRef::StmtFor(stmt_for) => stmt_for.target.range().contains_range(range),
|
|
// The AST does not produce `ast::AnyNodeRef::Parameter` nodes for keywords
|
|
// or otherwise invalid syntax. Rather they are captured in a
|
|
// `ast::AnyNodeRef::Parameters` node as "empty space". To ensure
|
|
// we still suppress suggestions even when the syntax is technically
|
|
// invalid we extract the token under the cursor and check if it makes
|
|
// up that "empty space" inside the Parameters Node. If it does, we know
|
|
// that we are still binding variables, just that the current state is
|
|
// syntatically invalid. Hence we suppress autocomplete suggestons
|
|
// also in those cases.
|
|
ast::AnyNodeRef::Parameters(params) => {
|
|
if !params.range.contains_range(range) {
|
|
return false;
|
|
}
|
|
params
|
|
.iter()
|
|
.map(|param| param.range())
|
|
.all(|r| !r.contains_range(range))
|
|
}
|
|
_ => false,
|
|
})
|
|
}
|
|
|
|
/// Returns true when the cursor is after a `raise` keyword.
|
|
fn is_raising_exception(tokens: &[Token]) -> bool {
|
|
/// The maximum number of tokens we're willing to
|
|
/// look-behind to find a `raise` keyword.
|
|
const LIMIT: usize = 10;
|
|
|
|
// This only looks for things like `raise foo.bar.baz.qu<CURSOR>`.
|
|
// Technically, any kind of expression is allowed after `raise`.
|
|
// But we may not always want to treat it specially. So we're
|
|
// rather conservative about what we consider "raising an
|
|
// exception" to be for the purposes of completions. The failure
|
|
// mode here is that we may wind up suggesting things that
|
|
// shouldn't be raised. The benefit is that when this heuristic
|
|
// does work, we won't suggest things that shouldn't be raised.
|
|
for token in tokens.iter().rev().take(LIMIT) {
|
|
match token.kind() {
|
|
TokenKind::Name | TokenKind::Dot => continue,
|
|
TokenKind::Raise => return true,
|
|
_ => return false,
|
|
}
|
|
}
|
|
false
|
|
}
|
|
|
|
/// Order completions according to the following rules:
|
|
///
|
|
/// 1) Names with no underscore prefix
|
|
/// 2) Names starting with `_` but not dunders
|
|
/// 3) `__dunder__` names
|
|
///
|
|
/// Among each category, type-check-only items are sorted last,
|
|
/// and otherwise completions are sorted lexicographically.
|
|
///
|
|
/// This has the effect of putting all dunder attributes after "normal"
|
|
/// attributes, and all single-underscore attributes after dunder attributes.
|
|
fn compare_suggestions(c1: &Completion, c2: &Completion) -> Ordering {
|
|
fn key<'a>(completion: &'a Completion) -> (bool, bool, bool, bool, NameKind, bool, &'a Name) {
|
|
(
|
|
// This is only true when we are both in a `raise` context
|
|
// *and* we know this suggestion is definitively usable
|
|
// in a `raise` context. So we should sort these before
|
|
// anything else.
|
|
!completion.is_definitively_raisable,
|
|
// When `None`, a completion is for something in the
|
|
// current module, which we should generally prefer over
|
|
// something from outside the module.
|
|
completion.module_name.is_some(),
|
|
// At time of writing (2025-11-11), keyword completions
|
|
// are classified as builtins, which makes them sort after
|
|
// everything else. But we probably want keyword completions
|
|
// to sort *before* anything else since they are so common.
|
|
// Moreover, it seems VS Code forcefully does this sorting.
|
|
// By doing it ourselves, we make our natural sorting match
|
|
// VS Code's, and thus our completion evaluation framework
|
|
// should be more representative of real world conditions.
|
|
completion.kind != Some(CompletionKind::Keyword),
|
|
completion.builtin,
|
|
NameKind::classify(&completion.name),
|
|
completion.is_type_check_only,
|
|
&completion.name,
|
|
)
|
|
}
|
|
|
|
key(c1).cmp(&key(c2))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use insta::assert_snapshot;
|
|
use ruff_python_ast::token::{TokenKind, Tokens};
|
|
use ruff_python_parser::{Mode, ParseOptions};
|
|
use ty_python_semantic::ModuleName;
|
|
|
|
use crate::completion::{Completion, completion};
|
|
use crate::tests::{CursorTest, CursorTestBuilder};
|
|
|
|
use super::{CompletionKind, CompletionSettings, token_suffix_by_kinds};
|
|
|
|
#[test]
|
|
fn token_suffixes_match() {
|
|
insta::assert_debug_snapshot!(
|
|
token_suffix_by_kinds(&tokenize("foo.x"), [TokenKind::Newline]),
|
|
@r"
|
|
Some(
|
|
[
|
|
Newline 5..5,
|
|
],
|
|
)
|
|
",
|
|
);
|
|
|
|
insta::assert_debug_snapshot!(
|
|
token_suffix_by_kinds(&tokenize("foo.x"), [TokenKind::Name, TokenKind::Newline]),
|
|
@r"
|
|
Some(
|
|
[
|
|
Name 4..5,
|
|
Newline 5..5,
|
|
],
|
|
)
|
|
",
|
|
);
|
|
|
|
let all = [
|
|
TokenKind::Name,
|
|
TokenKind::Dot,
|
|
TokenKind::Name,
|
|
TokenKind::Newline,
|
|
];
|
|
insta::assert_debug_snapshot!(
|
|
token_suffix_by_kinds(&tokenize("foo.x"), all),
|
|
@r"
|
|
Some(
|
|
[
|
|
Name 0..3,
|
|
Dot 3..4,
|
|
Name 4..5,
|
|
Newline 5..5,
|
|
],
|
|
)
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn token_suffixes_nomatch() {
|
|
insta::assert_debug_snapshot!(
|
|
token_suffix_by_kinds(&tokenize("foo.x"), [TokenKind::Name]),
|
|
@"None",
|
|
);
|
|
|
|
let too_many = [
|
|
TokenKind::Dot,
|
|
TokenKind::Name,
|
|
TokenKind::Dot,
|
|
TokenKind::Name,
|
|
TokenKind::Newline,
|
|
];
|
|
insta::assert_debug_snapshot!(
|
|
token_suffix_by_kinds(&tokenize("foo.x"), too_many),
|
|
@"None",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn empty() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn builtins() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
<CURSOR>
|
|
",
|
|
);
|
|
let test = builder.build();
|
|
|
|
test.contains("filter");
|
|
// Sunder items should be filtered out
|
|
test.not_contains("_T");
|
|
// Dunder attributes should not be stripped
|
|
test.contains("__annotations__");
|
|
// See `private_symbols_in_stub` for more comprehensive testing private of symbol filtering.
|
|
}
|
|
|
|
#[test]
|
|
fn keywords() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
test.skip_builtins().build().snapshot(),
|
|
@r"
|
|
and
|
|
as
|
|
assert
|
|
async
|
|
await
|
|
break
|
|
case
|
|
class
|
|
continue
|
|
def
|
|
del
|
|
elif
|
|
else
|
|
except
|
|
finally
|
|
for
|
|
from
|
|
global
|
|
if
|
|
import
|
|
in
|
|
is
|
|
lambda
|
|
match
|
|
nonlocal
|
|
not
|
|
or
|
|
pass
|
|
raise
|
|
return
|
|
try
|
|
while
|
|
with
|
|
yield
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn inside_token() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
foo_bar_baz = 1
|
|
x = foo<CURSOR>bad
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
test.skip_builtins().build().snapshot(),
|
|
@"foo_bar_baz",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn type_keyword_dedup() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
type<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
test.type_signatures().build().snapshot(),
|
|
@r"
|
|
TypeError :: <class 'TypeError'>
|
|
type :: <class 'type'>
|
|
",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn builtins_not_included_object_attr() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import re
|
|
|
|
re.<CURSOR>
|
|
",
|
|
);
|
|
builder.build().not_contains("filter");
|
|
}
|
|
|
|
#[test]
|
|
fn builtins_not_included_import() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from re import <CURSOR>
|
|
",
|
|
);
|
|
builder.build().not_contains("filter");
|
|
}
|
|
|
|
#[test]
|
|
fn imports1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import re
|
|
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"re");
|
|
}
|
|
|
|
#[test]
|
|
fn imports2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from os import path
|
|
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"path");
|
|
}
|
|
|
|
// N.B. We don't currently explore module APIs. This
|
|
// is still just emitting symbols from the detected scope.
|
|
#[test]
|
|
fn module_api() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import re
|
|
|
|
re.<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("findall");
|
|
}
|
|
|
|
#[test]
|
|
fn private_symbols_in_stub() {
|
|
let builder = CursorTest::builder()
|
|
.source(
|
|
"package/__init__.pyi",
|
|
r#"\
|
|
from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol
|
|
|
|
public_name = 1
|
|
_private_name = 1
|
|
__mangled_name = 1
|
|
__dunder_name__ = 1
|
|
|
|
public_type_var = TypeVar("public_type_var")
|
|
_private_type_var = TypeVar("_private_type_var")
|
|
__mangled_type_var = TypeVar("__mangled_type_var")
|
|
|
|
public_param_spec = ParamSpec("public_param_spec")
|
|
_private_param_spec = ParamSpec("_private_param_spec")
|
|
|
|
public_type_var_tuple = TypeVarTuple("public_type_var_tuple")
|
|
_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple")
|
|
|
|
public_explicit_type_alias: TypeAlias = Literal[1]
|
|
_private_explicit_type_alias: TypeAlias = Literal[1]
|
|
|
|
public_implicit_union_alias = int | str
|
|
_private_implicit_union_alias = int | str
|
|
|
|
class PublicProtocol(Protocol):
|
|
def method(self) -> None: ...
|
|
|
|
class _PrivateProtocol(Protocol):
|
|
def method(self) -> None: ...
|
|
"#,
|
|
)
|
|
.source("main.py", "import package; package.<CURSOR>")
|
|
.completion_test_builder();
|
|
|
|
let test = builder.build();
|
|
test.contains("public_name");
|
|
test.contains("_private_name");
|
|
test.contains("__mangled_name");
|
|
test.contains("__dunder_name__");
|
|
test.contains("public_type_var");
|
|
test.not_contains("_private_type_var");
|
|
test.not_contains("__mangled_type_var");
|
|
test.contains("public_param_spec");
|
|
test.not_contains("_private_param_spec");
|
|
test.contains("public_type_var_tuple");
|
|
test.not_contains("_private_type_var_tuple");
|
|
test.contains("public_explicit_type_alias");
|
|
test.not_contains("_private_explicit_type_alias");
|
|
test.contains("public_implicit_union_alias");
|
|
test.not_contains("_private_implicit_union_alias");
|
|
test.contains("PublicProtocol");
|
|
test.not_contains("_PrivateProtocol");
|
|
}
|
|
|
|
/// Unlike [`private_symbols_in_stub`], this test doesn't use a `.pyi` file so all of the names
|
|
/// are visible.
|
|
#[test]
|
|
fn private_symbols_in_module() {
|
|
let builder = CursorTest::builder()
|
|
.source(
|
|
"package/__init__.py",
|
|
r#"\
|
|
from typing import TypeAlias, Literal, TypeVar, ParamSpec, TypeVarTuple, Protocol
|
|
|
|
public_name = 1
|
|
_private_name = 1
|
|
__mangled_name = 1
|
|
__dunder_name__ = 1
|
|
|
|
public_type_var = TypeVar("public_type_var")
|
|
_private_type_var = TypeVar("_private_type_var")
|
|
__mangled_type_var = TypeVar("__mangled_type_var")
|
|
|
|
public_param_spec = ParamSpec("public_param_spec")
|
|
_private_param_spec = ParamSpec("_private_param_spec")
|
|
|
|
public_type_var_tuple = TypeVarTuple("public_type_var_tuple")
|
|
_private_type_var_tuple = TypeVarTuple("_private_type_var_tuple")
|
|
|
|
public_explicit_type_alias: TypeAlias = Literal[1]
|
|
_private_explicit_type_alias: TypeAlias = Literal[1]
|
|
|
|
class PublicProtocol(Protocol):
|
|
def method(self) -> None: ...
|
|
|
|
class _PrivateProtocol(Protocol):
|
|
def method(self) -> None: ...
|
|
"#,
|
|
)
|
|
.source("main.py", "import package; package.<CURSOR>")
|
|
.completion_test_builder();
|
|
|
|
let test = builder.build();
|
|
test.contains("public_name");
|
|
test.contains("_private_name");
|
|
test.contains("__mangled_name");
|
|
test.contains("__dunder_name__");
|
|
test.contains("public_type_var");
|
|
test.contains("_private_type_var");
|
|
test.contains("__mangled_type_var");
|
|
test.contains("public_param_spec");
|
|
test.contains("_private_param_spec");
|
|
test.contains("public_type_var_tuple");
|
|
test.contains("_private_type_var_tuple");
|
|
test.contains("public_explicit_type_alias");
|
|
test.contains("_private_explicit_type_alias");
|
|
test.contains("PublicProtocol");
|
|
test.contains("_PrivateProtocol");
|
|
}
|
|
|
|
#[test]
|
|
fn one_function_prefix() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(): ...
|
|
|
|
f<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn one_function_not_prefix() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(): ...
|
|
|
|
g<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn one_function_blank() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(): ...
|
|
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_function_prefix() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo(): ...
|
|
|
|
f<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_function_blank() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo(): ...
|
|
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_function_not_in_global_scope_prefix() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo(): ...
|
|
f<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
foofoo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_function_not_in_global_scope_blank() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo(): ...
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foofoo`.
|
|
//
|
|
// `foofoo` isn't included at present (2025-05-22). The problem
|
|
// here is that the AST for `def foo():` doesn't encompass the
|
|
// trailing indentation. So when the cursor position is in that
|
|
// trailing indentation, we can't (easily) get a handle to the
|
|
// right scope. And even if we could, the AST expressions for
|
|
// `def foo():` and `def foofoo(): ...` end at precisely the
|
|
// same point. So there is no AST we can hold after the end of
|
|
// `foofoo` but before the end of `foo`. So at the moment, it's
|
|
// not totally clear how to get the right scope.
|
|
//
|
|
// If we didn't want to change the ranges on the AST nodes,
|
|
// another approach here would be to get the inner most scope,
|
|
// and explore its ancestors until we get to a level that
|
|
// matches the current cursor's indentation. This seems fraught
|
|
// however. It's not clear to me that we can always assume a
|
|
// correspondence between scopes and indentation level.
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_prefix1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
f<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
foofoo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_prefix2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
f<CURSOR>",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
foofoo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_prefix3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
f<CURSOR>
|
|
def frob(): ...
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
foofoo
|
|
frob
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_prefix4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
f<CURSOR>
|
|
def frob(): ...
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
frob
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_prefix5() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
f<CURSOR>
|
|
def frob(): ...
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
foofoo
|
|
foofoofoo
|
|
frob
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_blank1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foofoo` (but not `foofoofoo`).
|
|
//
|
|
// The tests below fail for the same reason that
|
|
// `nested_function_not_in_global_scope_blank` fails: there is no
|
|
// space in the AST ranges after the end of `foofoofoo` but before
|
|
// the end of `foofoo`. So either the AST needs to be tweaked to
|
|
// account for the indented whitespace, or some other technique
|
|
// needs to be used to get the scope containing `foofoo` but not
|
|
// `foofoofoo`.
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_blank2() {
|
|
let builder = completion_test_builder(
|
|
" \
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
<CURSOR>",
|
|
);
|
|
|
|
// FIXME: Should include `foofoo` (but not `foofoofoo`).
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_blank3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
<CURSOR>
|
|
def frob(): ...
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foofoo` (but not `foofoofoo`).
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
frob
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_blank4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
<CURSOR>
|
|
|
|
def frob(): ...
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foofoo` (but not `foofoofoo`).
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
frob
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn double_nested_function_not_in_global_scope_blank5() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo():
|
|
def foofoo():
|
|
def foofoofoo(): ...
|
|
|
|
<CURSOR>
|
|
|
|
def frob(): ...
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foofoo` (but not `foofoofoo`).
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
frob
|
|
");
|
|
}
|
|
|
|
/// Regression test for <https://github.com/astral-sh/ty/issues/1392>
|
|
///
|
|
/// This test ensures completions work when the cursor is at the
|
|
/// start of a zero-length token.
|
|
#[test]
|
|
fn completion_at_eof() {
|
|
completion_test_builder("def f(msg: str):\n msg.<CURSOR>")
|
|
.build()
|
|
.contains("upper")
|
|
.contains("capitalize");
|
|
|
|
completion_test_builder("def f(msg: str):\n msg.u<CURSOR>")
|
|
.build()
|
|
.contains("upper")
|
|
.not_contains("capitalize");
|
|
}
|
|
|
|
#[test]
|
|
fn list_comprehension1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
[<CURSOR> for bar in [1, 2, 3]]
|
|
",
|
|
);
|
|
|
|
// TODO: it would be good if `bar` was included here, but
|
|
// the list comprehension is not yet valid and so we do not
|
|
// detect this as a definition of `bar`.
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn list_comprehension2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
[f<CURSOR> for foo in [1, 2, 3]]
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_prefix1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: (1 + f<CURSOR> + 2))(2)
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_prefix2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: f<CURSOR> + 1)(2)
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_prefix3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: (f<CURSOR> + 1))(2)
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_prefix4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: 1 + f<CURSOR>)(2)
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_blank1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: 1 + <CURSOR> + 2)(2)
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_blank2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: <CURSOR> + 1)(2)
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foo`.
|
|
//
|
|
// These fails for similar reasons as above: the body of the
|
|
// lambda doesn't include the position of <CURSOR> because
|
|
// <CURSOR> is inside leading or trailing whitespace. (Even
|
|
// when enclosed in parentheses. Specifically, parentheses
|
|
// aren't part of the node's range unless it's relevant e.g.,
|
|
// tuples.)
|
|
//
|
|
// The `lambda_blank1` test works because there are expressions
|
|
// on either side of <CURSOR>.
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_blank3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: (<CURSOR> + 1))(2)
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foo`.
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn lambda_blank4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(lambda foo: 1 + <CURSOR>)(2)
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `foo`.
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn class_prefix1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
bar = 1
|
|
quux = b<CURSOR>
|
|
frob = 3
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
bar
|
|
frob
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_prefix2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
bar = 1
|
|
quux = b<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"bar");
|
|
}
|
|
|
|
#[test]
|
|
fn class_blank1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
bar = 1
|
|
quux = <CURSOR>
|
|
frob = 3
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `bar`, `quux` and `frob`.
|
|
// (Unclear if `Foo` should be included, but a false
|
|
// positive isn't the end of the world.)
|
|
//
|
|
// These don't work for similar reasons as other
|
|
// tests above with the <CURSOR> inside of whitespace.
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
Foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_blank2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
bar = 1
|
|
quux = <CURSOR>
|
|
frob = 3
|
|
",
|
|
);
|
|
|
|
// FIXME: Should include `bar`, `quux` and `frob`.
|
|
// (Unclear if `Foo` should be included, but a false
|
|
// positive isn't the end of the world.)
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
Foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_super1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Bar: ...
|
|
|
|
class Foo(<CURSOR>):
|
|
bar = 1
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
Bar
|
|
Foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_super2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo(<CURSOR>):
|
|
bar = 1
|
|
|
|
class Bar: ...
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
Bar
|
|
Foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_super3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo(<CURSOR>
|
|
bar = 1
|
|
|
|
class Bar: ...
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
Bar
|
|
Foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_super4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Bar: ...
|
|
|
|
class Foo(<CURSOR>",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
Bar
|
|
Foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_init1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Quux:
|
|
def __init__(self):
|
|
self.foo = 1
|
|
self.bar = 2
|
|
self.baz = 3
|
|
|
|
quux = Quux()
|
|
quux.<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
|
|
bar :: Unknown | Literal[2]
|
|
baz :: Unknown | Literal[3]
|
|
foo :: Unknown | Literal[1]
|
|
__annotations__ :: dict[str, Any]
|
|
__class__ :: type[Quux]
|
|
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
|
|
__dict__ :: dict[str, Any]
|
|
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
|
|
__doc__ :: str | None
|
|
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
|
|
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
|
|
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
|
|
__getstate__ :: bound method Quux.__getstate__() -> object
|
|
__hash__ :: bound method Quux.__hash__() -> int
|
|
__init__ :: bound method Quux.__init__() -> Unknown
|
|
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
|
__module__ :: str
|
|
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
|
__new__ :: def __new__(cls) -> Self@__new__
|
|
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
|
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
|
__repr__ :: bound method Quux.__repr__() -> str
|
|
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
|
|
__sizeof__ :: bound method Quux.__sizeof__() -> int
|
|
__str__ :: bound method Quux.__str__() -> str
|
|
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_init2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Quux:
|
|
def __init__(self):
|
|
self.foo = 1
|
|
self.bar = 2
|
|
self.baz = 3
|
|
|
|
quux = Quux()
|
|
quux.b<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
|
|
bar :: Unknown | Literal[2]
|
|
baz :: Unknown | Literal[3]
|
|
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
|
|
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
|
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn metaclass1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Meta(type):
|
|
@property
|
|
def meta_attr(self) -> int:
|
|
return 0
|
|
|
|
class C(metaclass=Meta): ...
|
|
|
|
C.<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r###"
|
|
meta_attr :: int
|
|
mro :: bound method <class 'C'>.mro() -> list[type]
|
|
__annotate__ :: (() -> dict[str, Any]) | None
|
|
__annotations__ :: dict[str, Any]
|
|
__base__ :: type | None
|
|
__bases__ :: tuple[type, ...]
|
|
__basicsize__ :: int
|
|
__call__ :: bound method <class 'C'>.__call__(...) -> Any
|
|
__class__ :: <class 'Meta'>
|
|
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
|
__dict__ :: dict[str, Any]
|
|
__dictoffset__ :: int
|
|
__dir__ :: def __dir__(self) -> Iterable[str]
|
|
__doc__ :: str | None
|
|
__eq__ :: def __eq__(self, value: object, /) -> bool
|
|
__flags__ :: int
|
|
__format__ :: def __format__(self, format_spec: str, /) -> str
|
|
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
|
|
__getstate__ :: def __getstate__(self) -> object
|
|
__hash__ :: def __hash__(self) -> int
|
|
__init__ :: def __init__(self) -> None
|
|
__init_subclass__ :: bound method <class 'C'>.__init_subclass__() -> None
|
|
__instancecheck__ :: bound method <class 'C'>.__instancecheck__(instance: Any, /) -> bool
|
|
__itemsize__ :: int
|
|
__module__ :: str
|
|
__mro__ :: tuple[type, ...]
|
|
__name__ :: str
|
|
__ne__ :: def __ne__(self, value: object, /) -> bool
|
|
__new__ :: def __new__(cls) -> Self@__new__
|
|
__or__ :: bound method <class 'C'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
|
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
|
__qualname__ :: str
|
|
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
|
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
|
__repr__ :: def __repr__(self) -> str
|
|
__ror__ :: bound method <class 'C'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
|
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
|
__sizeof__ :: def __sizeof__(self) -> int
|
|
__str__ :: def __str__(self) -> str
|
|
__subclasscheck__ :: bound method <class 'C'>.__subclasscheck__(subclass: type, /) -> bool
|
|
__subclasses__ :: bound method <class 'C'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
|
__subclasshook__ :: bound method <class 'C'>.__subclasshook__(subclass: type, /) -> bool
|
|
__text_signature__ :: str | None
|
|
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
|
__weakrefoffset__ :: int
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn metaclass2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Meta(type):
|
|
@property
|
|
def meta_attr(self) -> int:
|
|
return 0
|
|
|
|
class C(metaclass=Meta): ...
|
|
|
|
Meta.<CURSOR>
|
|
",
|
|
);
|
|
|
|
insta::with_settings!({
|
|
// The formatting of some types are different depending on
|
|
// whether we're in release mode or not. These differences
|
|
// aren't really relevant for completion tests AFAIK, so
|
|
// just redact them. ---AG
|
|
filters => [(r"(?m)\s*__(annotations|new|annotate)__.+$", "")]},
|
|
{
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
|
|
meta_attr :: property
|
|
mro :: def mro(self) -> list[type]
|
|
__base__ :: type | None
|
|
__bases__ :: tuple[type, ...]
|
|
__basicsize__ :: int
|
|
__call__ :: def __call__(self, *args: Any, **kwds: Any) -> Any
|
|
__class__ :: <class 'type'>
|
|
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
|
__dict__ :: MappingProxyType[str, Any]
|
|
__dictoffset__ :: int
|
|
__dir__ :: def __dir__(self) -> Iterable[str]
|
|
__doc__ :: str | None
|
|
__eq__ :: def __eq__(self, value: object, /) -> bool
|
|
__flags__ :: int
|
|
__format__ :: def __format__(self, format_spec: str, /) -> str
|
|
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
|
|
__getstate__ :: def __getstate__(self) -> object
|
|
__hash__ :: def __hash__(self) -> int
|
|
__init__ :: Overload[(self, o: object, /) -> None, (self, name: str, bases: tuple[type, ...], dict: dict[str, Any], /, **kwds: Any) -> None]
|
|
__init_subclass__ :: bound method <class 'Meta'>.__init_subclass__() -> None
|
|
__instancecheck__ :: def __instancecheck__(self, instance: Any, /) -> bool
|
|
__itemsize__ :: int
|
|
__module__ :: str
|
|
__mro__ :: tuple[type, ...]
|
|
__name__ :: str
|
|
__ne__ :: def __ne__(self, value: object, /) -> bool
|
|
__or__ :: def __or__[Self](self: Self@__or__, value: Any, /) -> UnionType | Self@__or__
|
|
__prepare__ :: bound method <class 'Meta'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
|
__qualname__ :: str
|
|
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
|
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
|
__repr__ :: def __repr__(self) -> str
|
|
__ror__ :: def __ror__[Self](self: Self@__ror__, value: Any, /) -> UnionType | Self@__ror__
|
|
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
|
__sizeof__ :: def __sizeof__(self) -> int
|
|
__str__ :: def __str__(self) -> str
|
|
__subclasscheck__ :: def __subclasscheck__(self, subclass: type, /) -> bool
|
|
__subclasses__ :: def __subclasses__[Self](self: Self@__subclasses__) -> list[Self@__subclasses__]
|
|
__subclasshook__ :: bound method <class 'Meta'>.__subclasshook__(subclass: type, /) -> bool
|
|
__text_signature__ :: str | None
|
|
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
|
__weakrefoffset__ :: int
|
|
");
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn class_init3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Quux:
|
|
def __init__(self):
|
|
self.foo = 1
|
|
self.bar = 2
|
|
self.<CURSOR>
|
|
self.baz = 3
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
bar
|
|
baz
|
|
foo
|
|
__annotations__
|
|
__class__
|
|
__delattr__
|
|
__dict__
|
|
__dir__
|
|
__doc__
|
|
__eq__
|
|
__format__
|
|
__getattribute__
|
|
__getstate__
|
|
__hash__
|
|
__init__
|
|
__init_subclass__
|
|
__module__
|
|
__ne__
|
|
__new__
|
|
__reduce__
|
|
__reduce_ex__
|
|
__repr__
|
|
__setattr__
|
|
__sizeof__
|
|
__str__
|
|
__subclasshook__
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn class_attributes1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Quux:
|
|
some_attribute: int = 1
|
|
|
|
def __init__(self):
|
|
self.foo = 1
|
|
self.bar = 2
|
|
self.baz = 3
|
|
|
|
def some_method(self) -> int:
|
|
return 1
|
|
|
|
@property
|
|
def some_property(self) -> int:
|
|
return 1
|
|
|
|
@classmethod
|
|
def some_class_method(self) -> int:
|
|
return 1
|
|
|
|
@staticmethod
|
|
def some_static_method(self) -> int:
|
|
return 1
|
|
|
|
Quux.<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r###"
|
|
mro :: bound method <class 'Quux'>.mro() -> list[type]
|
|
some_attribute :: int
|
|
some_class_method :: bound method <class 'Quux'>.some_class_method() -> int
|
|
some_method :: def some_method(self) -> int
|
|
some_property :: property
|
|
some_static_method :: def some_static_method(self) -> int
|
|
__annotate__ :: (() -> dict[str, Any]) | None
|
|
__annotations__ :: dict[str, Any]
|
|
__base__ :: type | None
|
|
__bases__ :: tuple[type, ...]
|
|
__basicsize__ :: int
|
|
__call__ :: bound method <class 'Quux'>.__call__(...) -> Any
|
|
__class__ :: <class 'type'>
|
|
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
|
__dict__ :: dict[str, Any]
|
|
__dictoffset__ :: int
|
|
__dir__ :: def __dir__(self) -> Iterable[str]
|
|
__doc__ :: str | None
|
|
__eq__ :: def __eq__(self, value: object, /) -> bool
|
|
__flags__ :: int
|
|
__format__ :: def __format__(self, format_spec: str, /) -> str
|
|
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
|
|
__getstate__ :: def __getstate__(self) -> object
|
|
__hash__ :: def __hash__(self) -> int
|
|
__init__ :: def __init__(self) -> Unknown
|
|
__init_subclass__ :: bound method <class 'Quux'>.__init_subclass__() -> None
|
|
__instancecheck__ :: bound method <class 'Quux'>.__instancecheck__(instance: Any, /) -> bool
|
|
__itemsize__ :: int
|
|
__module__ :: str
|
|
__mro__ :: tuple[type, ...]
|
|
__name__ :: str
|
|
__ne__ :: def __ne__(self, value: object, /) -> bool
|
|
__new__ :: def __new__(cls) -> Self@__new__
|
|
__or__ :: bound method <class 'Quux'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
|
__prepare__ :: bound method <class 'type'>.__prepare__(name: str, bases: tuple[type, ...], /, **kwds: Any) -> MutableMapping[str, object]
|
|
__qualname__ :: str
|
|
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
|
__reduce_ex__ :: def __reduce_ex__(self, protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
|
__repr__ :: def __repr__(self) -> str
|
|
__ror__ :: bound method <class 'Quux'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
|
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
|
__sizeof__ :: def __sizeof__(self) -> int
|
|
__str__ :: def __str__(self) -> str
|
|
__subclasscheck__ :: bound method <class 'Quux'>.__subclasscheck__(subclass: type, /) -> bool
|
|
__subclasses__ :: bound method <class 'Quux'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
|
__subclasshook__ :: bound method <class 'Quux'>.__subclasshook__(subclass: type, /) -> bool
|
|
__text_signature__ :: str | None
|
|
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
|
__weakrefoffset__ :: int
|
|
"###);
|
|
}
|
|
|
|
#[test]
|
|
fn enum_attributes() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from enum import Enum
|
|
|
|
class Answer(Enum):
|
|
NO = 0
|
|
YES = 1
|
|
|
|
Answer.<CURSOR>
|
|
",
|
|
);
|
|
|
|
insta::with_settings!({
|
|
// See above: filter out some members which contain @Todo types that are
|
|
// rendered differently in release mode.
|
|
filters => [(r"(?m)\s*__(call|reduce_ex|annotate|signature)__.+$", "")]},
|
|
{
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
|
|
NO :: Literal[Answer.NO]
|
|
YES :: Literal[Answer.YES]
|
|
mro :: bound method <class 'Answer'>.mro() -> list[type]
|
|
name :: Any
|
|
value :: Any
|
|
__annotations__ :: dict[str, Any]
|
|
__base__ :: type | None
|
|
__bases__ :: tuple[type, ...]
|
|
__basicsize__ :: int
|
|
__bool__ :: bound method <class 'Answer'>.__bool__() -> Literal[True]
|
|
__class__ :: <class 'EnumMeta'>
|
|
__contains__ :: bound method <class 'Answer'>.__contains__(value: object) -> bool
|
|
__copy__ :: def __copy__(self) -> Self@__copy__
|
|
__deepcopy__ :: def __deepcopy__(self, memo: Any) -> Self@__deepcopy__
|
|
__delattr__ :: def __delattr__(self, name: str, /) -> None
|
|
__dict__ :: dict[str, Any]
|
|
__dictoffset__ :: int
|
|
__dir__ :: def __dir__(self) -> list[str]
|
|
__doc__ :: str | None
|
|
__eq__ :: def __eq__(self, value: object, /) -> bool
|
|
__flags__ :: int
|
|
__format__ :: def __format__(self, format_spec: str) -> str
|
|
__getattribute__ :: def __getattribute__(self, name: str, /) -> Any
|
|
__getitem__ :: bound method <class 'Answer'>.__getitem__[_EnumMemberT](name: str) -> _EnumMemberT@__getitem__
|
|
__getstate__ :: def __getstate__(self) -> object
|
|
__hash__ :: def __hash__(self) -> int
|
|
__init__ :: def __init__(self) -> None
|
|
__init_subclass__ :: bound method <class 'Answer'>.__init_subclass__() -> None
|
|
__instancecheck__ :: bound method <class 'Answer'>.__instancecheck__(instance: Any, /) -> bool
|
|
__itemsize__ :: int
|
|
__iter__ :: bound method <class 'Answer'>.__iter__[_EnumMemberT]() -> Iterator[_EnumMemberT@__iter__]
|
|
__len__ :: bound method <class 'Answer'>.__len__() -> int
|
|
__members__ :: MappingProxyType[str, Answer]
|
|
__module__ :: str
|
|
__mro__ :: tuple[type, ...]
|
|
__name__ :: str
|
|
__ne__ :: def __ne__(self, value: object, /) -> bool
|
|
__new__ :: def __new__(cls, value: object) -> Self@__new__
|
|
__or__ :: bound method <class 'Answer'>.__or__[Self](value: Any, /) -> UnionType | Self@__or__
|
|
__order__ :: str
|
|
__prepare__ :: bound method <class 'EnumMeta'>.__prepare__(cls: str, bases: tuple[type, ...], **kwds: Any) -> _EnumDict
|
|
__qualname__ :: str
|
|
__reduce__ :: def __reduce__(self) -> str | tuple[Any, ...]
|
|
__repr__ :: def __repr__(self) -> str
|
|
__reversed__ :: bound method <class 'Answer'>.__reversed__[_EnumMemberT]() -> Iterator[_EnumMemberT@__reversed__]
|
|
__ror__ :: bound method <class 'Answer'>.__ror__[Self](value: Any, /) -> UnionType | Self@__ror__
|
|
__setattr__ :: def __setattr__(self, name: str, value: Any, /) -> None
|
|
__sizeof__ :: def __sizeof__(self) -> int
|
|
__str__ :: def __str__(self) -> str
|
|
__subclasscheck__ :: bound method <class 'Answer'>.__subclasscheck__(subclass: type, /) -> bool
|
|
__subclasses__ :: bound method <class 'Answer'>.__subclasses__[Self]() -> list[Self@__subclasses__]
|
|
__subclasshook__ :: bound method <class 'Answer'>.__subclasshook__(subclass: type, /) -> bool
|
|
__text_signature__ :: str | None
|
|
__type_params__ :: tuple[TypeVar | ParamSpec | TypeVarTuple, ...]
|
|
__weakrefoffset__ :: int
|
|
_add_alias_ :: def _add_alias_(self, name: str) -> None
|
|
_add_value_alias_ :: def _add_value_alias_(self, value: Any) -> None
|
|
_generate_next_value_ :: def _generate_next_value_(name: str, start: int, count: int, last_values: list[Any]) -> Any
|
|
_ignore_ :: str | list[str]
|
|
_member_map_ :: dict[str, Enum]
|
|
_member_names_ :: list[str]
|
|
_missing_ :: bound method <class 'Answer'>._missing_(value: object) -> Any
|
|
_name_ :: str
|
|
_order_ :: str
|
|
_value2member_map_ :: dict[Any, Enum]
|
|
_value_ :: Any
|
|
");
|
|
}
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn namedtuple_methods() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from typing import NamedTuple
|
|
|
|
class Quux(NamedTuple):
|
|
x: int
|
|
y: str
|
|
|
|
quux = Quux()
|
|
quux.<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().type_signatures().build().snapshot(), @r"
|
|
count :: bound method Quux.count(value: Any, /) -> int
|
|
index :: bound method Quux.index(value: Any, start: SupportsIndex = Literal[0], stop: SupportsIndex = int, /) -> int
|
|
x :: int
|
|
y :: str
|
|
__add__ :: Overload[(value: tuple[int | str, ...], /) -> tuple[int | str, ...], (value: tuple[_T@__add__, ...], /) -> tuple[int | str | _T@__add__, ...]]
|
|
__annotations__ :: dict[str, Any]
|
|
__class__ :: type[Quux]
|
|
__class_getitem__ :: bound method type[Quux].__class_getitem__(item: Any, /) -> GenericAlias
|
|
__contains__ :: bound method Quux.__contains__(key: object, /) -> bool
|
|
__delattr__ :: bound method Quux.__delattr__(name: str, /) -> None
|
|
__dict__ :: dict[str, Any]
|
|
__dir__ :: bound method Quux.__dir__() -> Iterable[str]
|
|
__doc__ :: str | None
|
|
__eq__ :: bound method Quux.__eq__(value: object, /) -> bool
|
|
__format__ :: bound method Quux.__format__(format_spec: str, /) -> str
|
|
__ge__ :: bound method Quux.__ge__(value: tuple[int | str, ...], /) -> bool
|
|
__getattribute__ :: bound method Quux.__getattribute__(name: str, /) -> Any
|
|
__getitem__ :: Overload[(index: Literal[-2, 0], /) -> int, (index: Literal[-1, 1], /) -> str, (index: SupportsIndex, /) -> int | str, (index: slice[Any, Any, Any], /) -> tuple[int | str, ...]]
|
|
__getstate__ :: bound method Quux.__getstate__() -> object
|
|
__gt__ :: bound method Quux.__gt__(value: tuple[int | str, ...], /) -> bool
|
|
__hash__ :: bound method Quux.__hash__() -> int
|
|
__init__ :: bound method Quux.__init__() -> None
|
|
__init_subclass__ :: bound method type[Quux].__init_subclass__() -> None
|
|
__iter__ :: bound method Quux.__iter__() -> Iterator[int | str]
|
|
__le__ :: bound method Quux.__le__(value: tuple[int | str, ...], /) -> bool
|
|
__len__ :: () -> Literal[2]
|
|
__lt__ :: bound method Quux.__lt__(value: tuple[int | str, ...], /) -> bool
|
|
__module__ :: str
|
|
__mul__ :: bound method Quux.__mul__(value: SupportsIndex, /) -> tuple[int | str, ...]
|
|
__ne__ :: bound method Quux.__ne__(value: object, /) -> bool
|
|
__new__ :: (x: int, y: str) -> None
|
|
__orig_bases__ :: tuple[Any, ...]
|
|
__reduce__ :: bound method Quux.__reduce__() -> str | tuple[Any, ...]
|
|
__reduce_ex__ :: bound method Quux.__reduce_ex__(protocol: SupportsIndex, /) -> str | tuple[Any, ...]
|
|
__replace__ :: bound method NamedTupleFallback.__replace__(**kwargs: Any) -> NamedTupleFallback
|
|
__repr__ :: bound method Quux.__repr__() -> str
|
|
__reversed__ :: bound method Quux.__reversed__() -> Iterator[int | str]
|
|
__rmul__ :: bound method Quux.__rmul__(value: SupportsIndex, /) -> tuple[int | str, ...]
|
|
__setattr__ :: bound method Quux.__setattr__(name: str, value: Any, /) -> None
|
|
__sizeof__ :: bound method Quux.__sizeof__() -> int
|
|
__str__ :: bound method Quux.__str__() -> str
|
|
__subclasshook__ :: bound method type[Quux].__subclasshook__(subclass: type, /) -> bool
|
|
_asdict :: bound method NamedTupleFallback._asdict() -> dict[str, Any]
|
|
_field_defaults :: dict[str, Any]
|
|
_fields :: tuple[str, ...]
|
|
_make :: bound method type[NamedTupleFallback]._make(iterable: Iterable[Any]) -> NamedTupleFallback
|
|
_replace :: bound method NamedTupleFallback._replace(**kwargs: Any) -> NamedTupleFallback
|
|
");
|
|
}
|
|
|
|
// We don't yet take function parameters into account.
|
|
#[test]
|
|
fn call_prefix1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def bar(okay=None): ...
|
|
|
|
foo = 1
|
|
|
|
bar(o<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn call_blank1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def bar(okay=None): ...
|
|
|
|
foo = 1
|
|
|
|
bar(<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
bar
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn duplicate1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(): ...
|
|
|
|
class C:
|
|
def foo(self): ...
|
|
def bar(self):
|
|
f<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
self
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn instance_methods_are_not_regular_functions1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class C:
|
|
def foo(self): ...
|
|
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @"C");
|
|
}
|
|
|
|
#[test]
|
|
fn instance_methods_are_not_regular_functions2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class C:
|
|
def foo(self): ...
|
|
def bar(self):
|
|
f<CURSOR>
|
|
",
|
|
);
|
|
|
|
// FIXME: Should NOT include `foo` here, since
|
|
// that is only a method that can be called on
|
|
// `self`.
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
foo
|
|
self
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn identifier_keyword_clash1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
classy_variable_name = 1
|
|
|
|
class<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"classy_variable_name",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn identifier_keyword_clash2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
some_symbol = 1
|
|
|
|
print(f\"{some<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"some_symbol",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn statically_unreachable_symbols() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
if 1 + 2 != 3:
|
|
hidden_symbol = 1
|
|
|
|
hidden_<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn completions_inside_unreachable_sections() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import sys
|
|
|
|
if sys.platform == \"not-my-current-platform\":
|
|
only_available_in_this_branch = 1
|
|
|
|
on<CURSOR>
|
|
",
|
|
);
|
|
|
|
// TODO: ideally, `only_available_in_this_branch` should be available here, but we
|
|
// currently make no effort to provide a good IDE experience within sections that
|
|
// are unreachable
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn star_import() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from typing import *
|
|
|
|
Re<CURSOR>
|
|
",
|
|
);
|
|
|
|
// `ReadableBuffer` is a symbol in `typing`, but it is not re-exported
|
|
builder
|
|
.build()
|
|
.contains("Reversible")
|
|
.not_contains("ReadableBuffer");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_access_empty_list() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
[].<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("append");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_access_empty_dict() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
{}.<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("values").not_contains("add");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_access_set() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
{1}.<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("add").not_contains("values");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_parens() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class A:
|
|
x: str
|
|
|
|
a = A()
|
|
(a).<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("x");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_double_parens() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class A:
|
|
x: str
|
|
|
|
a = A()
|
|
((a)).<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("x");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_on_constructor_directly() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class A:
|
|
x: str
|
|
|
|
A().<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("x");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_not_on_integer() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
3.<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_on_integer() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
(3).<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("bit_length");
|
|
}
|
|
|
|
#[test]
|
|
fn attribute_on_float() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
3.14.<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("conjugate");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_attribute_access1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class A:
|
|
x: str
|
|
|
|
class B:
|
|
a: A
|
|
|
|
b = B()
|
|
b.a.<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().not_contains("a").contains("x");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_attribute_access2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class B:
|
|
c: int
|
|
|
|
class A:
|
|
b: B
|
|
|
|
a = A()
|
|
([1] + [a.b.<CURSOR>] + [3]).pop()
|
|
",
|
|
);
|
|
|
|
builder
|
|
.build()
|
|
.contains("c")
|
|
.not_contains("b")
|
|
.not_contains("pop");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_attribute_access3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
a = A()
|
|
([1] + [\"abc\".<CURSOR>] + [3]).pop()
|
|
",
|
|
);
|
|
|
|
builder
|
|
.build()
|
|
.contains("capitalize")
|
|
.not_contains("append")
|
|
.not_contains("pop");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_attribute_access4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class B:
|
|
c: int
|
|
|
|
class A:
|
|
b: B
|
|
|
|
def foo() -> A:
|
|
return A()
|
|
|
|
foo().<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("b").not_contains("c");
|
|
}
|
|
|
|
#[test]
|
|
fn nested_attribute_access5() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class B:
|
|
c: int
|
|
|
|
class A:
|
|
b: B
|
|
|
|
def foo() -> A:
|
|
return A()
|
|
|
|
foo().b.<CURSOR>
|
|
",
|
|
);
|
|
|
|
builder.build().contains("c").not_contains("b");
|
|
}
|
|
|
|
#[test]
|
|
fn betwixt_attribute_access1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
xyz: str
|
|
|
|
class Bar:
|
|
foo: Foo
|
|
|
|
class Quux:
|
|
bar: Bar
|
|
|
|
quux = Quux()
|
|
quux.<CURSOR>.foo.xyz
|
|
",
|
|
);
|
|
|
|
builder
|
|
.build()
|
|
.contains("bar")
|
|
.not_contains("xyz")
|
|
.not_contains("foo");
|
|
}
|
|
|
|
#[test]
|
|
fn betwixt_attribute_access2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
xyz: str
|
|
|
|
class Bar:
|
|
foo: Foo
|
|
|
|
class Quux:
|
|
bar: Bar
|
|
|
|
quux = Quux()
|
|
quux.b<CURSOR>.foo.xyz
|
|
",
|
|
);
|
|
|
|
builder
|
|
.build()
|
|
.contains("bar")
|
|
.not_contains("xyz")
|
|
.not_contains("foo");
|
|
}
|
|
|
|
#[test]
|
|
fn betwixt_attribute_access3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
xyz: str
|
|
|
|
class Bar:
|
|
foo: Foo
|
|
|
|
class Quux:
|
|
bar: Bar
|
|
|
|
quux = Quux()
|
|
<CURSOR>.foo.xyz
|
|
",
|
|
);
|
|
|
|
builder.build().contains("quux");
|
|
}
|
|
|
|
#[test]
|
|
fn betwixt_attribute_access4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
xyz: str
|
|
|
|
class Bar:
|
|
foo: Foo
|
|
|
|
class Quux:
|
|
bar: Bar
|
|
|
|
quux = Quux()
|
|
q<CURSOR>.foo.xyz
|
|
",
|
|
);
|
|
|
|
builder.build().contains("quux");
|
|
}
|
|
|
|
#[test]
|
|
fn ellipsis1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
...<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ellipsis2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
....<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"
|
|
__annotations__
|
|
__class__
|
|
__delattr__
|
|
__dict__
|
|
__dir__
|
|
__doc__
|
|
__eq__
|
|
__format__
|
|
__getattribute__
|
|
__getstate__
|
|
__hash__
|
|
__init__
|
|
__init_subclass__
|
|
__module__
|
|
__ne__
|
|
__new__
|
|
__reduce__
|
|
__reduce_ex__
|
|
__repr__
|
|
__setattr__
|
|
__sizeof__
|
|
__str__
|
|
__subclasshook__
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn ellipsis3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class Foo: ...<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn ordering() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class A:
|
|
foo: str
|
|
_foo: str
|
|
__foo__: str
|
|
__foo: str
|
|
FOO: str
|
|
_FOO: str
|
|
__FOO__: str
|
|
__FOO: str
|
|
|
|
A.<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.filter(|c| c.name.contains("FOO") || c.name.contains("foo")).build().snapshot(),
|
|
@r"
|
|
FOO
|
|
foo
|
|
__FOO__
|
|
__foo__
|
|
_FOO
|
|
__FOO
|
|
__foo
|
|
_foo
|
|
",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_function_identifier1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def m<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_function_identifier2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def m<CURSOR>(): pass
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn fscope_id_missing_function_identifier3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def m(): pass
|
|
<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(builder.skip_keywords().skip_builtins().build().snapshot(), @r"m");
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_class_identifier1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class M<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_type_alias1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
Fo<CURSOR> = float
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"Fo",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_import1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import fo<CURSOR>
|
|
",
|
|
);
|
|
|
|
// This snapshot would generate a big list of modules,
|
|
// which is kind of annoying. So just assert that it
|
|
// runs without panicking and produces some non-empty
|
|
// output.
|
|
assert!(
|
|
!builder
|
|
.skip_keywords()
|
|
.skip_builtins()
|
|
.build()
|
|
.completions()
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_import2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import foo as ba<CURSOR>
|
|
",
|
|
);
|
|
|
|
// This snapshot would generate a big list of modules,
|
|
// which is kind of annoying. So just assert that it
|
|
// runs without panicking and produces some non-empty
|
|
// output.
|
|
//
|
|
// ... some time passes ...
|
|
//
|
|
// Actually, this shouldn't offer any completions since
|
|
// the context here is introducing a new name.
|
|
assert!(
|
|
builder
|
|
.skip_keywords()
|
|
.skip_builtins()
|
|
.build()
|
|
.completions()
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_from_import1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from fo<CURSOR> import wat
|
|
",
|
|
);
|
|
|
|
// This snapshot would generate a big list of modules,
|
|
// which is kind of annoying. So just assert that it
|
|
// runs without panicking and produces some non-empty
|
|
// output.
|
|
assert!(
|
|
!builder
|
|
.skip_keywords()
|
|
.skip_builtins()
|
|
.build()
|
|
.completions()
|
|
.is_empty()
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_from_import2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from foo import wa<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_from_import3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from foo import wat as ba<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_try_except1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
try:
|
|
pass
|
|
except Type<CURSOR>:
|
|
pass
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
// Ref: https://github.com/astral-sh/ty/issues/572
|
|
#[test]
|
|
fn scope_id_missing_global1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def _():
|
|
global fo<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_dot_attr1() {
|
|
let builder = completion_test_builder(
|
|
r#"
|
|
foo = 1
|
|
bar = 2
|
|
|
|
class Foo:
|
|
def method(self): ...
|
|
|
|
f = Foo()
|
|
|
|
# String, this is not an attribute access
|
|
"f.<CURSOR>
|
|
"#,
|
|
);
|
|
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@r"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn string_dot_attr2() {
|
|
let builder = completion_test_builder(
|
|
r#"
|
|
foo = 1
|
|
bar = 2
|
|
|
|
class Foo:
|
|
def method(self): ...
|
|
|
|
f = Foo()
|
|
|
|
# F-string, this is an attribute access
|
|
f"{f.<CURSOR>
|
|
"#,
|
|
);
|
|
|
|
builder.build().contains("method");
|
|
}
|
|
|
|
#[test]
|
|
fn string_dot_attr3() {
|
|
let builder = completion_test_builder(
|
|
r#"
|
|
foo = 1
|
|
bar = 2
|
|
|
|
class Foo:
|
|
def method(self): ...
|
|
|
|
f = Foo()
|
|
|
|
# T-string, this is an attribute access
|
|
t"{f.<CURSOR>
|
|
"#,
|
|
);
|
|
|
|
builder.build().contains("method");
|
|
}
|
|
|
|
#[test]
|
|
fn no_panic_for_attribute_table_that_contains_subscript() {
|
|
let builder = completion_test_builder(
|
|
r#"
|
|
class Point:
|
|
def orthogonal_direction(self):
|
|
self[0].is_zero
|
|
|
|
def test_point(p2: Point):
|
|
p2.<CURSOR>
|
|
"#,
|
|
);
|
|
builder.build().contains("orthogonal_direction");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import abiflags, <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import <CURSOR>, abiflags
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import4() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import abiflags, \
|
|
<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import5() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import abiflags as foo, <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import6() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import abiflags as foo, g<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import7() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import abiflags as foo, \
|
|
<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import8() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import abiflags as foo, \
|
|
g<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import9() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import (
|
|
abiflags,
|
|
<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import10() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import (
|
|
abiflags,
|
|
<CURSOR>
|
|
)
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import11() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import (
|
|
<CURSOR>
|
|
)
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_unknown_in_module() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
from ? import <CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.skip_keywords().skip_builtins().build().snapshot(),
|
|
@r"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_unknown_in_import_names1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import ?, <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_unknown_in_import_names2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import ??, <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_unknown_in_import_names3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from sys import ??, <CURSOR>, ??
|
|
",
|
|
);
|
|
builder.build().contains("getsizeof");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_from_import1() {
|
|
CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source(
|
|
"package/foo.py",
|
|
"\
|
|
Cheetah = 1
|
|
Lion = 2
|
|
Cougar = 3
|
|
",
|
|
)
|
|
.source("package/sub1/sub2/bar.py", "from ...foo import <CURSOR>")
|
|
.completion_test_builder()
|
|
.build()
|
|
.contains("Cheetah");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_from_import2() {
|
|
CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source(
|
|
"package/sub1/foo.py",
|
|
"\
|
|
Cheetah = 1
|
|
Lion = 2
|
|
Cougar = 3
|
|
",
|
|
)
|
|
.source("package/sub1/sub2/bar.py", "from ..foo import <CURSOR>")
|
|
.completion_test_builder()
|
|
.build()
|
|
.contains("Cheetah");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_from_import3() {
|
|
CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source(
|
|
"package/sub1/sub2/foo.py",
|
|
"\
|
|
Cheetah = 1
|
|
Lion = 2
|
|
Cougar = 3
|
|
",
|
|
)
|
|
.source("package/sub1/sub2/bar.py", "from .foo import <CURSOR>")
|
|
.completion_test_builder()
|
|
.build()
|
|
.contains("Cheetah");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_with_submodule1() {
|
|
CursorTest::builder()
|
|
.source("main.py", "from package import <CURSOR>")
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source("package/bar.pyi", "")
|
|
.source("package/foo-bar.py", "")
|
|
.source("package/data.txt", "")
|
|
.source("package/sub/__init__.py", "")
|
|
.source("package/not-a-submodule/__init__.py", "")
|
|
.completion_test_builder()
|
|
.build()
|
|
.contains("foo")
|
|
.contains("bar")
|
|
.contains("sub")
|
|
.not_contains("foo-bar")
|
|
.not_contains("data")
|
|
.not_contains("not-a-submodule");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_with_vendored_submodule1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from http import <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("client");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_with_vendored_submodule2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from email import <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("mime").not_contains("base");
|
|
}
|
|
|
|
#[test]
|
|
fn import_submodule_not_attribute1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import importlib
|
|
importlib.<CURSOR>
|
|
",
|
|
);
|
|
builder.build().not_contains("resources");
|
|
}
|
|
|
|
#[test]
|
|
fn import_submodule_not_attribute2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import importlib.resources
|
|
importlib.<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("resources");
|
|
}
|
|
|
|
#[test]
|
|
fn import_submodule_not_attribute3() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import importlib
|
|
import importlib.resources
|
|
importlib.<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("resources");
|
|
}
|
|
|
|
#[test]
|
|
fn import_with_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import c<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_without_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_multiple_betwixt() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import re, c<CURSOR>, sys
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_multiple_end1() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import collections.abc, unico<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("unicodedata");
|
|
}
|
|
|
|
#[test]
|
|
fn import_multiple_end2() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import collections.abc, urllib.parse, bu<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("builtins");
|
|
}
|
|
|
|
#[test]
|
|
fn import_with_aliases() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import re as regexp, c<CURSOR>, sys as system
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_over_multiple_lines() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import re as regexp, \\
|
|
c<CURSOR>, \\
|
|
sys as system
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_unknown_in_module() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import ?, <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_via_from_with_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from c<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_via_from_without_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from <CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("collections");
|
|
}
|
|
|
|
#[test]
|
|
fn import_statement_with_submodule_with_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import os.p<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("path").not_contains("abspath");
|
|
}
|
|
|
|
#[test]
|
|
fn import_statement_with_submodule_multiple() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import re, os.p<CURSOR>, zlib
|
|
",
|
|
);
|
|
builder.build().contains("path").not_contains("abspath");
|
|
}
|
|
|
|
#[test]
|
|
fn import_statement_with_submodule_without_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import os.<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("path").not_contains("abspath");
|
|
}
|
|
|
|
#[test]
|
|
fn import_via_from_with_submodule_with_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from os.p<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("path").not_contains("abspath");
|
|
}
|
|
|
|
#[test]
|
|
fn import_via_from_with_submodule_without_leading_character() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from os.<CURSOR>
|
|
",
|
|
);
|
|
builder.build().contains("path").not_contains("abspath");
|
|
}
|
|
|
|
#[test]
|
|
fn auto_import_with_submodule() {
|
|
CursorTest::builder()
|
|
.source("main.py", "Abra<CURSOR>")
|
|
.source("package/__init__.py", "AbraKadabra = 1")
|
|
.completion_test_builder()
|
|
.auto_import()
|
|
.build()
|
|
.contains("AbraKadabra");
|
|
}
|
|
|
|
#[test]
|
|
fn auto_import_should_not_include_symbols_in_current_module() {
|
|
let snapshot = CursorTest::builder()
|
|
.source("main.py", "Kadabra = 1\nKad<CURSOR>")
|
|
.source("package/__init__.py", "AbraKadabra = 1")
|
|
.completion_test_builder()
|
|
.auto_import()
|
|
.type_signatures()
|
|
.module_names()
|
|
.filter(|c| c.name.contains("Kadabra"))
|
|
.build()
|
|
.snapshot();
|
|
assert_snapshot!(snapshot, @r"
|
|
Kadabra :: Literal[1] :: Current module
|
|
AbraKadabra :: Unavailable :: package
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn import_type_check_only_lowers_ranking() {
|
|
let builder = CursorTest::builder()
|
|
.source(
|
|
"main.py",
|
|
r#"
|
|
import foo
|
|
foo.A<CURSOR>
|
|
"#,
|
|
)
|
|
.source(
|
|
"foo/__init__.py",
|
|
r#"
|
|
from typing import type_check_only
|
|
|
|
@type_check_only
|
|
class Apple: pass
|
|
|
|
class Banana: pass
|
|
class Cat: pass
|
|
class Azorubine: pass
|
|
"#,
|
|
)
|
|
.completion_test_builder();
|
|
|
|
let test = builder.build();
|
|
let completions = test.completions();
|
|
|
|
let [apple_pos, banana_pos, cat_pos, azo_pos, ann_pos] =
|
|
["Apple", "Banana", "Cat", "Azorubine", "__annotations__"].map(|name| {
|
|
completions
|
|
.iter()
|
|
.position(|comp| comp.name == name)
|
|
.unwrap()
|
|
});
|
|
|
|
assert!(completions[apple_pos].is_type_check_only);
|
|
assert!(apple_pos > banana_pos.max(cat_pos).max(azo_pos));
|
|
assert!(ann_pos > apple_pos);
|
|
}
|
|
|
|
#[test]
|
|
fn type_check_only_is_type_check_only() {
|
|
// `@typing.type_check_only` is a function that's unavailable at runtime
|
|
// and so should be the last "non-underscore" completion in `typing`
|
|
let builder = completion_test_builder("from typing import t<CURSOR>");
|
|
let test = builder.build();
|
|
let last_nonunderscore = test
|
|
.completions()
|
|
.iter()
|
|
.filter(|c| !c.name.starts_with('_'))
|
|
.next_back()
|
|
.unwrap();
|
|
|
|
assert_eq!(&last_nonunderscore.name, "type_check_only");
|
|
assert!(last_nonunderscore.is_type_check_only);
|
|
}
|
|
|
|
#[test]
|
|
fn regression_test_issue_642() {
|
|
// Regression test for https://github.com/astral-sh/ty/issues/642
|
|
|
|
let test = completion_test_builder(
|
|
r#"
|
|
match 0:
|
|
case 1 i<CURSOR>:
|
|
pass
|
|
"#,
|
|
);
|
|
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found after filtering out completions>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn completion_kind_recursive_type_alias() {
|
|
let builder = completion_test_builder(
|
|
r#"
|
|
type T = T | None
|
|
def f(rec: T):
|
|
re<CURSOR>
|
|
"#,
|
|
);
|
|
let test = builder.build();
|
|
|
|
let completion = test.completions().iter().find(|c| c.name == "rec").unwrap();
|
|
assert_eq!(completion.kind(builder.db()), Some(CompletionKind::Struct));
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_comment() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
# zqzq<CURSOR>
|
|
",
|
|
);
|
|
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_double_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(\"zqzq<CURSOR>\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(\"Foo.zqzq<CURSOR>\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_incomplete_double_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(\"zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(\"Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_single_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print('zqzq<CURSOR>')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print('Foo.zqzq<CURSOR>')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_incomplete_single_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print('zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print('Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_double_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(\"\"\"zqzq<CURSOR>\"\"\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(\"\"\"Foo.zqzq<CURSOR>\"\"\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_incomplete_double_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(\"\"\"zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(\"\"\"Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_single_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print('''zqzq<CURSOR>''')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print('''Foo.zqzq<CURSOR>''')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_string_incomplete_single_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print('''zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print('''Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_double_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f\"zqzq<CURSOR>\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f\"{Foo} and Foo.zqzq<CURSOR>\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_incomplete_double_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f\"zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f\"{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_single_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f'zqzq<CURSOR>')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f'{Foo} and Foo.zqzq<CURSOR>')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_incomplete_single_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f'zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f'{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_double_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f\"\"\"zqzq<CURSOR>\"\"\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f\"\"\"{Foo} and Foo.zqzq<CURSOR>\"\"\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_incomplete_double_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f\"\"\"zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f\"\"\"{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_single_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f'''zqzq<CURSOR>''')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f'''{Foo} and Foo.zqzq<CURSOR>''')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_fstring_incomplete_single_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(f'''zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(f'''{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_double_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t\"zqzq<CURSOR>\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t\"{Foo} and Foo.zqzq<CURSOR>\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_incomplete_double_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t\"zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t\"{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_single_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t'zqzq<CURSOR>')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t'{Foo} and Foo.zqzq<CURSOR>')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_incomplete_single_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t'zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t'{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_double_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t\"\"\"zqzq<CURSOR>\"\"\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t\"\"\"{Foo} and Foo.zqzq<CURSOR>\"\"\")
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_incomplete_double_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t\"\"\"zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t\"\"\"{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_single_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t'''zqzq<CURSOR>''')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t'''{Foo} and Foo.zqzq<CURSOR>''')
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_tstring_incomplete_single_triple_quote() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
zqzqzq = 1
|
|
print(t'''zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
let test = completion_test_builder(
|
|
"\
|
|
class Foo:
|
|
zqzqzq = 1
|
|
print(t'''{Foo} and Foo.zqzq<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
test.skip_keywords().skip_builtins().build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn typevar_with_upper_bound() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def f[T: str](msg: T):
|
|
msg.<CURSOR>
|
|
",
|
|
);
|
|
let test = builder.build();
|
|
test.contains("upper");
|
|
test.contains("capitalize");
|
|
}
|
|
|
|
#[test]
|
|
fn typevar_with_constraints() {
|
|
// Test TypeVar with constraints
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from typing import TypeVar
|
|
|
|
class A:
|
|
only_on_a: int
|
|
on_a_and_b: str
|
|
|
|
class B:
|
|
only_on_b: float
|
|
on_a_and_b: str
|
|
|
|
T = TypeVar('T', A, B)
|
|
|
|
def f(x: T):
|
|
x.<CURSOR>
|
|
",
|
|
);
|
|
let test = builder.build();
|
|
|
|
test.contains("on_a_and_b");
|
|
test.not_contains("only_on_a");
|
|
test.not_contains("only_on_b");
|
|
}
|
|
|
|
#[test]
|
|
fn typevar_without_bounds_or_constraints() {
|
|
let test = completion_test_builder(
|
|
"\
|
|
def f[T](x: T):
|
|
x.<CURSOR>
|
|
",
|
|
);
|
|
test.build().contains("__repr__");
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_function_def_name() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
|
|
def f<CURSOR>
|
|
",
|
|
);
|
|
assert!(builder.build().completions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn completions_in_function_def_empty_name() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def <CURSOR>
|
|
",
|
|
);
|
|
assert!(builder.build().completions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_class_def_name() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
|
|
class f<CURSOR>
|
|
",
|
|
);
|
|
assert!(builder.build().completions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn completions_in_class_def_empty_name() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
class <CURSOR>
|
|
",
|
|
);
|
|
assert!(builder.build().completions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_type_def_name() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
|
|
type f<CURSOR> = int
|
|
",
|
|
);
|
|
|
|
assert!(builder.build().completions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_maybe_type_def_name() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
|
|
type f<CURSOR>
|
|
",
|
|
);
|
|
assert!(builder.build().completions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn completions_in_type_def_empty_name() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
type <CURSOR>
|
|
",
|
|
);
|
|
assert!(builder.build().completions().is_empty());
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_import_alias() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
import collections as f<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_from_import_alias() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
from collections import defaultdict as f<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn import_missing_alias_suggests_as_with_leading_char() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import collections a<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn import_missing_alias_suggests_as() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import collections <CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn import_dotted_module_missing_alias_suggests_as() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import collections.abc a<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn import_multiple_modules_missing_alias_suggests_as() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
import collections.abc as c, typing a<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_missing_alias_suggests_as_with_leading_char() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from collections.abc import Mapping a<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_missing_alias_suggests_as() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from collections import defaultdict <CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_parenthesized_missing_alias_suggests_as() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from typing import (
|
|
NamedTuple a<CURSOR>
|
|
)
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn from_relative_import_missing_alias_suggests_as() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
from ...foo import bar a<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"as");
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_with_alias() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
with open('bar') as f<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_except_alias() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
try:
|
|
[][0]
|
|
except IndexError as f<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_match_alias() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
status = 400
|
|
match status:
|
|
case 400 as f<CURSOR>:
|
|
return 'Bad request'
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
|
|
// Also check that completions are suppressed
|
|
// when nothing has been typed.
|
|
let builder = completion_test_builder(
|
|
"\
|
|
foo = 1
|
|
status = 400
|
|
match status:
|
|
case 400 as <CURSOR>:
|
|
return 'Bad request'
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_empty_for_variable_binding() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
for <CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_for_variable_binding() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
for foo<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_for_tuple_variable_binding() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
for foo, bar<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_function_param() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(p<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_function_param_keyword() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(in<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_function_param_multi_keyword() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(param, in<CURSOR>
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_function_param_multi_keyword_middle() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo(param, in<CURSOR>, param_two
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn no_completions_in_function_type_param() {
|
|
let builder = completion_test_builder(
|
|
"\
|
|
def foo[T<CURSOR>]
|
|
",
|
|
);
|
|
assert_snapshot!(
|
|
builder.build().snapshot(),
|
|
@"<No completions found>",
|
|
);
|
|
}
|
|
|
|
#[test]
|
|
fn completions_in_function_type_param_bound() {
|
|
completion_test_builder(
|
|
"\
|
|
def foo[T: s<CURSOR>]
|
|
",
|
|
)
|
|
.build()
|
|
.contains("str");
|
|
}
|
|
|
|
#[test]
|
|
fn completions_in_function_param_type_annotation() {
|
|
// Ensure that completions are no longer
|
|
// suppressed when have left the name
|
|
// definition block.
|
|
completion_test_builder(
|
|
"\
|
|
def foo(param: s<CURSOR>)
|
|
",
|
|
)
|
|
.build()
|
|
.contains("str");
|
|
}
|
|
|
|
#[test]
|
|
fn favour_symbols_currently_imported() {
|
|
let snapshot = CursorTest::builder()
|
|
.source("main.py", "long_nameb = 1\nlong_name<CURSOR>")
|
|
.source("foo.py", "def long_namea(): ...")
|
|
.completion_test_builder()
|
|
.type_signatures()
|
|
.auto_import()
|
|
.module_names()
|
|
.filter(|c| c.name.contains("long_name"))
|
|
.build()
|
|
.snapshot();
|
|
|
|
// Even though long_namea is alphabetically before long_nameb,
|
|
// long_nameb is currently imported and should be preferred.
|
|
assert_snapshot!(snapshot, @r"
|
|
long_nameb :: Literal[1] :: Current module
|
|
long_namea :: Unavailable :: foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn favour_imported_over_builtin() {
|
|
let snapshot =
|
|
completion_test_builder("from typing import Protocol\nclass Foo(P<CURSOR>: ...")
|
|
.filter(|c| c.name.starts_with('P'))
|
|
.build()
|
|
.snapshot();
|
|
|
|
// Here we favour `Protocol` over the other completions
|
|
// because `Protocol` has been imported, and the other completions are builtin.
|
|
assert_snapshot!(snapshot, @r"
|
|
Protocol
|
|
PendingDeprecationWarning
|
|
PermissionError
|
|
ProcessLookupError
|
|
PythonFinalizationError
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_i_suggests_import() {
|
|
let builder = completion_test_builder("from typing i<CURSOR>");
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_import_suggests_import() {
|
|
let builder = completion_test_builder("from typing import<CURSOR>");
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_importt_suggests_nothing() {
|
|
let builder = completion_test_builder("from typing importt<CURSOR>");
|
|
assert_snapshot!(builder.build().snapshot(), @"<No completions found>");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_space_suggests_import() {
|
|
let builder = completion_test_builder("from typing <CURSOR>");
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_no_space_not_suggests_import() {
|
|
let builder = completion_test_builder("from typing<CURSOR>");
|
|
assert_snapshot!(builder.build().snapshot(), @"typing");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_two_imports_suggests_import() {
|
|
let builder = completion_test_builder(
|
|
"from collections.abc import Sequence
|
|
from typing i<CURSOR>",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_random_name_suggests_nothing() {
|
|
let builder = completion_test_builder("from typing aa<CURSOR>");
|
|
assert_snapshot!(builder.build().snapshot(), @"<No completions found>");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_dotted_name_suggests_import() {
|
|
let builder = completion_test_builder("from collections.abc i<CURSOR>");
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_relative_import_suggests_import() {
|
|
let builder = CursorTest::builder()
|
|
.source("main.py", "from .foo i<CURSOR>")
|
|
.source("foo.py", "")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_dotted_name_relative_import_suggests_import() {
|
|
let builder = CursorTest::builder()
|
|
.source("main.py", "from .foo.bar i<CURSOR>")
|
|
.source("foo/bar.py", "")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_nested_dotted_name_relative_import_suggests_import() {
|
|
let builder = CursorTest::builder()
|
|
.source("src/main.py", "from ..foo i<CURSOR>")
|
|
.source("foo.py", "")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_nested_very_dotted_name_relative_import_suggests_import() {
|
|
let builder = CursorTest::builder()
|
|
// N.B. the `...` tokenizes as `TokenKind::Ellipsis`
|
|
.source("src/main.py", "from ...foo i<CURSOR>")
|
|
.source("foo.py", "")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @"import");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_only_dot() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source(
|
|
"package/sub1/sub2/bar.py",
|
|
"
|
|
import_zqzqzq = 1
|
|
from .<CURSOR>
|
|
",
|
|
)
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
import
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_only_dot_incomplete() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source(
|
|
"package/sub1/sub2/bar.py",
|
|
"
|
|
import_zqzqzq = 1
|
|
from .imp<CURSOR>
|
|
",
|
|
)
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
import
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn from_import_incomplete() {
|
|
let builder = completion_test_builder(
|
|
"from collections.abc i
|
|
|
|
ZQZQZQ = 1
|
|
ZQ<CURSOR>",
|
|
);
|
|
assert_snapshot!(builder.build().snapshot(), @"ZQZQZQ");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_dots1() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source("package/sub1/sub2/bar.py", "from ...<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
import
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_dots2() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo/__init__.py", "")
|
|
.source("package/foo/bar.py", "")
|
|
.source("package/foo/baz.py", "")
|
|
.source("package/sub1/sub2/bar.py", "from ...foo.<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
bar
|
|
baz
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_dots3() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source("package/sub1/sub2/bar.py", "from.<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
import
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_dots4() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source("package/sub1/bar.py", "from ..<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
import
|
|
foo
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_typing1() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source("package/sub1/sub2/bar.py", "from ...fo<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @"foo");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_typing2() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo/__init__.py", "")
|
|
.source("package/foo/bar.py", "")
|
|
.source("package/foo/baz.py", "")
|
|
.source("package/sub1/sub2/bar.py", "from ...foo.ba<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
bar
|
|
baz
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_typing3() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/foo.py", "")
|
|
.source("package/imposition.py", "")
|
|
.source("package/sub1/sub2/bar.py", "from ...im<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
import
|
|
imposition
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn relative_import_module_after_typing4() {
|
|
let builder = CursorTest::builder()
|
|
.source("package/__init__.py", "")
|
|
.source("package/sub1/__init__.py", "")
|
|
.source("package/sub1/foo.py", "")
|
|
.source("package/sub1/imposition.py", "")
|
|
.source("package/sub1/bar.py", "from ..sub1.<CURSOR>")
|
|
.completion_test_builder();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
bar
|
|
foo
|
|
imposition
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn typing_extensions_excluded_from_import() {
|
|
let builder = completion_test_builder("from typing<CURSOR>").module_names();
|
|
assert_snapshot!(builder.build().snapshot(), @"typing :: Current module");
|
|
}
|
|
|
|
#[test]
|
|
fn typing_extensions_excluded_from_auto_import() {
|
|
let builder = completion_test_builder("deprecated<CURSOR>")
|
|
.auto_import()
|
|
.module_names();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
Deprecated :: importlib.metadata
|
|
DeprecatedList :: importlib.metadata
|
|
DeprecatedNonAbstract :: importlib.metadata
|
|
DeprecatedTuple :: importlib.metadata
|
|
deprecated :: warnings
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn typing_extensions_included_from_import() {
|
|
let builder = CursorTest::builder()
|
|
.source("typing_extensions.py", "deprecated = 1")
|
|
.source("foo.py", "from typing<CURSOR>")
|
|
.completion_test_builder()
|
|
.module_names();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
typing :: Current module
|
|
typing_extensions :: Current module
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn typing_extensions_included_from_auto_import() {
|
|
let builder = CursorTest::builder()
|
|
.source("typing_extensions.py", "deprecated = 1")
|
|
.source("foo.py", "deprecated<CURSOR>")
|
|
.completion_test_builder()
|
|
.auto_import()
|
|
.module_names();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
Deprecated :: importlib.metadata
|
|
DeprecatedList :: importlib.metadata
|
|
DeprecatedNonAbstract :: importlib.metadata
|
|
DeprecatedTuple :: importlib.metadata
|
|
deprecated :: typing_extensions
|
|
deprecated :: warnings
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn typing_extensions_included_from_import_in_stub() {
|
|
let builder = CursorTest::builder()
|
|
.source("foo.pyi", "from typing<CURSOR>")
|
|
.completion_test_builder()
|
|
.module_names();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
typing :: Current module
|
|
typing_extensions :: Current module
|
|
");
|
|
}
|
|
|
|
#[test]
|
|
fn typing_extensions_included_from_auto_import_in_stub() {
|
|
let builder = CursorTest::builder()
|
|
.source("foo.pyi", "deprecated<CURSOR>")
|
|
.completion_test_builder()
|
|
.auto_import()
|
|
.module_names();
|
|
assert_snapshot!(builder.build().snapshot(), @r"
|
|
Deprecated :: importlib.metadata
|
|
DeprecatedList :: importlib.metadata
|
|
DeprecatedNonAbstract :: importlib.metadata
|
|
DeprecatedTuple :: importlib.metadata
|
|
deprecated :: typing_extensions
|
|
deprecated :: warnings
|
|
");
|
|
}
|
|
|
|
/// A way to create a simple single-file (named `main.py`) completion test
|
|
/// builder.
|
|
///
|
|
/// Use cases that require multiple files with a `<CURSOR>` marker
|
|
/// in a file other than `main.py` can use `CursorTest::builder()`
|
|
/// and then `CursorTestBuilder::completion_test_builder()`.
|
|
fn completion_test_builder(source: &str) -> CompletionTestBuilder {
|
|
CursorTest::builder()
|
|
.source("main.py", source)
|
|
.completion_test_builder()
|
|
}
|
|
|
|
/// A builder for executing a completion test.
|
|
///
|
|
/// This mostly owns the responsibility for generating snapshots
|
|
/// of completions from a cursor position in source code. Most of
|
|
/// the options involve some kind of filtering or adjustment to
|
|
/// apply to the snapshots, depending on what one wants to test.
|
|
#[expect(clippy::struct_excessive_bools)] // free the bools!
|
|
struct CompletionTestBuilder {
|
|
cursor_test: CursorTest,
|
|
settings: CompletionSettings,
|
|
skip_builtins: bool,
|
|
skip_keywords: bool,
|
|
type_signatures: bool,
|
|
module_names: bool,
|
|
// This doesn't seem like a "very complex" type to me... ---AG
|
|
#[allow(clippy::type_complexity)]
|
|
predicate: Option<Box<dyn Fn(&Completion) -> bool>>,
|
|
}
|
|
|
|
impl CompletionTestBuilder {
|
|
/// Returns completions based on this configuration.
|
|
fn build(&self) -> CompletionTest<'_> {
|
|
let original = completion(
|
|
&self.cursor_test.db,
|
|
&self.settings,
|
|
self.cursor_test.cursor.file,
|
|
self.cursor_test.cursor.offset,
|
|
);
|
|
let filtered = original
|
|
.iter()
|
|
.filter(|c| !self.skip_builtins || !c.builtin)
|
|
.filter(|c| !self.skip_keywords || c.kind != Some(CompletionKind::Keyword))
|
|
.filter(|c| {
|
|
self.predicate
|
|
.as_ref()
|
|
.map(|predicate| predicate(c))
|
|
.unwrap_or(true)
|
|
})
|
|
.cloned()
|
|
.collect();
|
|
CompletionTest {
|
|
db: self.db(),
|
|
original,
|
|
filtered,
|
|
type_signatures: self.type_signatures,
|
|
module_names: self.module_names,
|
|
}
|
|
}
|
|
|
|
/// Returns the underlying test DB.
|
|
fn db(&self) -> &ty_project::TestDb {
|
|
&self.cursor_test.db
|
|
}
|
|
|
|
/// When enabled, symbols that aren't in scope but available
|
|
/// in the environment will be included.
|
|
///
|
|
/// Not enabled by default.
|
|
fn auto_import(mut self) -> CompletionTestBuilder {
|
|
self.settings.auto_import = true;
|
|
self
|
|
}
|
|
|
|
/// When set, builtins from completions are skipped. This is
|
|
/// useful in tests to reduce noise for scope based completions.
|
|
///
|
|
/// Not enabled by default.
|
|
fn skip_builtins(mut self) -> CompletionTestBuilder {
|
|
self.skip_builtins = true;
|
|
self
|
|
}
|
|
|
|
/// When set, keywords from completions are skipped. This
|
|
/// is useful in tests to reduce noise for scope based
|
|
/// completions.
|
|
///
|
|
/// Not enabled by default.
|
|
///
|
|
/// Note that, at time of writing (2025-11-11), keywords are
|
|
/// *also* considered builtins. So `skip_builtins()` will also
|
|
/// skip keywords. But this may not always be true. And one
|
|
/// might want to skip keywords but *not* builtins.
|
|
fn skip_keywords(mut self) -> CompletionTestBuilder {
|
|
self.skip_keywords = true;
|
|
self
|
|
}
|
|
|
|
/// When set, type signatures of each completion item are
|
|
/// included in the snapshot. This is useful when one wants
|
|
/// to specifically test types, but it usually best to leave
|
|
/// off as it can add lots of noise.
|
|
///
|
|
/// Not enabled by default.
|
|
fn type_signatures(mut self) -> CompletionTestBuilder {
|
|
self.type_signatures = true;
|
|
self
|
|
}
|
|
|
|
/// When set, the module name for each symbol is included
|
|
/// in the snapshot (if available).
|
|
fn module_names(mut self) -> CompletionTestBuilder {
|
|
self.module_names = true;
|
|
self
|
|
}
|
|
|
|
/// Apply arbitrary filtering to completions.
|
|
fn filter(
|
|
mut self,
|
|
predicate: impl Fn(&Completion) -> bool + 'static,
|
|
) -> CompletionTestBuilder {
|
|
self.predicate = Some(Box::new(predicate));
|
|
self
|
|
}
|
|
}
|
|
|
|
struct CompletionTest<'db> {
|
|
db: &'db ty_project::TestDb,
|
|
/// The original completions returned before any additional
|
|
/// test-specific filtering. We keep this around in order to
|
|
/// slightly modify the test snapshot generated. This
|
|
/// lets us differentiate between "absolutely no completions
|
|
/// were returned" and "completions were returned, but you
|
|
/// filtered them out."
|
|
original: Vec<Completion<'db>>,
|
|
/// The completions that the test should act upon. These are
|
|
/// filtered by things like `skip_builtins`.
|
|
filtered: Vec<Completion<'db>>,
|
|
/// Whether type signatures should be included in the snapshot
|
|
/// generated by `CompletionTest::snapshot`.
|
|
type_signatures: bool,
|
|
/// Whether module names should be included in the snapshot
|
|
/// generated by `CompletionTest::snapshot`.
|
|
module_names: bool,
|
|
}
|
|
|
|
impl<'db> CompletionTest<'db> {
|
|
fn snapshot(&self) -> String {
|
|
if self.original.is_empty() {
|
|
return "<No completions found>".to_string();
|
|
} else if self.filtered.is_empty() {
|
|
// It'd be nice to include the actual number of
|
|
// completions filtered out, but in practice, the
|
|
// number is environment dependent. For example, on
|
|
// Windows, there are 231 builtins, but on Unix, there
|
|
// are 230. So we just leave out the number I guess.
|
|
// ---AG
|
|
return "<No completions found after filtering out completions>".to_string();
|
|
}
|
|
self.filtered
|
|
.iter()
|
|
.map(|c| {
|
|
let mut snapshot = c.name.as_str().to_string();
|
|
if self.type_signatures {
|
|
let ty =
|
|
c.ty.map(|ty| ty.display(self.db).to_string())
|
|
.unwrap_or_else(|| "Unavailable".to_string());
|
|
snapshot = format!("{snapshot} :: {ty}");
|
|
}
|
|
if self.module_names {
|
|
let module_name = c
|
|
.module_name
|
|
.map(ModuleName::as_str)
|
|
.unwrap_or("Current module");
|
|
snapshot = format!("{snapshot} :: {module_name}");
|
|
}
|
|
snapshot
|
|
})
|
|
.collect::<Vec<String>>()
|
|
.join("\n")
|
|
}
|
|
|
|
#[track_caller]
|
|
fn contains(&self, expected: &str) -> &CompletionTest<'db> {
|
|
assert!(
|
|
self.filtered
|
|
.iter()
|
|
.any(|completion| completion.name == expected),
|
|
"Expected completions to include `{expected}`"
|
|
);
|
|
self
|
|
}
|
|
|
|
#[track_caller]
|
|
fn not_contains(&self, unexpected: &str) -> &CompletionTest<'db> {
|
|
assert!(
|
|
self.filtered
|
|
.iter()
|
|
.all(|completion| completion.name != unexpected),
|
|
"Expected completions to not include `{unexpected}`",
|
|
);
|
|
self
|
|
}
|
|
|
|
/// Returns the underlying completions if the convenience assertions
|
|
/// aren't sufficiently expressive.
|
|
fn completions(&self) -> &[Completion<'db>] {
|
|
&self.filtered
|
|
}
|
|
}
|
|
|
|
impl CursorTestBuilder {
|
|
fn completion_test_builder(&self) -> CompletionTestBuilder {
|
|
CompletionTestBuilder {
|
|
cursor_test: self.build(),
|
|
settings: CompletionSettings::default(),
|
|
skip_builtins: false,
|
|
skip_keywords: false,
|
|
type_signatures: false,
|
|
module_names: false,
|
|
predicate: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
fn tokenize(src: &str) -> Tokens {
|
|
let parsed = ruff_python_parser::parse(src, ParseOptions::from(Mode::Module))
|
|
.expect("valid Python source for token stream");
|
|
parsed.tokens().clone()
|
|
}
|
|
}
|