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
118fn push_named_definition(
119    node: &tree_sitter::Node,
120    source: &[u8],
121    dk: DefinitionKind,
122    parent: Option<&str>,
123    out: &mut Vec<Definition>,
124) {
125    if let Some(name_node) = node.child_by_field_name("name") {
126        let name = node_text(&name_node, source).to_string();
127        if name.is_empty() {
128            return;
129        }
130        out.push(Definition {
131            name,
132            kind: dk,
133            start_line: node.start_position().row as u32 + 1,
134            end_line: node.end_position().row as u32 + 1,
135            parent_name: parent.map(String::from),
136        });
137    }
138}
139
140/// Iterative DFS over a `Vec<(Node, parent)>` worklist — mirrors
141/// [`symbol_extraction::find_definitions`]: a recursive walker would recurse
142/// for every child of every non-scope node, so deeply-parseable input drives
143/// call depth proportional to AST depth.
144fn walk_definitions(
145    root: tree_sitter::Node,
146    source: &[u8],
147    out: &mut Vec<Definition>,
148) {
149    let mut stack: Vec<(tree_sitter::Node, Option<Rc<str>>)> = vec![(root, None)];
150
151    while let Some((node, parent)) = stack.pop() {
152        let current_parent = parent.as_deref();
153        let kind = node.kind();
154        let mut descended_with_new_parent = false;
155
156        match kind {
157            // ── Rust ──────────────────────────────────────────────
158            "function_item" => {
159                push_named_definition(&node, source, DefinitionKind::Function, current_parent, out)
160            }
161            "struct_item" => {
162                push_named_definition(&node, source, DefinitionKind::Type, current_parent, out)
163            }
164            "enum_item" => {
165                push_named_definition(&node, source, DefinitionKind::EnumDef, current_parent, out)
166            }
167            "trait_item" => {
168                push_named_definition(&node, source, DefinitionKind::Trait, current_parent, out)
169            }
170            "type_item" => {
171                push_named_definition(&node, source, DefinitionKind::TypeAlias, current_parent, out)
172            }
173            "const_item" | "static_item" => {
174                push_named_definition(&node, source, DefinitionKind::ConstDecl, current_parent, out)
175            }
176            "mod_item" => {
177                push_named_definition(&node, source, DefinitionKind::Module, current_parent, out)
178            }
179            "impl_item" => {
180                let parent_name: Option<Rc<str>> =
181                    extract_rust_impl_type_name(&node, source).map(Rc::from);
182                let mut cursor = node.walk();
183                let children: Vec<_> = node.children(&mut cursor).collect();
184                for child in children.into_iter().rev() {
185                    stack.push((child, parent_name.clone()));
186                }
187                descended_with_new_parent = true;
188            }
189
190            // ── Python ───────────────────────────────────────────
191            "function_definition" => {
192                push_named_definition(&node, source, DefinitionKind::Function, current_parent, out)
193            }
194            "class_definition" => {
195                let class_name: Option<Rc<str>> = node
196                    .child_by_field_name("name")
197                    .map(|n| Rc::from(node_text(&n, source)));
198                if let Some(ref name) = class_name
199                    && !name.is_empty()
200                {
201                    out.push(Definition {
202                        name: name.to_string(),
203                        kind: DefinitionKind::Class,
204                        start_line: node.start_position().row as u32 + 1,
205                        end_line: node.end_position().row as u32 + 1,
206                        parent_name: current_parent.map(String::from),
207                    });
208                }
209                let mut cursor = node.walk();
210                let children: Vec<_> = node.children(&mut cursor).collect();
211                for child in children.into_iter().rev() {
212                    stack.push((child, class_name.clone()));
213                }
214                descended_with_new_parent = true;
215            }
216
217            // ── Go ───────────────────────────────────────────────
218            "function_declaration" => {
219                push_named_definition(&node, source, DefinitionKind::Function, current_parent, out)
220            }
221            "method_declaration" => {
222                if let Some(name_node) = node.child_by_field_name("name") {
223                    let name = node_text(&name_node, source).to_string();
224                    if !name.is_empty() {
225                        let receiver = extract_go_receiver_type(&node, source);
226                        out.push(Definition {
227                            name,
228                            kind: DefinitionKind::Function,
229                            start_line: node.start_position().row as u32 + 1,
230                            end_line: node.end_position().row as u32 + 1,
231                            parent_name: receiver.or_else(|| current_parent.map(String::from)),
232                        });
233                    }
234                }
235            }
236            "type_declaration" => {
237                let mut cursor = node.walk();
238                for child in node.children(&mut cursor) {
239                    if child.kind() == "type_spec"
240                        && let Some(name_node) = child.child_by_field_name("name")
241                    {
242                        let name = node_text(&name_node, source).to_string();
243                        if name.is_empty() {
244                            continue;
245                        }
246                        let dk = match child.child_by_field_name("type").map(|t| t.kind()) {
247                            Some("interface_type") => DefinitionKind::Interface,
248                            Some("struct_type") => DefinitionKind::Type,
249                            _ => DefinitionKind::TypeAlias,
250                        };
251                        out.push(Definition {
252                            name,
253                            kind: dk,
254                            start_line: child.start_position().row as u32 + 1,
255                            end_line: child.end_position().row as u32 + 1,
256                            parent_name: current_parent.map(String::from),
257                        });
258                    }
259                }
260            }
261
262            // ── JavaScript / TypeScript ──────────────────────────
263            "method_definition" => {
264                push_named_definition(&node, source, DefinitionKind::Function, current_parent, out)
265            }
266            "class_declaration" => {
267                let class_name: Option<Rc<str>> = node
268                    .child_by_field_name("name")
269                    .map(|n| Rc::from(node_text(&n, source)));
270                if let Some(ref name) = class_name
271                    && !name.is_empty()
272                {
273                    out.push(Definition {
274                        name: name.to_string(),
275                        kind: DefinitionKind::Class,
276                        start_line: node.start_position().row as u32 + 1,
277                        end_line: node.end_position().row as u32 + 1,
278                        parent_name: current_parent.map(String::from),
279                    });
280                }
281                let mut cursor = node.walk();
282                let children: Vec<_> = node.children(&mut cursor).collect();
283                for child in children.into_iter().rev() {
284                    stack.push((child, class_name.clone()));
285                }
286                descended_with_new_parent = true;
287            }
288            "interface_declaration" => {
289                push_named_definition(&node, source, DefinitionKind::Interface, current_parent, out)
290            }
291            "type_alias_declaration" => {
292                push_named_definition(&node, source, DefinitionKind::TypeAlias, current_parent, out)
293            }
294            "enum_declaration" => {
295                push_named_definition(&node, source, DefinitionKind::EnumDef, current_parent, out)
296            }
297            "lexical_declaration" | "variable_declaration" => {
298                let mut cursor = node.walk();
299                for child in node.children(&mut cursor) {
300                    if child.kind() == "variable_declarator"
301                        && let Some(name_node) = child.child_by_field_name("name")
302                    {
303                        let name = node_text(&name_node, source).to_string();
304                        if name.is_empty() {
305                            continue;
306                        }
307                        if let Some(value_node) = child.child_by_field_name("value") {
308                            let vkind = value_node.kind();
309                            let dk = if vkind == "arrow_function"
310                                || vkind == "function"
311                                || vkind == "function_expression"
312                            {
313                                DefinitionKind::Function
314                            } else {
315                                DefinitionKind::ConstDecl
316                            };
317                            out.push(Definition {
318                                name,
319                                kind: dk,
320                                start_line: node.start_position().row as u32 + 1,
321                                end_line: node.end_position().row as u32 + 1,
322                                parent_name: current_parent.map(String::from),
323                            });
324                        }
325                    }
326                }
327            }
328
329            // ── C / C++ / Java ───────────────────────────────────
330            "struct_specifier" | "class_specifier" => {
331                push_named_definition(&node, source, DefinitionKind::Class, current_parent, out)
332            }
333            "namespace_definition" => {
334                push_named_definition(&node, source, DefinitionKind::Module, current_parent, out)
335            }
336            "enum_specifier" => {
337                push_named_definition(&node, source, DefinitionKind::EnumDef, current_parent, out)
338            }
339            "constructor_declaration" => {
340                push_named_definition(&node, source, DefinitionKind::Function, current_parent, out)
341            }
342
343            _ => {}
344        }
345
346        if !descended_with_new_parent {
347            let mut cursor = node.walk();
348            let children: Vec<_> = node.children(&mut cursor).collect();
349            for child in children.into_iter().rev() {
350                stack.push((child, parent.clone()));
351            }
352        }
353    }
354}
355
356fn extract_rust_impl_type_name(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
357    let type_node = node.child_by_field_name("type")?;
358    Some(extract_type_identifier(&type_node, source))
359}
360
361fn extract_type_identifier(node: &tree_sitter::Node, source: &[u8]) -> String {
362    match node.kind() {
363        "type_identifier" | "identifier" => node_text(node, source).to_string(),
364        "generic_type" | "scoped_type_identifier" => {
365            let mut cursor = node.walk();
366            for child in node.children(&mut cursor) {
367                if child.kind() == "type_identifier" || child.kind() == "identifier" {
368                    return node_text(&child, source).to_string();
369                }
370            }
371            node_text(node, source).to_string()
372        }
373        _ => node_text(node, source).to_string(),
374    }
375}
376
377fn extract_go_receiver_type(node: &tree_sitter::Node, source: &[u8]) -> Option<String> {
378    let params = node.child_by_field_name("receiver")?;
379    let mut cursor = params.walk();
380    for child in params.children(&mut cursor) {
381        if child.kind() == "parameter_declaration"
382            && let Some(type_node) = child.child_by_field_name("type")
383        {
384            let text = node_text(&type_node, source);
385            return Some(text.trim_start_matches('*').to_string());
386        }
387    }
388    None
389}
390
391/// Resolve a symbol name to a line range in source code.
392///
393/// Supports qualified names like `Repository::open` (splits on `::`).
394/// For qualified names, the part before `::` is matched against the parent
395/// scope (impl block, class, etc.) and the part after is the definition name.
396///
397/// Returns `(start_line, end_line)` as 1-indexed, inclusive line numbers.
398pub fn resolve_symbol_lines(
399    source: &[u8],
400    path: &Path,
401    symbol: &str,
402) -> Result<(u32, u32), SymbolResolveError> {
403    let language = Language::from_path(path);
404    language.parser_handle().ok_or_else(|| {
405        SymbolResolveError::UnsupportedLanguage(
406            path.extension()
407                .map(|e| e.to_string_lossy().into_owned())
408                .unwrap_or_else(|| "<none>".to_string()),
409        )
410    })?;
411    let source_text = std::str::from_utf8(source).map_err(|_| SymbolResolveError::ParseFailed)?;
412    let parsed = ParsedFile::parse(source_text, language).ok_or(SymbolResolveError::ParseFailed)?;
413
414    // Split qualified name: "Repository::open" -> parent="Repository", target="open"
415    let (parent_filter, target_name) = if let Some(pos) = symbol.rfind("::") {
416        (Some(&symbol[..pos]), &symbol[pos + 2..])
417    } else {
418        (None, symbol)
419    };
420
421    let definitions = find_definitions(&parsed.root_node(), source, target_name);
422
423    // If a parent filter is specified, prefer matches where the parent matches.
424    let matched = if let Some(parent) = parent_filter {
425        definitions
426            .iter()
427            .find(|d| {
428                d.parent_name
429                    .as_deref()
430                    .map(|p| p == parent)
431                    .unwrap_or(false)
432            })
433            .or_else(|| definitions.first())
434    } else {
435        definitions.first()
436    };
437
438    match matched {
439        Some(sym) => Ok((sym.start_line, sym.end_line)),
440        None => Err(SymbolResolveError::SymbolNotFound(symbol.to_string())),
441    }
442}
443
444/// Resolve all definitions of a symbol name, returning all matches.
445///
446/// This is useful when a symbol appears in multiple contexts (e.g.,
447/// multiple impl blocks). Returns an empty vec if no matches found.
448pub fn resolve_all_symbols(
449    source: &[u8],
450    path: &Path,
451    symbol: &str,
452) -> Result<Vec<ResolvedSymbol>, SymbolResolveError> {
453    let language = Language::from_path(path);
454    language.parser_handle().ok_or_else(|| {
455        SymbolResolveError::UnsupportedLanguage(
456            path.extension()
457                .map(|e| e.to_string_lossy().into_owned())
458                .unwrap_or_else(|| "<none>".to_string()),
459        )
460    })?;
461    let source_text = std::str::from_utf8(source).map_err(|_| SymbolResolveError::ParseFailed)?;
462    let parsed = ParsedFile::parse(source_text, language).ok_or(SymbolResolveError::ParseFailed)?;
463
464    let (parent_filter, target_name) = if let Some(pos) = symbol.rfind("::") {
465        (Some(&symbol[..pos]), &symbol[pos + 2..])
466    } else {
467        (None, symbol)
468    };
469
470    let definitions = find_definitions(&parsed.root_node(), source, target_name);
471
472    if let Some(parent) = parent_filter {
473        let filtered: Vec<_> = definitions
474            .into_iter()
475            .filter(|d| {
476                d.parent_name
477                    .as_deref()
478                    .map(|p| p == parent)
479                    .unwrap_or(false)
480            })
481            .collect();
482        Ok(filtered)
483    } else {
484        Ok(definitions)
485    }
486}
487
488/// Extract a range of lines from source bytes.
489///
490/// `start` and `end` are 1-indexed, inclusive. Returns the bytes
491/// for those lines (including newlines).
492pub fn extract_line_range(source: &[u8], start: u32, end: u32) -> Vec<u8> {
493    let mut line: u32 = 1;
494    let mut byte_start = 0;
495
496    for (i, &b) in source.iter().enumerate() {
497        if line == start {
498            byte_start = i;
499            break;
500        }
501        if b == b'\n' {
502            line += 1;
503        }
504    }
505
506    if line < start {
507        return Vec::new();
508    }
509
510    for (i, &b) in source[byte_start..].iter().enumerate() {
511        if b == b'\n' {
512            line += 1;
513            if line > end {
514                return source[byte_start..byte_start + i + 1].to_vec();
515            }
516        }
517    }
518
519    source[byte_start..].to_vec()
520}
521
522#[cfg(test)]
523mod tests {
524    use super::*;
525
526    #[test]
527    fn resolve_rust_fn_main() {
528        let source = br#"
529fn helper() -> bool {
530    true
531}
532
533fn main() {
534    println!("hello");
535    let x = 1;
536}
537
538fn after() {}
539"#;
540        let path = Path::new("test.rs");
541        let (start, end) = resolve_symbol_lines(source, path, "main").unwrap();
542        assert_eq!(start, 6);
543        assert_eq!(end, 9);
544    }
545
546    #[test]
547    fn resolve_rust_qualified_impl_method() {
548        let source = br#"
549struct Repository {
550    path: String,
551}
552
553impl Repository {
554    pub fn open(path: &str) -> Self {
555        Repository {
556            path: path.to_string(),
557        }
558    }
559
560    pub fn close(&self) {}
561}
562
563impl Default for Repository {
564    fn default() -> Self {
565        Repository::open(".")
566    }
567}
568"#;
569        let path = Path::new("repo.rs");
570        let (start, end) = resolve_symbol_lines(source, path, "Repository::open").unwrap();
571        assert_eq!(start, 7);
572        assert_eq!(end, 11);
573    }
574
575    #[test]
576    fn resolve_rust_struct() {
577        let source = br#"
578pub struct Config {
579    pub name: String,
580    pub value: u32,
581}
582"#;
583        let path = Path::new("config.rs");
584        let (start, end) = resolve_symbol_lines(source, path, "Config").unwrap();
585        assert_eq!(start, 2);
586        assert_eq!(end, 5);
587    }
588
589    #[test]
590    fn resolve_python_function() {
591        let source = br#"
592def helper():
593    pass
594
595def process_data(items):
596    result = []
597    for item in items:
598        result.append(item * 2)
599    return result
600
601def cleanup():
602    pass
603"#;
604        let path = Path::new("main.py");
605        let (start, end) = resolve_symbol_lines(source, path, "process_data").unwrap();
606        assert_eq!(start, 5);
607        assert_eq!(end, 9);
608    }
609
610    #[test]
611    fn resolve_python_class_method() {
612        let source = br#"
613class Repository:
614    def __init__(self, path):
615        self.path = path
616
617    def open(self):
618        return True
619"#;
620        let path = Path::new("repo.py");
621        let (start, end) = resolve_symbol_lines(source, path, "Repository::open").unwrap();
622        assert_eq!(start, 6);
623        assert_eq!(end, 7);
624    }
625
626    #[test]
627    #[cfg(feature = "lang-go")]
628    fn resolve_go_function() {
629        let source = br#"package main
630
631func helper() bool {
632    return true
633}
634
635func processData(items []int) []int {
636    result := make([]int, 0)
637    for _, item := range items {
638        result = append(result, item*2)
639    }
640    return result
641}
642"#;
643        let path = Path::new("main.go");
644        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
645        assert_eq!(start, 7);
646        assert_eq!(end, 13);
647    }
648
649    #[test]
650    fn resolve_symbol_not_found() {
651        let source = br#"
652fn main() {}
653"#;
654        let path = Path::new("test.rs");
655        let err = resolve_symbol_lines(source, path, "nonexistent").unwrap_err();
656        assert!(matches!(err, SymbolResolveError::SymbolNotFound(_)));
657    }
658
659    #[test]
660    fn resolve_unsupported_extension() {
661        let source = b"some content";
662        let path = Path::new("test.xyz");
663        let err = resolve_symbol_lines(source, path, "main").unwrap_err();
664        assert!(matches!(err, SymbolResolveError::UnsupportedLanguage(_)));
665    }
666
667    #[test]
668    fn extract_line_range_basic() {
669        let source = b"line 1\nline 2\nline 3\nline 4\nline 5\n";
670        let result = extract_line_range(source, 2, 4);
671        assert_eq!(result, b"line 2\nline 3\nline 4\n");
672    }
673
674    #[test]
675    fn extract_line_range_single_line() {
676        let source = b"line 1\nline 2\nline 3\n";
677        let result = extract_line_range(source, 2, 2);
678        assert_eq!(result, b"line 2\n");
679    }
680
681    #[test]
682    fn resolve_js_function_declaration() {
683        let source = br#"
684function helper() {
685    return true;
686}
687
688function processData(items) {
689    return items.map(x => x * 2);
690}
691"#;
692        let path = Path::new("main.js");
693        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
694        assert_eq!(start, 6);
695        assert_eq!(end, 8);
696    }
697
698    #[test]
699    fn resolve_js_arrow_function_const() {
700        let source = br#"
701const helper = () => true;
702
703const processData = (items) => {
704    return items.map(x => x * 2);
705};
706"#;
707        let path = Path::new("utils.js");
708        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
709        assert_eq!(start, 4);
710        assert_eq!(end, 6);
711    }
712
713    /// Regression: real-world TS code often defines methods as arrow-
714    /// function properties of an object literal (e.g. a `db` helper).
715    /// The variable_declarator branch missed these — `pair` handling
716    /// catches them. Without this, `heddle context set --scope symbol:insert`
717    /// against `export const db = { insert: async () => {...} }` shipped
718    /// `resolved_lines: None` and the chip never rendered.
719    #[test]
720    fn resolve_typescript_object_literal_property_arrow_function() {
721        let source = br#"
722export const db = {
723    query: async (sql: string) => {
724        return [];
725    },
726    insert: async (table: string, data: Record<string, any>) => {
727        const keys = Object.keys(data);
728        return keys;
729    },
730};
731"#;
732        let path = Path::new("db.ts");
733        let (start, end) = resolve_symbol_lines(source, path, "insert").unwrap();
734        // `insert` lives at lines 6–9 in the source above (1-indexed,
735        // counting the leading newline as line 1).
736        assert!((5..=7).contains(&start), "got start={start}");
737        assert!(end > start && end <= 10, "got end={end}");
738    }
739
740    #[test]
741    fn resolve_typescript_function() {
742        let source = br#"
743function helper(): boolean {
744    return true;
745}
746
747function processData(items: number[]): number[] {
748    return items.map(x => x * 2);
749}
750"#;
751        let path = Path::new("main.ts");
752        let (start, end) = resolve_symbol_lines(source, path, "processData").unwrap();
753        assert_eq!(start, 6);
754        assert_eq!(end, 8);
755    }
756
757    #[test]
758    fn resolve_all_returns_multiple_matches() {
759        let source = br#"
760impl Foo {
761    fn do_thing(&self) {}
762}
763
764impl Bar {
765    fn do_thing(&self) {}
766}
767"#;
768        let path = Path::new("test.rs");
769        let results = resolve_all_symbols(source, path, "do_thing").unwrap();
770        assert_eq!(results.len(), 2);
771        assert_eq!(results[0].parent_name.as_deref(), Some("Foo"));
772        assert_eq!(results[1].parent_name.as_deref(), Some("Bar"));
773    }
774
775    #[test]
776    fn extract_definitions_reports_rust_taxonomy_parent_scopes_and_ranges() {
777        let source = br#"const LIMIT: usize = 10;
778pub mod outer {
779    pub struct Widget {
780        pub id: u64,
781    }
782
783    pub enum Mode {
784        Fast,
785        Slow,
786    }
787
788    pub trait Runner {
789        fn run(&self);
790    }
791
792    pub type WidgetResult<T> = Result<T, Error>;
793
794    impl Widget {
795        pub fn build(id: u64) -> Self {
796            Self { id }
797        }
798    }
799}
800"#;
801
802        let defs = extract_definitions(source, Path::new("lib.rs")).unwrap();
803
804        assert_definition(&defs, "LIMIT", DefinitionKind::ConstDecl, 1, 1, None);
805        assert_definition(&defs, "outer", DefinitionKind::Module, 2, 23, None);
806        assert_definition(&defs, "Widget", DefinitionKind::Type, 3, 5, None);
807        assert_definition(&defs, "Mode", DefinitionKind::EnumDef, 7, 10, None);
808        assert_definition(&defs, "Runner", DefinitionKind::Trait, 12, 14, None);
809        assert_definition(
810            &defs,
811            "WidgetResult",
812            DefinitionKind::TypeAlias,
813            16,
814            16,
815            None,
816        );
817        assert_definition(
818            &defs,
819            "build",
820            DefinitionKind::Function,
821            19,
822            21,
823            Some("Widget"),
824        );
825    }
826
827    #[test]
828    fn extract_definitions_reports_typescript_taxonomy_parent_scopes_and_ranges() {
829        let source = br#"interface Service {
830    run(): void;
831}
832
833type Handler = (value: string) => void;
834
835enum Status {
836    Ready,
837    Done,
838}
839
840class Controller {
841    start(): void {
842        handle("start");
843    }
844}
845
846export const handle = (value: string): void => {
847    console.log(value);
848};
849
850export const settings = { retry: 2 };
851"#;
852
853        let defs = extract_definitions(source, Path::new("controller.ts")).unwrap();
854
855        assert_definition(&defs, "Service", DefinitionKind::Interface, 1, 3, None);
856        assert_definition(&defs, "Handler", DefinitionKind::TypeAlias, 5, 5, None);
857        assert_definition(&defs, "Status", DefinitionKind::EnumDef, 7, 10, None);
858        assert_definition(&defs, "Controller", DefinitionKind::Class, 12, 16, None);
859        assert_definition(
860            &defs,
861            "start",
862            DefinitionKind::Function,
863            13,
864            15,
865            Some("Controller"),
866        );
867        assert_definition(&defs, "handle", DefinitionKind::Function, 18, 20, None);
868        assert_definition(&defs, "settings", DefinitionKind::ConstDecl, 22, 22, None);
869    }
870
871    #[test]
872    fn extract_definitions_rejects_parse_error_trees() {
873        let err =
874            extract_definitions(b"fn broken( -> usize { 1 }", Path::new("broken.rs")).unwrap_err();
875
876        assert!(matches!(err, SymbolResolveError::ParseFailed));
877    }
878
879    /// Characterization: iterative `walk_definitions` must emit the same
880    /// definitions in the same source order with the same parent scopes as
881    /// the former recursive walker on a multi-level, multi-kind fixture.
882    #[test]
883    fn walk_definitions_iterative_matches_recursive_output_on_nested_fixture() {
884        let source = br#"const LIMIT: usize = 10;
885pub mod outer {
886    pub struct Widget {
887        pub id: u64,
888    }
889
890    pub enum Mode {
891        Fast,
892        Slow,
893    }
894
895    pub trait Runner {
896        fn run(&self);
897    }
898
899    pub type WidgetResult<T> = Result<T, Error>;
900
901    impl Widget {
902        pub fn build(id: u64) -> Self {
903            Self { id }
904        }
905    }
906}
907"#;
908
909        let defs = extract_definitions(source, Path::new("lib.rs")).unwrap();
910
911        let expected: &[(&str, DefinitionKind, u32, u32, Option<&str>)] = &[
912            ("LIMIT", DefinitionKind::ConstDecl, 1, 1, None),
913            ("outer", DefinitionKind::Module, 2, 23, None),
914            ("Widget", DefinitionKind::Type, 3, 5, None),
915            ("Mode", DefinitionKind::EnumDef, 7, 10, None),
916            ("Runner", DefinitionKind::Trait, 12, 14, None),
917            ("WidgetResult", DefinitionKind::TypeAlias, 16, 16, None),
918            ("build", DefinitionKind::Function, 19, 21, Some("Widget")),
919        ];
920
921        assert_eq!(defs.len(), expected.len(), "definition count: {defs:?}");
922        for (def, (name, kind, start, end, parent)) in defs.iter().zip(expected.iter()) {
923            assert_eq!(&def.name, name);
924            assert_eq!(def.kind, *kind);
925            assert_eq!(def.start_line, *start);
926            assert_eq!(def.end_line, *end);
927            assert_eq!(def.parent_name.as_deref(), *parent);
928        }
929    }
930
931    // HEDDLE-DR-4 / #876: walk_definitions must not stack-overflow on
932    // deeply-nested but syntactically-valid trees (mirrors symbol_extraction).
933    #[cfg(feature = "lang-rust")]
934    #[test]
935    fn deeply_nested_rust_modules_walk_definitions_does_not_stack_overflow() {
936        let depth = 2000usize;
937        let mut s = String::new();
938        for i in 0..depth {
939            s.push_str(&format!("mod m{i} {{\n"));
940        }
941        s.push_str("fn target() {}\n");
942        for _ in 0..depth {
943            s.push_str("}\n");
944        }
945
946        let source = s.into_bytes();
947        let path = Path::new("nested.rs");
948
949        let handle = std::thread::Builder::new()
950            .stack_size(128 * 1024)
951            .spawn(move || extract_definitions(&source, path))
952            .expect("spawn");
953        let defs = handle
954            .join()
955            .expect("walk_definitions must not stack-overflow on deeply-nested input")
956            .expect("parse nested modules");
957        assert!(
958            defs.iter().any(|d| d.name == "target"),
959            "deep target fn must be returned, not silently dropped; got {defs:?}"
960        );
961    }
962
963    fn assert_definition(
964        defs: &[Definition],
965        name: &str,
966        kind: DefinitionKind,
967        start_line: u32,
968        end_line: u32,
969        parent_name: Option<&str>,
970    ) {
971        assert!(
972            defs.iter().any(|def| {
973                def.name == name
974                    && def.kind == kind
975                    && def.start_line == start_line
976                    && def.end_line == end_line
977                    && def.parent_name.as_deref() == parent_name
978            }),
979            "expected {name:?} {kind:?} lines {start_line}-{end_line} parent {parent_name:?}, got: {defs:?}"
980        );
981    }
982}