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