Skip to main content

semantic/
symbol_resolver.rs

1// SPDX-License-Identifier: Apache-2.0
2//! Tree-sitter based symbol resolution for source files.
3//!
4//! Resolves symbol names (like `Repository::open` or `cmd_context_get`)
5//! to line ranges in source files by parsing the AST with tree-sitter.
6//!
7//! Lives in the `semantic` crate so anchor-travel code in `objects`-adjacent
8//! modules can use it without a `repo` dependency. The `repo` crate
9//! re-exports the public surface for backwards compatibility.
10
11use std::{path::Path, rc::Rc};
12
13use crate::parser::{Language, ParsedFile};
14
15/// Result of resolving a symbol to lines in a source file.
16#[derive(Debug, Clone, PartialEq, Eq)]
17pub struct ResolvedSymbol {
18    /// The matched symbol name.
19    pub name: String,
20    /// 1-indexed start line (inclusive).
21    pub start_line: u32,
22    /// 1-indexed end line (inclusive).
23    pub end_line: u32,
24    /// Parent scope name, if any (e.g., the impl block or class name).
25    pub parent_name: Option<String>,
26}
27
28/// Errors that can occur during symbol resolution.
29#[derive(Debug, thiserror::Error)]
30pub enum SymbolResolveError {
31    #[error("unsupported file extension: {0}")]
32    UnsupportedLanguage(String),
33
34    #[error("failed to parse source file")]
35    ParseFailed,
36
37    #[error("symbol not found: {0}")]
38    SymbolNotFound(String),
39}
40
41/// Durable symbol taxonomy shared by semantic indexes and review payloads.
42pub use objects::object::SymbolKindTag as DefinitionKind;
43
44/// One definition found in a source file.
45#[derive(Debug, Clone, PartialEq, Eq)]
46pub struct Definition {
47    /// Symbol name as it appears in the AST. For methods this is the
48    /// bare name; the parent scope is captured separately so callers
49    /// can build a qualified `Parent::method` form when they want one.
50    pub name: String,
51    pub kind: DefinitionKind,
52    /// 1-indexed start line, inclusive.
53    pub start_line: u32,
54    /// 1-indexed end line, inclusive.
55    pub end_line: u32,
56    /// Surrounding scope name (impl block, class, namespace, ...).
57    pub parent_name: Option<String>,
58}
59
60/// Walk the source file and return one [`Definition`] per top-level or
61/// nested definition node. Returns `Ok(vec![])` for files we can parse
62/// but contain no definitions, `Err(UnsupportedLanguage)` for files
63/// without a tree-sitter parser (binaries, unknown extensions),
64/// `Err(ParseFailed)` if the parser errored. Callers should treat the
65/// `UnsupportedLanguage` arm as "fall back to path-only projection".
66pub fn extract_definitions(
67    source: &[u8],
68    path: &Path,
69) -> Result<Vec<Definition>, SymbolResolveError> {
70    let language = Language::from_path(path);
71    language.parser_handle().ok_or_else(|| {
72        SymbolResolveError::UnsupportedLanguage(
73            path.extension()
74                .map(|e| e.to_string_lossy().into_owned())
75                .unwrap_or_else(|| "<none>".to_string()),
76        )
77    })?;
78    let source_text = std::str::from_utf8(source).map_err(|_| SymbolResolveError::ParseFailed)?;
79    let parsed = ParsedFile::parse(source_text, language).ok_or(SymbolResolveError::ParseFailed)?;
80
81    let mut out = Vec::new();
82    walk_definitions(parsed.root_node(), source, &mut out);
83    Ok(out)
84}
85
86fn node_text<'a>(node: &tree_sitter::Node, source: &'a [u8]) -> &'a str {
87    std::str::from_utf8(&source[node.byte_range()]).unwrap_or("")
88}
89
90/// One definition discovered by [`visit_definitions`], carrying the AST node
91/// itself so callers that need the definition's subtree (e.g. the semantic
92/// index token stream) can reach it, alongside the resolved metadata.
93pub(crate) struct DefinitionSite<'tree> {
94    pub node: tree_sitter::Node<'tree>,
95    pub name: String,
96    pub kind: DefinitionKind,
97    pub parent_name: Option<String>,
98    pub start_line: u32,
99    pub end_line: u32,
100}
101
102fn emit_named_definition<'tree>(
103    node: tree_sitter::Node<'tree>,
104    source: &[u8],
105    dk: DefinitionKind,
106    parent: Option<&str>,
107    emit: &mut impl FnMut(DefinitionSite<'tree>),
108) {
109    if let Some(name_node) = node.child_by_field_name("name") {
110        let name = node_text(&name_node, source).to_string();
111        if name.is_empty() {
112            return;
113        }
114        emit(DefinitionSite {
115            node,
116            name,
117            kind: dk,
118            parent_name: parent.map(String::from),
119            start_line: node.start_position().row as u32 + 1,
120            end_line: node.end_position().row as u32 + 1,
121        });
122    }
123}
124
125/// Iterative DFS over a `Vec<(Node, parent)>` worklist — mirrors
126/// A recursive walker would recurse for every child of every non-scope node,
127/// so deeply-parseable input drives call depth proportional to AST depth.
128///
129/// The single source of truth for the definition taxonomy. [`walk_definitions`]
130/// collects the metadata; the semantic index reuses the same walk to reach
131/// each definition's AST node without a second, drifting copy of these arms.
132pub(crate) fn visit_definitions<'tree>(
133    root: tree_sitter::Node<'tree>,
134    source: &[u8],
135    emit: &mut impl FnMut(DefinitionSite<'tree>),
136) {
137    let mut stack: Vec<(tree_sitter::Node<'tree>, Option<Rc<str>>)> = vec![(root, None)];
138
139    while let Some((node, parent)) = stack.pop() {
140        let current_parent = parent.as_deref();
141        let kind = node.kind();
142        let mut descended_with_new_parent = false;
143
144        match kind {
145            // ── Rust ──────────────────────────────────────────────
146            "function_item" => {
147                emit_named_definition(node, source, DefinitionKind::Function, current_parent, emit)
148            }
149            "struct_item" => {
150                emit_named_definition(node, source, DefinitionKind::Type, current_parent, emit)
151            }
152            "enum_item" => {
153                emit_named_definition(node, source, DefinitionKind::Enum, current_parent, emit)
154            }
155            "trait_item" => {
156                emit_named_definition(node, source, DefinitionKind::Trait, current_parent, emit)
157            }
158            "type_item" => emit_named_definition(
159                node,
160                source,
161                DefinitionKind::TypeAlias,
162                current_parent,
163                emit,
164            ),
165            "const_item" | "static_item" => {
166                emit_named_definition(node, source, DefinitionKind::Const, current_parent, emit)
167            }
168            "mod_item" => {
169                // Emit the module, then descend with the module name as the new
170                // container so `mod a { fn f }` yields `f` with `["a"]` — not
171                // the outer scope. Without this, `mod a::f` and `mod b::f`
172                // collapse to the same address.
173                let mod_name: Option<Rc<str>> = node
174                    .child_by_field_name("name")
175                    .map(|n| Rc::from(node_text(&n, source)))
176                    .filter(|name: &Rc<str>| !name.is_empty());
177                emit_named_definition(node, source, DefinitionKind::Module, current_parent, emit);
178                if let Some(name) = mod_name {
179                    let mut cursor = node.walk();
180                    let children: Vec<_> = node.children(&mut cursor).collect();
181                    for child in children.into_iter().rev() {
182                        stack.push((child, Some(name.clone())));
183                    }
184                    descended_with_new_parent = true;
185                }
186            }
187            "impl_item" => {
188                let parent_name: Option<Rc<str>> =
189                    extract_rust_impl_type_name(&node, source).map(Rc::from);
190                let mut cursor = node.walk();
191                let children: Vec<_> = node.children(&mut cursor).collect();
192                for child in children.into_iter().rev() {
193                    stack.push((child, parent_name.clone()));
194                }
195                descended_with_new_parent = true;
196            }
197
198            // ── Python ───────────────────────────────────────────
199            "function_definition" => {
200                emit_named_definition(node, source, DefinitionKind::Function, current_parent, emit)
201            }
202            "class_definition" => {
203                let class_name: Option<Rc<str>> = node
204                    .child_by_field_name("name")
205                    .map(|n| Rc::from(node_text(&n, source)));
206                if let Some(ref name) = class_name
207                    && !name.is_empty()
208                {
209                    emit(DefinitionSite {
210                        node,
211                        name: name.to_string(),
212                        kind: DefinitionKind::Class,
213                        parent_name: current_parent.map(String::from),
214                        start_line: node.start_position().row as u32 + 1,
215                        end_line: node.end_position().row as u32 + 1,
216                    });
217                }
218                let mut cursor = node.walk();
219                let children: Vec<_> = node.children(&mut cursor).collect();
220                for child in children.into_iter().rev() {
221                    stack.push((child, class_name.clone()));
222                }
223                descended_with_new_parent = true;
224            }
225
226            // ── Go ───────────────────────────────────────────────
227            "function_declaration" => {
228                emit_named_definition(node, source, DefinitionKind::Function, current_parent, emit)
229            }
230            "method_declaration" => {
231                if let Some(name_node) = node.child_by_field_name("name") {
232                    let name = node_text(&name_node, source).to_string();
233                    if !name.is_empty() {
234                        let receiver = extract_go_receiver_type(&node, source);
235                        emit(DefinitionSite {
236                            node,
237                            name,
238                            kind: DefinitionKind::Function,
239                            parent_name: receiver.or_else(|| current_parent.map(String::from)),
240                            start_line: node.start_position().row as u32 + 1,
241                            end_line: node.end_position().row as u32 + 1,
242                        });
243                    }
244                }
245            }
246            "type_declaration" => {
247                let mut cursor = node.walk();
248                for child in node.children(&mut cursor) {
249                    if child.kind() == "type_spec"
250                        && let Some(name_node) = child.child_by_field_name("name")
251                    {
252                        let name = node_text(&name_node, source).to_string();
253                        if name.is_empty() {
254                            continue;
255                        }
256                        let dk = match child.child_by_field_name("type").map(|t| t.kind()) {
257                            Some("interface_type") => DefinitionKind::Interface,
258                            Some("struct_type") => DefinitionKind::Type,
259                            _ => DefinitionKind::TypeAlias,
260                        };
261                        emit(DefinitionSite {
262                            node: child,
263                            name,
264                            kind: dk,
265                            parent_name: current_parent.map(String::from),
266                            start_line: child.start_position().row as u32 + 1,
267                            end_line: child.end_position().row as u32 + 1,
268                        });
269                    }
270                }
271            }
272
273            // ── JavaScript / TypeScript ──────────────────────────
274            "method_definition" => {
275                emit_named_definition(node, source, DefinitionKind::Function, current_parent, emit)
276            }
277            "class_declaration" => {
278                let class_name: Option<Rc<str>> = node
279                    .child_by_field_name("name")
280                    .map(|n| Rc::from(node_text(&n, source)));
281                if let Some(ref name) = class_name
282                    && !name.is_empty()
283                {
284                    emit(DefinitionSite {
285                        node,
286                        name: name.to_string(),
287                        kind: DefinitionKind::Class,
288                        parent_name: current_parent.map(String::from),
289                        start_line: node.start_position().row as u32 + 1,
290                        end_line: node.end_position().row as u32 + 1,
291                    });
292                }
293                let mut cursor = node.walk();
294                let children: Vec<_> = node.children(&mut cursor).collect();
295                for child in children.into_iter().rev() {
296                    stack.push((child, class_name.clone()));
297                }
298                descended_with_new_parent = true;
299            }
300            "interface_declaration" => emit_named_definition(
301                node,
302                source,
303                DefinitionKind::Interface,
304                current_parent,
305                emit,
306            ),
307            "type_alias_declaration" => emit_named_definition(
308                node,
309                source,
310                DefinitionKind::TypeAlias,
311                current_parent,
312                emit,
313            ),
314            "enum_declaration" => {
315                emit_named_definition(node, source, DefinitionKind::Enum, current_parent, emit)
316            }
317            "lexical_declaration" | "variable_declaration" => {
318                let mut cursor = node.walk();
319                let mut saw_declarator = false;
320                for child in node.children(&mut cursor) {
321                    if child.kind() == "variable_declarator"
322                        && let Some(name_node) = child.child_by_field_name("name")
323                    {
324                        saw_declarator = true;
325                        let name = node_text(&name_node, source).to_string();
326                        if name.is_empty() {
327                            continue;
328                        }
329                        if let Some(value_node) = child.child_by_field_name("value") {
330                            let vkind = value_node.kind();
331                            let dk = if vkind == "arrow_function"
332                                || vkind == "function"
333                                || vkind == "function_expression"
334                            {
335                                DefinitionKind::Function
336                            } else {
337                                DefinitionKind::Const
338                            };
339                            emit(DefinitionSite {
340                                node,
341                                name,
342                                kind: dk,
343                                parent_name: current_parent.map(String::from),
344                                start_line: node.start_position().row as u32 + 1,
345                                end_line: node.end_position().row as u32 + 1,
346                            });
347                        }
348                    }
349                }
350                // ── Zig ──────────────────────────────────────────
351                // Zig reuses `variable_declaration` for both declarations and
352                // locals, and has no `variable_declarator`: the binding name is
353                // a direct `identifier` child. `const Name = struct|union|
354                // enum|opaque {…}` is Zig's container-as-value idiom, whose
355                // members we walk under `[Name]`; a bare `const`/`var` binding
356                // at container scope is a `Const`.
357                if kind == "variable_declaration" && !saw_declarator {
358                    descended_with_new_parent = zig_visit_variable_declaration(
359                        node,
360                        source,
361                        current_parent,
362                        emit,
363                        &mut stack,
364                    );
365                }
366            }
367            "pair" => {
368                let Some(name_node) = node.child_by_field_name("key") else {
369                    continue;
370                };
371                let Some(value_node) = node.child_by_field_name("value") else {
372                    continue;
373                };
374                if matches!(
375                    value_node.kind(),
376                    "arrow_function" | "function" | "function_expression"
377                ) {
378                    let name = node_text(&name_node, source).to_string();
379                    if !name.is_empty() {
380                        emit(DefinitionSite {
381                            node,
382                            name,
383                            kind: DefinitionKind::Function,
384                            parent_name: current_parent.map(String::from),
385                            start_line: node.start_position().row as u32 + 1,
386                            end_line: node.end_position().row as u32 + 1,
387                        });
388                    }
389                }
390            }
391            "test_declaration" => {
392                // Zig `test "name" {…}` / `test Name {…}` → a `Function` named
393                // `test:"name"` / `test:Name` so risk-signal test-reachability
394                // treats them as tests.
395                let mut cursor = node.walk();
396                let test_name = node.children(&mut cursor).find_map(|c| match c.kind() {
397                    "string" | "identifier" => Some(format!("test:{}", node_text(&c, source))),
398                    _ => None,
399                });
400                if let Some(name) = test_name {
401                    emit(DefinitionSite {
402                        node,
403                        name,
404                        kind: DefinitionKind::Function,
405                        parent_name: current_parent.map(String::from),
406                        start_line: node.start_position().row as u32 + 1,
407                        end_line: node.end_position().row as u32 + 1,
408                    });
409                }
410            }
411
412            // ── C / C++ / Java ───────────────────────────────────
413            "struct_specifier" | "class_specifier" => {
414                emit_named_definition(node, source, DefinitionKind::Class, current_parent, emit)
415            }
416            "namespace_definition" => {
417                emit_named_definition(node, source, DefinitionKind::Module, current_parent, emit)
418            }
419            "enum_specifier" => {
420                emit_named_definition(node, source, DefinitionKind::Enum, current_parent, emit)
421            }
422            "constructor_declaration" => {
423                emit_named_definition(node, source, DefinitionKind::Function, current_parent, emit)
424            }
425
426            _ => {}
427        }
428
429        if !descended_with_new_parent {
430            let mut cursor = node.walk();
431            let children: Vec<_> = node.children(&mut cursor).collect();
432            for child in children.into_iter().rev() {
433                stack.push((child, parent.clone()));
434            }
435        }
436    }
437}
438
439/// Collect one [`Definition`] per definition node, in document order.
440fn walk_definitions(root: tree_sitter::Node, source: &[u8], out: &mut Vec<Definition>) {
441    visit_definitions(root, source, &mut |site| {
442        out.push(Definition {
443            name: site.name,
444            kind: site.kind,
445            start_line: site.start_line,
446            end_line: site.end_line,
447            parent_name: site.parent_name,
448        });
449    });
450}
451
452fn find_definitions(
453    root: tree_sitter::Node<'_>,
454    source: &[u8],
455    target_name: &str,
456) -> Vec<ResolvedSymbol> {
457    let mut matches = Vec::new();
458    visit_definitions(root, source, &mut |site| {
459        if site.name == target_name {
460            matches.push(ResolvedSymbol {
461                name: site.name,
462                start_line: site.start_line,
463                end_line: site.end_line,
464                parent_name: site.parent_name,
465            });
466        }
467    });
468    matches
469}
470
471fn extract_rust_impl_type_name(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
472    let type_node = node.child_by_field_name("type")?;
473    Some(extract_type_identifier(&type_node, source))
474}
475
476fn extract_type_identifier(node: &tree_sitter::Node, source: &[u8]) -> String {
477    match node.kind() {
478        "type_identifier" | "identifier" => node_text(node, source).to_string(),
479        "generic_type" | "scoped_type_identifier" => {
480            let mut cursor = node.walk();
481            for child in node.children(&mut cursor) {
482                if child.kind() == "type_identifier" || child.kind() == "identifier" {
483                    return node_text(&child, source).to_string();
484                }
485            }
486            node_text(node, source).to_string()
487        }
488        _ => node_text(node, source).to_string(),
489    }
490}
491
492fn extract_go_receiver_type(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
493    let params = node.child_by_field_name("receiver")?;
494    let mut cursor = params.walk();
495    for child in params.children(&mut cursor) {
496        if child.kind() == "parameter_declaration"
497            && let Some(type_node) = child.child_by_field_name("type")
498        {
499            let text = node_text(&type_node, source);
500            return Some(text.trim_start_matches('*').to_string());
501        }
502    }
503    None
504}
505
506/// Zig container scopes whose direct `variable_declaration` children are
507/// declarations rather than function-body locals: the file root and the four
508/// container-type bodies. Used to gate bare `const`/`var` bindings so a
509/// function's local variables don't each become a symbol.
510fn is_zig_container_scope(kind: &str) -> bool {
511    matches!(
512        kind,
513        "source_file"
514            | "struct_declaration"
515            | "union_declaration"
516            | "enum_declaration"
517            | "opaque_declaration"
518    )
519}
520
521/// Map a Zig container-declaration node kind to its symbol taxonomy kind.
522/// `struct`/`union`/`opaque` are `Type`; `enum` is `Enum`.
523fn zig_container_kind(kind: &str) -> Option<DefinitionKind> {
524    match kind {
525        "struct_declaration" | "union_declaration" | "opaque_declaration" => {
526            Some(DefinitionKind::Type)
527        }
528        "enum_declaration" => Some(DefinitionKind::Enum),
529        _ => None,
530    }
531}
532
533/// Handle a Zig `variable_declaration` (no `variable_declarator`). Returns
534/// `true` when it descended into a container body with a new `container_path`
535/// parent (so the caller skips the default same-parent descent).
536///
537/// `const Name = struct|union|enum|opaque {…}` emits `Name` with the matching
538/// kind and walks the container's members under `[Name]`. A bare `const`/`var`
539/// binding at container scope emits a `Const`; inside a function body it is
540/// a local and is skipped.
541fn zig_visit_variable_declaration<'tree>(
542    node: tree_sitter::Node<'tree>,
543    source: &[u8],
544    current_parent: Option<&str>,
545    emit: &mut impl FnMut(DefinitionSite<'tree>),
546    stack: &mut Vec<(tree_sitter::Node<'tree>, Option<Rc<str>>)>,
547) -> bool {
548    let mut cursor = node.walk();
549    let children: Vec<tree_sitter::Node<'tree>> = node.children(&mut cursor).collect();
550
551    // Binding name = first direct `identifier` child (it precedes `=`; a value
552    // identifier like `const A = B;` comes after and is never first).
553    let Some(name) = children
554        .iter()
555        .find(|c| c.kind() == "identifier")
556        .map(|c| node_text(c, source).to_string())
557        .filter(|s| !s.is_empty())
558    else {
559        return false;
560    };
561
562    if let Some(container) = children
563        .iter()
564        .find(|c| zig_container_kind(c.kind()).is_some())
565    {
566        let dk = zig_container_kind(container.kind()).expect("checked by find");
567        emit(DefinitionSite {
568            node,
569            name: name.clone(),
570            kind: dk,
571            parent_name: current_parent.map(String::from),
572            start_line: node.start_position().row as u32 + 1,
573            end_line: node.end_position().row as u32 + 1,
574        });
575        let child_parent: Rc<str> = Rc::from(name.as_str());
576        let mut member_cursor = container.walk();
577        let members: Vec<_> = container.children(&mut member_cursor).collect();
578        for member in members.into_iter().rev() {
579            stack.push((member, Some(child_parent.clone())));
580        }
581        return true;
582    }
583
584    // Bare binding: a declaration only at container scope; otherwise a local.
585    let at_container_scope = node
586        .parent()
587        .map(|p| is_zig_container_scope(p.kind()))
588        .unwrap_or(false);
589    if at_container_scope {
590        emit(DefinitionSite {
591            node,
592            name,
593            kind: DefinitionKind::Const,
594            parent_name: current_parent.map(String::from),
595            start_line: node.start_position().row as u32 + 1,
596            end_line: node.end_position().row as u32 + 1,
597        });
598    }
599    false
600}
601
602/// Resolve a symbol name to a line range in source code.
603///
604/// Supports qualified names like `Repository::open` (splits on `::`).
605/// For qualified names, the part before `::` is matched against the parent
606/// scope (impl block, class, etc.) and the part after is the definition name.
607///
608/// Returns `(start_line, end_line)` as 1-indexed, inclusive line numbers.
609pub fn resolve_symbol_lines(
610    source: &[u8],
611    path: &Path,
612    symbol: &str,
613) -> Result<(u32, u32), SymbolResolveError> {
614    let language = Language::from_path(path);
615    language.parser_handle().ok_or_else(|| {
616        SymbolResolveError::UnsupportedLanguage(
617            path.extension()
618                .map(|e| e.to_string_lossy().into_owned())
619                .unwrap_or_else(|| "<none>".to_string()),
620        )
621    })?;
622    let source_text = std::str::from_utf8(source).map_err(|_| SymbolResolveError::ParseFailed)?;
623    let parsed = ParsedFile::parse(source_text, language).ok_or(SymbolResolveError::ParseFailed)?;
624
625    // Split qualified name: "Repository::open" -> parent="Repository", target="open"
626    let (parent_filter, target_name) = if let Some(pos) = symbol.rfind("::") {
627        (Some(&symbol[..pos]), &symbol[pos + 2..])
628    } else {
629        (None, symbol)
630    };
631
632    let definitions = find_definitions(parsed.root_node(), source, target_name);
633
634    // If a parent filter is specified, prefer matches where the parent matches.
635    let matched = if let Some(parent) = parent_filter {
636        definitions
637            .iter()
638            .find(|d| {
639                d.parent_name
640                    .as_deref()
641                    .map(|p| p == parent)
642                    .unwrap_or(false)
643            })
644            .or_else(|| definitions.first())
645    } else {
646        definitions.first()
647    };
648
649    match matched {
650        Some(sym) => Ok((sym.start_line, sym.end_line)),
651        None => Err(SymbolResolveError::SymbolNotFound(symbol.to_string())),
652    }
653}
654
655/// Resolve all definitions of a symbol name, returning all matches.
656///
657/// This is useful when a symbol appears in multiple contexts (e.g.,
658/// multiple impl blocks). Returns an empty vec if no matches found.
659pub fn resolve_all_symbols(
660    source: &[u8],
661    path: &Path,
662    symbol: &str,
663) -> Result<Vec<ResolvedSymbol>, SymbolResolveError> {
664    let language = Language::from_path(path);
665    language.parser_handle().ok_or_else(|| {
666        SymbolResolveError::UnsupportedLanguage(
667            path.extension()
668                .map(|e| e.to_string_lossy().into_owned())
669                .unwrap_or_else(|| "<none>".to_string()),
670        )
671    })?;
672    let source_text = std::str::from_utf8(source).map_err(|_| SymbolResolveError::ParseFailed)?;
673    let parsed = ParsedFile::parse(source_text, language).ok_or(SymbolResolveError::ParseFailed)?;
674
675    let (parent_filter, target_name) = if let Some(pos) = symbol.rfind("::") {
676        (Some(&symbol[..pos]), &symbol[pos + 2..])
677    } else {
678        (None, symbol)
679    };
680
681    let definitions = find_definitions(parsed.root_node(), source, target_name);
682
683    if let Some(parent) = parent_filter {
684        let filtered: Vec<_> = definitions
685            .into_iter()
686            .filter(|d| {
687                d.parent_name
688                    .as_deref()
689                    .map(|p| p == parent)
690                    .unwrap_or(false)
691            })
692            .collect();
693        Ok(filtered)
694    } else {
695        Ok(definitions)
696    }
697}
698
699/// Extract a range of lines from source bytes.
700///
701/// `start` and `end` are 1-indexed, inclusive. Returns the bytes
702/// for those lines (including newlines).
703pub fn extract_line_range(source: &[u8], start: u32, end: u32) -> Vec<u8> {
704    let mut line: u32 = 1;
705    let mut byte_start = 0;
706
707    for (i, &b) in source.iter().enumerate() {
708        if line == start {
709            byte_start = i;
710            break;
711        }
712        if b == b'\n' {
713            line += 1;
714        }
715    }
716
717    if line < start {
718        return Vec::new();
719    }
720
721    for (i, &b) in source[byte_start..].iter().enumerate() {
722        if b == b'\n' {
723            line += 1;
724            if line > end {
725                return source[byte_start..byte_start + i + 1].to_vec();
726            }
727        }
728    }
729
730    source[byte_start..].to_vec()
731}
732
733#[cfg(test)]
734mod tests {
735    use super::*;
736
737    #[test]
738    fn resolve_rust_fn_main() {
739        let source = br#"
740fn helper() -> bool {
741    true
742}
743
744fn main() {
745    println!("hello");
746    let x = 1;
747}
748
749fn after() {}
750"#;
751        let path = Path::new("test.rs");
752        let (start, end) = resolve_symbol_lines(source, path, "main").unwrap();
753        assert_eq!(start, 6);
754        assert_eq!(end, 9);
755    }
756
757    #[test]
758    fn resolve_rust_qualified_impl_method() {
759        let source = br#"
760struct Repository {
761    path: String,
762}
763
764impl Repository {
765    pub fn open(path: &str) -> Self {
766        Repository {
767            path: path.to_string(),
768        }
769    }
770
771    pub fn close(&self) {}
772}
773
774impl Default for Repository {
775    fn default() -> Self {
776        Repository::open(".")
777    }
778}
779"#;
780        let path = Path::new("repo.rs");
781        let (start, end) = resolve_symbol_lines(source, path, "Repository::open").unwrap();
782        assert_eq!(start, 7);
783        assert_eq!(end, 11);
784    }
785
786    #[test]
787    fn resolve_rust_struct() {
788        let source = br#"
789pub struct Config {
790    pub name: String,
791    pub value: u32,
792}
793"#;
794        let path = Path::new("config.rs");
795        let (start, end) = resolve_symbol_lines(source, path, "Config").unwrap();
796        assert_eq!(start, 2);
797        assert_eq!(end, 5);
798    }
799
800    #[test]
801    fn resolve_python_function() {
802        let source = br#"
803def helper():
804    pass
805
806def process_data(items):
807    result = []
808    for item in items:
809        result.append(item * 2)
810    return result
811
812def cleanup():
813    pass
814"#;
815        let path = Path::new("main.py");
816        let (start, end) = resolve_symbol_lines(source, path, "process_data").unwrap();
817        assert_eq!(start, 5);
818        assert_eq!(end, 9);
819    }
820
821    #[test]
822    fn resolve_python_class_method() {
823        let source = br#"
824class Repository:
825    def __init__(self, path):
826        self.path = path
827
828    def open(self):
829        return True
830"#;
831        let path = Path::new("repo.py");
832        let (start, end) = resolve_symbol_lines(source, path, "Repository::open").unwrap();
833        assert_eq!(start, 6);
834        assert_eq!(end, 7);
835    }
836
837    #[test]
838    #[cfg(feature = "lang-go")]
839    fn resolve_go_function() {
840        let source = br#"package main
841
842func helper() bool {
843    return true
844}
845
846func processData(items []int) []int {
847    result := make([]int, 0)
848    for _, item := range items {
849        result = append(result, item*2)
850    }
851    return result
852}
853"#;
854        let path = Path::new("main.go");
855        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
856        assert_eq!(start, 7);
857        assert_eq!(end, 13);
858    }
859
860    #[test]
861    fn resolve_symbol_not_found() {
862        let source = br#"
863fn main() {}
864"#;
865        let path = Path::new("test.rs");
866        let err = resolve_symbol_lines(source, path, "nonexistent").unwrap_err();
867        assert!(matches!(err, SymbolResolveError::SymbolNotFound(_)));
868    }
869
870    #[test]
871    fn resolve_unsupported_extension() {
872        let source = b"some content";
873        let path = Path::new("test.xyz");
874        let err = resolve_symbol_lines(source, path, "main").unwrap_err();
875        assert!(matches!(err, SymbolResolveError::UnsupportedLanguage(_)));
876    }
877
878    #[test]
879    fn extract_line_range_basic() {
880        let source = b"line 1\nline 2\nline 3\nline 4\nline 5\n";
881        let result = extract_line_range(source, 2, 4);
882        assert_eq!(result, b"line 2\nline 3\nline 4\n");
883    }
884
885    #[test]
886    fn extract_line_range_single_line() {
887        let source = b"line 1\nline 2\nline 3\n";
888        let result = extract_line_range(source, 2, 2);
889        assert_eq!(result, b"line 2\n");
890    }
891
892    #[test]
893    fn resolve_js_function_declaration() {
894        let source = br#"
895function helper() {
896    return true;
897}
898
899function processData(items) {
900    return items.map(x => x * 2);
901}
902"#;
903        let path = Path::new("main.js");
904        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
905        assert_eq!(start, 6);
906        assert_eq!(end, 8);
907    }
908
909    #[test]
910    fn resolve_js_arrow_function_const() {
911        let source = br#"
912const helper = () => true;
913
914const processData = (items) => {
915    return items.map(x => x * 2);
916};
917"#;
918        let path = Path::new("utils.js");
919        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
920        assert_eq!(start, 4);
921        assert_eq!(end, 6);
922    }
923
924    /// Regression: real-world TS code often defines methods as arrow-
925    /// function properties of an object literal (e.g. a `db` helper).
926    /// The variable_declarator branch missed these — `pair` handling
927    /// catches them. Without this, `heddle context set --scope symbol:insert`
928    /// against `export const db = { insert: async () => {...} }` shipped
929    /// `resolved_lines: None` and the chip never rendered.
930    #[test]
931    fn resolve_typescript_object_literal_property_arrow_function() {
932        let source = br#"
933export const db = {
934    query: async (sql: string) => {
935        return [];
936    },
937    insert: async (table: string, data: Record<string, any>) => {
938        const keys = Object.keys(data);
939        return keys;
940    },
941};
942"#;
943        let path = Path::new("db.ts");
944        let (start, end) = resolve_symbol_lines(source, path, "insert").unwrap();
945        // `insert` lives at lines 6–9 in the source above (1-indexed,
946        // counting the leading newline as line 1).
947        assert!((5..=7).contains(&start), "got start={start}");
948        assert!(end > start && end <= 10, "got end={end}");
949    }
950
951    #[test]
952    fn resolve_typescript_function() {
953        let source = br#"
954function helper(): boolean {
955    return true;
956}
957
958function processData(items: number[]): number[] {
959    return items.map(x => x * 2);
960}
961"#;
962        let path = Path::new("main.ts");
963        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
964        assert_eq!(start, 6);
965        assert_eq!(end, 8);
966    }
967
968    #[test]
969    fn resolve_all_returns_multiple_matches() {
970        let source = br#"
971impl Foo {
972    fn do_thing(&self) {}
973}
974
975impl Bar {
976    fn do_thing(&self) {}
977}
978"#;
979        let path = Path::new("test.rs");
980        let results = resolve_all_symbols(source, path, "do_thing").unwrap();
981        assert_eq!(results.len(), 2);
982        assert_eq!(results[0].parent_name.as_deref(), Some("Foo"));
983        assert_eq!(results[1].parent_name.as_deref(), Some("Bar"));
984    }
985
986    #[test]
987    fn extract_definitions_reports_rust_taxonomy_parent_scopes_and_ranges() {
988        let source = br#"const LIMIT: usize = 10;
989pub mod outer {
990    pub struct Widget {
991        pub id: u64,
992    }
993
994    pub enum Mode {
995        Fast,
996        Slow,
997    }
998
999    pub trait Runner {
1000        fn run(&self);
1001    }
1002
1003    pub type WidgetResult<T> = Result<T, Error>;
1004
1005    impl Widget {
1006        pub fn build(id: u64) -> Self {
1007            Self { id }
1008        }
1009    }
1010}
1011"#;
1012
1013        let defs = extract_definitions(source, Path::new("lib.rs")).unwrap();
1014
1015        assert_definition(&defs, "LIMIT", DefinitionKind::Const, 1, 1, None);
1016        assert_definition(&defs, "outer", DefinitionKind::Module, 2, 23, None);
1017        // Items inside `mod outer` now carry `outer` as their container.
1018        assert_definition(&defs, "Widget", DefinitionKind::Type, 3, 5, Some("outer"));
1019        assert_definition(&defs, "Mode", DefinitionKind::Enum, 7, 10, Some("outer"));
1020        assert_definition(
1021            &defs,
1022            "Runner",
1023            DefinitionKind::Trait,
1024            12,
1025            14,
1026            Some("outer"),
1027        );
1028        assert_definition(
1029            &defs,
1030            "WidgetResult",
1031            DefinitionKind::TypeAlias,
1032            16,
1033            16,
1034            Some("outer"),
1035        );
1036        assert_definition(
1037            &defs,
1038            "build",
1039            DefinitionKind::Function,
1040            19,
1041            21,
1042            Some("Widget"),
1043        );
1044    }
1045
1046    #[test]
1047    fn extract_definitions_reports_typescript_taxonomy_parent_scopes_and_ranges() {
1048        let source = br#"interface Service {
1049    run(): void;
1050}
1051
1052type Handler = (value: string) => void;
1053
1054enum Status {
1055    Ready,
1056    Done,
1057}
1058
1059class Controller {
1060    start(): void {
1061        handle("start");
1062    }
1063}
1064
1065export const handle = (value: string): void => {
1066    console.log(value);
1067};
1068
1069export const settings = { retry: 2 };
1070"#;
1071
1072        let defs = extract_definitions(source, Path::new("controller.ts")).unwrap();
1073
1074        assert_definition(&defs, "Service", DefinitionKind::Interface, 1, 3, None);
1075        assert_definition(&defs, "Handler", DefinitionKind::TypeAlias, 5, 5, None);
1076        assert_definition(&defs, "Status", DefinitionKind::Enum, 7, 10, None);
1077        assert_definition(&defs, "Controller", DefinitionKind::Class, 12, 16, None);
1078        assert_definition(
1079            &defs,
1080            "start",
1081            DefinitionKind::Function,
1082            13,
1083            15,
1084            Some("Controller"),
1085        );
1086        assert_definition(&defs, "handle", DefinitionKind::Function, 18, 20, None);
1087        assert_definition(&defs, "settings", DefinitionKind::Const, 22, 22, None);
1088    }
1089
1090    #[test]
1091    fn extract_definitions_rejects_parse_error_trees() {
1092        let err =
1093            extract_definitions(b"fn broken( -> usize { 1 }", Path::new("broken.rs")).unwrap_err();
1094
1095        assert!(matches!(err, SymbolResolveError::ParseFailed));
1096    }
1097
1098    /// Characterization: iterative `walk_definitions` must emit the same
1099    /// definitions in the same source order with the same parent scopes as
1100    /// the former recursive walker on a multi-level, multi-kind fixture.
1101    #[test]
1102    fn walk_definitions_iterative_matches_recursive_output_on_nested_fixture() {
1103        let source = br#"const LIMIT: usize = 10;
1104pub mod outer {
1105    pub struct Widget {
1106        pub id: u64,
1107    }
1108
1109    pub enum Mode {
1110        Fast,
1111        Slow,
1112    }
1113
1114    pub trait Runner {
1115        fn run(&self);
1116    }
1117
1118    pub type WidgetResult<T> = Result<T, Error>;
1119
1120    impl Widget {
1121        pub fn build(id: u64) -> Self {
1122            Self { id }
1123        }
1124    }
1125}
1126"#;
1127
1128        let defs = extract_definitions(source, Path::new("lib.rs")).unwrap();
1129
1130        let expected: &[(&str, DefinitionKind, u32, u32, Option<&str>)] = &[
1131            ("LIMIT", DefinitionKind::Const, 1, 1, None),
1132            ("outer", DefinitionKind::Module, 2, 23, None),
1133            ("Widget", DefinitionKind::Type, 3, 5, Some("outer")),
1134            ("Mode", DefinitionKind::Enum, 7, 10, Some("outer")),
1135            ("Runner", DefinitionKind::Trait, 12, 14, Some("outer")),
1136            (
1137                "WidgetResult",
1138                DefinitionKind::TypeAlias,
1139                16,
1140                16,
1141                Some("outer"),
1142            ),
1143            ("build", DefinitionKind::Function, 19, 21, Some("Widget")),
1144        ];
1145
1146        assert_eq!(defs.len(), expected.len(), "definition count: {defs:?}");
1147        for (def, (name, kind, start, end, parent)) in defs.iter().zip(expected.iter()) {
1148            assert_eq!(&def.name, name);
1149            assert_eq!(def.kind, *kind);
1150            assert_eq!(def.start_line, *start);
1151            assert_eq!(def.end_line, *end);
1152            assert_eq!(def.parent_name.as_deref(), *parent);
1153        }
1154    }
1155
1156    // HEDDLE-DR-4 / #876: the shared definition walker must not stack-overflow
1157    // on deeply-nested but syntactically-valid trees.
1158    #[cfg(feature = "lang-rust")]
1159    #[test]
1160    fn deeply_nested_rust_modules_walk_definitions_does_not_stack_overflow() {
1161        let depth = 2000usize;
1162        let mut s = String::new();
1163        for i in 0..depth {
1164            s.push_str(&format!("mod m{i} {{\n"));
1165        }
1166        s.push_str("fn target() {}\n");
1167        for _ in 0..depth {
1168            s.push_str("}\n");
1169        }
1170
1171        let source = s.into_bytes();
1172        let path = Path::new("nested.rs");
1173
1174        let handle = std::thread::Builder::new()
1175            .stack_size(128 * 1024)
1176            .spawn(move || extract_definitions(&source, path))
1177            .expect("spawn");
1178        let defs = handle
1179            .join()
1180            .expect("walk_definitions must not stack-overflow on deeply-nested input")
1181            .expect("parse nested modules");
1182        assert!(
1183            defs.iter().any(|d| d.name == "target"),
1184            "deep target fn must be returned, not silently dropped; got {defs:?}"
1185        );
1186    }
1187
1188    /// heddle#1068: Zig's container-as-value idiom, `fn`/`test` blocks, and
1189    /// `const`/`var` bindings map onto the shared taxonomy — and function-body
1190    /// locals (Zig reuses `const`/`var` for locals) must NOT leak as symbols.
1191    #[cfg(feature = "lang-zig")]
1192    #[test]
1193    fn extract_definitions_reports_zig_taxonomy_parents_and_ranges() {
1194        let source = br#"const std = @import("std");
1195
1196pub const MAX: usize = 100;
1197var counter: u32 = 0;
1198
1199pub fn add(a: i32, b: i32) i32 {
1200    const local = 1;
1201    return a + b + local;
1202}
1203
1204pub const Point = struct {
1205    x: f64,
1206    pub fn dist(self: Point) f64 {
1207        const scale = 2.0;
1208        return self.x * scale;
1209    }
1210};
1211
1212const Color = enum { red, green };
1213
1214const Shape = union(enum) { circle: f64 };
1215
1216const Handle = opaque {
1217    pub fn get() void {}
1218};
1219
1220test "addition works" {
1221    const r = add(1, 2);
1222    _ = r;
1223}
1224"#;
1225
1226        let defs = extract_definitions(source, Path::new("sample.zig")).unwrap();
1227
1228        assert_definition(&defs, "std", DefinitionKind::Const, 1, 1, None);
1229        assert_definition(&defs, "MAX", DefinitionKind::Const, 3, 3, None);
1230        assert_definition(&defs, "counter", DefinitionKind::Const, 4, 4, None);
1231        assert_definition(&defs, "add", DefinitionKind::Function, 6, 9, None);
1232        assert_definition(&defs, "Point", DefinitionKind::Type, 11, 17, None);
1233        assert_definition(
1234            &defs,
1235            "dist",
1236            DefinitionKind::Function,
1237            13,
1238            16,
1239            Some("Point"),
1240        );
1241        assert_definition(&defs, "Color", DefinitionKind::Enum, 19, 19, None);
1242        assert_definition(&defs, "Shape", DefinitionKind::Type, 21, 21, None);
1243        assert_definition(&defs, "Handle", DefinitionKind::Type, 23, 25, None);
1244        assert_definition(
1245            &defs,
1246            "get",
1247            DefinitionKind::Function,
1248            24,
1249            24,
1250            Some("Handle"),
1251        );
1252        assert_definition(
1253            &defs,
1254            "test:\"addition works\"",
1255            DefinitionKind::Function,
1256            27,
1257            30,
1258            None,
1259        );
1260
1261        // Function-body / method-body / test-body locals must never surface as
1262        // symbols — they are `variable_declaration`s at non-container scope.
1263        for leaked in ["local", "scale", "r"] {
1264            assert!(
1265                !defs.iter().any(|d| d.name == leaked),
1266                "local {leaked:?} leaked as a symbol: {defs:?}"
1267            );
1268        }
1269    }
1270
1271    fn assert_definition(
1272        defs: &[Definition],
1273        name: &str,
1274        kind: DefinitionKind,
1275        start_line: u32,
1276        end_line: u32,
1277        parent_name: Option<&str>,
1278    ) {
1279        assert!(
1280            defs.iter().any(|def| {
1281                def.name == name
1282                    && def.kind == kind
1283                    && def.start_line == start_line
1284                    && def.end_line == end_line
1285                    && def.parent_name.as_deref() == parent_name
1286            }),
1287            "expected {name:?} {kind:?} lines {start_line}-{end_line} parent {parent_name:?}, got: {defs:?}"
1288        );
1289    }
1290}