mirror of https://github.com/astral-sh/ruff
994 lines
39 KiB
Rust
994 lines
39 KiB
Rust
use crate::docstring::Docstring;
|
|
pub use crate::goto_declaration::goto_declaration;
|
|
pub use crate::goto_definition::goto_definition;
|
|
pub use crate::goto_type_definition::goto_type_definition;
|
|
|
|
use std::borrow::Cow;
|
|
|
|
use crate::find_node::covering_node;
|
|
use crate::stub_mapping::StubMapper;
|
|
use ruff_db::parsed::ParsedModuleRef;
|
|
use ruff_python_ast::{self as ast, AnyNodeRef};
|
|
use ruff_python_parser::{TokenKind, Tokens};
|
|
use ruff_text_size::{Ranged, TextRange, TextSize};
|
|
|
|
use ty_python_semantic::ResolvedDefinition;
|
|
use ty_python_semantic::types::Type;
|
|
use ty_python_semantic::types::ide_support::{
|
|
call_signature_details, definitions_for_keyword_argument,
|
|
};
|
|
use ty_python_semantic::{
|
|
HasDefinition, HasType, ImportAliasResolution, SemanticModel, definitions_for_imported_symbol,
|
|
definitions_for_name,
|
|
};
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub(crate) enum GotoTarget<'a> {
|
|
Expression(ast::ExprRef<'a>),
|
|
FunctionDef(&'a ast::StmtFunctionDef),
|
|
ClassDef(&'a ast::StmtClassDef),
|
|
Parameter(&'a ast::Parameter),
|
|
|
|
/// Go to on the operator of a binary operation.
|
|
///
|
|
/// ```py
|
|
/// a + b
|
|
/// ^
|
|
/// ```
|
|
BinOp {
|
|
expression: &'a ast::ExprBinOp,
|
|
operator_range: TextRange,
|
|
},
|
|
|
|
/// Go to where the operator of a unary operation is defined.
|
|
///
|
|
/// ```py
|
|
/// -a
|
|
/// ^
|
|
/// ```
|
|
UnaryOp {
|
|
expression: &'a ast::ExprUnaryOp,
|
|
operator_range: TextRange,
|
|
},
|
|
|
|
/// Multi-part module names
|
|
/// Handles both `import foo.bar` and `from foo.bar import baz` cases
|
|
/// ```py
|
|
/// import foo.bar
|
|
/// ^^^
|
|
/// from foo.bar import baz
|
|
/// ^^^
|
|
/// ```
|
|
ImportModuleComponent {
|
|
module_name: String,
|
|
component_index: usize,
|
|
component_range: TextRange,
|
|
},
|
|
|
|
/// Import alias in standard import statement
|
|
/// ```py
|
|
/// import foo.bar as baz
|
|
/// ^^^
|
|
/// ```
|
|
ImportModuleAlias {
|
|
alias: &'a ast::Alias,
|
|
},
|
|
|
|
/// Import alias in from import statement
|
|
/// ```py
|
|
/// from foo import bar as baz
|
|
/// ^^^
|
|
/// from foo import bar as baz
|
|
/// ^^^
|
|
/// ```
|
|
ImportSymbolAlias {
|
|
alias: &'a ast::Alias,
|
|
range: TextRange,
|
|
import_from: &'a ast::StmtImportFrom,
|
|
},
|
|
|
|
/// Go to on the exception handler variable
|
|
/// ```py
|
|
/// try: ...
|
|
/// except Exception as e: ...
|
|
/// ^
|
|
/// ```
|
|
ExceptVariable(&'a ast::ExceptHandlerExceptHandler),
|
|
|
|
/// Go to on a keyword argument
|
|
/// ```py
|
|
/// test(a = 1)
|
|
/// ^
|
|
/// ```
|
|
KeywordArgument {
|
|
keyword: &'a ast::Keyword,
|
|
call_expression: &'a ast::ExprCall,
|
|
},
|
|
|
|
/// Go to on the rest parameter of a pattern match
|
|
///
|
|
/// ```py
|
|
/// match x:
|
|
/// case {"a": a, "b": b, **rest}: ...
|
|
/// ^^^^
|
|
/// ```
|
|
PatternMatchRest(&'a ast::PatternMatchMapping),
|
|
|
|
/// Go to on a keyword argument of a class pattern
|
|
///
|
|
/// ```py
|
|
/// match Point3D(0, 0, 0):
|
|
/// case Point3D(x=0, y=0, z=0): ...
|
|
/// ^ ^ ^
|
|
/// ```
|
|
PatternKeywordArgument(&'a ast::PatternKeyword),
|
|
|
|
/// Go to on a pattern star argument
|
|
///
|
|
/// ```py
|
|
/// match array:
|
|
/// case [*args]: ...
|
|
/// ^^^^
|
|
PatternMatchStarName(&'a ast::PatternMatchStar),
|
|
|
|
/// Go to on the name of a pattern match as pattern
|
|
///
|
|
/// ```py
|
|
/// match x:
|
|
/// case [x] as y: ...
|
|
/// ^
|
|
PatternMatchAsName(&'a ast::PatternMatchAs),
|
|
|
|
/// Go to on the name of a type variable
|
|
///
|
|
/// ```py
|
|
/// type Alias[T: int = bool] = list[T]
|
|
/// ^
|
|
/// ```
|
|
TypeParamTypeVarName(&'a ast::TypeParamTypeVar),
|
|
|
|
/// Go to on the name of a type param spec
|
|
///
|
|
/// ```py
|
|
/// type Alias[**P = [int, str]] = Callable[P, int]
|
|
/// ^
|
|
/// ```
|
|
TypeParamParamSpecName(&'a ast::TypeParamParamSpec),
|
|
|
|
/// Go to on the name of a type var tuple
|
|
///
|
|
/// ```py
|
|
/// type Alias[*Ts = ()] = tuple[*Ts]
|
|
/// ^^
|
|
/// ```
|
|
TypeParamTypeVarTupleName(&'a ast::TypeParamTypeVarTuple),
|
|
|
|
NonLocal {
|
|
identifier: &'a ast::Identifier,
|
|
},
|
|
Globals {
|
|
identifier: &'a ast::Identifier,
|
|
},
|
|
/// Go to on the invocation of a callable
|
|
///
|
|
/// ```py
|
|
/// x = mymodule.MyClass(1, 2)
|
|
/// ^^^^^^^
|
|
/// ```
|
|
///
|
|
/// This is equivalent to `GotoTarget::Expression(callable)` but enriched
|
|
/// with information about the actual callable implementation.
|
|
///
|
|
/// That is, if you click on `MyClass` in `MyClass()` it is *both* a
|
|
/// reference to the class and to the initializer of the class. Therefore
|
|
/// it would be ideal for goto-* and docstrings to be some intelligent
|
|
/// merging of both the class and the initializer.
|
|
Call {
|
|
/// The callable that can actually be selected by a cursor
|
|
callable: ast::ExprRef<'a>,
|
|
/// The call of the callable
|
|
call: &'a ast::ExprCall,
|
|
},
|
|
}
|
|
|
|
/// The resolved definitions for a `GotoTarget`
|
|
#[derive(Debug, Clone)]
|
|
pub(crate) enum DefinitionsOrTargets<'db> {
|
|
/// We computed actual Definitions we can do followup queries on.
|
|
Definitions(Vec<ResolvedDefinition<'db>>),
|
|
/// We directly computed a navigation.
|
|
///
|
|
/// We can't get docs or usefully compute goto-definition for this.
|
|
Targets(crate::NavigationTargets),
|
|
}
|
|
|
|
impl<'db> DefinitionsOrTargets<'db> {
|
|
pub(crate) fn from_ty(db: &'db dyn crate::Db, ty: Type<'db>) -> Option<Self> {
|
|
let ty_def = ty.definition(db)?;
|
|
let resolved = match ty_def {
|
|
ty_python_semantic::types::TypeDefinition::Module(module) => {
|
|
ResolvedDefinition::Module(module.file(db)?)
|
|
}
|
|
ty_python_semantic::types::TypeDefinition::Class(definition)
|
|
| ty_python_semantic::types::TypeDefinition::Function(definition)
|
|
| ty_python_semantic::types::TypeDefinition::TypeVar(definition)
|
|
| ty_python_semantic::types::TypeDefinition::TypeAlias(definition)
|
|
| ty_python_semantic::types::TypeDefinition::NewType(definition) => {
|
|
ResolvedDefinition::Definition(definition)
|
|
}
|
|
};
|
|
Some(DefinitionsOrTargets::Definitions(vec![resolved]))
|
|
}
|
|
|
|
/// Get the "goto-declaration" interpretation of this definition
|
|
///
|
|
/// In this case it basically returns exactly what was found.
|
|
pub(crate) fn declaration_targets(
|
|
self,
|
|
db: &'db dyn crate::Db,
|
|
) -> Option<crate::NavigationTargets> {
|
|
match self {
|
|
DefinitionsOrTargets::Definitions(definitions) => {
|
|
definitions_to_navigation_targets(db, None, definitions)
|
|
}
|
|
DefinitionsOrTargets::Targets(targets) => Some(targets),
|
|
}
|
|
}
|
|
|
|
/// Get the "goto-definition" interpretation of this definition
|
|
///
|
|
/// In this case we apply stub-mapping to try to find the "real" implementation
|
|
/// if the definition we have is found in a stub file.
|
|
pub(crate) fn definition_targets(
|
|
self,
|
|
db: &'db dyn crate::Db,
|
|
) -> Option<crate::NavigationTargets> {
|
|
match self {
|
|
DefinitionsOrTargets::Definitions(definitions) => {
|
|
definitions_to_navigation_targets(db, Some(&StubMapper::new(db)), definitions)
|
|
}
|
|
DefinitionsOrTargets::Targets(targets) => Some(targets),
|
|
}
|
|
}
|
|
|
|
/// Get the docstring for this definition
|
|
///
|
|
/// Typically documentation only appears on implementations and not stubs,
|
|
/// so this will check both the goto-declarations and goto-definitions (in that order)
|
|
/// and return the first one found.
|
|
pub(crate) fn docstring(self, db: &'db dyn crate::Db) -> Option<Docstring> {
|
|
let definitions = match self {
|
|
DefinitionsOrTargets::Definitions(definitions) => definitions,
|
|
// Can't find docs for these
|
|
// (make more cases DefinitionOrTargets::Definitions to get more docs!)
|
|
DefinitionsOrTargets::Targets(_) => return None,
|
|
};
|
|
for definition in &definitions {
|
|
// If we got a docstring from the original definition, use it
|
|
if let Some(docstring) = definition.docstring(db) {
|
|
return Some(Docstring::new(docstring));
|
|
}
|
|
}
|
|
|
|
// If the definition is located within a stub file and no docstring
|
|
// is present, try to map the symbol to an implementation file and extract
|
|
// the docstring from that location.
|
|
let stub_mapper = StubMapper::new(db);
|
|
|
|
// Try to find the corresponding implementation definition
|
|
for definition in stub_mapper.map_definitions(definitions) {
|
|
if let Some(docstring) = definition.docstring(db) {
|
|
return Some(Docstring::new(docstring));
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
}
|
|
|
|
impl GotoTarget<'_> {
|
|
pub(crate) fn inferred_type<'db>(&self, model: &SemanticModel<'db>) -> Option<Type<'db>> {
|
|
let ty = match self {
|
|
GotoTarget::Expression(expression) => expression.inferred_type(model),
|
|
GotoTarget::FunctionDef(function) => function.inferred_type(model),
|
|
GotoTarget::ClassDef(class) => class.inferred_type(model),
|
|
GotoTarget::Parameter(parameter) => parameter.inferred_type(model),
|
|
GotoTarget::ImportSymbolAlias { alias, .. } => alias.inferred_type(model),
|
|
GotoTarget::ImportModuleAlias { alias } => alias.inferred_type(model),
|
|
GotoTarget::ExceptVariable(except) => except.inferred_type(model),
|
|
GotoTarget::KeywordArgument { keyword, .. } => keyword.value.inferred_type(model),
|
|
// When asking the type of a callable, usually you want the callable itself?
|
|
// (i.e. the type of `MyClass` in `MyClass()` is `<class MyClass>` and not `() -> MyClass`)
|
|
GotoTarget::Call { callable, .. } => callable.inferred_type(model),
|
|
GotoTarget::TypeParamTypeVarName(typevar) => typevar.inferred_type(model),
|
|
// TODO: Support identifier targets
|
|
GotoTarget::PatternMatchRest(_)
|
|
| GotoTarget::PatternKeywordArgument(_)
|
|
| GotoTarget::PatternMatchStarName(_)
|
|
| GotoTarget::PatternMatchAsName(_)
|
|
| GotoTarget::ImportModuleComponent { .. }
|
|
| GotoTarget::TypeParamParamSpecName(_)
|
|
| GotoTarget::TypeParamTypeVarTupleName(_)
|
|
| GotoTarget::NonLocal { .. }
|
|
| GotoTarget::Globals { .. } => return None,
|
|
GotoTarget::BinOp { expression, .. } => {
|
|
let (_, ty) =
|
|
ty_python_semantic::definitions_for_bin_op(model.db(), model, expression)?;
|
|
ty
|
|
}
|
|
GotoTarget::UnaryOp { expression, .. } => {
|
|
let (_, ty) =
|
|
ty_python_semantic::definitions_for_unary_op(model.db(), model, expression)?;
|
|
ty
|
|
}
|
|
};
|
|
|
|
Some(ty)
|
|
}
|
|
|
|
/// Gets the definitions for this goto target.
|
|
///
|
|
/// The `alias_resolution` parameter controls whether import aliases
|
|
/// (i.e. "x" in "from a import b as x") are resolved or returned as is.
|
|
/// We want to resolve them in some cases (like "goto declaration") but not in others
|
|
/// (like find references or rename).
|
|
///
|
|
///
|
|
/// Ideally this would always return `DefinitionsOrTargets::Definitions`
|
|
/// as this is more useful for doing stub mapping (goto-definition) and
|
|
/// retrieving docstrings. However for now some cases are stubbed out
|
|
/// as just returning a raw `NavigationTarget`.
|
|
pub(crate) fn get_definition_targets<'db>(
|
|
&self,
|
|
file: ruff_db::files::File,
|
|
db: &'db dyn crate::Db,
|
|
alias_resolution: ImportAliasResolution,
|
|
) -> Option<DefinitionsOrTargets<'db>> {
|
|
use crate::NavigationTarget;
|
|
|
|
match self {
|
|
GotoTarget::Expression(expression) => definitions_for_expression(db, file, expression)
|
|
.map(DefinitionsOrTargets::Definitions),
|
|
|
|
// For already-defined symbols, they are their own definitions
|
|
GotoTarget::FunctionDef(function) => {
|
|
let model = SemanticModel::new(db, file);
|
|
Some(DefinitionsOrTargets::Definitions(vec![
|
|
ResolvedDefinition::Definition(function.definition(&model)),
|
|
]))
|
|
}
|
|
|
|
GotoTarget::ClassDef(class) => {
|
|
let model = SemanticModel::new(db, file);
|
|
Some(DefinitionsOrTargets::Definitions(vec![
|
|
ResolvedDefinition::Definition(class.definition(&model)),
|
|
]))
|
|
}
|
|
|
|
GotoTarget::Parameter(parameter) => {
|
|
let model = SemanticModel::new(db, file);
|
|
Some(DefinitionsOrTargets::Definitions(vec![
|
|
ResolvedDefinition::Definition(parameter.definition(&model)),
|
|
]))
|
|
}
|
|
|
|
// For import aliases (offset within 'y' or 'z' in "from x import y as z")
|
|
GotoTarget::ImportSymbolAlias {
|
|
alias, import_from, ..
|
|
} => {
|
|
let symbol_name = alias.name.as_str();
|
|
Some(DefinitionsOrTargets::Definitions(
|
|
definitions_for_imported_symbol(
|
|
db,
|
|
file,
|
|
import_from,
|
|
symbol_name,
|
|
alias_resolution,
|
|
),
|
|
))
|
|
}
|
|
|
|
GotoTarget::ImportModuleComponent {
|
|
module_name,
|
|
component_index,
|
|
..
|
|
} => {
|
|
// Handle both `import foo.bar` and `from foo.bar import baz` where offset is within module component
|
|
let components: Vec<&str> = module_name.split('.').collect();
|
|
|
|
// Build the module name up to and including the component containing the offset
|
|
let target_module_name = components[..=*component_index].join(".");
|
|
|
|
// Try to resolve the module
|
|
definitions_for_module(db, &target_module_name)
|
|
}
|
|
|
|
// Handle import aliases (offset within 'z' in "import x.y as z")
|
|
GotoTarget::ImportModuleAlias { alias } => {
|
|
if alias_resolution == ImportAliasResolution::ResolveAliases {
|
|
let full_module_name = alias.name.as_str();
|
|
// Try to resolve the module
|
|
definitions_for_module(db, full_module_name)
|
|
} else {
|
|
let alias_range = alias.asname.as_ref().unwrap().range;
|
|
Some(DefinitionsOrTargets::Targets(
|
|
crate::NavigationTargets::single(NavigationTarget {
|
|
file,
|
|
focus_range: alias_range,
|
|
full_range: alias.range(),
|
|
}),
|
|
))
|
|
}
|
|
}
|
|
|
|
// Handle keyword arguments in call expressions
|
|
GotoTarget::KeywordArgument {
|
|
keyword,
|
|
call_expression,
|
|
} => Some(DefinitionsOrTargets::Definitions(
|
|
definitions_for_keyword_argument(db, file, keyword, call_expression),
|
|
)),
|
|
|
|
// For exception variables, they are their own definitions (like parameters)
|
|
GotoTarget::ExceptVariable(except_handler) => {
|
|
let model = SemanticModel::new(db, file);
|
|
Some(DefinitionsOrTargets::Definitions(vec![
|
|
ResolvedDefinition::Definition(except_handler.definition(&model)),
|
|
]))
|
|
}
|
|
|
|
// For pattern match rest variables, they are their own definitions
|
|
GotoTarget::PatternMatchRest(pattern_mapping) => {
|
|
if let Some(rest_name) = &pattern_mapping.rest {
|
|
let range = rest_name.range;
|
|
Some(DefinitionsOrTargets::Targets(
|
|
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// For pattern match as names, they are their own definitions
|
|
GotoTarget::PatternMatchAsName(pattern_as) => {
|
|
if let Some(name) = &pattern_as.name {
|
|
let range = name.range;
|
|
Some(DefinitionsOrTargets::Targets(
|
|
crate::NavigationTargets::single(NavigationTarget::new(file, range)),
|
|
))
|
|
} else {
|
|
None
|
|
}
|
|
}
|
|
|
|
// For callables, both the definition of the callable and the actual function impl are relevant.
|
|
//
|
|
// Prefer the function impl over the callable so that its docstrings win if defined.
|
|
GotoTarget::Call { callable, call } => {
|
|
let mut definitions = definitions_for_callable(db, file, call);
|
|
let expr_definitions =
|
|
definitions_for_expression(db, file, callable).unwrap_or_default();
|
|
definitions.extend(expr_definitions);
|
|
|
|
if definitions.is_empty() {
|
|
None
|
|
} else {
|
|
Some(DefinitionsOrTargets::Definitions(definitions))
|
|
}
|
|
}
|
|
|
|
GotoTarget::BinOp { expression, .. } => {
|
|
let model = SemanticModel::new(db, file);
|
|
|
|
let (definitions, _) =
|
|
ty_python_semantic::definitions_for_bin_op(db, &model, expression)?;
|
|
|
|
Some(DefinitionsOrTargets::Definitions(definitions))
|
|
}
|
|
|
|
GotoTarget::UnaryOp { expression, .. } => {
|
|
let model = SemanticModel::new(db, file);
|
|
let (definitions, _) =
|
|
ty_python_semantic::definitions_for_unary_op(db, &model, expression)?;
|
|
|
|
Some(DefinitionsOrTargets::Definitions(definitions))
|
|
}
|
|
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Returns the text representation of this goto target.
|
|
/// Returns `None` if no meaningful string representation can be provided.
|
|
/// This is used by the "references" feature, which looks for references
|
|
/// to this goto target.
|
|
pub(crate) fn to_string(&self) -> Option<Cow<'_, str>> {
|
|
match self {
|
|
GotoTarget::Call {
|
|
callable: expression,
|
|
..
|
|
}
|
|
| GotoTarget::Expression(expression) => match expression {
|
|
ast::ExprRef::Name(name) => Some(Cow::Borrowed(name.id.as_str())),
|
|
ast::ExprRef::Attribute(attr) => Some(Cow::Borrowed(attr.attr.as_str())),
|
|
_ => None,
|
|
},
|
|
GotoTarget::FunctionDef(function) => Some(Cow::Borrowed(function.name.as_str())),
|
|
GotoTarget::ClassDef(class) => Some(Cow::Borrowed(class.name.as_str())),
|
|
GotoTarget::Parameter(parameter) => Some(Cow::Borrowed(parameter.name.as_str())),
|
|
GotoTarget::ImportSymbolAlias { alias, .. } => {
|
|
if let Some(asname) = &alias.asname {
|
|
Some(Cow::Borrowed(asname.as_str()))
|
|
} else {
|
|
Some(Cow::Borrowed(alias.name.as_str()))
|
|
}
|
|
}
|
|
GotoTarget::ImportModuleComponent {
|
|
module_name,
|
|
component_index,
|
|
..
|
|
} => {
|
|
let components: Vec<&str> = module_name.split('.').collect();
|
|
if let Some(component) = components.get(*component_index) {
|
|
Some(Cow::Borrowed(*component))
|
|
} else {
|
|
Some(Cow::Borrowed(module_name))
|
|
}
|
|
}
|
|
GotoTarget::ImportModuleAlias { alias } => {
|
|
if let Some(asname) = &alias.asname {
|
|
Some(Cow::Borrowed(asname.as_str()))
|
|
} else {
|
|
Some(Cow::Borrowed(alias.name.as_str()))
|
|
}
|
|
}
|
|
GotoTarget::ExceptVariable(except) => {
|
|
Some(Cow::Borrowed(except.name.as_ref()?.as_str()))
|
|
}
|
|
GotoTarget::KeywordArgument { keyword, .. } => {
|
|
Some(Cow::Borrowed(keyword.arg.as_ref()?.as_str()))
|
|
}
|
|
GotoTarget::PatternMatchRest(rest) => Some(Cow::Borrowed(rest.rest.as_ref()?.as_str())),
|
|
GotoTarget::PatternKeywordArgument(keyword) => {
|
|
Some(Cow::Borrowed(keyword.attr.as_str()))
|
|
}
|
|
GotoTarget::PatternMatchStarName(star) => {
|
|
Some(Cow::Borrowed(star.name.as_ref()?.as_str()))
|
|
}
|
|
GotoTarget::PatternMatchAsName(as_name) => {
|
|
Some(Cow::Borrowed(as_name.name.as_ref()?.as_str()))
|
|
}
|
|
GotoTarget::TypeParamTypeVarName(type_var) => {
|
|
Some(Cow::Borrowed(type_var.name.as_str()))
|
|
}
|
|
GotoTarget::TypeParamParamSpecName(spec) => Some(Cow::Borrowed(spec.name.as_str())),
|
|
GotoTarget::TypeParamTypeVarTupleName(tuple) => {
|
|
Some(Cow::Borrowed(tuple.name.as_str()))
|
|
}
|
|
GotoTarget::NonLocal { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
|
|
GotoTarget::Globals { identifier, .. } => Some(Cow::Borrowed(identifier.as_str())),
|
|
GotoTarget::BinOp { .. } | GotoTarget::UnaryOp { .. } => None,
|
|
}
|
|
}
|
|
|
|
/// Creates a `GotoTarget` from a `CoveringNode` and an offset within the node
|
|
pub(crate) fn from_covering_node<'a>(
|
|
covering_node: &crate::find_node::CoveringNode<'a>,
|
|
offset: TextSize,
|
|
tokens: &Tokens,
|
|
) -> Option<GotoTarget<'a>> {
|
|
tracing::trace!("Covering node is of kind {:?}", covering_node.node().kind());
|
|
|
|
match covering_node.node() {
|
|
AnyNodeRef::Identifier(identifier) => match covering_node.parent() {
|
|
Some(AnyNodeRef::StmtFunctionDef(function)) => {
|
|
Some(GotoTarget::FunctionDef(function))
|
|
}
|
|
Some(AnyNodeRef::StmtClassDef(class)) => Some(GotoTarget::ClassDef(class)),
|
|
Some(AnyNodeRef::Parameter(parameter)) => Some(GotoTarget::Parameter(parameter)),
|
|
Some(AnyNodeRef::Alias(alias)) => {
|
|
// Find the containing import statement to determine the type
|
|
let import_stmt = covering_node.ancestors().find(|node| {
|
|
matches!(
|
|
node,
|
|
AnyNodeRef::StmtImport(_) | AnyNodeRef::StmtImportFrom(_)
|
|
)
|
|
});
|
|
|
|
match import_stmt {
|
|
Some(AnyNodeRef::StmtImport(_)) => {
|
|
// Regular import statement like "import x.y as z"
|
|
|
|
// Is the offset within the alias name (asname) part?
|
|
if let Some(asname) = &alias.asname {
|
|
if asname.range.contains_inclusive(offset) {
|
|
return Some(GotoTarget::ImportModuleAlias { alias });
|
|
}
|
|
}
|
|
|
|
// Is the offset in the module name part?
|
|
if alias.name.range.contains_inclusive(offset) {
|
|
let full_name = alias.name.as_str();
|
|
|
|
if let Some((component_index, component_range)) =
|
|
find_module_component(
|
|
full_name,
|
|
alias.name.range.start(),
|
|
offset,
|
|
)
|
|
{
|
|
return Some(GotoTarget::ImportModuleComponent {
|
|
module_name: full_name.to_string(),
|
|
component_index,
|
|
component_range,
|
|
});
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
Some(AnyNodeRef::StmtImportFrom(import_from)) => {
|
|
// From import statement like "from x import y as z"
|
|
|
|
// Is the offset within the alias name (asname) part?
|
|
if let Some(asname) = &alias.asname {
|
|
if asname.range.contains_inclusive(offset) {
|
|
return Some(GotoTarget::ImportSymbolAlias {
|
|
alias,
|
|
range: asname.range,
|
|
import_from,
|
|
});
|
|
}
|
|
}
|
|
|
|
// Is the offset in the original name part?
|
|
if alias.name.range.contains_inclusive(offset) {
|
|
return Some(GotoTarget::ImportSymbolAlias {
|
|
alias,
|
|
range: alias.name.range,
|
|
import_from,
|
|
});
|
|
}
|
|
|
|
None
|
|
}
|
|
_ => None,
|
|
}
|
|
}
|
|
Some(AnyNodeRef::StmtImportFrom(from)) => {
|
|
// Handle offset within module name in from import statements
|
|
if let Some(module_expr) = &from.module {
|
|
let full_module_name = module_expr.to_string();
|
|
|
|
if let Some((component_index, component_range)) = find_module_component(
|
|
&full_module_name,
|
|
module_expr.range.start(),
|
|
offset,
|
|
) {
|
|
return Some(GotoTarget::ImportModuleComponent {
|
|
module_name: full_module_name,
|
|
component_index,
|
|
component_range,
|
|
});
|
|
}
|
|
}
|
|
|
|
None
|
|
}
|
|
Some(AnyNodeRef::ExceptHandlerExceptHandler(handler)) => {
|
|
Some(GotoTarget::ExceptVariable(handler))
|
|
}
|
|
Some(AnyNodeRef::Keyword(keyword)) => {
|
|
// Find the containing call expression from the ancestor chain
|
|
let call_expression = covering_node
|
|
.ancestors()
|
|
.find_map(ruff_python_ast::AnyNodeRef::expr_call)?;
|
|
Some(GotoTarget::KeywordArgument {
|
|
keyword,
|
|
call_expression,
|
|
})
|
|
}
|
|
Some(AnyNodeRef::PatternMatchMapping(mapping)) => {
|
|
Some(GotoTarget::PatternMatchRest(mapping))
|
|
}
|
|
Some(AnyNodeRef::PatternKeyword(keyword)) => {
|
|
Some(GotoTarget::PatternKeywordArgument(keyword))
|
|
}
|
|
Some(AnyNodeRef::PatternMatchStar(star)) => {
|
|
Some(GotoTarget::PatternMatchStarName(star))
|
|
}
|
|
Some(AnyNodeRef::PatternMatchAs(as_pattern)) => {
|
|
Some(GotoTarget::PatternMatchAsName(as_pattern))
|
|
}
|
|
Some(AnyNodeRef::TypeParamTypeVar(var)) => {
|
|
Some(GotoTarget::TypeParamTypeVarName(var))
|
|
}
|
|
Some(AnyNodeRef::TypeParamParamSpec(bound)) => {
|
|
Some(GotoTarget::TypeParamParamSpecName(bound))
|
|
}
|
|
Some(AnyNodeRef::TypeParamTypeVarTuple(var_tuple)) => {
|
|
Some(GotoTarget::TypeParamTypeVarTupleName(var_tuple))
|
|
}
|
|
Some(AnyNodeRef::ExprAttribute(attribute)) => {
|
|
// Check if this is seemingly a callable being invoked (the `y` in `x.y(...)`)
|
|
let grandparent_expr = covering_node.ancestors().nth(2);
|
|
let attribute_expr = attribute.into();
|
|
if let Some(AnyNodeRef::ExprCall(call)) = grandparent_expr {
|
|
if ruff_python_ast::ExprRef::from(&call.func) == attribute_expr {
|
|
return Some(GotoTarget::Call {
|
|
call,
|
|
callable: attribute_expr,
|
|
});
|
|
}
|
|
}
|
|
Some(GotoTarget::Expression(attribute_expr))
|
|
}
|
|
Some(AnyNodeRef::StmtNonlocal(_)) => Some(GotoTarget::NonLocal { identifier }),
|
|
Some(AnyNodeRef::StmtGlobal(_)) => Some(GotoTarget::Globals { identifier }),
|
|
None => None,
|
|
Some(parent) => {
|
|
tracing::debug!(
|
|
"Missing `GoToTarget` for identifier with parent {:?}",
|
|
parent.kind()
|
|
);
|
|
None
|
|
}
|
|
},
|
|
|
|
AnyNodeRef::ExprBinOp(binary) => {
|
|
if offset >= binary.left.end() && offset < binary.right.start() {
|
|
let between_operands =
|
|
tokens.in_range(TextRange::new(binary.left.end(), binary.right.start()));
|
|
if let Some(operator_token) = between_operands
|
|
.iter()
|
|
.find(|token| token.kind().as_binary_operator().is_some())
|
|
&& operator_token.range().contains_inclusive(offset)
|
|
{
|
|
return Some(GotoTarget::BinOp {
|
|
expression: binary,
|
|
operator_range: operator_token.range(),
|
|
});
|
|
}
|
|
}
|
|
|
|
Some(GotoTarget::Expression(binary.into()))
|
|
}
|
|
|
|
AnyNodeRef::ExprUnaryOp(unary) => {
|
|
if offset >= unary.start() && offset < unary.operand.start() {
|
|
let before_operand =
|
|
tokens.in_range(TextRange::new(unary.start(), unary.operand.start()));
|
|
|
|
if let Some(operator_token) = before_operand
|
|
.iter()
|
|
.find(|token| token.kind().as_unary_operator().is_some())
|
|
&& operator_token.range().contains_inclusive(offset)
|
|
{
|
|
return Some(GotoTarget::UnaryOp {
|
|
expression: unary,
|
|
operator_range: operator_token.range(),
|
|
});
|
|
}
|
|
}
|
|
Some(GotoTarget::Expression(unary.into()))
|
|
}
|
|
|
|
node => {
|
|
// Check if this is seemingly a callable being invoked (the `x` in `x(...)`)
|
|
let parent = covering_node.parent();
|
|
if let (Some(AnyNodeRef::ExprCall(call)), AnyNodeRef::ExprName(name)) =
|
|
(parent, node)
|
|
{
|
|
return Some(GotoTarget::Call {
|
|
call,
|
|
callable: name.into(),
|
|
});
|
|
}
|
|
node.as_expr_ref().map(GotoTarget::Expression)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Ranged for GotoTarget<'_> {
|
|
fn range(&self) -> TextRange {
|
|
match self {
|
|
GotoTarget::Call {
|
|
callable: expression,
|
|
..
|
|
}
|
|
| GotoTarget::Expression(expression) => match expression {
|
|
ast::ExprRef::Attribute(attribute) => attribute.attr.range,
|
|
_ => expression.range(),
|
|
},
|
|
GotoTarget::FunctionDef(function) => function.name.range,
|
|
GotoTarget::ClassDef(class) => class.name.range,
|
|
GotoTarget::Parameter(parameter) => parameter.name.range,
|
|
GotoTarget::ImportSymbolAlias { range, .. } => *range,
|
|
GotoTarget::ImportModuleComponent {
|
|
component_range, ..
|
|
} => *component_range,
|
|
GotoTarget::ImportModuleAlias { alias } => alias.asname.as_ref().unwrap().range,
|
|
GotoTarget::ExceptVariable(except) => except.name.as_ref().unwrap().range,
|
|
GotoTarget::KeywordArgument { keyword, .. } => keyword.arg.as_ref().unwrap().range,
|
|
GotoTarget::PatternMatchRest(rest) => rest.rest.as_ref().unwrap().range,
|
|
GotoTarget::PatternKeywordArgument(keyword) => keyword.attr.range,
|
|
GotoTarget::PatternMatchStarName(star) => star.name.as_ref().unwrap().range,
|
|
GotoTarget::PatternMatchAsName(as_name) => as_name.name.as_ref().unwrap().range,
|
|
GotoTarget::TypeParamTypeVarName(type_var) => type_var.name.range,
|
|
GotoTarget::TypeParamParamSpecName(spec) => spec.name.range,
|
|
GotoTarget::TypeParamTypeVarTupleName(tuple) => tuple.name.range,
|
|
GotoTarget::NonLocal { identifier, .. } => identifier.range,
|
|
GotoTarget::Globals { identifier, .. } => identifier.range,
|
|
GotoTarget::BinOp { operator_range, .. }
|
|
| GotoTarget::UnaryOp { operator_range, .. } => *operator_range,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Converts a collection of `ResolvedDefinition` items into `NavigationTarget` items.
|
|
fn convert_resolved_definitions_to_targets(
|
|
db: &dyn crate::Db,
|
|
definitions: Vec<ty_python_semantic::ResolvedDefinition<'_>>,
|
|
) -> Vec<crate::NavigationTarget> {
|
|
definitions
|
|
.into_iter()
|
|
.map(|resolved_definition| match resolved_definition {
|
|
ty_python_semantic::ResolvedDefinition::Definition(definition) => {
|
|
// Get the parsed module for range calculation
|
|
let definition_file = definition.file(db);
|
|
let module = ruff_db::parsed::parsed_module(db, definition_file).load(db);
|
|
|
|
// Get the ranges for this definition
|
|
let focus_range = definition.focus_range(db, &module);
|
|
let full_range = definition.full_range(db, &module);
|
|
|
|
crate::NavigationTarget {
|
|
file: focus_range.file(),
|
|
focus_range: focus_range.range(),
|
|
full_range: full_range.range(),
|
|
}
|
|
}
|
|
ty_python_semantic::ResolvedDefinition::Module(file) => {
|
|
// For modules, navigate to the start of the file
|
|
crate::NavigationTarget::new(file, TextRange::default())
|
|
}
|
|
ty_python_semantic::ResolvedDefinition::FileWithRange(file_range) => {
|
|
// For file ranges, navigate to the specific range within the file
|
|
crate::NavigationTarget::from(file_range)
|
|
}
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Shared helper to get definitions for an expr (that is presumably a name/attr)
|
|
fn definitions_for_expression<'db>(
|
|
db: &'db dyn crate::Db,
|
|
file: ruff_db::files::File,
|
|
expression: &ruff_python_ast::ExprRef<'_>,
|
|
) -> Option<Vec<ResolvedDefinition<'db>>> {
|
|
match expression {
|
|
ast::ExprRef::Name(name) => Some(definitions_for_name(db, file, name)),
|
|
ast::ExprRef::Attribute(attribute) => Some(ty_python_semantic::definitions_for_attribute(
|
|
db, file, attribute,
|
|
)),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
fn definitions_for_callable<'db>(
|
|
db: &'db dyn crate::Db,
|
|
file: ruff_db::files::File,
|
|
call: &ast::ExprCall,
|
|
) -> Vec<ResolvedDefinition<'db>> {
|
|
let model = SemanticModel::new(db, file);
|
|
// Attempt to refine to a specific call
|
|
let signature_info = call_signature_details(db, &model, call);
|
|
signature_info
|
|
.into_iter()
|
|
.filter_map(|signature| signature.definition.map(ResolvedDefinition::Definition))
|
|
.collect()
|
|
}
|
|
|
|
/// Shared helper to map and convert resolved definitions into navigation targets.
|
|
fn definitions_to_navigation_targets<'db>(
|
|
db: &dyn crate::Db,
|
|
stub_mapper: Option<&StubMapper<'db>>,
|
|
mut definitions: Vec<ty_python_semantic::ResolvedDefinition<'db>>,
|
|
) -> Option<crate::NavigationTargets> {
|
|
if let Some(mapper) = stub_mapper {
|
|
definitions = mapper.map_definitions(definitions);
|
|
}
|
|
if definitions.is_empty() {
|
|
None
|
|
} else {
|
|
let targets = convert_resolved_definitions_to_targets(db, definitions);
|
|
Some(crate::NavigationTargets::unique(targets))
|
|
}
|
|
}
|
|
|
|
pub(crate) fn find_goto_target(
|
|
parsed: &ParsedModuleRef,
|
|
offset: TextSize,
|
|
) -> Option<GotoTarget<'_>> {
|
|
let token = parsed
|
|
.tokens()
|
|
.at_offset(offset)
|
|
.max_by_key(|token| match token.kind() {
|
|
TokenKind::Name
|
|
| TokenKind::String
|
|
| TokenKind::Complex
|
|
| TokenKind::Float
|
|
| TokenKind::Int => 1,
|
|
|
|
TokenKind::Comment => -1,
|
|
|
|
// if we have a<CURSOR>+b`, prefer the `+` token (by respecting the token ordering)
|
|
// This matches VS Code's behavior where it sends the start of the clicked token as offset.
|
|
kind if kind.as_binary_operator().is_some() || kind.as_unary_operator().is_some() => 1,
|
|
_ => 0,
|
|
})?;
|
|
|
|
if token.kind().is_comment() {
|
|
return None;
|
|
}
|
|
|
|
let covering_node = covering_node(parsed.syntax().into(), token.range())
|
|
.find_first(|node| node.is_identifier() || node.is_expression())
|
|
.ok()?;
|
|
|
|
GotoTarget::from_covering_node(&covering_node, offset, parsed.tokens())
|
|
}
|
|
|
|
/// Helper function to resolve a module name and create a navigation target.
|
|
fn definitions_for_module<'db>(
|
|
db: &'db dyn crate::Db,
|
|
module_name_str: &str,
|
|
) -> Option<DefinitionsOrTargets<'db>> {
|
|
use ty_python_semantic::{ModuleName, resolve_module};
|
|
|
|
if let Some(module_name) = ModuleName::new(module_name_str) {
|
|
if let Some(resolved_module) = resolve_module(db, &module_name) {
|
|
if let Some(module_file) = resolved_module.file(db) {
|
|
return Some(DefinitionsOrTargets::Definitions(vec![
|
|
ResolvedDefinition::Module(module_file),
|
|
]));
|
|
}
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Helper function to extract module component information from a dotted module name
|
|
fn find_module_component(
|
|
full_module_name: &str,
|
|
module_start: TextSize,
|
|
offset: TextSize,
|
|
) -> Option<(usize, TextRange)> {
|
|
let pos_in_module = offset - module_start;
|
|
let pos_in_module = pos_in_module.to_usize();
|
|
|
|
// Split the module name into components and find which one contains the offset
|
|
let mut current_pos = 0;
|
|
let components: Vec<&str> = full_module_name.split('.').collect();
|
|
|
|
for (i, component) in components.iter().enumerate() {
|
|
let component_start = current_pos;
|
|
let component_end = current_pos + component.len();
|
|
|
|
// Check if the offset is within this component or at its right boundary
|
|
if pos_in_module >= component_start && pos_in_module <= component_end {
|
|
let component_range = TextRange::new(
|
|
module_start + TextSize::from(u32::try_from(component_start).ok()?),
|
|
module_start + TextSize::from(u32::try_from(component_end).ok()?),
|
|
);
|
|
return Some((i, component_range));
|
|
}
|
|
|
|
// Move past this component and the dot
|
|
current_pos = component_end + 1; // +1 for the dot
|
|
}
|
|
|
|
None
|
|
}
|