use crate::{
expr::scope::LocalScopeId, expr::BodySourceMap, ids::DefWithBodyId, resolver_for_scope, Body,
ExprId, ExprScopes, FileId, HirDatabase, InFile, InferenceResult, Resolver, Ty,
};
use mun_syntax::{ast, AstNode, SyntaxNode, TextRange, TextSize};
use std::sync::Arc;
pub(crate) struct SourceAnalyzer {
pub(crate) file_id: FileId,
pub(crate) resolver: Resolver,
body: Option<Arc<Body>>,
body_source_map: Option<Arc<BodySourceMap>>,
infer: Option<Arc<InferenceResult>>,
scopes: Option<Arc<ExprScopes>>,
}
impl SourceAnalyzer {
pub(crate) fn new_for_body(
db: &dyn HirDatabase,
def: DefWithBodyId,
node: InFile<&SyntaxNode>,
offset: Option<TextSize>,
) -> Self {
let (body, source_map) = db.body_with_source_map(def);
let scopes = db.expr_scopes(def);
let scope = match offset {
None => scope_for(&scopes, &source_map, node),
Some(offset) => scope_for_offset(db, &scopes, &source_map, node.with_value(offset)),
};
let resolver = resolver_for_scope(db.upcast(), def, scope);
SourceAnalyzer {
resolver,
body: Some(body),
body_source_map: Some(source_map),
infer: Some(db.infer(def)),
scopes: Some(scopes),
file_id: node.file_id,
}
}
pub(crate) fn new_for_resolver(
resolver: Resolver,
node: InFile<&SyntaxNode>,
) -> SourceAnalyzer {
SourceAnalyzer {
resolver,
body: None,
body_source_map: None,
infer: None,
scopes: None,
file_id: node.file_id,
}
}
pub(crate) fn type_of_expr(&self, db: &dyn HirDatabase, expr: &ast::Expr) -> Option<Ty> {
let expr_id = self.expr_id(db, expr)?;
Some(self.infer.as_ref()?[expr_id].clone())
}
fn expr_id(&self, _db: &dyn HirDatabase, expr: &ast::Expr) -> Option<ExprId> {
let sm = self.body_source_map.as_ref()?;
sm.node_expr(expr)
}
}
fn scope_for(
scopes: &ExprScopes,
source_map: &BodySourceMap,
node: InFile<&SyntaxNode>,
) -> Option<LocalScopeId> {
node.value
.ancestors()
.filter_map(ast::Expr::cast)
.filter_map(|it| source_map.node_expr(&it))
.find_map(|it| scopes.scope_for(it))
}
fn scope_for_offset(
db: &dyn HirDatabase,
scopes: &ExprScopes,
source_map: &BodySourceMap,
offset: InFile<TextSize>,
) -> Option<LocalScopeId> {
let scopes_and_ranges = scopes.scope_by_expr().iter().filter_map(|(id, scope)| {
let source = source_map.expr_syntax(*id)?;
if source.file_id != offset.file_id {
return None;
}
let root = source.file_syntax(db.upcast());
let node = source
.value
.either(|ptr| ptr.syntax_node_ptr(), |ptr| ptr.syntax_node_ptr());
Some((node.to_node(&root).text_range(), scope))
});
let smallest_scope_containing_offset = scopes_and_ranges.min_by_key(|(expr_range, _scope)| {
(
!(expr_range.start() <= offset.value && offset.value <= expr_range.end()),
expr_range.len(),
)
});
smallest_scope_containing_offset.map(|(expr_range, scope)| {
adjust(db, scopes, source_map, expr_range, offset).unwrap_or(*scope)
})
}
fn adjust(
db: &dyn HirDatabase,
scopes: &ExprScopes,
source_map: &BodySourceMap,
expr_range: TextRange,
offset: InFile<TextSize>,
) -> Option<LocalScopeId> {
let child_scopes = scopes
.scope_by_expr()
.iter()
.filter_map(|(id, scope)| {
let source = source_map.expr_syntax(*id)?;
if source.file_id != offset.file_id {
return None;
}
let root = source.file_syntax(db.upcast());
let node = source
.value
.either(|ptr| ptr.syntax_node_ptr(), |ptr| ptr.syntax_node_ptr());
Some((node.to_node(&root).text_range(), scope))
})
.filter(|&(range, _)| {
range.start() <= offset.value
&& expr_range.contains_range(range)
&& range != expr_range
});
child_scopes
.into_iter()
.max_by(|&(r1, _), &(r2, _)| {
if r1.contains_range(r2) {
std::cmp::Ordering::Greater
} else if r2.contains_range(r1) {
std::cmp::Ordering::Less
} else {
r1.start().cmp(&r2.start())
}
})
.map(|(_ptr, scope)| *scope)
}