use std::path::PathBuf;
use bock_air::{resolve_names_with_registry, ModuleRegistry, NameKind, SymbolTable};
use bock_ast::NodeId;
use bock_errors::{FileId, Span};
use bock_lexer::Lexer;
use bock_parser::Parser;
use bock_source::SourceMap;
use crate::goto_definition::position_to_offset;
use crate::symbol_index::SymbolIndex;
pub struct SymbolOccurrences {
pub source_map: SourceMap,
pub file_id: FileId,
pub name: String,
pub origin_span: Span,
pub decl_span: Span,
pub reference_spans: Vec<Span>,
}
#[must_use]
pub fn find_occurrences(
path: PathBuf,
content: String,
line: u32,
character: u32,
) -> Option<SymbolOccurrences> {
let mut source_map = SourceMap::new();
let file_id = source_map.add_file(path, content);
let source_file = source_map.get_file(file_id);
let offset = position_to_offset(&source_file.content, line, character)?;
let mut lexer = Lexer::new(source_file);
let tokens = lexer.tokenize();
let mut parser = Parser::new(tokens, source_file);
let module = parser.parse_module();
let registry = ModuleRegistry::new();
let mut symbols = SymbolTable::new();
let _ = resolve_names_with_registry(&module, &mut symbols, ®istry);
let index = SymbolIndex::build(&module);
let (def_id, decl_span, origin_span) = locate_target(&index, &symbols, offset)?;
if index.opaque_member_ids.contains(&def_id) {
return None;
}
let name = source_file.slice(decl_span).to_string();
let mut refs: Vec<Span> = Vec::new();
for (use_id, span) in &index.ident_uses {
if symbols
.resolutions
.get(use_id)
.is_some_and(|r| r.def_id == def_id)
{
refs.push(*span);
}
}
if index.type_decl_ids.contains(&def_id) || index.variant_ids.contains(&def_id) {
collect_path_refs(&index, &name, &mut refs);
}
if index.variant_ids.contains(&def_id) {
for (member, span) in &index.member_accesses {
if member == &name {
refs.push(*span);
}
}
}
refs.sort_unstable_by_key(|s| (s.start, s.end));
refs.dedup_by_key(|s| (s.start, s.end));
refs.retain(|s| !(s.start == decl_span.start && s.end == decl_span.end));
Some(SymbolOccurrences {
source_map,
file_id,
name,
origin_span,
decl_span,
reference_spans: refs,
})
}
fn locate_target(
index: &SymbolIndex,
symbols: &SymbolTable,
offset: usize,
) -> Option<(NodeId, Span, Span)> {
if let Some((use_id, use_span)) = index.ident_use_at(offset) {
if let Some(resolved) = symbols
.resolutions
.get(&use_id)
.filter(|r| r.kind != NameKind::Builtin)
{
if let Some(&decl_span) = index.def_spans.get(&resolved.def_id) {
return Some((resolved.def_id, decl_span, use_span));
}
}
}
if let Some((decl_id, decl_span)) = index.decl_name_at(offset) {
return Some((decl_id, decl_span, decl_span));
}
if let Some((seg_name, seg_span)) = index.type_segment_at(offset) {
if let Some(&(decl_id, decl_span)) = index.toplevel_by_name.get(seg_name) {
return Some((decl_id, decl_span, seg_span));
}
}
None
}
fn collect_path_refs(index: &SymbolIndex, name: &str, refs: &mut Vec<Span>) {
for path in &index.type_paths {
let local_root = path
.first()
.is_some_and(|(first, _)| index.toplevel_by_name.contains_key(first));
for (i, (seg, span)) in path.iter().enumerate() {
if (i == 0 || local_root) && seg == name {
refs.push(*span);
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn run(src: &str, line: u32, ch: u32) -> Option<SymbolOccurrences> {
find_occurrences(PathBuf::from("test.bock"), src.to_string(), line, ch)
}
fn texts(occ: &SymbolOccurrences) -> Vec<String> {
let file = occ.source_map.get_file(occ.file_id);
occ.reference_spans
.iter()
.map(|s| file.slice(*s).to_string())
.collect()
}
fn positions(occ: &SymbolOccurrences) -> Vec<(usize, usize)> {
let file = occ.source_map.get_file(occ.file_id);
occ.reference_spans
.iter()
.map(|s| file.line_col(s.start))
.collect()
}
#[test]
fn references_from_use_site_find_all_uses_and_decl() {
let src = "\
module m
fn main() -> Int {
let answer = 42
let doubled = answer + answer
doubled
}
";
let occ = run(src, 4, 18).expect("occurrences found");
assert_eq!(occ.name, "answer");
assert_eq!(texts(&occ), vec!["answer", "answer"]);
let file = occ.source_map.get_file(occ.file_id);
assert_eq!(file.slice(occ.decl_span), "answer");
assert_eq!(file.line_col(occ.decl_span.start).0, 4);
assert_eq!(positions(&occ), vec![(5, 19), (5, 28)]);
}
#[test]
fn references_from_declaration_site_match_use_site_query() {
let src = "\
module m
public fn greet(name: String) -> String {
name
}
fn caller() -> String {
greet(\"world\")
}
";
let from_decl = run(src, 2, 10).expect("occurrences found");
assert_eq!(from_decl.name, "greet");
assert_eq!(texts(&from_decl), vec!["greet"]);
let from_use = run(src, 7, 4).expect("occurrences found");
assert_eq!(from_use.decl_span, from_decl.decl_span);
assert_eq!(from_use.reference_spans, from_decl.reference_spans);
}
#[test]
fn shadowed_names_in_sibling_scopes_do_not_cross_match() {
let src = "\
module m
fn first() -> Int {
let value = 1
value
}
fn second() -> Int {
let value = 2
value + value
}
";
let occ = run(src, 4, 4).expect("occurrences found");
let file = occ.source_map.get_file(occ.file_id);
assert_eq!(file.line_col(occ.decl_span.start).0, 4);
assert_eq!(positions(&occ), vec![(5, 5)]);
}
#[test]
fn type_references_found_from_declaration() {
let src = "\
module m
public record Point { x: Int, y: Int }
fn origin() -> Point {
Point { x: 0, y: 0 }
}
";
let occ = run(src, 2, 14).expect("occurrences found");
assert_eq!(occ.name, "Point");
assert_eq!(positions(&occ), vec![(5, 16), (6, 5)]);
}
#[test]
fn type_references_found_from_annotation() {
let src = "\
module m
public record Point { x: Int, y: Int }
fn origin() -> Point {
Point { x: 0, y: 0 }
}
";
let occ = run(src, 4, 17).expect("occurrences found");
let file = occ.source_map.get_file(occ.file_id);
assert_eq!(file.line_col(occ.decl_span.start).0, 3);
assert_eq!(occ.reference_spans.len(), 2);
}
#[test]
fn enum_variant_references_include_bare_constructor_uses() {
let src = "\
module m
public enum Color { Red, Green, Blue }
fn favorite() -> Color {
Red
}
fn other() -> Color {
Red
}
";
let occ = run(src, 2, 20).expect("occurrences found");
assert_eq!(occ.name, "Red");
assert_eq!(texts(&occ), vec!["Red", "Red"]);
assert_eq!(positions(&occ), vec![(6, 5), (10, 5)]);
}
#[test]
fn enum_variant_references_include_match_patterns() {
let src = "\
module m
public enum Color { Red, Green }
fn score(c: Color) -> Int {
match c {
Red => 1
Green => 2
}
}
fn pick() -> Color {
Red
}
";
let occ = run(src, 2, 20).expect("occurrences found");
assert_eq!(occ.name, "Red");
assert_eq!(texts(&occ), vec!["Red", "Red"]);
assert_eq!(positions(&occ), vec![(7, 9), (13, 5)]);
}
#[test]
fn builtin_yields_no_occurrences() {
let src = "\
module m
fn main() {
print(\"hi\")
}
";
assert!(run(src, 3, 4).is_none());
}
#[test]
fn whitespace_yields_no_occurrences() {
let src = "\
module m
fn main() {
let x = 1
}
";
assert!(run(src, 3, 0).is_none());
}
#[test]
fn record_field_is_refused() {
let src = "\
module m
public record Point { x: Int, y: Int }
fn use_point(p: Point) -> Int {
p.x
}
";
assert!(run(src, 2, 22).is_none());
}
#[test]
fn impl_method_is_refused() {
let src = "\
module m
record Card { rank: Int }
impl Card {
public fn beats(self, other: Card) -> Bool {
self.rank > other.rank
}
}
";
assert!(run(src, 5, 14).is_none());
}
#[test]
fn origin_span_is_token_under_cursor() {
let src = "\
module m
fn main() -> Int {
let answer = 42
answer
}
";
let occ = run(src, 4, 4).expect("occurrences found");
let file = occ.source_map.get_file(occ.file_id);
assert_eq!(file.slice(occ.origin_span), "answer");
assert_eq!(file.line_col(occ.origin_span.start).0, 5);
}
}