use std::sync::Arc;
use crate::base::FileId;
use crate::hir::{HirSymbol, ResolveResult, SymbolIndex, TypeRef, TypeRefKind};
#[derive(Clone, Debug)]
pub struct TypeInfo {
pub target_name: Arc<str>,
pub type_ref: TypeRef,
pub resolved_symbol: Option<HirSymbol>,
pub container: Option<Arc<str>>,
}
impl TypeInfo {
pub fn resolved_name(&self) -> &str {
self.type_ref
.resolved_target
.as_ref()
.map(|s| s.as_ref())
.unwrap_or(self.target_name.as_ref())
}
}
pub struct TypeRefContext<'a> {
pub target_name: Arc<str>,
pub type_ref: &'a TypeRef,
pub containing_symbol: Option<&'a HirSymbol>,
pub chain_prefix: Vec<&'a TypeRef>,
}
pub fn type_info_at(index: &SymbolIndex, file: FileId, line: u32, col: u32) -> Option<TypeInfo> {
let ctx = find_type_ref_at_position(index, file, line, col)?;
let resolved_symbol = resolve_type_ref_with_chain(index, &ctx);
Some(TypeInfo {
target_name: ctx.target_name,
type_ref: ctx.type_ref.clone(),
resolved_symbol,
container: ctx.containing_symbol.map(|s| s.qualified_name.clone()),
})
}
pub fn resolve_type_ref_with_chain(
index: &SymbolIndex,
ctx: &TypeRefContext<'_>,
) -> Option<HirSymbol> {
if let Some(resolved) = &ctx.type_ref.resolved_target {
return index.lookup_qualified(resolved).cloned();
}
if !ctx.chain_prefix.is_empty() {
return resolve_chain_member(index, ctx);
}
let containing_qn = ctx
.containing_symbol
.map(|s| s.qualified_name.as_ref())
.unwrap_or("");
let scope = containing_qn
.rsplit_once("::")
.map(|(s, _)| s)
.unwrap_or(containing_qn);
let direct_qn = format!("{}::{}", scope, ctx.target_name);
if let Some(sym) = index.lookup_qualified(&direct_qn).cloned() {
return Some(sym);
}
let resolver = index.resolver_for_scope(scope);
match resolver.resolve(&ctx.target_name) {
ResolveResult::Found(sym) => Some(sym),
ResolveResult::Ambiguous(syms) => syms.into_iter().next(),
ResolveResult::NotFound => None,
}
}
fn resolve_chain_member(index: &SymbolIndex, ctx: &TypeRefContext<'_>) -> Option<HirSymbol> {
let containing_qn = ctx
.containing_symbol
.map(|s| s.qualified_name.as_ref())
.unwrap_or("");
let base_scope = containing_qn
.rsplit_once("::")
.map(|(s, _)| s)
.unwrap_or(containing_qn);
let first_part = ctx.chain_prefix.first()?;
let resolver = index.resolver_for_scope(base_scope);
let mut current_symbol = match resolver.resolve(&first_part.target) {
ResolveResult::Found(sym) => sym,
ResolveResult::Ambiguous(syms) => syms.into_iter().next()?,
ResolveResult::NotFound => return None,
};
for part in ctx.chain_prefix.iter().skip(1) {
current_symbol = resolve_member_of_symbol(index, ¤t_symbol, &part.target)?;
}
resolve_member_of_symbol(index, ¤t_symbol, &ctx.target_name)
}
fn resolve_member_of_symbol(
index: &SymbolIndex,
symbol: &HirSymbol,
member_name: &str,
) -> Option<HirSymbol> {
let direct_child = format!("{}::{}", symbol.qualified_name, member_name);
if let Some(sym) = index.lookup_qualified(&direct_child).cloned() {
return Some(sym);
}
resolve_member_in_type(index, symbol, member_name)
}
fn resolve_member_in_type(
index: &SymbolIndex,
symbol: &HirSymbol,
member_name: &str,
) -> Option<HirSymbol> {
use crate::hir::RelationshipKind;
let type_name = symbol
.supertypes
.first()
.map(|s| s.as_ref())
.or_else(|| {
symbol
.type_refs
.iter()
.filter_map(|tr| tr.as_refs().into_iter().next())
.find(|tr| matches!(tr.kind, crate::hir::RefKind::TypedBy))
.and_then(|tr| {
tr.resolved_target
.as_ref()
.map(|s| s.as_ref())
.or(Some(tr.target.as_ref()))
})
})
.or_else(|| {
symbol
.relationships
.iter()
.find(|r| {
matches!(
r.kind,
RelationshipKind::Performs
| RelationshipKind::Exhibits
| RelationshipKind::Includes
| RelationshipKind::Satisfies
| RelationshipKind::Asserts
| RelationshipKind::Verifies
)
})
.map(|r| r.target.as_ref())
})?;
let type_symbol = index
.lookup_qualified(type_name)
.or_else(|| index.lookup_definition(type_name))
.cloned()
.or_else(|| {
let scope = symbol
.qualified_name
.rsplit_once("::")
.map(|(s, _)| s)
.unwrap_or("");
let resolver = index.resolver_for_scope(scope);
match resolver.resolve(type_name) {
ResolveResult::Found(sym) => Some(sym),
ResolveResult::Ambiguous(syms) => syms.into_iter().next(),
ResolveResult::NotFound => None,
}
})?;
let member_qualified = format!("{}::{}", type_symbol.qualified_name, member_name);
index
.lookup_qualified(&member_qualified)
.cloned()
.or_else(|| {
let resolver = index.resolver_for_scope(&type_symbol.qualified_name);
match resolver.resolve(member_name) {
ResolveResult::Found(sym) => Some(sym),
ResolveResult::Ambiguous(syms) => syms.into_iter().next(),
ResolveResult::NotFound => None,
}
})
}
pub fn resolve_type_ref(
index: &SymbolIndex,
type_ref: &TypeRef,
target_name: &str,
containing_symbol: Option<&HirSymbol>,
) -> Option<HirSymbol> {
if let Some(resolved) = &type_ref.resolved_target {
return index.lookup_qualified(resolved).cloned();
}
let scope = containing_symbol
.map(|s| s.qualified_name.as_ref())
.unwrap_or("");
let resolver = index.resolver_for_scope(scope);
match resolver.resolve(target_name) {
ResolveResult::Found(sym) => Some(sym),
ResolveResult::Ambiguous(syms) => syms.into_iter().next(),
ResolveResult::NotFound => {
index.lookup_qualified(target_name).cloned()
}
}
}
pub fn find_type_ref_at_position(
index: &SymbolIndex,
file: FileId,
line: u32,
col: u32,
) -> Option<TypeRefContext<'_>> {
let symbols = index.symbols_in_file(file);
let mut best_match: Option<(&HirSymbol, usize, &TypeRef, Vec<&TypeRef>)> = None;
for symbol in symbols {
for type_ref_kind in symbol.type_refs.iter() {
if type_ref_kind.contains(line, col) {
if let Some((part_idx, tr)) = type_ref_kind.part_at(line, col) {
let chain_prefix: Vec<&TypeRef> = match type_ref_kind {
TypeRefKind::Simple(_) => Vec::new(),
TypeRefKind::Chain(chain) => chain.parts.iter().take(part_idx).collect(),
};
let is_better = match &best_match {
None => true,
Some((best_sym, _, _, _)) => {
symbol.qualified_name.len() > best_sym.qualified_name.len()
}
};
if is_better {
best_match = Some((symbol, part_idx, tr, chain_prefix));
}
}
}
}
}
best_match.map(|(symbol, _part_idx, tr, chain_prefix)| TypeRefContext {
target_name: tr.target.clone(),
type_ref: tr,
containing_symbol: Some(symbol),
chain_prefix,
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::hir::{RefKind, SymbolKind, new_element_id};
fn make_symbol_with_type_ref(
name: &str,
qualified: &str,
kind: SymbolKind,
type_ref_target: &str,
line: u32,
) -> HirSymbol {
HirSymbol {
name: Arc::from(name),
short_name: None,
qualified_name: Arc::from(qualified),
element_id: new_element_id(),
kind,
file: FileId::new(0),
start_line: line,
start_col: 0,
end_line: line + 1,
end_col: 0,
short_name_start_line: None,
short_name_start_col: None,
short_name_end_line: None,
short_name_end_col: None,
doc: None,
supertypes: vec![Arc::from(type_ref_target)],
relationships: Vec::new(),
type_refs: vec![crate::hir::TypeRefKind::Simple(TypeRef::new(
type_ref_target,
RefKind::TypedBy,
line,
10,
line,
20,
))],
is_public: false,
view_data: None,
metadata_annotations: Vec::new(),
is_abstract: false,
is_variation: false,
is_readonly: false,
is_derived: false,
is_parallel: false,
is_individual: false,
is_end: false,
is_default: false,
is_ordered: false,
is_nonunique: false,
is_portion: false,
direction: None,
multiplicity: None,
}
}
#[test]
fn test_type_info_at_type_ref() {
let mut index = SymbolIndex::new();
let def = HirSymbol {
name: Arc::from("Engine"),
short_name: None,
qualified_name: Arc::from("Engine"),
element_id: new_element_id(),
kind: SymbolKind::PartDefinition,
file: FileId::new(0),
start_line: 0,
start_col: 0,
end_line: 5,
end_col: 0,
short_name_start_line: None,
short_name_start_col: None,
short_name_end_line: None,
short_name_end_col: None,
doc: None,
supertypes: Vec::new(),
relationships: Vec::new(),
type_refs: Vec::new(),
is_public: false,
view_data: None,
metadata_annotations: Vec::new(),
is_abstract: false,
is_variation: false,
is_readonly: false,
is_derived: false,
is_parallel: false,
is_individual: false,
is_end: false,
is_default: false,
is_ordered: false,
is_nonunique: false,
is_portion: false,
direction: None,
multiplicity: None,
};
let usage =
make_symbol_with_type_ref("engine", "Car::engine", SymbolKind::PartUsage, "Engine", 10);
index.add_file(FileId::new(0), vec![def, usage]);
let info = type_info_at(&index, FileId::new(0), 10, 15);
assert!(info.is_some());
let info = info.unwrap();
assert_eq!(info.target_name.as_ref(), "Engine");
assert!(info.resolved_symbol.is_some());
assert_eq!(
info.resolved_symbol.unwrap().qualified_name.as_ref(),
"Engine"
);
}
#[test]
fn test_type_info_not_on_type_ref() {
let mut index = SymbolIndex::new();
let symbol = HirSymbol {
name: Arc::from("Car"),
short_name: None,
qualified_name: Arc::from("Car"),
element_id: new_element_id(),
kind: SymbolKind::PartDefinition,
file: FileId::new(0),
start_line: 0,
start_col: 0,
end_line: 10,
end_col: 0,
short_name_start_line: None,
short_name_start_col: None,
short_name_end_line: None,
short_name_end_col: None,
doc: None,
supertypes: Vec::new(),
relationships: Vec::new(),
type_refs: Vec::new(),
is_public: false,
view_data: None,
metadata_annotations: Vec::new(),
is_abstract: false,
is_variation: false,
is_readonly: false,
is_derived: false,
is_parallel: false,
is_individual: false,
is_end: false,
is_default: false,
is_ordered: false,
is_nonunique: false,
is_portion: false,
direction: None,
multiplicity: None,
};
index.add_file(FileId::new(0), vec![symbol]);
let info = type_info_at(&index, FileId::new(0), 5, 5);
assert!(info.is_none());
}
}