Skip to main content

normalize_scope/
lib.rs

1//! Scope analysis engine using tree-sitter locals queries.
2//!
3//! Parses `locals.scm` query files to resolve symbol references to their
4//! definitions within a single source file. Uses the tree-sitter locals
5//! convention:
6//!
7//! - `@local.scope` — marks a node that creates a new lexical scope
8//! - `@local.definition` / `@local.definition.*` — marks a name-binding site
9//! - `@local.reference` — marks an identifier that refers to a bound name
10//!
11//! ## Handling destructuring patterns: `@local.definition.each` + `@local.binding-leaf`
12//!
13//! Tree-sitter queries only match direct children (`(A (B))` requires `B` to be
14//! a named child of `A`). Arbitrarily nested destructuring (`{ a: { b } }`)
15//! can't be expressed in a finite set of fixed-depth query patterns.
16//!
17//! This engine supports two extension captures that work together:
18//!
19//! - **`@local.binding-leaf`** — declares which node kinds count as binding
20//!   identifiers in this language. The engine collects these kinds from all
21//!   matches in the query pass.
22//! - **`@local.definition.each`** — captures a container node (e.g. a pattern
23//!   or parameter node) and triggers recursive descent, emitting a definition
24//!   for every descendant leaf whose kind is in the `@local.binding-leaf` set.
25//!
26//! Example (`javascript.locals.scm`):
27//!
28//! ```text
29//! ; Declare binding leaf kinds for this language
30//! (identifier) @local.binding-leaf
31//! (shorthand_property_identifier_pattern) @local.binding-leaf
32//!
33//! ; Recurse into each parameter child — handles f(x), f({ a: { b } }), f([x, y])
34//! (formal_parameters (_) @local.definition.each)
35//! ```
36//!
37//! The engine has no hardcoded knowledge of which node kinds are bindings in any
38//! given language — that belongs entirely in the `.scm` file.
39//!
40//! # Usage
41//!
42//! ```ignore
43//! use normalize_scope::ScopeEngine;
44//! use normalize_languages::GrammarLoader;
45//!
46//! let loader = GrammarLoader::new();
47//! let engine = ScopeEngine::new(&loader);
48//!
49//! let refs = engine.find_references("javascript", source, "myVar");
50//! for r in refs {
51//!     println!("{}:{} -> def at {:?}", r.location.line, r.location.column, r.definition);
52//! }
53//! ```
54
55use normalize_languages::GrammarLoader;
56use streaming_iterator::StreamingIterator;
57
58/// A location in source code.
59#[derive(Debug, Clone, serde::Serialize, schemars::JsonSchema)]
60pub struct Location {
61    /// 1-based line number.
62    pub line: usize,
63    /// 1-based column number.
64    pub column: usize,
65    pub start_byte: usize,
66    pub end_byte: usize,
67}
68
69/// A reference to a symbol, with optional resolved definition.
70#[derive(Debug, Clone, serde::Serialize, schemars::JsonSchema)]
71pub struct Reference {
72    pub name: String,
73    pub location: Location,
74    /// Definition this reference resolves to, if resolvable via scope walk.
75    pub definition: Option<Location>,
76}
77
78/// A symbol definition site.
79#[derive(Debug, Clone, serde::Serialize, schemars::JsonSchema)]
80pub struct Definition {
81    pub name: String,
82    pub location: Location,
83    /// Sub-kind of the definition, derived from the capture name suffix.
84    ///
85    /// `@local.definition` → `None`
86    /// `@local.definition.parameter` → `Some("parameter")`
87    /// `@local.definition.function` → `Some("function")`
88    #[serde(skip_serializing_if = "Option::is_none")]
89    pub kind: Option<String>,
90}
91
92/// Scope analysis engine backed by tree-sitter locals queries.
93///
94/// Requires `locals.scm` query files to be present in the grammar search paths
95/// (copied there by `cargo xtask build-grammars`).
96pub struct ScopeEngine<'a> {
97    loader: &'a GrammarLoader,
98}
99
100impl<'a> ScopeEngine<'a> {
101    pub fn new(loader: &'a GrammarLoader) -> Self {
102        Self { loader }
103    }
104
105    /// Returns true if locals.scm is available for this language.
106    pub fn has_locals(&self, lang: &str) -> bool {
107        self.loader.get_locals(lang).is_some()
108    }
109
110    /// Find all definitions of `name` in `source`.
111    pub fn find_definitions(&self, lang: &str, source: &str, name: &str) -> Vec<Definition> {
112        let Some(analysis) = self.analyze(lang, source) else {
113            return Vec::new();
114        };
115        analysis
116            .definitions
117            .into_iter()
118            .filter(|d| d.name == name)
119            .map(|d| Definition {
120                name: d.name,
121                location: d.location,
122                kind: d.kind,
123            })
124            .collect()
125    }
126
127    /// Find all references to `name` in `source`, with definition resolution.
128    ///
129    /// Returns both definition sites and reference sites that resolve to any
130    /// definition of `name` in this file.
131    pub fn find_references(&self, lang: &str, source: &str, name: &str) -> Vec<Reference> {
132        let Some(analysis) = self.analyze(lang, source) else {
133            return Vec::new();
134        };
135        analysis
136            .references
137            .into_iter()
138            .filter(|r| r.name == name)
139            .collect()
140    }
141
142    /// Get all definitions in `source`.
143    pub fn all_definitions(&self, lang: &str, source: &str) -> Vec<Definition> {
144        let Some(analysis) = self.analyze(lang, source) else {
145            return Vec::new();
146        };
147        analysis
148            .definitions
149            .into_iter()
150            .map(|d| Definition {
151                name: d.name,
152                location: d.location,
153                kind: d.kind,
154            })
155            .collect()
156    }
157
158    /// Find parameters that are never referenced in their enclosing function body.
159    ///
160    /// Returns definitions whose `kind` is `"parameter"` and whose `name` has no
161    /// resolved reference in the same file. Underscore-prefixed names (`_`, `_foo`)
162    /// are excluded — those are the conventional way to mark intentionally unused
163    /// parameters in most languages.
164    ///
165    /// Requires `@local.definition.parameter` captures in the language's `locals.scm`.
166    /// Languages without that capture produce an empty result.
167    pub fn find_unused_parameters(&self, lang: &str, source: &str) -> Vec<Definition> {
168        let Some(analysis) = self.analyze(lang, source) else {
169            return Vec::new();
170        };
171
172        // Build a set of names that have at least one resolved reference.
173        let referenced_names: std::collections::HashSet<&str> = analysis
174            .references
175            .iter()
176            .filter(|r| r.definition.is_some())
177            .map(|r| r.name.as_str())
178            .collect();
179
180        analysis
181            .definitions
182            .into_iter()
183            .filter(|d| d.kind.as_deref() == Some("parameter"))
184            // Underscore-prefixed names are intentionally unused by convention.
185            .filter(|d| !d.name.starts_with('_'))
186            .filter(|d| !referenced_names.contains(d.name.as_str()))
187            .map(|d| Definition {
188                name: d.name,
189                location: d.location,
190                kind: d.kind,
191            })
192            .collect()
193    }
194
195    /// Analyze a source file: collect all scopes, definitions, and references,
196    /// resolving each reference to its definition via scope walk.
197    fn analyze(&self, lang: &str, source: &str) -> Option<FileAnalysis> {
198        let grammar = self.loader.get(lang).ok()?;
199        let locals_src = self.loader.get_locals(lang)?;
200
201        let query = tree_sitter::Query::new(&grammar, &locals_src).ok()?;
202
203        let mut parser = tree_sitter::Parser::new();
204        parser.set_language(&grammar).ok()?;
205        let tree = parser.parse(source, None)?;
206
207        let capture_names: Vec<String> = query
208            .capture_names()
209            .iter()
210            .map(|s| s.to_string())
211            .collect();
212
213        // First pass: collect binding leaf kinds declared via @local.binding-leaf
214        // and defer @local.definition.each nodes (can't expand until leaf kinds known).
215        let mut binding_leaf_kinds: std::collections::HashSet<String> =
216            std::collections::HashSet::new();
217        let mut deferred_each: Vec<tree_sitter::Node> = Vec::new();
218        let mut scopes: Vec<ScopeRange> = Vec::new();
219        let mut raw_defs: Vec<RawCapture> = Vec::new();
220        let mut raw_refs: Vec<RawCapture> = Vec::new();
221
222        let mut cursor = tree_sitter::QueryCursor::new();
223        let mut matches = cursor.matches(&query, tree.root_node(), source.as_bytes());
224
225        while let Some(m) = matches.next() {
226            // Evaluate custom predicates (general_predicates) that tree-sitter
227            // doesn't handle natively. This covers `#is-match-op!` and similar
228            // language-specific filters in locals.scm files.
229            if !check_general_predicates(m, &query, source.as_bytes()) {
230                continue;
231            }
232
233            for cap in m.captures {
234                let name = &capture_names[cap.index as usize];
235                let node = cap.node;
236                let Ok(text) = node.utf8_text(source.as_bytes()) else {
237                    continue;
238                };
239
240                let loc = Location {
241                    line: node.start_position().row + 1,
242                    column: node.start_position().column + 1,
243                    start_byte: node.start_byte(),
244                    end_byte: node.end_byte(),
245                };
246
247                if name == "local.binding-leaf" {
248                    // Declares which leaf node kinds @local.definition.each should
249                    // collect when recursing. The .scm file is the authority on
250                    // what counts as a binding identifier in that language.
251                    binding_leaf_kinds.insert(node.kind().to_string());
252                } else if name == "local.definition.each" {
253                    // Defer: we need binding_leaf_kinds fully populated first.
254                    deferred_each.push(node);
255                } else if name == "local.scope" {
256                    scopes.push(ScopeRange {
257                        start_byte: node.start_byte(),
258                        end_byte: node.end_byte(),
259                    });
260                } else if name.starts_with("local.definition") {
261                    // Extract subkind: "local.definition.parameter" → Some("parameter")
262                    let kind = name
263                        .strip_prefix("local.definition.")
264                        .filter(|s| !s.is_empty())
265                        .map(|s| s.to_string());
266                    raw_defs.push(RawCapture {
267                        name: text.to_string(),
268                        location: loc,
269                        kind,
270                    });
271                } else if name == "local.reference" {
272                    raw_refs.push(RawCapture {
273                        name: text.to_string(),
274                        location: loc,
275                        kind: None,
276                    });
277                }
278            }
279        }
280
281        // Expand deferred @local.definition.each nodes now that binding_leaf_kinds is complete.
282        for node in deferred_each {
283            collect_binding_identifiers(
284                node,
285                source.as_bytes(),
286                &binding_leaf_kinds,
287                &mut raw_defs,
288            );
289        }
290
291        // Resolve references to definitions via scope walk
292        let references: Vec<Reference> = raw_refs
293            .into_iter()
294            .map(|r| {
295                let definition = resolve_reference(&r, &scopes, &raw_defs);
296                Reference {
297                    name: r.name,
298                    location: r.location,
299                    definition,
300                }
301            })
302            .collect();
303
304        Some(FileAnalysis {
305            definitions: raw_defs,
306            references,
307        })
308    }
309}
310
311// ── Custom predicate evaluation ─────────────────────────────────────────────
312
313/// Evaluate custom (general) predicates that tree-sitter doesn't handle natively.
314///
315/// Built-in text predicates (`#eq?`, `#any-of?`, `#match?`) are evaluated
316/// automatically by `QueryCursor::matches` via `satisfies_text_predicates`.
317/// However, they only work reliably on **named** node captures. Unnamed node
318/// captures in field position (e.g. `operator: _ @op`) are silently skipped.
319///
320/// This function handles custom predicates defined in locals.scm files:
321///
322/// - `#is-match-op!` — checks that the captured node has an unnamed `=` child.
323///   Used to filter `binary_operator` matches to only assignment (`x = expr`).
324///
325/// Unknown predicates pass through (return true) to avoid breaking other queries.
326fn check_general_predicates(
327    m: &tree_sitter::QueryMatch<'_, '_>,
328    query: &tree_sitter::Query,
329    source: &[u8],
330) -> bool {
331    use tree_sitter::QueryPredicateArg;
332    query
333        .general_predicates(m.pattern_index)
334        .iter()
335        .all(|pred| match pred.operator.as_ref() {
336            "is-match-op!" => {
337                // Expect one capture arg: the node to inspect for an `=` child.
338                if let Some(QueryPredicateArg::Capture(cap_idx)) = pred.args.first() {
339                    m.captures
340                        .iter()
341                        .filter(|c| c.index == *cap_idx)
342                        .any(|c| node_has_unnamed_child(c.node, "=", source))
343                } else {
344                    true
345                }
346            }
347            _ => true,
348        })
349}
350
351/// Returns true if `node` has an unnamed child whose source text equals `text`.
352fn node_has_unnamed_child(node: tree_sitter::Node<'_>, text: &str, source: &[u8]) -> bool {
353    (0..node.child_count()).any(|i| {
354        node.child(i as u32)
355            .is_some_and(|child| !child.is_named() && child.utf8_text(source) == Ok(text))
356    })
357}
358
359// ── Internal types ──────────────────────────────────────────────────────────
360
361struct FileAnalysis {
362    definitions: Vec<RawCapture>,
363    references: Vec<Reference>,
364}
365
366struct ScopeRange {
367    start_byte: usize,
368    end_byte: usize,
369}
370
371struct RawCapture {
372    name: String,
373    location: Location,
374    /// Sub-kind extracted from the capture name (e.g. "parameter" from "local.definition.parameter").
375    kind: Option<String>,
376}
377
378/// Resolve a reference to its definition by walking up the scope chain.
379///
380/// Algorithm:
381/// 1. Find all scope ranges that contain the reference (by byte offset)
382/// 2. Sort them innermost-first (smallest range = most specific scope)
383/// 3. For each scope: look for a definition with matching name that:
384///    a. Is within that scope's byte range
385///    b. Appears before the reference (textual order, handles forward refs only for types)
386/// 4. Return the first match found
387fn resolve_reference(
388    r: &RawCapture,
389    scopes: &[ScopeRange],
390    definitions: &[RawCapture],
391) -> Option<Location> {
392    let ref_start = r.location.start_byte;
393
394    // Find all scopes containing this reference, sorted innermost-first
395    let mut containing: Vec<&ScopeRange> = scopes
396        .iter()
397        .filter(|s| s.start_byte <= ref_start && ref_start < s.end_byte)
398        .collect();
399    // Sort by scope size ascending (smallest = innermost = highest priority)
400    containing.sort_by_key(|s| s.end_byte - s.start_byte);
401
402    for scope in &containing {
403        let def = definitions.iter().find(|d| {
404            d.name == r.name
405                && d.location.start_byte >= scope.start_byte
406                && d.location.start_byte < scope.end_byte
407                && d.location.start_byte < ref_start
408        });
409        if let Some(d) = def {
410            return Some(d.location.clone());
411        }
412    }
413
414    None
415}
416
417/// Recursively collect all binding identifier leaf nodes from a pattern node.
418///
419/// Used by `@local.definition.each`. Recurses into the subtree and emits a
420/// `RawCapture` for every named leaf node whose kind is in `binding_leaf_kinds`.
421/// That set is populated from `@local.binding-leaf` captures in the same query,
422/// so the `.scm` file (not the engine) defines what counts as a binding identifier.
423fn collect_binding_identifiers(
424    node: tree_sitter::Node,
425    source: &[u8],
426    binding_leaf_kinds: &std::collections::HashSet<String>,
427    out: &mut Vec<RawCapture>,
428) {
429    if !node.is_named() {
430        return;
431    }
432    if node.child_count() == 0 {
433        if binding_leaf_kinds.contains(node.kind())
434            && let Ok(text) = node.utf8_text(source)
435        {
436            out.push(RawCapture {
437                name: text.to_string(),
438                location: Location {
439                    line: node.start_position().row + 1,
440                    column: node.start_position().column + 1,
441                    start_byte: node.start_byte(),
442                    end_byte: node.end_byte(),
443                },
444                kind: None,
445            });
446        }
447    } else {
448        let mut cursor = node.walk();
449        for child in node.children(&mut cursor) {
450            collect_binding_identifiers(child, source, binding_leaf_kinds, out);
451        }
452    }
453}
454
455#[cfg(test)]
456mod tests {
457    use super::*;
458
459    fn print_tree(lang: &str, src: &str, loader: &GrammarLoader) {
460        let Some(grammar) = loader.get(lang).ok() else {
461            eprintln!("no grammar for {lang}");
462            return;
463        };
464        let mut parser = tree_sitter::Parser::new();
465        // normalize-syntax-allow: rust/unwrap-in-impl - test helper, panic is appropriate
466        parser.set_language(&grammar).unwrap();
467        // normalize-syntax-allow: rust/unwrap-in-impl - test helper, panic is appropriate
468        let tree = parser.parse(src, None).unwrap();
469        fn walk(node: tree_sitter::Node, src: &[u8], indent: usize) {
470            let text = node.utf8_text(src).unwrap_or("?");
471            let display = if text.len() > 40 { &text[..40] } else { text };
472            eprintln!(
473                "{}{} [{}..{}] {:?}",
474                "  ".repeat(indent),
475                node.kind(),
476                node.start_byte(),
477                node.end_byte(),
478                display
479            );
480            for child in node.children(&mut node.walk()) {
481                walk(child, src, indent + 1);
482            }
483        }
484        walk(tree.root_node(), src.as_bytes(), 0);
485    }
486
487    fn grammar_dir() -> Option<std::path::PathBuf> {
488        let p = std::path::Path::new(env!("CARGO_MANIFEST_DIR"))
489            .parent()
490            .and_then(|p| p.parent())
491            .map(|p| p.join("target/grammars"));
492        p.filter(|p| p.exists())
493    }
494
495    fn loader() -> GrammarLoader {
496        let mut l = GrammarLoader::new();
497        if let Some(dir) = grammar_dir() {
498            l.add_path(dir);
499        }
500        l
501    }
502
503    #[test]
504    fn test_has_locals_javascript() {
505        let l = loader();
506        let engine = ScopeEngine::new(&l);
507        // If grammars aren't built, skip gracefully
508        if l.get("javascript").is_err() {
509            return;
510        }
511        // javascript has locals.scm in arborium
512        let _ = engine.has_locals("javascript");
513    }
514
515    #[test]
516    fn test_no_locals_graceful() {
517        let l = GrammarLoader::with_paths(vec![]);
518        let engine = ScopeEngine::new(&l);
519        let refs = engine.find_references("rust", "fn main() {}", "main");
520        assert!(
521            refs.is_empty(),
522            "should return empty when no grammar/locals"
523        );
524    }
525
526    fn skip_if_no(l: &GrammarLoader, lang: &str) -> bool {
527        l.get(lang).is_err() || l.get_locals(lang).is_none()
528    }
529
530    fn skip_if_no_rust(l: &GrammarLoader) -> bool {
531        skip_if_no(l, "rust")
532    }
533
534    #[test]
535    fn test_rust_has_locals() {
536        let l = loader();
537        if l.get("rust").is_err() {
538            return;
539        }
540        let engine = ScopeEngine::new(&l);
541        assert!(
542            engine.has_locals("rust"),
543            "rust.locals.scm should be present after xtask build-grammars"
544        );
545    }
546
547    #[test]
548    fn test_rust_function_parameter() {
549        let l = loader();
550        if skip_if_no_rust(&l) {
551            return;
552        }
553        let engine = ScopeEngine::new(&l);
554        let src = "fn add(x: i32, y: i32) -> i32 { x + y }";
555        // `x` should be found: one definition (parameter) and one reference (body)
556        let refs = engine.find_references("rust", src, "x");
557        assert!(!refs.is_empty(), "x should appear as reference");
558        let has_def = refs.iter().any(|r| r.definition.is_some());
559        assert!(
560            has_def,
561            "x reference should resolve to its parameter definition"
562        );
563        let defs = engine.find_definitions("rust", src, "x");
564        assert_eq!(defs.len(), 1, "x should have exactly one definition");
565    }
566
567    #[test]
568    fn test_rust_let_binding() {
569        let l = loader();
570        if skip_if_no_rust(&l) {
571            return;
572        }
573        let engine = ScopeEngine::new(&l);
574        let src = "fn f() { let v = 1; let w = v + 1; w }";
575        let defs = engine.find_definitions("rust", src, "v");
576        assert_eq!(defs.len(), 1, "v should have one definition");
577        let refs = engine.find_references("rust", src, "v");
578        // At least one reference to v that resolves to the let binding
579        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
580        assert!(
581            resolved >= 1,
582            "v reference in body should resolve to let binding"
583        );
584    }
585
586    #[test]
587    fn test_rust_for_loop_variable() {
588        let l = loader();
589        if skip_if_no_rust(&l) {
590            return;
591        }
592        let engine = ScopeEngine::new(&l);
593        let src = "fn f() { for i in 0..10 { let _ = i; } }";
594        let defs = engine.find_definitions("rust", src, "i");
595        assert_eq!(
596            defs.len(),
597            1,
598            "for loop variable i should have one definition"
599        );
600        let refs = engine.find_references("rust", src, "i");
601        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
602        assert!(resolved >= 1, "i inside loop should resolve to for pattern");
603    }
604
605    #[test]
606    fn test_rust_closure_parameter() {
607        let l = loader();
608        if skip_if_no_rust(&l) {
609            return;
610        }
611        let engine = ScopeEngine::new(&l);
612        let src = "fn f() { let g = |a: i32| a * 2; }";
613        let defs = engine.find_definitions("rust", src, "a");
614        assert_eq!(defs.len(), 1, "closure param a should have one definition");
615        let refs = engine.find_references("rust", src, "a");
616        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
617        assert!(
618            resolved >= 1,
619            "a in closure body should resolve to closure param"
620        );
621    }
622
623    #[test]
624    fn test_rust_no_cross_scope_leakage() {
625        let l = loader();
626        if skip_if_no_rust(&l) {
627            return;
628        }
629        let engine = ScopeEngine::new(&l);
630        // x in first function should not resolve to x in second function
631        let src = "fn f(x: i32) -> i32 { x } fn g(x: i32) -> i32 { x }";
632        let defs = engine.find_definitions("rust", src, "x");
633        assert_eq!(defs.len(), 2, "two separate x parameter definitions");
634    }
635
636    // ── Python ───────────────────────────────────────────────────────────────
637
638    #[test]
639    fn test_python_function_parameter() {
640        let l = loader();
641        if skip_if_no(&l, "python") {
642            return;
643        }
644        let engine = ScopeEngine::new(&l);
645        let src = "def add(x, y):\n    return x + y\n";
646        let defs = engine.find_definitions("python", src, "x");
647        assert_eq!(defs.len(), 1, "python: x should have one definition");
648        let refs = engine.find_references("python", src, "x");
649        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
650        assert!(resolved >= 1, "python: x reference should resolve to param");
651    }
652
653    #[test]
654    fn test_python_assignment() {
655        let l = loader();
656        if skip_if_no(&l, "python") {
657            return;
658        }
659        let engine = ScopeEngine::new(&l);
660        let src = "def f():\n    v = 1\n    return v\n";
661        let defs = engine.find_definitions("python", src, "v");
662        assert_eq!(defs.len(), 1, "python: v should have one definition");
663    }
664
665    #[test]
666    fn test_python_for_variable() {
667        let l = loader();
668        if skip_if_no(&l, "python") {
669            return;
670        }
671        let engine = ScopeEngine::new(&l);
672        let src = "def f():\n    for i in range(10):\n        print(i)\n";
673        let defs = engine.find_definitions("python", src, "i");
674        assert_eq!(defs.len(), 1, "python: for loop variable i");
675    }
676
677    // ── Go ────────────────────────────────────────────────────────────────────
678
679    #[test]
680    fn test_go_function_parameter() {
681        let l = loader();
682        if skip_if_no(&l, "go") {
683            return;
684        }
685        let engine = ScopeEngine::new(&l);
686        let src = "package p\nfunc add(x int, y int) int { return x + y }\n";
687        let defs = engine.find_definitions("go", src, "x");
688        assert_eq!(defs.len(), 1, "go: x should have one definition");
689        let refs = engine.find_references("go", src, "x");
690        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
691        assert!(resolved >= 1, "go: x reference should resolve to param");
692    }
693
694    #[test]
695    fn test_go_short_var_decl() {
696        let l = loader();
697        if skip_if_no(&l, "go") {
698            return;
699        }
700        let engine = ScopeEngine::new(&l);
701        let src = "package p\nfunc f() {\n    v := 1\n    _ = v\n}\n";
702        let defs = engine.find_definitions("go", src, "v");
703        assert_eq!(defs.len(), 1, "go: short var decl v");
704    }
705
706    // ── Java ──────────────────────────────────────────────────────────────────
707
708    #[test]
709    fn test_java_method_parameter() {
710        let l = loader();
711        if skip_if_no(&l, "java") {
712            return;
713        }
714        let engine = ScopeEngine::new(&l);
715        let src = "class A { int add(int x, int y) { return x + y; } }\n";
716        let defs = engine.find_definitions("java", src, "x");
717        assert_eq!(defs.len(), 1, "java: x should have one definition");
718        let refs = engine.find_references("java", src, "x");
719        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
720        assert!(resolved >= 1, "java: x should resolve to param def");
721    }
722
723    #[test]
724    fn test_java_local_variable() {
725        let l = loader();
726        if skip_if_no(&l, "java") {
727            return;
728        }
729        let engine = ScopeEngine::new(&l);
730        let src = "class A { void f() { int v = 1; int w = v + 1; } }\n";
731        let defs = engine.find_definitions("java", src, "v");
732        assert_eq!(defs.len(), 1, "java: local variable v");
733    }
734
735    // ── C ─────────────────────────────────────────────────────────────────────
736
737    #[test]
738    fn test_c_function_parameter() {
739        let l = loader();
740        if skip_if_no(&l, "c") {
741            return;
742        }
743        let engine = ScopeEngine::new(&l);
744        let src = "int add(int x, int y) { return x + y; }\n";
745        let defs = engine.find_definitions("c", src, "x");
746        assert_eq!(defs.len(), 1, "c: x should have one definition");
747        let refs = engine.find_references("c", src, "x");
748        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
749        assert!(resolved >= 1, "c: x reference should resolve to param");
750    }
751
752    #[test]
753    fn test_c_local_variable() {
754        let l = loader();
755        if skip_if_no(&l, "c") {
756            return;
757        }
758        let engine = ScopeEngine::new(&l);
759        let src = "void f() { int v = 1; int w = v + 1; }\n";
760        let defs = engine.find_definitions("c", src, "v");
761        assert_eq!(defs.len(), 1, "c: local variable v");
762    }
763
764    // ── C++ ───────────────────────────────────────────────────────────────────
765
766    #[test]
767    fn test_cpp_function_parameter() {
768        let l = loader();
769        if skip_if_no(&l, "cpp") {
770            return;
771        }
772        let engine = ScopeEngine::new(&l);
773        let src = "int add(int x, int y) { return x + y; }\n";
774        let defs = engine.find_definitions("cpp", src, "x");
775        assert_eq!(defs.len(), 1, "cpp: x should have one definition");
776        let refs = engine.find_references("cpp", src, "x");
777        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
778        assert!(resolved >= 1, "cpp: x reference should resolve to param");
779    }
780
781    #[test]
782    fn test_cpp_local_variable() {
783        let l = loader();
784        if skip_if_no(&l, "cpp") {
785            return;
786        }
787        let engine = ScopeEngine::new(&l);
788        let src = "void f() { int v = 1; int w = v + 1; }\n";
789        let defs = engine.find_definitions("cpp", src, "v");
790        assert_eq!(defs.len(), 1, "cpp: local variable v");
791    }
792
793    // ── C# ────────────────────────────────────────────────────────────────────
794
795    #[test]
796    fn test_csharp_method_parameter() {
797        let l = loader();
798        if skip_if_no(&l, "c-sharp") {
799            return;
800        }
801        let engine = ScopeEngine::new(&l);
802        let src = "class A { int Add(int x, int y) { return x + y; } }\n";
803        let defs = engine.find_definitions("c-sharp", src, "x");
804        assert_eq!(defs.len(), 1, "c#: x should have one definition");
805        let refs = engine.find_references("c-sharp", src, "x");
806        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
807        assert!(resolved >= 1, "c#: x reference should resolve to param");
808    }
809
810    #[test]
811    fn test_csharp_local_variable() {
812        let l = loader();
813        if skip_if_no(&l, "c-sharp") {
814            return;
815        }
816        let engine = ScopeEngine::new(&l);
817        let src = "class A { void F() { int v = 1; int w = v + 1; } }\n";
818        let defs = engine.find_definitions("c-sharp", src, "v");
819        assert_eq!(defs.len(), 1, "c#: local variable v");
820    }
821
822    // ── Ruby ──────────────────────────────────────────────────────────────────
823
824    #[test]
825    fn test_ruby_method_parameter() {
826        let l = loader();
827        if skip_if_no(&l, "ruby") {
828            return;
829        }
830        let engine = ScopeEngine::new(&l);
831        let src = "def add(x, y)\n  x + y\nend\n";
832        let defs = engine.find_definitions("ruby", src, "x");
833        assert_eq!(defs.len(), 1, "ruby: x should have one definition");
834        let refs = engine.find_references("ruby", src, "x");
835        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
836        assert!(resolved >= 1, "ruby: x reference should resolve to param");
837    }
838
839    #[test]
840    fn test_ruby_assignment() {
841        let l = loader();
842        if skip_if_no(&l, "ruby") {
843            return;
844        }
845        let engine = ScopeEngine::new(&l);
846        let src = "def f\n  v = 1\n  v + 2\nend\n";
847        let defs = engine.find_definitions("ruby", src, "v");
848        assert_eq!(defs.len(), 1, "ruby: assignment v");
849    }
850
851    // ── Bash ──────────────────────────────────────────────────────────────────
852
853    #[test]
854    fn test_bash_function_and_variable() {
855        let l = loader();
856        if skip_if_no(&l, "bash") {
857            return;
858        }
859        let engine = ScopeEngine::new(&l);
860        let src = "greet() {\n  name=\"world\"\n  echo \"$name\"\n}\n";
861        let defs = engine.find_definitions("bash", src, "name");
862        assert_eq!(defs.len(), 1, "bash: variable assignment name");
863    }
864
865    #[test]
866    fn test_bash_for_variable() {
867        let l = loader();
868        if skip_if_no(&l, "bash") {
869            return;
870        }
871        let engine = ScopeEngine::new(&l);
872        let src = "for item in a b c; do\n  echo \"$item\"\ndone\n";
873        let defs = engine.find_definitions("bash", src, "item");
874        assert_eq!(defs.len(), 1, "bash: for loop variable item");
875    }
876
877    // ── Kotlin ────────────────────────────────────────────────────────────────
878
879    #[test]
880    fn test_kotlin_function_parameter() {
881        let l = loader();
882        if skip_if_no(&l, "kotlin") {
883            return;
884        }
885        let engine = ScopeEngine::new(&l);
886        let src = "fun add(x: Int, y: Int): Int = x + y\n";
887        let defs = engine.find_definitions("kotlin", src, "x");
888        assert_eq!(defs.len(), 1, "kotlin: x should have one definition");
889        let resolved = engine.find_references("kotlin", src, "x");
890        assert!(!resolved.is_empty(), "kotlin: x reference should exist");
891    }
892
893    #[test]
894    fn test_kotlin_variable_declaration() {
895        let l = loader();
896        if skip_if_no(&l, "kotlin") {
897            return;
898        }
899        let engine = ScopeEngine::new(&l);
900        let src = "fun f() {\n    val v = 1\n    val w = v + 1\n}\n";
901        let defs = engine.find_definitions("kotlin", src, "v");
902        assert_eq!(defs.len(), 1, "kotlin: val declaration v");
903    }
904
905    // ── PHP ───────────────────────────────────────────────────────────────────
906
907    #[test]
908    fn test_php_function_parameter() {
909        let l = loader();
910        if skip_if_no(&l, "php") {
911            return;
912        }
913        let engine = ScopeEngine::new(&l);
914        let src = "<?php\nfunction add($x, $y) {\n    return $x + $y;\n}\n";
915        // PHP variables are captured as variable_name including $ sigil
916        let defs = engine.find_definitions("php", src, "$x");
917        assert_eq!(defs.len(), 1, "php: $x should have one definition");
918        let refs = engine.find_references("php", src, "$x");
919        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
920        assert!(resolved >= 1, "php: $x reference should resolve to param");
921    }
922
923    // ── Zig ───────────────────────────────────────────────────────────────────
924
925    #[test]
926    fn test_zig_function_parameter() {
927        let l = loader();
928        if skip_if_no(&l, "zig") {
929            return;
930        }
931        let engine = ScopeEngine::new(&l);
932        let src = "fn add(x: i32, y: i32) i32 { return x + y; }\n";
933        // ParamDecl.parameter captures x as a definition
934        let defs = engine.find_definitions("zig", src, "x");
935        assert_eq!(defs.len(), 1, "zig: x should have one definition");
936        // References are captured (IDENTIFIER matches everywhere including body)
937        let refs = engine.find_references("zig", src, "x");
938        assert!(!refs.is_empty(), "zig: x should appear as a reference");
939    }
940
941    #[test]
942    fn test_zig_var_decl() {
943        let l = loader();
944        if skip_if_no(&l, "zig") {
945            return;
946        }
947        let engine = ScopeEngine::new(&l);
948        let src = "fn f() void {\n    const v = 1;\n    const w = v + 1;\n}\n";
949        let defs = engine.find_definitions("zig", src, "v");
950        assert_eq!(defs.len(), 1, "zig: const declaration v");
951    }
952
953    // ── Dart ──────────────────────────────────────────────────────────────────
954
955    #[test]
956    fn test_dart_function_parameter() {
957        let l = loader();
958        if skip_if_no(&l, "dart") {
959            return;
960        }
961        let engine = ScopeEngine::new(&l);
962        let src = "int add(int x, int y) { return x + y; }\n";
963        // Parameters are captured as definitions (within function_signature scope).
964        // Cross-scope resolution to the sibling function_body is not supported by
965        // this grammar structure, so we only verify the definition is found.
966        let defs = engine.find_definitions("dart", src, "x");
967        assert_eq!(defs.len(), 1, "dart: x should have one definition");
968    }
969
970    #[test]
971    fn test_dart_local_variable() {
972        let l = loader();
973        if skip_if_no(&l, "dart") {
974            return;
975        }
976        let engine = ScopeEngine::new(&l);
977        let src = "void f() {\n  var result = 42;\n  print(result);\n}\n";
978        let defs = engine.find_definitions("dart", src, "result");
979        assert_eq!(defs.len(), 1, "dart: local variable result");
980        let refs = engine.find_references("dart", src, "result");
981        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
982        assert!(resolved >= 1, "dart: result reference should resolve");
983    }
984
985    // ── Elixir ────────────────────────────────────────────────────────────────
986
987    #[test]
988    fn test_elixir_anon_function_parameter() {
989        let l = loader();
990        if skip_if_no(&l, "elixir") {
991            return;
992        }
993        let engine = ScopeEngine::new(&l);
994        let src = "f = fn x, y -> x + y end\n";
995        let defs = engine.find_definitions("elixir", src, "x");
996        assert_eq!(defs.len(), 1, "elixir: anonymous function parameter x");
997        let refs = engine.find_references("elixir", src, "x");
998        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
999        assert!(resolved >= 1, "elixir: x reference should resolve to param");
1000    }
1001
1002    #[test]
1003    fn test_elixir_pattern_match() {
1004        let l = loader();
1005        if skip_if_no(&l, "elixir") {
1006            return;
1007        }
1008        let engine = ScopeEngine::new(&l);
1009        // x = 42 defines x via the "=" string literal pattern
1010        let src = "x = 42\n";
1011        let defs = engine.find_definitions("elixir", src, "x");
1012        assert_eq!(defs.len(), 1, "elixir: x = 42 should define x");
1013    }
1014
1015    #[test]
1016    fn test_elixir_no_false_definitions() {
1017        let l = loader();
1018        if skip_if_no(&l, "elixir") {
1019            return;
1020        }
1021        let engine = ScopeEngine::new(&l);
1022        // x + y should not produce definitions — "=" literal pattern only matches =
1023        let src = "x + y\n";
1024        let defs = engine.find_definitions("elixir", src, "x");
1025        assert_eq!(
1026            defs.len(),
1027            0,
1028            "elixir: x in x + y should not be a definition"
1029        );
1030    }
1031
1032    // ── Erlang ────────────────────────────────────────────────────────────────
1033
1034    #[test]
1035    fn test_erlang_function_parameter() {
1036        let l = loader();
1037        if skip_if_no(&l, "erlang") {
1038            return;
1039        }
1040        let engine = ScopeEngine::new(&l);
1041        let src = "add(X, Y) -> X + Y.\n";
1042        let defs = engine.find_definitions("erlang", src, "X");
1043        assert_eq!(defs.len(), 1, "erlang: function parameter X");
1044        let refs = engine.find_references("erlang", src, "X");
1045        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1046        assert!(resolved >= 1, "erlang: X reference should resolve to param");
1047    }
1048
1049    #[test]
1050    fn test_erlang_anon_function() {
1051        let l = loader();
1052        if skip_if_no(&l, "erlang") {
1053            return;
1054        }
1055        let engine = ScopeEngine::new(&l);
1056        let src = "F = fun(X) -> X * 2 end.\n";
1057        let defs = engine.find_definitions("erlang", src, "X");
1058        assert_eq!(defs.len(), 1, "erlang: anonymous function parameter X");
1059    }
1060
1061    // ── Clojure ───────────────────────────────────────────────────────────────
1062
1063    #[test]
1064    fn test_clojure_defn_name() {
1065        let l = loader();
1066        if skip_if_no(&l, "clojure") {
1067            return;
1068        }
1069        let engine = ScopeEngine::new(&l);
1070        let src = "(defn add [x y] (+ x y))\n";
1071        let defs = engine.find_definitions("clojure", src, "add");
1072        assert_eq!(defs.len(), 1, "clojure: defn should define function name");
1073    }
1074
1075    #[test]
1076    fn test_clojure_fn_parameter() {
1077        let l = loader();
1078        if skip_if_no(&l, "clojure") {
1079            return;
1080        }
1081        let engine = ScopeEngine::new(&l);
1082        let src = "(fn [x y] (+ x y))\n";
1083        let defs = engine.find_definitions("clojure", src, "x");
1084        assert_eq!(defs.len(), 1, "clojure: fn parameter x");
1085        let refs = engine.find_references("clojure", src, "x");
1086        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1087        assert!(
1088            resolved >= 1,
1089            "clojure: x reference should resolve to param"
1090        );
1091    }
1092
1093    #[test]
1094    fn test_clojure_let_binding() {
1095        let l = loader();
1096        if skip_if_no(&l, "clojure") {
1097            return;
1098        }
1099        let engine = ScopeEngine::new(&l);
1100        let src = "(let [x 1] (inc x))\n";
1101        let defs = engine.find_definitions("clojure", src, "x");
1102        assert_eq!(defs.len(), 1, "clojure: let binding x");
1103        let refs = engine.find_references("clojure", src, "x");
1104        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1105        assert!(resolved >= 1, "clojure: x in (inc x) should resolve");
1106    }
1107
1108    #[test]
1109    fn test_clojure_no_false_scope() {
1110        let l = loader();
1111        if skip_if_no(&l, "clojure") {
1112            return;
1113        }
1114        let engine = ScopeEngine::new(&l);
1115        // A regular function call (not a def form) should not create definitions
1116        let src = "(foo x y)\n";
1117        let defs = engine.find_definitions("clojure", src, "foo");
1118        assert_eq!(defs.len(), 0, "clojure: (foo x y) should not define foo");
1119    }
1120
1121    // ── Julia ─────────────────────────────────────────────────────────────────
1122
1123    #[test]
1124    fn test_julia_function_parameter() {
1125        let l = loader();
1126        if skip_if_no(&l, "julia") {
1127            return;
1128        }
1129        let engine = ScopeEngine::new(&l);
1130        let src = "function add(x, y)\n  x + y\nend\n";
1131        let defs = engine.find_definitions("julia", src, "x");
1132        assert_eq!(
1133            defs.len(),
1134            1,
1135            "julia: function param x should have one definition"
1136        );
1137        let refs = engine.find_references("julia", src, "x");
1138        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1139        assert!(resolved >= 1, "julia: x reference should resolve to param");
1140    }
1141
1142    #[test]
1143    fn test_julia_for_variable() {
1144        let l = loader();
1145        if skip_if_no(&l, "julia") {
1146            return;
1147        }
1148        let engine = ScopeEngine::new(&l);
1149        let src = "for i in 1:10\n  println(i)\nend\n";
1150        let defs = engine.find_definitions("julia", src, "i");
1151        assert_eq!(defs.len(), 1, "julia: for loop variable i");
1152        let refs = engine.find_references("julia", src, "i");
1153        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1154        assert!(
1155            resolved >= 1,
1156            "julia: i reference should resolve to for binding"
1157        );
1158    }
1159
1160    #[test]
1161    fn test_julia_let_binding() {
1162        let l = loader();
1163        if skip_if_no(&l, "julia") {
1164            return;
1165        }
1166        let engine = ScopeEngine::new(&l);
1167        let src = "let a = 1\n  a + 1\nend\n";
1168        let defs = engine.find_definitions("julia", src, "a");
1169        assert_eq!(defs.len(), 1, "julia: let binding a");
1170    }
1171
1172    // ── Perl ──────────────────────────────────────────────────────────────────
1173
1174    #[test]
1175    fn test_perl_my_scalar() {
1176        let l = loader();
1177        if skip_if_no(&l, "perl") {
1178            return;
1179        }
1180        let engine = ScopeEngine::new(&l);
1181        // my $name defines "name"; print $name references "name"
1182        let src = "sub greet {\n  my $name = \"world\";\n  print $name;\n}\n";
1183        let defs = engine.find_definitions("perl", src, "name");
1184        assert_eq!(defs.len(), 1, "perl: my $name should define name");
1185        let refs = engine.find_references("perl", src, "name");
1186        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1187        assert!(resolved >= 1, "perl: $name reference should resolve");
1188    }
1189
1190    #[test]
1191    fn test_perl_my_list() {
1192        let l = loader();
1193        if skip_if_no(&l, "perl") {
1194            return;
1195        }
1196        let engine = ScopeEngine::new(&l);
1197        let src = "my ($a, $b) = (1, 2);\n";
1198        let defs_a = engine.find_definitions("perl", src, "a");
1199        assert_eq!(defs_a.len(), 1, "perl: my ($a, $b) should define a");
1200        let defs_b = engine.find_definitions("perl", src, "b");
1201        assert_eq!(defs_b.len(), 1, "perl: my ($a, $b) should define b");
1202    }
1203
1204    // ── Groovy ────────────────────────────────────────────────────────────────
1205
1206    #[test]
1207    fn test_groovy_function_parameter() {
1208        let l = loader();
1209        if skip_if_no(&l, "groovy") {
1210            return;
1211        }
1212        let engine = ScopeEngine::new(&l);
1213        let src = "def add(x, y) {\n  return x + y\n}\n";
1214        let defs = engine.find_definitions("groovy", src, "x");
1215        assert_eq!(
1216            defs.len(),
1217            1,
1218            "groovy: function param x should have one definition"
1219        );
1220        let refs = engine.find_references("groovy", src, "x");
1221        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1222        assert!(resolved >= 1, "groovy: x reference should resolve to param");
1223    }
1224
1225    #[test]
1226    fn test_groovy_closure_parameter() {
1227        let l = loader();
1228        if skip_if_no(&l, "groovy") {
1229            return;
1230        }
1231        let engine = ScopeEngine::new(&l);
1232        let src = "def f = { a, b -> a + b }\n";
1233        let defs = engine.find_definitions("groovy", src, "a");
1234        assert_eq!(
1235            defs.len(),
1236            1,
1237            "groovy: closure param a should have one definition"
1238        );
1239        let refs = engine.find_references("groovy", src, "a");
1240        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1241        assert!(
1242            resolved >= 1,
1243            "groovy: a reference should resolve to closure param"
1244        );
1245    }
1246
1247    #[test]
1248    fn test_groovy_variable_declaration() {
1249        let l = loader();
1250        if skip_if_no(&l, "groovy") {
1251            return;
1252        }
1253        let engine = ScopeEngine::new(&l);
1254        let src = "def f() {\n  def v = 1\n  return v\n}\n";
1255        let defs = engine.find_definitions("groovy", src, "v");
1256        assert_eq!(defs.len(), 1, "groovy: def v should define v");
1257    }
1258
1259    // ── D ─────────────────────────────────────────────────────────────────────
1260
1261    #[test]
1262    fn test_d_function_parameter() {
1263        let l = loader();
1264        if skip_if_no(&l, "d") {
1265            return;
1266        }
1267        let engine = ScopeEngine::new(&l);
1268        let src = "int add(int x, int y) {\n  return x + y;\n}\n";
1269        let defs = engine.find_definitions("d", src, "x");
1270        assert_eq!(
1271            defs.len(),
1272            1,
1273            "d: function param x should have one definition"
1274        );
1275        let refs = engine.find_references("d", src, "x");
1276        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1277        assert!(resolved >= 1, "d: x reference should resolve to param");
1278    }
1279
1280    #[test]
1281    fn test_d_auto_declaration() {
1282        let l = loader();
1283        if skip_if_no(&l, "d") {
1284            return;
1285        }
1286        let engine = ScopeEngine::new(&l);
1287        let src = "void f() {\n  auto v = 42;\n  writeln(v);\n}\n";
1288        let defs = engine.find_definitions("d", src, "v");
1289        assert_eq!(defs.len(), 1, "d: auto v should define v");
1290        let refs = engine.find_references("d", src, "v");
1291        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1292        assert!(
1293            resolved >= 1,
1294            "d: v reference should resolve to auto binding"
1295        );
1296    }
1297
1298    // ── TypeScript ────────────────────────────────────────────────────────────
1299
1300    #[test]
1301    fn test_typescript_function_parameter() {
1302        let l = loader();
1303        if skip_if_no(&l, "typescript") {
1304            return;
1305        }
1306        let engine = ScopeEngine::new(&l);
1307        let src = "function add(x: number, y: number): number { return x + y; }";
1308        let defs = engine.find_definitions("typescript", src, "x");
1309        assert_eq!(
1310            defs.len(),
1311            1,
1312            "typescript: required parameter x should have one definition"
1313        );
1314        let refs = engine.find_references("typescript", src, "x");
1315        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1316        assert!(
1317            resolved >= 1,
1318            "typescript: x reference should resolve to param"
1319        );
1320    }
1321
1322    #[test]
1323    fn test_typescript_variable_declarator() {
1324        let l = loader();
1325        if skip_if_no(&l, "typescript") {
1326            return;
1327        }
1328        let engine = ScopeEngine::new(&l);
1329        // const inside a function scope so resolution works
1330        let src = "function f() { const x = 1; return x; }";
1331        let defs = engine.find_definitions("typescript", src, "x");
1332        assert_eq!(defs.len(), 1, "typescript: const x should define x");
1333        let refs = engine.find_references("typescript", src, "x");
1334        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1335        assert!(
1336            resolved >= 1,
1337            "typescript: x reference should resolve to const"
1338        );
1339    }
1340
1341    #[test]
1342    fn test_typescript_function_declaration() {
1343        let l = loader();
1344        if skip_if_no(&l, "typescript") {
1345            return;
1346        }
1347        let engine = ScopeEngine::new(&l);
1348        let src = "function greet(name: string): void { console.log(name); }";
1349        let defs = engine.find_definitions("typescript", src, "greet");
1350        assert_eq!(
1351            defs.len(),
1352            1,
1353            "typescript: function declaration greet should be defined"
1354        );
1355    }
1356
1357    #[test]
1358    fn test_typescript_arrow_function_single_param() {
1359        let l = loader();
1360        if skip_if_no(&l, "typescript") {
1361            return;
1362        }
1363        let engine = ScopeEngine::new(&l);
1364        let src = "const double = x => x * 2;";
1365        let defs = engine.find_definitions("typescript", src, "x");
1366        assert_eq!(
1367            defs.len(),
1368            1,
1369            "typescript: arrow function single param x should be defined"
1370        );
1371        let refs = engine.find_references("typescript", src, "x");
1372        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1373        assert!(resolved >= 1, "typescript: x in arrow body should resolve");
1374    }
1375
1376    #[test]
1377    fn test_typescript_object_destructuring_param() {
1378        let l = loader();
1379        if skip_if_no(&l, "typescript") {
1380            return;
1381        }
1382        let engine = ScopeEngine::new(&l);
1383        let src = "function f({ a, b }: T) { return a + b; }";
1384        let defs_a = engine.find_definitions("typescript", src, "a");
1385        assert_eq!(
1386            defs_a.len(),
1387            1,
1388            "typescript: destructured param a should be defined"
1389        );
1390        let refs_a = engine.find_references("typescript", src, "a");
1391        let resolved = refs_a.iter().filter(|r| r.definition.is_some()).count();
1392        assert!(
1393            resolved >= 1,
1394            "typescript: a in body should resolve to destructured param"
1395        );
1396    }
1397
1398    #[test]
1399    fn test_typescript_array_destructuring_param() {
1400        let l = loader();
1401        if skip_if_no(&l, "typescript") {
1402            return;
1403        }
1404        let engine = ScopeEngine::new(&l);
1405        let src = "function f([x, y]: U) { return x + y; }";
1406        let defs = engine.find_definitions("typescript", src, "x");
1407        assert_eq!(
1408            defs.len(),
1409            1,
1410            "typescript: destructured array param x should be defined"
1411        );
1412        let refs = engine.find_references("typescript", src, "x");
1413        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1414        assert!(
1415            resolved >= 1,
1416            "typescript: x in body should resolve to destructured param"
1417        );
1418    }
1419
1420    // ── JavaScript ────────────────────────────────────────────────────────────
1421
1422    #[test]
1423    fn test_javascript_function_parameter() {
1424        let l = loader();
1425        if skip_if_no(&l, "javascript") {
1426            return;
1427        }
1428        let engine = ScopeEngine::new(&l);
1429        let src = "function add(x, y) { return x + y; }";
1430        let defs = engine.find_definitions("javascript", src, "x");
1431        assert_eq!(
1432            defs.len(),
1433            1,
1434            "javascript: function param x should have one definition"
1435        );
1436        let refs = engine.find_references("javascript", src, "x");
1437        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1438        assert!(
1439            resolved >= 1,
1440            "javascript: x reference should resolve to param"
1441        );
1442    }
1443
1444    #[test]
1445    fn test_javascript_variable_declarator() {
1446        let l = loader();
1447        if skip_if_no(&l, "javascript") {
1448            return;
1449        }
1450        let engine = ScopeEngine::new(&l);
1451        // const inside a function scope so resolution works
1452        let src = "function f() { const x = 1; return x; }";
1453        let defs = engine.find_definitions("javascript", src, "x");
1454        assert_eq!(defs.len(), 1, "javascript: const x should define x");
1455        let refs = engine.find_references("javascript", src, "x");
1456        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1457        assert!(resolved >= 1, "javascript: x reference should resolve");
1458    }
1459
1460    #[test]
1461    fn test_javascript_function_name() {
1462        let l = loader();
1463        if skip_if_no(&l, "javascript") {
1464            return;
1465        }
1466        let engine = ScopeEngine::new(&l);
1467        let src = "function greet() { return 'hello'; }";
1468        let defs = engine.find_definitions("javascript", src, "greet");
1469        assert_eq!(
1470            defs.len(),
1471            1,
1472            "javascript: function declaration greet should be defined"
1473        );
1474    }
1475
1476    #[test]
1477    fn test_javascript_arrow_function_single_param() {
1478        let l = loader();
1479        if skip_if_no(&l, "javascript") {
1480            return;
1481        }
1482        let engine = ScopeEngine::new(&l);
1483        let src = "const double = x => x * 2;";
1484        let defs = engine.find_definitions("javascript", src, "x");
1485        assert_eq!(
1486            defs.len(),
1487            1,
1488            "javascript: arrow single param x should be defined"
1489        );
1490        let refs = engine.find_references("javascript", src, "x");
1491        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1492        assert!(resolved >= 1, "javascript: x in arrow body should resolve");
1493    }
1494
1495    #[test]
1496    fn test_javascript_object_destructuring_param() {
1497        let l = loader();
1498        if skip_if_no(&l, "javascript") {
1499            return;
1500        }
1501        let engine = ScopeEngine::new(&l);
1502        let src = "function f({ a, b }) { return a + b; }";
1503        let defs_a = engine.find_definitions("javascript", src, "a");
1504        assert_eq!(
1505            defs_a.len(),
1506            1,
1507            "javascript: destructured param a should be defined"
1508        );
1509        let refs_a = engine.find_references("javascript", src, "a");
1510        let resolved = refs_a.iter().filter(|r| r.definition.is_some()).count();
1511        assert!(
1512            resolved >= 1,
1513            "javascript: a in body should resolve to destructured param"
1514        );
1515    }
1516
1517    #[test]
1518    fn test_javascript_array_destructuring_param() {
1519        let l = loader();
1520        if skip_if_no(&l, "javascript") {
1521            return;
1522        }
1523        let engine = ScopeEngine::new(&l);
1524        let src = "function f([x, y]) { return x + y; }";
1525        let defs = engine.find_definitions("javascript", src, "x");
1526        assert_eq!(
1527            defs.len(),
1528            1,
1529            "javascript: destructured array param x should be defined"
1530        );
1531        let refs = engine.find_references("javascript", src, "x");
1532        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1533        assert!(
1534            resolved >= 1,
1535            "javascript: x in body should resolve to destructured param"
1536        );
1537    }
1538
1539    #[test]
1540    fn test_javascript_default_param() {
1541        let l = loader();
1542        if skip_if_no(&l, "javascript") {
1543            return;
1544        }
1545        let engine = ScopeEngine::new(&l);
1546        let src = "function f(c = 1) { return c; }";
1547        let defs = engine.find_definitions("javascript", src, "c");
1548        assert_eq!(
1549            defs.len(),
1550            1,
1551            "javascript: default param c should be defined"
1552        );
1553        let refs = engine.find_references("javascript", src, "c");
1554        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1555        assert!(
1556            resolved >= 1,
1557            "javascript: c in body should resolve to default param"
1558        );
1559    }
1560
1561    #[test]
1562    fn test_javascript_nested_destructuring_param() {
1563        let l = loader();
1564        if skip_if_no(&l, "javascript") {
1565            return;
1566        }
1567        let engine = ScopeEngine::new(&l);
1568        // Nested: { a: { b } } — b is two levels deep inside object_pattern
1569        let src = "function f({ a: { b } }) { return b; }";
1570        let defs = engine.find_definitions("javascript", src, "b");
1571        assert_eq!(
1572            defs.len(),
1573            1,
1574            "javascript: nested destructured param b should be defined"
1575        );
1576        let refs = engine.find_references("javascript", src, "b");
1577        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1578        assert!(
1579            resolved >= 1,
1580            "javascript: b should resolve from nested destructuring"
1581        );
1582    }
1583
1584    // ── Lua ───────────────────────────────────────────────────────────────────
1585
1586    #[test]
1587    fn test_lua_function_parameter() {
1588        let l = loader();
1589        if skip_if_no(&l, "lua") {
1590            return;
1591        }
1592        let engine = ScopeEngine::new(&l);
1593        let src = "function add(x, y) return x + y end";
1594        let defs = engine.find_definitions("lua", src, "x");
1595        assert_eq!(
1596            defs.len(),
1597            1,
1598            "lua: function param x should have one definition"
1599        );
1600        let refs = engine.find_references("lua", src, "x");
1601        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1602        assert!(resolved >= 1, "lua: x reference should resolve to param");
1603    }
1604
1605    #[test]
1606    fn test_lua_function_name() {
1607        let l = loader();
1608        if skip_if_no(&l, "lua") {
1609            return;
1610        }
1611        let engine = ScopeEngine::new(&l);
1612        let src = "function greet() return 'hello' end";
1613        let defs = engine.find_definitions("lua", src, "greet");
1614        assert_eq!(
1615            defs.len(),
1616            1,
1617            "lua: function declaration greet should be defined"
1618        );
1619    }
1620
1621    #[test]
1622    fn test_lua_for_numeric() {
1623        let l = loader();
1624        if skip_if_no(&l, "lua") {
1625            return;
1626        }
1627        let engine = ScopeEngine::new(&l);
1628        let src = "for i = 1, 10 do print(i) end";
1629        let defs = engine.find_definitions("lua", src, "i");
1630        assert_eq!(
1631            defs.len(),
1632            1,
1633            "lua: for numeric variable i should be defined"
1634        );
1635        let refs = engine.find_references("lua", src, "i");
1636        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1637        assert!(resolved >= 1, "lua: i in for body should resolve");
1638    }
1639
1640    // ── Scala ─────────────────────────────────────────────────────────────────
1641
1642    #[test]
1643    fn test_scala_function_parameter() {
1644        let l = loader();
1645        if skip_if_no(&l, "scala") {
1646            return;
1647        }
1648        let engine = ScopeEngine::new(&l);
1649        let src = "def add(x: Int, y: Int): Int = x + y";
1650        let defs = engine.find_definitions("scala", src, "x");
1651        assert_eq!(
1652            defs.len(),
1653            1,
1654            "scala: function param x should have one definition"
1655        );
1656        let refs = engine.find_references("scala", src, "x");
1657        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1658        assert!(resolved >= 1, "scala: x reference should resolve to param");
1659    }
1660
1661    #[test]
1662    fn test_scala_val_definition() {
1663        let l = loader();
1664        if skip_if_no(&l, "scala") {
1665            return;
1666        }
1667        let engine = ScopeEngine::new(&l);
1668        let src = "val x = 42";
1669        let defs = engine.find_definitions("scala", src, "x");
1670        assert_eq!(defs.len(), 1, "scala: val x should be defined");
1671    }
1672
1673    #[test]
1674    fn test_scala_function_name() {
1675        let l = loader();
1676        if skip_if_no(&l, "scala") {
1677            return;
1678        }
1679        let engine = ScopeEngine::new(&l);
1680        let src = "def greet(name: String): String = \"hello \" + name";
1681        let defs = engine.find_definitions("scala", src, "greet");
1682        assert_eq!(
1683            defs.len(),
1684            1,
1685            "scala: def greet should define function name"
1686        );
1687    }
1688
1689    // ── R ─────────────────────────────────────────────────────────────────────
1690
1691    #[test]
1692    fn test_r_arrow_assignment() {
1693        let l = loader();
1694        if skip_if_no(&l, "r") {
1695            return;
1696        }
1697        let engine = ScopeEngine::new(&l);
1698        // assignment inside function scope so x resolves
1699        let src = "f <- function(a) { x <- a * 2; x }\n";
1700        let defs = engine.find_definitions("r", src, "x");
1701        assert_eq!(defs.len(), 1, "r: x <- ... should define x");
1702        let refs = engine.find_references("r", src, "x");
1703        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1704        assert!(resolved >= 1, "r: x reference in body should resolve");
1705    }
1706
1707    #[test]
1708    fn test_r_function_parameter() {
1709        let l = loader();
1710        if skip_if_no(&l, "r") {
1711            return;
1712        }
1713        let engine = ScopeEngine::new(&l);
1714        let src = "f <- function(a, b) { a + b }\n";
1715        let defs = engine.find_definitions("r", src, "a");
1716        assert_eq!(defs.len(), 1, "r: function param a should be defined");
1717        let refs = engine.find_references("r", src, "a");
1718        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1719        assert!(resolved >= 1, "r: a reference in body should resolve");
1720    }
1721
1722    // ── OCaml ─────────────────────────────────────────────────────────────────
1723
1724    #[test]
1725    fn test_ocaml_let_binding() {
1726        let l = loader();
1727        if skip_if_no(&l, "ocaml") {
1728            return;
1729        }
1730        let engine = ScopeEngine::new(&l);
1731        // Let binding: x is a value_pattern (definition)
1732        let src = "let x = 42 in x + 1";
1733        let defs = engine.find_definitions("ocaml", src, "x");
1734        assert_eq!(defs.len(), 1, "ocaml: let x = 42 should define x");
1735    }
1736
1737    #[test]
1738    fn test_ocaml_function_params() {
1739        let l = loader();
1740        if skip_if_no(&l, "ocaml") {
1741            return;
1742        }
1743        let engine = ScopeEngine::new(&l);
1744        // OCaml curried function: params are value_patterns
1745        let src = "let add x y = x + y";
1746        let defs = engine.find_definitions("ocaml", src, "x");
1747        assert_eq!(
1748            defs.len(),
1749            1,
1750            "ocaml: curried function param x should be defined"
1751        );
1752    }
1753
1754    // ── TSX ───────────────────────────────────────────────────────────────────
1755
1756    #[test]
1757    fn test_tsx_function_parameter() {
1758        let l = loader();
1759        if skip_if_no(&l, "tsx") {
1760            return;
1761        }
1762        let engine = ScopeEngine::new(&l);
1763        let src = "function add(x: number, y: number): number { return x + y; }";
1764        let defs = engine.find_definitions("tsx", src, "x");
1765        assert_eq!(defs.len(), 1, "tsx: required parameter x should be defined");
1766        let refs = engine.find_references("tsx", src, "x");
1767        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1768        assert!(resolved >= 1, "tsx: x reference should resolve to param");
1769    }
1770
1771    #[test]
1772    fn test_tsx_variable_declarator() {
1773        let l = loader();
1774        if skip_if_no(&l, "tsx") {
1775            return;
1776        }
1777        let engine = ScopeEngine::new(&l);
1778        let src = "function f() { const x = 1; return x; }";
1779        let defs = engine.find_definitions("tsx", src, "x");
1780        assert_eq!(defs.len(), 1, "tsx: const x should define x");
1781    }
1782
1783    // ── Gleam ─────────────────────────────────────────────────────────────────
1784
1785    #[test]
1786    fn test_gleam_function_parameter() {
1787        let l = loader();
1788        if skip_if_no(&l, "gleam") {
1789            return;
1790        }
1791        let engine = ScopeEngine::new(&l);
1792        let src = "fn add(x, y) { x + y }";
1793        let defs = engine.find_definitions("gleam", src, "x");
1794        assert_eq!(
1795            defs.len(),
1796            1,
1797            "gleam: function_parameter x should be defined"
1798        );
1799        let refs = engine.find_references("gleam", src, "x");
1800        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1801        assert!(resolved >= 1, "gleam: x reference should resolve to param");
1802    }
1803
1804    #[test]
1805    fn test_gleam_let_binding() {
1806        let l = loader();
1807        if skip_if_no(&l, "gleam") {
1808            return;
1809        }
1810        let engine = ScopeEngine::new(&l);
1811        let src = "fn f() { let x = 1 x }";
1812        let defs = engine.find_definitions("gleam", src, "x");
1813        assert_eq!(defs.len(), 1, "gleam: let x should define x");
1814    }
1815
1816    // ── TLA+ ──────────────────────────────────────────────────────────────────
1817
1818    #[test]
1819    fn test_tlaplus_operator_definition() {
1820        let l = loader();
1821        if skip_if_no(&l, "tlaplus") {
1822            return;
1823        }
1824        let engine = ScopeEngine::new(&l);
1825        // TLA+ operator definition with parameters
1826        let src = "---- MODULE Test ----\nOp(x, y) == x + y\n====";
1827        let defs = engine.find_definitions("tlaplus", src, "x");
1828        assert_eq!(
1829            defs.len(),
1830            1,
1831            "tlaplus: operator parameter x should be defined"
1832        );
1833        let refs = engine.find_references("tlaplus", src, "x");
1834        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1835        assert!(resolved >= 1, "tlaplus: x reference should resolve");
1836    }
1837
1838    #[test]
1839    fn test_tlaplus_let_in() {
1840        let l = loader();
1841        if skip_if_no(&l, "tlaplus") {
1842            return;
1843        }
1844        let engine = ScopeEngine::new(&l);
1845        let src = "---- MODULE Test ----\nExpr == LET x == 1 IN x + 1\n====";
1846        let defs = engine.find_definitions("tlaplus", src, "x");
1847        assert_eq!(defs.len(), 1, "tlaplus: LET x should define x");
1848    }
1849
1850    // ── Swift ─────────────────────────────────────────────────────────────────
1851
1852    #[test]
1853    fn test_swift_function_parameter() {
1854        let l = loader();
1855        if skip_if_no(&l, "swift") {
1856            return;
1857        }
1858        let engine = ScopeEngine::new(&l);
1859        // Swift function with internal parameter name
1860        let src = "func add(_ x: Int, _ y: Int) -> Int { return x + y }";
1861        let defs = engine.find_definitions("swift", src, "x");
1862        assert_eq!(
1863            defs.len(),
1864            1,
1865            "swift: function parameter x should be defined"
1866        );
1867        let refs = engine.find_references("swift", src, "x");
1868        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1869        assert!(resolved >= 1, "swift: x reference should resolve to param");
1870    }
1871
1872    #[test]
1873    fn test_swift_function_name() {
1874        let l = loader();
1875        if skip_if_no(&l, "swift") {
1876            return;
1877        }
1878        let engine = ScopeEngine::new(&l);
1879        let src = "func greet() -> String { return \"hello\" }";
1880        let defs = engine.find_definitions("swift", src, "greet");
1881        assert_eq!(
1882            defs.len(),
1883            1,
1884            "swift: function name greet should be defined"
1885        );
1886    }
1887
1888    // ── Elm ───────────────────────────────────────────────────────────────────
1889
1890    #[test]
1891    fn test_elm_function_definition() {
1892        let l = loader();
1893        if skip_if_no(&l, "elm") {
1894            return;
1895        }
1896        let engine = ScopeEngine::new(&l);
1897        // Elm function with parameters
1898        let src = "add x y = x + y";
1899        let defs = engine.find_definitions("elm", src, "x");
1900        assert_eq!(defs.len(), 1, "elm: function parameter x should be defined");
1901    }
1902
1903    // ── F# ────────────────────────────────────────────────────────────────────
1904
1905    #[test]
1906    fn test_fsharp_function_parameter() {
1907        let l = loader();
1908        if skip_if_no(&l, "fsharp") {
1909            return;
1910        }
1911        let engine = ScopeEngine::new(&l);
1912        let src = "let add x y = x + y";
1913        let defs = engine.find_definitions("fsharp", src, "x");
1914        assert_eq!(
1915            defs.len(),
1916            1,
1917            "fsharp: function parameter x should be defined"
1918        );
1919    }
1920
1921    #[test]
1922    fn test_fsharp_value_binding() {
1923        let l = loader();
1924        if skip_if_no(&l, "fsharp") {
1925            return;
1926        }
1927        let engine = ScopeEngine::new(&l);
1928        let src = "let x = 42";
1929        let defs = engine.find_definitions("fsharp", src, "x");
1930        assert_eq!(defs.len(), 1, "fsharp: let x should define x");
1931    }
1932
1933    // ── Ada ───────────────────────────────────────────────────────────────────
1934
1935    #[test]
1936    fn test_ada_parameter() {
1937        let l = loader();
1938        if skip_if_no(&l, "ada") {
1939            return;
1940        }
1941        let engine = ScopeEngine::new(&l);
1942        let src = "procedure Add(X : Integer; Y : Integer) is begin null; end Add;";
1943        let defs = engine.find_definitions("ada", src, "X");
1944        assert_eq!(defs.len(), 1, "ada: parameter X should be defined");
1945    }
1946
1947    // ── Starlark ──────────────────────────────────────────────────────────────
1948
1949    #[test]
1950    fn test_starlark_function_parameter() {
1951        let l = loader();
1952        if skip_if_no(&l, "starlark") {
1953            return;
1954        }
1955        let engine = ScopeEngine::new(&l);
1956        let src = "def add(x, y):\n  return x + y\n";
1957        let defs = engine.find_definitions("starlark", src, "x");
1958        assert_eq!(
1959            defs.len(),
1960            1,
1961            "starlark: function param x should be defined"
1962        );
1963        let refs = engine.find_references("starlark", src, "x");
1964        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
1965        assert!(resolved >= 1, "starlark: x in body should resolve");
1966    }
1967
1968    #[test]
1969    fn test_starlark_assignment() {
1970        let l = loader();
1971        if skip_if_no(&l, "starlark") {
1972            return;
1973        }
1974        let engine = ScopeEngine::new(&l);
1975        let src = "def f():\n  x = 1\n  return x\n";
1976        let defs = engine.find_definitions("starlark", src, "x");
1977        assert_eq!(defs.len(), 1, "starlark: assignment x should be defined");
1978    }
1979
1980    // ── Thrift ────────────────────────────────────────────────────────────────
1981
1982    #[test]
1983    fn test_thrift_function_parameter() {
1984        let l = loader();
1985        if skip_if_no(&l, "thrift") {
1986            return;
1987        }
1988        let engine = ScopeEngine::new(&l);
1989        let src = "service Calc { i32 add(1: i32 x, 2: i32 y) }";
1990        let defs = engine.find_definitions("thrift", src, "x");
1991        assert_eq!(defs.len(), 1, "thrift: parameter x should be defined");
1992    }
1993
1994    #[test]
1995    fn test_thrift_service_name() {
1996        let l = loader();
1997        if skip_if_no(&l, "thrift") {
1998            return;
1999        }
2000        let engine = ScopeEngine::new(&l);
2001        let src = "service Calc { i32 add(1: i32 x) }";
2002        let defs = engine.find_definitions("thrift", src, "Calc");
2003        assert_eq!(defs.len(), 1, "thrift: service name Calc should be defined");
2004    }
2005
2006    // ── Objective-C ───────────────────────────────────────────────────────────
2007
2008    #[test]
2009    fn test_objc_method_parameter() {
2010        let l = loader();
2011        if skip_if_no(&l, "objc") {
2012            return;
2013        }
2014        let engine = ScopeEngine::new(&l);
2015        // ObjC method syntax requires @implementation context.
2016        let src = "@implementation Foo\n- (int)add:(int)x {\n    return x + 1;\n}\n@end";
2017        let defs = engine.find_definitions("objc", src, "x");
2018        assert_eq!(defs.len(), 1, "objc: method parameter x should be defined");
2019        let refs = engine.find_references("objc", src, "x");
2020        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2021        assert!(resolved >= 1, "objc: x reference should resolve");
2022    }
2023
2024    // ── Nix ───────────────────────────────────────────────────────────────────
2025
2026    #[test]
2027    fn test_nix_function_formal() {
2028        let l = loader();
2029        if skip_if_no(&l, "nix") {
2030            return;
2031        }
2032        let engine = ScopeEngine::new(&l);
2033        // Nix attrset destructuring function: { x, y }: x + y
2034        let src = "{ x, y }: x + y";
2035        let defs = engine.find_definitions("nix", src, "x");
2036        assert_eq!(defs.len(), 1, "nix: formal parameter x should be defined");
2037    }
2038
2039    #[test]
2040    fn test_nix_let_binding() {
2041        let l = loader();
2042        if skip_if_no(&l, "nix") {
2043            return;
2044        }
2045        let engine = ScopeEngine::new(&l);
2046        let src = "let x = 1; in x + 1";
2047        let defs = engine.find_definitions("nix", src, "x");
2048        assert_eq!(defs.len(), 1, "nix: let binding x should be defined");
2049    }
2050
2051    // ── ReScript ──────────────────────────────────────────────────────────────
2052
2053    #[test]
2054    fn test_rescript_function_parameter() {
2055        let l = loader();
2056        if skip_if_no(&l, "rescript") {
2057            return;
2058        }
2059        let engine = ScopeEngine::new(&l);
2060        let src = "let add = (x, y) => x + y";
2061        let defs = engine.find_definitions("rescript", src, "x");
2062        assert_eq!(
2063            defs.len(),
2064            1,
2065            "rescript: function parameter x should be defined"
2066        );
2067    }
2068
2069    #[test]
2070    fn test_rescript_let_binding() {
2071        let l = loader();
2072        if skip_if_no(&l, "rescript") {
2073            return;
2074        }
2075        let engine = ScopeEngine::new(&l);
2076        let src = "let x = 1";
2077        let defs = engine.find_definitions("rescript", src, "x");
2078        assert_eq!(defs.len(), 1, "rescript: let x should be defined");
2079    }
2080
2081    // ── Haskell ───────────────────────────────────────────────────────────────
2082
2083    #[test]
2084    fn test_haskell_function_parameter() {
2085        let l = loader();
2086        if skip_if_no(&l, "haskell") {
2087            return;
2088        }
2089        let engine = ScopeEngine::new(&l);
2090        let src = "add x y = x + y";
2091        let defs = engine.find_definitions("haskell", src, "x");
2092        assert_eq!(
2093            defs.len(),
2094            1,
2095            "haskell: function parameter x should be defined"
2096        );
2097        let refs = engine.find_references("haskell", src, "x");
2098        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2099        assert!(resolved >= 1, "haskell: x reference should resolve");
2100    }
2101
2102    #[test]
2103    fn test_haskell_let_binding() {
2104        let l = loader();
2105        if skip_if_no(&l, "haskell") {
2106            return;
2107        }
2108        let engine = ScopeEngine::new(&l);
2109        let src = "f = let x = 1 in x + 1";
2110        let defs = engine.find_definitions("haskell", src, "x");
2111        assert_eq!(defs.len(), 1, "haskell: let x should be defined");
2112    }
2113
2114    // ── Cap'n Proto ───────────────────────────────────────────────────────────
2115
2116    #[test]
2117    fn test_capnp_field_definition() {
2118        let l = loader();
2119        if skip_if_no(&l, "capnp") {
2120            return;
2121        }
2122        let engine = ScopeEngine::new(&l);
2123        let src = "@0xdbb9ad1f14bf0b36;\nstruct Point { x @0 :Float64; y @1 :Float64; }";
2124        let defs = engine.find_definitions("capnp", src, "x");
2125        assert_eq!(defs.len(), 1, "capnp: field x should be defined");
2126    }
2127
2128    // ── Scheme ────────────────────────────────────────────────────────────
2129
2130    #[test]
2131    fn test_scheme_function_define() {
2132        let l = loader();
2133        if skip_if_no(&l, "scheme") {
2134            return;
2135        }
2136        let engine = ScopeEngine::new(&l);
2137        let src = "(define (f x y) (+ x y))";
2138        let defs = engine.find_definitions("scheme", src, "x");
2139        assert_eq!(
2140            defs.len(),
2141            1,
2142            "scheme: parameter x in (define (f x y) ...) should be defined"
2143        );
2144        let refs = engine.find_references("scheme", src, "x");
2145        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2146        assert!(resolved >= 1, "scheme: x reference should resolve");
2147    }
2148
2149    #[test]
2150    fn test_scheme_variable_define() {
2151        let l = loader();
2152        if skip_if_no(&l, "scheme") {
2153            return;
2154        }
2155        let engine = ScopeEngine::new(&l);
2156        let src = "(define x 42)";
2157        let defs = engine.find_definitions("scheme", src, "x");
2158        assert_eq!(defs.len(), 1, "scheme: (define x val) should define x");
2159    }
2160
2161    #[test]
2162    fn test_scheme_let_binding() {
2163        let l = loader();
2164        if skip_if_no(&l, "scheme") {
2165            return;
2166        }
2167        let engine = ScopeEngine::new(&l);
2168        let src = "(lambda (unused) (let ((z 1)) z))";
2169        let defs = engine.find_definitions("scheme", src, "z");
2170        assert_eq!(defs.len(), 1, "scheme: let binding z should be defined");
2171        let refs = engine.find_references("scheme", src, "z");
2172        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2173        assert!(
2174            resolved >= 1,
2175            "scheme: z reference should resolve to let binding"
2176        );
2177    }
2178
2179    #[test]
2180    fn test_scheme_lambda_param() {
2181        let l = loader();
2182        if skip_if_no(&l, "scheme") {
2183            return;
2184        }
2185        let engine = ScopeEngine::new(&l);
2186        let src = "(lambda (x y) (+ x y))";
2187        let defs = engine.find_definitions("scheme", src, "x");
2188        assert_eq!(
2189            defs.len(),
2190            1,
2191            "scheme: lambda parameter x should be defined"
2192        );
2193    }
2194
2195    // ── Common Lisp ───────────────────────────────────────────────────────
2196
2197    #[test]
2198    fn test_commonlisp_defun_parameter() {
2199        let l = loader();
2200        if skip_if_no(&l, "commonlisp") {
2201            return;
2202        }
2203        let engine = ScopeEngine::new(&l);
2204        let src = "(defun add (x y) (+ x y))";
2205        let defs = engine.find_definitions("commonlisp", src, "x");
2206        assert_eq!(
2207            defs.len(),
2208            1,
2209            "commonlisp: defun parameter x should be defined"
2210        );
2211        let refs = engine.find_references("commonlisp", src, "x");
2212        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2213        assert!(resolved >= 1, "commonlisp: x reference should resolve");
2214    }
2215
2216    #[test]
2217    fn test_commonlisp_defun_name() {
2218        let l = loader();
2219        if skip_if_no(&l, "commonlisp") {
2220            return;
2221        }
2222        let engine = ScopeEngine::new(&l);
2223        let src = "(defun add (x y) (+ x y))";
2224        let defs = engine.find_definitions("commonlisp", src, "add");
2225        assert_eq!(
2226            defs.len(),
2227            1,
2228            "commonlisp: defun name add should be defined"
2229        );
2230    }
2231
2232    #[test]
2233    fn test_commonlisp_let_binding() {
2234        let l = loader();
2235        if skip_if_no(&l, "commonlisp") {
2236            return;
2237        }
2238        let engine = ScopeEngine::new(&l);
2239        let src = "(defun f () (let ((z 1)) z))";
2240        let defs = engine.find_definitions("commonlisp", src, "z");
2241        assert_eq!(defs.len(), 1, "commonlisp: let binding z should be defined");
2242        let refs = engine.find_references("commonlisp", src, "z");
2243        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2244        assert!(
2245            resolved >= 1,
2246            "commonlisp: z reference should resolve to let binding"
2247        );
2248    }
2249
2250    // ── Emacs Lisp ────────────────────────────────────────────────────────
2251
2252    #[test]
2253    fn test_elisp_defun_parameter() {
2254        let l = loader();
2255        if skip_if_no(&l, "elisp") {
2256            return;
2257        }
2258        let engine = ScopeEngine::new(&l);
2259        let src = "(defun add (x y) (+ x y))";
2260        let defs = engine.find_definitions("elisp", src, "x");
2261        assert_eq!(defs.len(), 1, "elisp: defun parameter x should be defined");
2262        let refs = engine.find_references("elisp", src, "x");
2263        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2264        assert!(resolved >= 1, "elisp: x reference should resolve");
2265    }
2266
2267    #[test]
2268    fn test_elisp_defun_name() {
2269        let l = loader();
2270        if skip_if_no(&l, "elisp") {
2271            return;
2272        }
2273        let engine = ScopeEngine::new(&l);
2274        let src = "(defun add (x y) (+ x y))";
2275        let defs = engine.find_definitions("elisp", src, "add");
2276        assert_eq!(defs.len(), 1, "elisp: defun name add should be defined");
2277    }
2278
2279    #[test]
2280    fn test_elisp_let_binding() {
2281        let l = loader();
2282        if skip_if_no(&l, "elisp") {
2283            return;
2284        }
2285        let engine = ScopeEngine::new(&l);
2286        let src = "(defun f () (let ((z 1)) z))";
2287        let defs = engine.find_definitions("elisp", src, "z");
2288        assert_eq!(defs.len(), 1, "elisp: let binding z should be defined");
2289        let refs = engine.find_references("elisp", src, "z");
2290        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2291        assert!(
2292            resolved >= 1,
2293            "elisp: z reference should resolve to let binding"
2294        );
2295    }
2296
2297    // ── Prolog ────────────────────────────────────────────────────────────
2298
2299    #[test]
2300    fn test_prolog_clause_variable() {
2301        let l = loader();
2302        if skip_if_no(&l, "prolog") {
2303            return;
2304        }
2305        let engine = ScopeEngine::new(&l);
2306        let src = "foo(X, Y) :- bar(X), baz(Y).";
2307        let defs = engine.find_definitions("prolog", src, "X");
2308        assert!(!defs.is_empty(), "prolog: variable X should be captured");
2309        let refs = engine.find_references("prolog", src, "X");
2310        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2311        assert!(
2312            resolved >= 1,
2313            "prolog: X reference should resolve within clause"
2314        );
2315    }
2316
2317    #[test]
2318    fn test_prolog_anon_variable_excluded() {
2319        let l = loader();
2320        if skip_if_no(&l, "prolog") {
2321            return;
2322        }
2323        let engine = ScopeEngine::new(&l);
2324        let src = "foo(_, X) :- bar(X).";
2325        let defs = engine.find_definitions("prolog", src, "_");
2326        assert_eq!(
2327            defs.len(),
2328            0,
2329            "prolog: anonymous variable _ should not be defined"
2330        );
2331    }
2332
2333    // ── Fish ──────────────────────────────────────────────────────────────
2334
2335    #[test]
2336    fn test_fish_function_name() {
2337        let l = loader();
2338        if skip_if_no(&l, "fish") {
2339            return;
2340        }
2341        let engine = ScopeEngine::new(&l);
2342        let src = "function myfunc\n  echo hello\nend";
2343        let defs = engine.find_definitions("fish", src, "myfunc");
2344        assert_eq!(
2345            defs.len(),
2346            1,
2347            "fish: function name myfunc should be defined"
2348        );
2349    }
2350
2351    #[test]
2352    fn test_fish_for_variable() {
2353        let l = loader();
2354        if skip_if_no(&l, "fish") {
2355            return;
2356        }
2357        let engine = ScopeEngine::new(&l);
2358        let src = "function outer\n  for i in a b c\n    echo $i\n  end\nend";
2359        let defs = engine.find_definitions("fish", src, "i");
2360        assert_eq!(defs.len(), 1, "fish: for loop variable i should be defined");
2361        let refs = engine.find_references("fish", src, "i");
2362        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2363        assert!(
2364            resolved >= 1,
2365            "fish: $i reference should resolve to for variable"
2366        );
2367    }
2368
2369    // ── Zsh ───────────────────────────────────────────────────────────────
2370
2371    #[test]
2372    fn test_zsh_variable_assignment() {
2373        let l = loader();
2374        if skip_if_no(&l, "zsh") {
2375            return;
2376        }
2377        let engine = ScopeEngine::new(&l);
2378        let src = "x=1";
2379        let defs = engine.find_definitions("zsh", src, "x");
2380        assert_eq!(
2381            defs.len(),
2382            1,
2383            "zsh: bare variable assignment x should be defined"
2384        );
2385    }
2386
2387    // ── PowerShell ────────────────────────────────────────────────────────
2388
2389    #[test]
2390    fn test_powershell_function_param() {
2391        let l = loader();
2392        if skip_if_no(&l, "powershell") {
2393            return;
2394        }
2395        let engine = ScopeEngine::new(&l);
2396        let src = "function foo {\n  param($x)\n  $x + 1\n}";
2397        let defs = engine.find_definitions("powershell", src, "$x");
2398        assert_eq!(defs.len(), 1, "powershell: param $x should be defined");
2399        let refs = engine.find_references("powershell", src, "$x");
2400        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2401        assert!(resolved >= 1, "powershell: $x reference should resolve");
2402    }
2403
2404    #[test]
2405    fn test_powershell_function_name() {
2406        let l = loader();
2407        if skip_if_no(&l, "powershell") {
2408            return;
2409        }
2410        let engine = ScopeEngine::new(&l);
2411        let src = "function foo {\n  param($x)\n  $x + 1\n}";
2412        let defs = engine.find_definitions("powershell", src, "foo");
2413        assert_eq!(
2414            defs.len(),
2415            1,
2416            "powershell: function name foo should be defined"
2417        );
2418    }
2419
2420    #[test]
2421    fn test_powershell_foreach_variable() {
2422        let l = loader();
2423        if skip_if_no(&l, "powershell") {
2424            return;
2425        }
2426        let engine = ScopeEngine::new(&l);
2427        let src = "foreach ($item in @(1,2,3)) { $item }";
2428        let defs = engine.find_definitions("powershell", src, "$item");
2429        assert_eq!(
2430            defs.len(),
2431            1,
2432            "powershell: foreach variable $item should be defined"
2433        );
2434    }
2435
2436    // ── Vim ───────────────────────────────────────────────────────────────
2437
2438    #[test]
2439    fn test_vim_let_binding() {
2440        let l = loader();
2441        if skip_if_no(&l, "vim") {
2442            return;
2443        }
2444        let engine = ScopeEngine::new(&l);
2445        let src = "function! Foo()\n  let x = 1\n  let y = x + 1\nendfunction";
2446        let defs = engine.find_definitions("vim", src, "x");
2447        assert_eq!(defs.len(), 1, "vim: let x should be defined");
2448        let refs = engine.find_references("vim", src, "x");
2449        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2450        assert!(resolved >= 1, "vim: x reference should resolve");
2451    }
2452
2453    #[test]
2454    fn test_vim_function_param() {
2455        let l = loader();
2456        if skip_if_no(&l, "vim") {
2457            return;
2458        }
2459        let engine = ScopeEngine::new(&l);
2460        let src = "function! Foo(bar)\n  let l:x = bar\nendfunction";
2461        let defs = engine.find_definitions("vim", src, "bar");
2462        assert_eq!(
2463            defs.len(),
2464            1,
2465            "vim: function parameter bar should be defined"
2466        );
2467    }
2468
2469    #[test]
2470    fn test_vim_scoped_let() {
2471        let l = loader();
2472        if skip_if_no(&l, "vim") {
2473            return;
2474        }
2475        let engine = ScopeEngine::new(&l);
2476        let src = "function! Foo()\n  let l:y = 1\nendfunction";
2477        let defs = engine.find_definitions("vim", src, "y");
2478        assert_eq!(defs.len(), 1, "vim: let l:y should define y");
2479    }
2480
2481    // ── SQL ───────────────────────────────────────────────────────────────
2482
2483    #[test]
2484    fn test_sql_cte_name() {
2485        let l = loader();
2486        if skip_if_no(&l, "sql") {
2487            return;
2488        }
2489        let engine = ScopeEngine::new(&l);
2490        let src = "WITH x AS (SELECT 1) SELECT * FROM x";
2491        let defs = engine.find_definitions("sql", src, "x");
2492        assert_eq!(defs.len(), 1, "sql: CTE name x should be defined");
2493    }
2494
2495    #[test]
2496    fn test_sql_table_alias() {
2497        let l = loader();
2498        if skip_if_no(&l, "sql") {
2499            return;
2500        }
2501        let engine = ScopeEngine::new(&l);
2502        let src = "SELECT t.col FROM tbl AS t";
2503        let defs = engine.find_definitions("sql", src, "t");
2504        assert_eq!(defs.len(), 1, "sql: table alias t should be defined");
2505    }
2506
2507    // ── Debug AST probes ──────────────────────────────────────────────────
2508
2509    // ── MATLAB ────────────────────────────────────────────────────────────
2510
2511    #[test]
2512    fn test_matlab_function_parameter() {
2513        let l = loader();
2514        if skip_if_no(&l, "matlab") {
2515            return;
2516        }
2517        let engine = ScopeEngine::new(&l);
2518        let src = "function y = foo(x)\n  y = x + 1;\nend";
2519        let defs = engine.find_definitions("matlab", src, "x");
2520        assert_eq!(
2521            defs.len(),
2522            1,
2523            "matlab: function parameter x should be defined"
2524        );
2525        let refs = engine.find_references("matlab", src, "x");
2526        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2527        assert!(resolved >= 1, "matlab: x reference should resolve");
2528    }
2529
2530    #[test]
2531    fn test_matlab_return_variable() {
2532        let l = loader();
2533        if skip_if_no(&l, "matlab") {
2534            return;
2535        }
2536        let engine = ScopeEngine::new(&l);
2537        let src = "function y = foo(x)\n  y = x + 1;\nend";
2538        let defs = engine.find_definitions("matlab", src, "y");
2539        assert!(
2540            defs.len() >= 1,
2541            "matlab: return variable y should be defined"
2542        );
2543    }
2544
2545    #[test]
2546    fn test_matlab_function_name() {
2547        let l = loader();
2548        if skip_if_no(&l, "matlab") {
2549            return;
2550        }
2551        let engine = ScopeEngine::new(&l);
2552        let src = "function y = foo(x)\n  y = x + 1;\nend";
2553        let defs = engine.find_definitions("matlab", src, "foo");
2554        assert_eq!(defs.len(), 1, "matlab: function name foo should be defined");
2555    }
2556
2557    // ── AWK ───────────────────────────────────────────────────────────────
2558
2559    #[test]
2560    fn test_awk_function_parameter() {
2561        let l = loader();
2562        if skip_if_no(&l, "awk") {
2563            return;
2564        }
2565        let engine = ScopeEngine::new(&l);
2566        let src = "function foo(x, y) { return x + y }";
2567        let defs = engine.find_definitions("awk", src, "x");
2568        assert_eq!(defs.len(), 1, "awk: function parameter x should be defined");
2569        let refs = engine.find_references("awk", src, "x");
2570        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2571        assert!(resolved >= 1, "awk: x reference should resolve");
2572    }
2573
2574    #[test]
2575    fn test_awk_function_name() {
2576        let l = loader();
2577        if skip_if_no(&l, "awk") {
2578            return;
2579        }
2580        let engine = ScopeEngine::new(&l);
2581        let src = "function foo(x) { return x }";
2582        let defs = engine.find_definitions("awk", src, "foo");
2583        assert_eq!(defs.len(), 1, "awk: function name foo should be defined");
2584    }
2585
2586    // ── CMake ─────────────────────────────────────────────────────────────
2587
2588    #[test]
2589    fn test_cmake_function_name() {
2590        let l = loader();
2591        if skip_if_no(&l, "cmake") {
2592            return;
2593        }
2594        let engine = ScopeEngine::new(&l);
2595        let src = "function(foo x y)\n  set(z 1)\nendfunction()";
2596        let defs = engine.find_definitions("cmake", src, "foo");
2597        assert_eq!(defs.len(), 1, "cmake: function name foo should be defined");
2598    }
2599
2600    #[test]
2601    fn test_cmake_set_variable() {
2602        let l = loader();
2603        if skip_if_no(&l, "cmake") {
2604            return;
2605        }
2606        let engine = ScopeEngine::new(&l);
2607        let src = "set(MY_VAR hello)";
2608        let defs = engine.find_definitions("cmake", src, "MY_VAR");
2609        assert_eq!(defs.len(), 1, "cmake: set(MY_VAR ...) should define MY_VAR");
2610    }
2611
2612    #[test]
2613    fn test_cmake_foreach_variable() {
2614        let l = loader();
2615        if skip_if_no(&l, "cmake") {
2616            return;
2617        }
2618        let engine = ScopeEngine::new(&l);
2619        let src = "foreach(item IN LISTS mylist)\n  message(${item})\nendforeach()";
2620        let defs = engine.find_definitions("cmake", src, "item");
2621        assert_eq!(
2622            defs.len(),
2623            1,
2624            "cmake: foreach loop variable item should be defined"
2625        );
2626    }
2627
2628    // ── Typst ─────────────────────────────────────────────────────────────
2629
2630    #[test]
2631    fn test_typst_let_binding() {
2632        let l = loader();
2633        if skip_if_no(&l, "typst") {
2634            return;
2635        }
2636        let engine = ScopeEngine::new(&l);
2637        let src = "#let x = 1\n#let y = x + 1";
2638        let defs = engine.find_definitions("typst", src, "x");
2639        assert_eq!(defs.len(), 1, "typst: #let x = 1 should define x");
2640    }
2641
2642    #[test]
2643    fn test_typst_function_parameter() {
2644        let l = loader();
2645        if skip_if_no(&l, "typst") {
2646            return;
2647        }
2648        let engine = ScopeEngine::new(&l);
2649        let src = "#let foo(a, b) = a + b";
2650        let defs = engine.find_definitions("typst", src, "a");
2651        assert_eq!(
2652            defs.len(),
2653            1,
2654            "typst: function parameter a should be defined"
2655        );
2656    }
2657
2658    #[test]
2659    fn test_typst_function_name() {
2660        let l = loader();
2661        if skip_if_no(&l, "typst") {
2662            return;
2663        }
2664        let engine = ScopeEngine::new(&l);
2665        let src = "#let foo(a) = a";
2666        let defs = engine.find_definitions("typst", src, "foo");
2667        assert_eq!(defs.len(), 1, "typst: function name foo should be defined");
2668    }
2669
2670    // ── HCL ───────────────────────────────────────────────────────────────
2671
2672    #[test]
2673    fn test_hcl_locals_attribute() {
2674        let l = loader();
2675        if skip_if_no(&l, "hcl") {
2676            return;
2677        }
2678        let engine = ScopeEngine::new(&l);
2679        let src = "locals {\n  x = 1\n  y = local.x + 1\n}";
2680        let defs = engine.find_definitions("hcl", src, "x");
2681        assert_eq!(defs.len(), 1, "hcl: locals block should define x");
2682    }
2683
2684    // ── Verilog ───────────────────────────────────────────────────────────
2685
2686    #[test]
2687    fn test_verilog_module_port() {
2688        let l = loader();
2689        if skip_if_no(&l, "verilog") {
2690            return;
2691        }
2692        let engine = ScopeEngine::new(&l);
2693        let src = "module foo (input x, output y); endmodule";
2694        let defs = engine.find_definitions("verilog", src, "x");
2695        assert_eq!(defs.len(), 1, "verilog: input port x should be defined");
2696    }
2697
2698    #[test]
2699    fn test_verilog_wire_declaration() {
2700        let l = loader();
2701        if skip_if_no(&l, "verilog") {
2702            return;
2703        }
2704        let engine = ScopeEngine::new(&l);
2705        let src = "module foo (); wire z; endmodule";
2706        let defs = engine.find_definitions("verilog", src, "z");
2707        assert_eq!(defs.len(), 1, "verilog: wire z should be defined");
2708    }
2709
2710    // ── VHDL ──────────────────────────────────────────────────────────────
2711
2712    #[test]
2713    fn test_vhdl_signal_declaration() {
2714        let l = loader();
2715        if skip_if_no(&l, "vhdl") {
2716            return;
2717        }
2718        let engine = ScopeEngine::new(&l);
2719        let src = "architecture Foo of Bar is\n  signal x : std_logic;\nbegin\nend Foo;";
2720        let defs = engine.find_definitions("vhdl", src, "x");
2721        assert_eq!(defs.len(), 1, "vhdl: signal x should be defined");
2722    }
2723
2724    #[test]
2725    fn test_vhdl_process_variable() {
2726        let l = loader();
2727        if skip_if_no(&l, "vhdl") {
2728            return;
2729        }
2730        let engine = ScopeEngine::new(&l);
2731        let src = "architecture Foo of Bar is\nbegin\nprocess\n  variable y : integer;\nbegin\nend process;\nend Foo;";
2732        let defs = engine.find_definitions("vhdl", src, "y");
2733        assert_eq!(defs.len(), 1, "vhdl: process variable y should be defined");
2734    }
2735
2736    // ── jq ────────────────────────────────────────────────────────────────
2737
2738    #[test]
2739    fn test_jq_function_parameter() {
2740        let l = loader();
2741        if skip_if_no(&l, "jq") {
2742            return;
2743        }
2744        let engine = ScopeEngine::new(&l);
2745        let src = "def foo(x): x + 1;";
2746        let defs = engine.find_definitions("jq", src, "x");
2747        assert_eq!(defs.len(), 1, "jq: function parameter x should be defined");
2748    }
2749
2750    #[test]
2751    fn test_jq_function_name() {
2752        let l = loader();
2753        if skip_if_no(&l, "jq") {
2754            return;
2755        }
2756        let engine = ScopeEngine::new(&l);
2757        let src = "def foo(x): x;";
2758        let defs = engine.find_definitions("jq", src, "foo");
2759        assert_eq!(defs.len(), 1, "jq: function name foo should be defined");
2760    }
2761
2762    #[test]
2763    fn test_jq_as_binding() {
2764        let l = loader();
2765        if skip_if_no(&l, "jq") {
2766            return;
2767        }
2768        let engine = ScopeEngine::new(&l);
2769        let src = "def f: . as $item | $item; f";
2770        let defs = engine.find_definitions("jq", src, "$item");
2771        assert_eq!(defs.len(), 1, "jq: as binding $item should be defined");
2772        let refs = engine.find_references("jq", src, "$item");
2773        let resolved = refs.iter().filter(|r| r.definition.is_some()).count();
2774        assert!(resolved >= 1, "jq: $item reference should resolve");
2775    }
2776
2777    // ── Meson ─────────────────────────────────────────────────────────────
2778
2779    #[test]
2780    fn test_meson_variable_assignment() {
2781        let l = loader();
2782        if skip_if_no(&l, "meson") {
2783            return;
2784        }
2785        let engine = ScopeEngine::new(&l);
2786        let src = "x = 1\ny = x";
2787        let defs = engine.find_definitions("meson", src, "x");
2788        assert_eq!(
2789            defs.len(),
2790            1,
2791            "meson: variable assignment x should be defined"
2792        );
2793    }
2794
2795    #[test]
2796    fn test_meson_foreach_variable() {
2797        let l = loader();
2798        if skip_if_no(&l, "meson") {
2799            return;
2800        }
2801        let engine = ScopeEngine::new(&l);
2802        let src = "foreach item : ['a', 'b']\n  message(item)\nendforeach";
2803        let defs = engine.find_definitions("meson", src, "item");
2804        assert_eq!(
2805            defs.len(),
2806            1,
2807            "meson: foreach variable item should be defined"
2808        );
2809    }
2810
2811    // --- AST exploration tests for locals.scm authoring ---
2812    // Run with: cargo test -p normalize-scope -- --nocapture ast_batch
2813    // (or ast_vb, ast_idris, ast_wit)
2814
2815    // --- vb ---
2816
2817    #[test]
2818    fn scope_vb_method_param() {
2819        let l = loader();
2820        if skip_if_no(&l, "vb") {
2821            return;
2822        }
2823        let engine = ScopeEngine::new(&l);
2824        let src = "Module M\r\n    Function Add(ByVal x As Integer, ByVal y As Integer) As Integer\r\n        Return x + y\r\n    End Function\r\nEnd Module\r\n";
2825        let defs = engine.find_definitions("vb", src, "x");
2826        assert_eq!(defs.len(), 1, "vb: x should be defined as parameter");
2827        let refs = engine.find_references("vb", src, "x");
2828        assert!(
2829            refs.iter().any(|r| r.definition.is_some()),
2830            "vb: x reference should resolve to param"
2831        );
2832    }
2833
2834    #[test]
2835    fn scope_vb_dim_statement() {
2836        let l = loader();
2837        if skip_if_no(&l, "vb") {
2838            return;
2839        }
2840        let engine = ScopeEngine::new(&l);
2841        let src = "Module M\r\n    Sub Run()\r\n        Dim result As Integer = 42\r\n        Console.WriteLine(result)\r\n    End Sub\r\nEnd Module\r\n";
2842        let defs = engine.find_definitions("vb", src, "result");
2843        assert_eq!(defs.len(), 1, "vb: result should be defined via Dim");
2844        let refs = engine.find_references("vb", src, "result");
2845        assert!(
2846            refs.iter().any(|r| r.definition.is_some()),
2847            "vb: result should resolve to Dim"
2848        );
2849    }
2850
2851    #[test]
2852    fn scope_vb_for_each() {
2853        let l = loader();
2854        if skip_if_no(&l, "vb") {
2855            return;
2856        }
2857        let engine = ScopeEngine::new(&l);
2858        let src = "Module M\r\n    Sub Run(items As Object)\r\n        For Each item In items\r\n            Console.WriteLine(item)\r\n        Next\r\n    End Sub\r\nEnd Module\r\n";
2859        let defs = engine.find_definitions("vb", src, "item");
2860        assert_eq!(defs.len(), 1, "vb: item should be defined by For Each");
2861    }
2862
2863    // --- idris ---
2864
2865    #[test]
2866    fn scope_idris_function_param() {
2867        let l = loader();
2868        if skip_if_no(&l, "idris") {
2869            return;
2870        }
2871        let engine = ScopeEngine::new(&l);
2872        let src = "add : Int -> Int -> Int\nadd x y = x + y\n";
2873        let defs = engine.find_definitions("idris", src, "x");
2874        assert_eq!(defs.len(), 1, "idris: x should be defined as pattern param");
2875        let refs = engine.find_references("idris", src, "x");
2876        assert!(
2877            refs.iter().any(|r| r.definition.is_some()),
2878            "idris: x should resolve to param definition"
2879        );
2880    }
2881
2882    #[test]
2883    fn scope_idris_function_name() {
2884        let l = loader();
2885        if skip_if_no(&l, "idris") {
2886            return;
2887        }
2888        let engine = ScopeEngine::new(&l);
2889        let src = "double : Int -> Int\ndouble n = n + n\n";
2890        let defs = engine.find_definitions("idris", src, "n");
2891        assert_eq!(defs.len(), 1, "idris: n should be defined as pattern param");
2892    }
2893
2894    // --- lean ---
2895
2896    #[test]
2897    fn scope_lean_def_param() {
2898        let l = loader();
2899        if skip_if_no(&l, "lean") {
2900            return;
2901        }
2902        let engine = ScopeEngine::new(&l);
2903        let src = "def double (n : Nat) : Nat := n + n\n";
2904        let defs = engine.find_definitions("lean", src, "n");
2905        assert_eq!(
2906            defs.len(),
2907            1,
2908            "lean: n should be defined as explicit binder"
2909        );
2910        let refs = engine.find_references("lean", src, "n");
2911        assert!(
2912            refs.iter().any(|r| r.definition.is_some()),
2913            "lean: n should resolve to param"
2914        );
2915    }
2916
2917    #[test]
2918    fn scope_lean_let_binding() {
2919        let l = loader();
2920        if skip_if_no(&l, "lean") {
2921            return;
2922        }
2923        let engine = ScopeEngine::new(&l);
2924        let src = "def example : Nat :=\n  let x := 5\n  x + x\n";
2925        let defs = engine.find_definitions("lean", src, "x");
2926        assert_eq!(defs.len(), 1, "lean: x should be defined by let binding");
2927        let refs = engine.find_references("lean", src, "x");
2928        assert!(
2929            refs.iter().any(|r| r.definition.is_some()),
2930            "lean: x should resolve to let binding"
2931        );
2932    }
2933
2934    #[test]
2935    fn scope_lean_fun_param() {
2936        let l = loader();
2937        if skip_if_no(&l, "lean") {
2938            return;
2939        }
2940        let engine = ScopeEngine::new(&l);
2941        let src = "def apply := fun x => x + 1\n";
2942        let defs = engine.find_definitions("lean", src, "x");
2943        assert_eq!(defs.len(), 1, "lean: x should be defined as fun parameter");
2944    }
2945
2946    // --- agda ---
2947
2948    #[test]
2949    fn scope_agda_type_sig_def() {
2950        let l = loader();
2951        if skip_if_no(&l, "agda") {
2952            return;
2953        }
2954        let engine = ScopeEngine::new(&l);
2955        let src = "module Main where\n\ndouble : Nat -> Nat\ndouble n = n + n\n";
2956        let defs = engine.find_definitions("agda", src, "double");
2957        assert_eq!(
2958            defs.len(),
2959            1,
2960            "agda: double type sig should be captured as definition"
2961        );
2962    }
2963
2964    #[test]
2965    fn scope_agda_reference_resolves() {
2966        let l = loader();
2967        if skip_if_no(&l, "agda") {
2968            return;
2969        }
2970        let engine = ScopeEngine::new(&l);
2971        // double is defined via type signature; call site in 'main' should resolve
2972        let src = "module Main where\n\ndouble : Nat -> Nat\ndouble n = n + n\n\nmain : IO ()\nmain = double 5\n";
2973        let defs = engine.find_definitions("agda", src, "double");
2974        assert_eq!(
2975            defs.len(),
2976            1,
2977            "agda: double type sig should be a definition"
2978        );
2979        let refs = engine.find_references("agda", src, "double");
2980        assert!(
2981            refs.iter().any(|r| r.definition.is_some()),
2982            "agda: double call should resolve to type sig definition"
2983        );
2984    }
2985
2986    // --- glsl ---
2987
2988    #[test]
2989    fn scope_glsl_function_param() {
2990        let l = loader();
2991        if skip_if_no(&l, "glsl") {
2992            return;
2993        }
2994        let engine = ScopeEngine::new(&l);
2995        let src = "float computeLight(vec3 pos, float intensity) {\n    float result = pos.x * intensity;\n    return result;\n}\n";
2996        let defs = engine.find_definitions("glsl", src, "intensity");
2997        assert_eq!(
2998            defs.len(),
2999            1,
3000            "glsl: intensity should be defined as parameter"
3001        );
3002        let refs = engine.find_references("glsl", src, "intensity");
3003        assert!(
3004            refs.iter().any(|r| r.definition.is_some()),
3005            "glsl: intensity should resolve to param"
3006        );
3007    }
3008
3009    #[test]
3010    fn scope_glsl_local_var() {
3011        let l = loader();
3012        if skip_if_no(&l, "glsl") {
3013            return;
3014        }
3015        let engine = ScopeEngine::new(&l);
3016        let src = "void main() {\n    vec3 color = vec3(1.0, 0.0, 0.0);\n    gl_FragColor = vec4(color, 1.0);\n}\n";
3017        let defs = engine.find_definitions("glsl", src, "color");
3018        assert_eq!(defs.len(), 1, "glsl: color should be defined as local var");
3019        let refs = engine.find_references("glsl", src, "color");
3020        assert!(
3021            refs.iter().any(|r| r.definition.is_some()),
3022            "glsl: color should resolve to local var"
3023        );
3024    }
3025
3026    // --- hlsl ---
3027
3028    #[test]
3029    fn scope_hlsl_function_param() {
3030        let l = loader();
3031        if skip_if_no(&l, "hlsl") {
3032            return;
3033        }
3034        let engine = ScopeEngine::new(&l);
3035        let src = "float computeValue(float x, float y) {\n    float result = x + y;\n    return result;\n}\n";
3036        let defs = engine.find_definitions("hlsl", src, "x");
3037        assert_eq!(defs.len(), 1, "hlsl: x should be defined as parameter");
3038        let refs = engine.find_references("hlsl", src, "x");
3039        assert!(
3040            refs.iter().any(|r| r.definition.is_some()),
3041            "hlsl: x should resolve to param"
3042        );
3043    }
3044
3045    #[test]
3046    fn scope_hlsl_local_var() {
3047        let l = loader();
3048        if skip_if_no(&l, "hlsl") {
3049            return;
3050        }
3051        let engine = ScopeEngine::new(&l);
3052        let src = "float4 PSMain() : SV_Target {\n    float brightness = 1.0;\n    return float4(brightness, 0.0, 0.0, 1.0);\n}\n";
3053        let defs = engine.find_definitions("hlsl", src, "brightness");
3054        assert_eq!(
3055            defs.len(),
3056            1,
3057            "hlsl: brightness should be defined as local var"
3058        );
3059        let refs = engine.find_references("hlsl", src, "brightness");
3060        assert!(
3061            refs.iter().any(|r| r.definition.is_some()),
3062            "hlsl: brightness should resolve to local var"
3063        );
3064    }
3065
3066    // --- yuri ---
3067
3068    #[test]
3069    fn scope_yuri_function_param() {
3070        let l = loader();
3071        if skip_if_no(&l, "yuri") {
3072            return;
3073        }
3074        let engine = ScopeEngine::new(&l);
3075        // Yuri syntax: fn name(params) : return_type { body }
3076        // No semicolons — variable_item value extends to end of expression
3077        let src = "fn add(a: f32, b: f32) : f32 {\n    let result = a\n    result\n}\n";
3078        let defs = engine.find_definitions("yuri", src, "a");
3079        assert_eq!(defs.len(), 1, "yuri: a should be defined as parameter");
3080        let refs = engine.find_references("yuri", src, "a");
3081        assert!(
3082            refs.iter().any(|r| r.definition.is_some()),
3083            "yuri: a should resolve to param definition"
3084        );
3085    }
3086
3087    #[test]
3088    fn scope_yuri_let_binding() {
3089        let l = loader();
3090        if skip_if_no(&l, "yuri") {
3091            return;
3092        }
3093        let engine = ScopeEngine::new(&l);
3094        let src = "fn add(a: f32, b: f32) : f32 {\n    let result = a\n    result\n}\n";
3095        let defs = engine.find_definitions("yuri", src, "result");
3096        assert_eq!(
3097            defs.len(),
3098            1,
3099            "yuri: result should be defined by let binding"
3100        );
3101        let refs = engine.find_references("yuri", src, "result");
3102        assert!(
3103            refs.iter().any(|r| r.definition.is_some()),
3104            "yuri: result should resolve to let binding"
3105        );
3106    }
3107}