use std::collections::{HashMap, HashSet};
use std::iter::once;
use ra_ap_syntax::ast::{HasAttrs, HasName};
use ra_ap_syntax::{ast, AstNode, Edition, SyntaxKind, SyntaxNode, SyntaxToken};
use crate::traits::IsSyntax;
pub fn closest_ancestor_ast_type<I, T>(item: &I) -> Option<T>
where
I: IsSyntax,
T: AstNode,
{
let parent = item.parent()?;
if T::can_cast(parent.kind()) {
T::cast(parent)
} else {
closest_ancestor_ast_type(&parent)
}
}
pub fn parent_ast_item<T>(node: &T) -> Option<ast::Item>
where
T: IsSyntax,
{
closest_ancestor_ast_type::<T, ast::Item>(node).and_then(|item| {
if node.kind() == SyntaxKind::ATTR {
item.attrs()
.any(|attr| {
attr.syntax().kind() == node.kind()
&& attr.syntax().text_range() == node.text_range()
})
.then_some(item)
} else {
Some(item)
}
})
}
pub fn closest_non_trivia_token<F>(token: &SyntaxToken, step_expr: F) -> Option<SyntaxToken>
where
F: Fn(&SyntaxToken) -> Option<SyntaxToken>,
{
closest_item_which(
token,
step_expr,
|subject| !subject.kind().is_trivia(),
|subject| !subject.kind().is_trivia(),
)
}
pub fn closest_item_which<T, S, G, H>(
token: &T,
step_expr: S,
goal_expr: G,
halt_expr: H,
) -> Option<T>
where
S: Fn(&T) -> Option<T>,
G: Fn(&T) -> bool,
H: Fn(&T) -> bool,
{
(step_expr)(token).and_then(|subject| {
if goal_expr(&subject) {
Some(subject)
} else if halt_expr(&subject) {
None
} else {
closest_item_which(&subject, step_expr, goal_expr, halt_expr)
}
})
}
#[macro_export]
macro_rules! resolve_item_path_from_use_scope_and_aliases {
($name: ident, $root_node: expr) => {{
let (use_paths, item_aliases) = $crate::simple_use_paths_and_aliases_in_scope($root_node);
std::iter::once(item_aliases.get(&$name).cloned())
.flatten()
.chain(use_paths.into_iter().filter_map(|use_path| {
let path_str = use_path.replace(' ', "");
if path_str.ends_with(&format!("::{}", $name)) {
Some(path_str)
} else if let Some(prefix) = path_str.strip_suffix("::*") {
Some(format!("{prefix}::{}", $name))
} else {
None
}
}))
.filter_map(|path_str| $crate::path_from_str(&path_str))
.filter(|path| path.qualifier().is_some())
}};
}
pub fn resolve_item<T>(path: &ast::Path, ref_node: &SyntaxNode) -> Option<T>
where
T: AstNode + HasName,
{
let target = path.segment()?;
let target_name = target.name_ref()?;
match path.qualifier() {
Some(qualifier) => resolve_qualifier(&qualifier, ref_node),
None => resolve_current_module(ref_node),
}
.as_ref()
.map(resolve_item_list_root)
.and_then(|root_node| {
let resolve_as_module_child = || {
root_node
.children()
.filter(|node| T::can_cast(node.kind()))
.find_map(|node| {
T::cast(node).filter(|item| {
item.name()
.is_some_and(|item_name| item_name.text() == target_name.text())
})
})
};
let resolve_from_module_use_scope = || {
let item_name = target_name.to_string();
resolve_item_path_from_use_scope_and_aliases!(item_name, &root_node).find_map(
|resolved_path| {
if path_to_string(&resolved_path) != path_to_string(path) {
resolve_item(&resolved_path, &root_node)
} else {
None
}
},
)
};
let resolve_from_ref_item_use_scope = || {
let is_module_child = ref_node.parent().is_some_and(|parent| parent == root_node);
if is_module_child {
let item_name = target_name.to_string();
resolve_item_path_from_use_scope_and_aliases!(item_name, ref_node).find_map(
|resolved_path| {
if path_to_string(&resolved_path) != path_to_string(path) {
resolve_item(&resolved_path, ref_node)
} else {
None
}
},
)
} else {
None
}
};
resolve_as_module_child()
.or_else(resolve_from_module_use_scope)
.or_else(resolve_from_ref_item_use_scope)
})
}
pub fn resolve_current_module(node: &SyntaxNode) -> Option<SyntaxNode> {
ast::Module::can_cast(node.kind())
.then(|| node.clone())
.or_else(|| {
node.ancestors()
.find(|it| ast::Module::can_cast(it.kind()))
.or_else(|| node.ancestors().last())
})
}
pub fn resolve_qualifier(path: &ast::Path, ref_node: &SyntaxNode) -> Option<SyntaxNode> {
let resolve_next_child_module = |name: &ast::NameRef, root: &SyntaxNode| {
let resolve_child = || {
root.children().find(|it| {
ast::Module::can_cast(it.kind())
&& ast::Module::cast(it.clone())
.and_then(|module| module.name())
.is_some_and(|module_name| module_name.text() == name.text())
})
};
let resolve_from_use_scope = || {
let item_name = name.to_string();
resolve_item_path_from_use_scope_and_aliases!(item_name, root).find_map(
|resolved_path| {
if path_to_string(&resolved_path) != path_to_string(path) {
resolve_qualifier(&resolved_path, root)
} else {
None
}
},
)
};
resolve_child().or_else(resolve_from_use_scope)
};
let mut path_segments = path.segments();
let mut resolution_root_option = path_segments.next().and_then(|root_segment| {
if root_segment.coloncolon_token().is_some() || root_segment.crate_token().is_some() {
ref_node.ancestors().last().and_then(|crate_root| {
match root_segment.coloncolon_token() {
Some(_) => root_segment
.name_ref()
.and_then(|name| resolve_next_child_module(&name, &crate_root)),
None => Some(crate_root),
}
})
} else if root_segment.self_token().is_some() {
resolve_current_module(ref_node)
} else if root_segment.super_token().is_some() {
resolve_current_module(ref_node)
.as_ref()
.and_then(SyntaxNode::parent)
.as_ref()
.and_then(resolve_current_module)
} else {
resolve_current_module(ref_node)
.zip(root_segment.name_ref())
.and_then(|(current_module, name)| {
resolve_next_child_module(&name, ¤t_module)
})
}
});
while let Some((node, segment)) = resolution_root_option.as_ref().zip(path_segments.next()) {
resolution_root_option = segment
.name_ref()
.and_then(|name| resolve_next_child_module(&name, node));
}
resolution_root_option
}
pub fn simple_use_paths_and_aliases_in_scope(
ref_node: &SyntaxNode,
) -> (HashSet<String>, HashMap<String, String>) {
let mut use_paths = HashSet::new();
let mut item_aliases = HashMap::new();
let use_results = resolve_item_list_root(ref_node)
.children()
.filter_map(|node| {
if ast::TypeAlias::can_cast(node.kind()) {
ast::TypeAlias::cast(node).and_then(|type_alias| {
type_alias
.name()
.zip(type_alias.ty().as_ref().and_then(path_from_type))
.map(|(name, path)| vec![(path_to_string(&path), Some(name.to_string()))])
})
} else {
ast::Use::cast(node)
.as_ref()
.and_then(ast::Use::use_tree)
.as_ref()
.map(flatten_use_tree)
}
})
.flatten();
for (path, alias_option) in use_results {
let path = remove_whitespace(&path);
match alias_option {
None => {
use_paths.insert(path);
}
Some(alias) => {
item_aliases.insert(alias, path);
}
}
}
(use_paths, item_aliases)
}
pub fn path_from_str(path_str: &str) -> Option<ast::Path> {
ra_ap_syntax::hacks::parse_expr_from_str(path_str, Edition::Edition2021).and_then(|expr| {
match expr {
ast::Expr::PathExpr(path_expr) => path_expr.path(),
_ => None,
}
})
}
pub fn path_from_type(ty: &ast::Type) -> Option<ast::Path> {
match ty {
ast::Type::PathType(path_type) => path_type.path(),
_ => None,
}
}
pub fn path_to_string(path: &ast::Path) -> String {
remove_whitespace(&path.to_string())
}
fn resolve_item_list_root(node: &SyntaxNode) -> SyntaxNode {
ast::Item::cast(node.clone())
.and_then(|item| match item {
ast::Item::Module(it) => it.item_list().as_ref().map(AstNode::syntax).cloned(),
ast::Item::Fn(it) => it
.body()
.as_ref()
.and_then(ast::BlockExpr::stmt_list)
.as_ref()
.map(AstNode::syntax)
.cloned(),
_ => None,
})
.unwrap_or(node.clone())
}
fn flatten_use_tree(use_tree: &ast::UseTree) -> Vec<(String, Option<String>)> {
let alias = use_tree.rename().and_then(|rename| {
rename
.name()
.as_ref()
.map(ToString::to_string)
.or_else(|| rename.underscore_token().as_ref().map(ToString::to_string))
});
fn add_prefix(
tree: &ast::UseTree,
sub_paths: impl Iterator<Item = (String, Option<String>)>,
) -> Vec<(String, Option<String>)> {
match tree.path() {
None => sub_paths
.filter(|(sub_path, _)| sub_path != "self")
.collect(),
Some(path_prefix) => sub_paths
.map(|(sub_path, alias)| {
(
if sub_path == "self" {
path_prefix.to_string()
} else {
format!("{path_prefix}::{sub_path}")
},
alias,
)
})
.collect(),
}
}
if let Some(use_tree_list) = use_tree.use_tree_list() {
add_prefix(
use_tree,
use_tree_list
.use_trees()
.flat_map(|subtree| flatten_use_tree(&subtree)),
)
} else if use_tree.star_token().is_some() {
add_prefix(use_tree, once((String::from("*"), alias)))
} else if let Some(path) = use_tree.path() {
vec![(path_to_string(&path), alias)]
} else {
Vec::new()
}
}
fn remove_whitespace(text: &str) -> String {
text.replace(' ', "")
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_utils::*;
use crate::{InkEntity, InkFile};
use quote::quote;
use test_utils::quote_as_str;
#[test]
fn parent_ast_item_works() {
let code = quote! {
#[ink::contract]
mod my_contract {
#[ink(event)]
pub struct MyEvent {
#[ink(topic)]
field_1: i32,
field_2: bool,
}
}
};
let module = parse_first_ast_node_of_type::<ast::Module>(quote_as_str! { #code });
let struct_item = parse_first_ast_node_of_type::<ast::Struct>(quote_as_str! { #code });
let field = parse_first_ast_node_of_type::<ast::RecordField>(quote_as_str! { #code });
assert_eq!(
parent_ast_item(field.syntax())
.unwrap()
.syntax()
.text_range(),
struct_item.syntax().text_range()
);
assert_eq!(
parent_ast_item(struct_item.syntax())
.unwrap()
.syntax()
.text_range(),
module.syntax().text_range()
);
}
#[test]
fn resolve_item_works() {
let item = quote! { struct MyItem; };
let ref_name = quote! { ref_item };
for (code, path_str) in [
(quote_as_str! { #item }, quote_as_str! { MyItem }),
(quote_as_str! { #item }, quote_as_str! { self::MyItem }),
(quote_as_str! { #item }, quote_as_str! { crate::MyItem }),
(quote_as_str! { #item }, quote_as_str! { ::MyItem }),
(
quote_as_str! {
mod my_items {
#item
}
},
quote_as_str! { my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
},
quote_as_str! { crate::my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
},
quote_as_str! { ::my_items::MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
}
},
quote_as_str! { super::MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
}
},
quote_as_str! { crate::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
}
},
quote_as_str! { super::my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
}
},
quote_as_str! { crate::my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
}
},
quote_as_str! { ::my_items::MyItem },
),
(
quote_as_str! {
#item
fn #ref_name {
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
fn #ref_name {
}
},
quote_as_str! { self::MyItem },
),
(
quote_as_str! {
#item
fn #ref_name {
}
},
quote_as_str! { crate::MyItem },
),
(
quote_as_str! {
#item
fn #ref_name {
}
},
quote_as_str! { ::MyItem },
),
(
quote_as_str! {
#item
mod my_mod {
fn #ref_name {
}
}
},
quote_as_str! { crate::MyItem },
),
(
quote_as_str! {
#item
mod my_mod {
fn #ref_name {
}
}
},
quote_as_str! { super::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
fn #ref_name {
}
},
quote_as_str! { my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod my_mod {
fn #ref_name {
}
}
},
quote_as_str! { crate::my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod my_mod {
fn #ref_name {
}
}
},
quote_as_str! { super::my_items::MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use crate::MyItem;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use super::MyItem;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use crate::{MyItem};
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use crate::*;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use super::*;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::my_items::MyItem;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use super::my_items::MyItem;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use ::my_items::MyItem;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::my_items::*;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use super::my_items::*;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::my_items;
}
},
quote_as_str! { my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::my_items::{self};
}
},
quote_as_str! { my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use super::my_items;
}
},
quote_as_str! { my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::*;
}
},
quote_as_str! { my_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use super::*;
}
},
quote_as_str! { my_items::MyItem },
),
(
quote_as_str! {
#item
fn #ref_name {
use crate::MyItem;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
mod my_mod {
use crate::MyItem;
fn #ref_name {
}
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
mod my_mod {
fn #ref_name {
use crate::MyItem;
}
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
use my_items::MyItem;
fn #ref_name {
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
fn #ref_name {
use my_items::MyItem;
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod my_mod {
use super::my_items::MyItem;
fn #ref_name {
}
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod my_mod {
fn #ref_name {
use super::my_items::MyItem;
}
}
},
quote_as_str! { MyItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use crate::MyItem as CustomItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use super::MyItem as CustomItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
mod #ref_name {
use crate::{MyItem as CustomItem};
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::my_items::MyItem as CustomItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use super::my_items::MyItem as CustomItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::my_items as custom_items;
}
},
quote_as_str! { custom_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use crate::my_items::{self as custom_items};
}
},
quote_as_str! { custom_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
use super::my_items as custom_items;
}
},
quote_as_str! { custom_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
use self::my_items as custom_items;
mod #ref_name {
}
},
quote_as_str! { super::custom_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
use crate::my_items as custom_items;
mod #ref_name {
}
},
quote_as_str! { super::custom_items::MyItem },
),
(
quote_as_str! {
mod my_items {
#item
}
use self::my_items as custom_items;
mod #ref_name {
use super::custom_items::MyItem as CustomItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
fn #ref_name {
use crate::MyItem as CustomItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
mod my_mod {
fn #ref_name {
use crate::MyItem as CustomItem;
}
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
fn #ref_name {
use my_items::MyItem as CustomItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod my_mod {
fn #ref_name {
use super::my_items::MyItem as CustomItem;
}
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
type CustomItem = self::MyItem;
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
type CustomItem = self::MyItem;
},
quote_as_str! { self::CustomItem },
),
(
quote_as_str! {
#item
type CustomItem = self::MyItem;
},
quote_as_str! { crate::CustomItem },
),
(
quote_as_str! {
#item
mod #ref_name {
type CustomItem = crate::MyItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
mod #ref_name {
type CustomItem = super::MyItem;
}
},
quote_as_str! { self::CustomItem },
),
(
quote_as_str! {
#item
mod #ref_name {
type CustomItem = crate::MyItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
type CustomItem = crate::my_items::MyItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod #ref_name {
type CustomItem = super::my_items::MyItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
type CustomItem = my_items::MyItem;
mod #ref_name {
}
},
quote_as_str! { crate::CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
type CustomItem = my_items::MyItem;
mod #ref_name {
}
},
quote_as_str! { super::CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
type CustomItem = my_items::MyItem;
mod #ref_name {
type RenamedItem = super::CustomItem;
}
},
quote_as_str! { RenamedItem },
),
(
quote_as_str! {
#item
fn #ref_name {
type CustomItem = crate::MyItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
#item
mod my_mod {
fn #ref_name {
type CustomItem = crate::MyItem;
}
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
fn #ref_name {
type CustomItem = my_items::MyItem;
}
},
quote_as_str! { CustomItem },
),
(
quote_as_str! {
mod my_items {
#item
}
mod my_mod {
fn #ref_name {
type CustomItem = super::my_items::MyItem;
}
}
},
quote_as_str! { CustomItem },
),
] {
let file = InkFile::parse(code);
let path: ast::Path = parse_first_ast_node_of_type(path_str);
let ref_item_opt = parse_source(code).syntax().descendants().find_map(|node| {
if ast::Module::can_cast(node.kind()) {
ast::Module::cast(node.clone())
.filter(|item| {
item.name()
.is_some_and(|name| name.to_string() == ref_name.to_string())
})
.map(|mod_item| mod_item.syntax().clone())
} else {
ast::Fn::cast(node)
.filter(|item| {
item.name()
.is_some_and(|name| name.to_string() == ref_name.to_string())
})
.map(|fn_item| fn_item.syntax().clone())
}
});
assert!(
resolve_item::<ast::Adt>(&path, ref_item_opt.as_ref().unwrap_or(file.syntax()))
.is_some(),
"code: {code} | path: {path_str}"
);
}
}
}