use std::ops::ControlFlow;
use cairo_lang_filesystem::db::get_originating_location;
use cairo_lang_filesystem::ids::SpanInFile;
use cairo_lang_filesystem::span::{TextOffset, TextSpan, TextWidth};
use cairo_lang_semantic::keyword::SELF_TYPE_KW;
use cairo_lang_semantic::resolve::{ResolvedConcreteItem, ResolvedGenericItem};
use cairo_lang_syntax::node::ast::TerminalIdentifier;
use cairo_lang_syntax::node::db::SyntaxGroup;
use cairo_lang_syntax::node::ids::SyntaxStablePtrId;
use cairo_lang_syntax::node::{SyntaxNode, Terminal, TypedStablePtr, TypedSyntaxNode};
use cairo_language_common::CommonGroup;
use memchr::memmem::Finder;
use search_scope::SearchScope;
use crate::lang::db::AnalysisDatabase;
use crate::lang::defs::{ResolvedItem, SymbolDef, SymbolSearch};
pub mod search_scope;
macro_rules! flow {
($expr:expr) => {
let ControlFlow::Continue(()) = $expr else {
return;
};
};
}
pub struct FindUsages<'db> {
symbol: SymbolDef<'db>,
symbol_item: ResolvedItem<'db>,
db: &'db AnalysisDatabase,
include_declaration: bool,
in_scope: Option<SearchScope<'db>>,
}
#[derive(Copy, Clone, Debug, Eq, PartialEq)]
pub struct FoundUsage<'db>(SpanInFile<'db>);
impl<'db> FoundUsage<'db> {
fn originating_location(&self, db: &'db AnalysisDatabase) -> Self {
Self(get_originating_location(db, self.0, None))
}
}
impl<'db> FindUsages<'db> {
pub(super) fn new(
symbol: SymbolDef<'db>,
symbol_item: ResolvedItem<'db>,
db: &'db AnalysisDatabase,
) -> Self {
Self { symbol, symbol_item, db, include_declaration: false, in_scope: None }
}
pub fn include_declaration(mut self, include: bool) -> Self {
self.include_declaration = include;
self
}
pub fn in_scope(mut self, scope: SearchScope<'db>) -> Self {
self.in_scope = Some(scope);
self
}
pub fn collect(self) -> Vec<FoundUsage<'db>> {
let mut result = vec![];
self.search(&mut |usage| {
result.push(usage);
ControlFlow::Continue(())
});
result
}
pub fn originating_locations(
self,
db: &'db AnalysisDatabase,
) -> impl Iterator<Item = SpanInFile<'db>> {
self.collect().into_iter().map(|usage| usage.originating_location(db).location())
}
#[tracing::instrument(skip_all)]
pub fn search(self, sink: &mut dyn FnMut(FoundUsage<'db>) -> ControlFlow<(), ()>) {
let db = self.db;
#[allow(unused_doc_comments)]
if self.include_declaration
&& let Some(stable_ptr) = self.symbol.definition_stable_ptr(db)
{
let usage = FoundUsage::from_stable_ptr(db, stable_ptr);
flow!(sink(usage));
}
let search_for_self_usages = matches!(
self.symbol_item,
ResolvedItem::Concrete(ResolvedConcreteItem::Impl(_))
| ResolvedItem::Concrete(ResolvedConcreteItem::SelfTrait(_))
| ResolvedItem::Generic(ResolvedGenericItem::Trait(_))
);
let search_scope = self.in_scope.clone().unwrap_or_else(|| self.symbol.search_scope(db));
let needle = match &self.symbol {
SymbolDef::PluginInlineMacro(macro_name) => format!("{macro_name}!"),
symbol => symbol.name(db).to_string(),
};
let finder = Finder::new(needle.as_bytes());
let self_finder = Finder::new(SELF_TYPE_KW.as_bytes());
for (file, text, search_span) in search_scope.files_contents_and_spans(db) {
let mut found_offsets: Vec<TextOffset> =
Self::match_offsets(&finder, text, search_span).collect();
if search_for_self_usages {
let mut self_usages_offsets =
Self::match_offsets(&self_finder, text, search_span).collect();
found_offsets.append(&mut self_usages_offsets);
}
for offset in found_offsets {
if let Some(node) = db.find_syntax_node_at_offset(file, offset)
&& let Some(identifier) = TerminalIdentifier::cast_token(db, node)
{
flow!(self.found_identifier(db, identifier, sink));
}
}
}
}
fn match_offsets<'b>(
finder: &'b Finder<'b>,
text: &'b str,
search_span: Option<TextSpan>,
) -> impl Iterator<Item = TextOffset> + use<'b> {
finder
.find_iter(text.as_bytes())
.map(|offset| TextWidth::at(text, offset).as_offset())
.filter(move |&offset| {
search_span.is_none_or(|span| span.start <= offset && offset <= span.end)
})
.filter(|offset| {
let idx = offset.as_u32() as usize;
!{
let char_before = text[..idx].chars().next_back();
char_before.is_some_and(|ch| ch.is_alphabetic() || ch == '_')
} && !{
let char_after = text[idx + finder.needle().len()..].chars().next();
char_after.is_some_and(|ch| ch.is_alphanumeric() || ch == '_')
}
})
}
fn found_identifier(
&self,
db: &'db dyn SyntaxGroup,
identifier: TerminalIdentifier<'db>,
sink: &mut dyn FnMut(FoundUsage<'db>) -> ControlFlow<(), ()>,
) -> ControlFlow<(), ()> {
if Some(identifier.stable_ptr(self.db).untyped()) == self.symbol.definition_stable_ptr(db) {
return ControlFlow::Continue(());
}
let found_symbol_definition =
SymbolSearch::find_definition(self.db, &identifier).map(|ss| ss.def);
let found_symbol_declaration =
SymbolSearch::find_declaration(self.db, &identifier).map(|ss| ss.def);
if found_symbol_definition.as_ref() == Some(&self.symbol)
|| found_symbol_declaration.as_ref() == Some(&self.symbol)
{
let usage = FoundUsage::from_syntax_node(self.db, identifier.as_syntax_node());
sink(usage)
} else {
ControlFlow::Continue(())
}
}
}
impl<'db> FoundUsage<'db> {
fn from_syntax_node(db: &'db AnalysisDatabase, syntax_node: SyntaxNode<'db>) -> Self {
Self(SpanInFile {
file_id: syntax_node.stable_ptr(db).file_id(db),
span: syntax_node.span_without_trivia(db),
})
}
fn from_stable_ptr(db: &'db AnalysisDatabase, stable_ptr: SyntaxStablePtrId<'db>) -> Self {
Self::from_syntax_node(db, stable_ptr.lookup(db))
}
pub fn location(self) -> SpanInFile<'db> {
self.0
}
}