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