Skip to main content

pine_sema/
symbols.rs

1//! A durable symbol table.
2//!
3//! Every declaration a script makes — its kind, where it is written, its
4//! parameters or type — arranged in the scope tree they belong to. That is what
5//! a tool queries: go-to-definition (a symbol's `decl`), hover (its kind +
6//! signature), completion (the symbols visible in a scope, or the members of a
7//! type).
8//!
9//! This is the analyzer's *only* scope structure: [`Analyzer`](crate::Analyzer)
10//! both resolves names against it and records declarations into it as it walks.
11//! Imported libraries are analyzed into the same table under their own
12//! [`FileId`] and their own global (root) scope, so `alias.export` resolves
13//! cross-file while names never leak between files.
14
15use crate::scope::Namespace;
16pub use crate::scope::SymbolKind;
17
18/// A scope's index within a [`SymbolTable`]. `0` is always the main file's
19/// global scope.
20pub type ScopeId = usize;
21
22/// A symbol's index within a [`SymbolTable`].
23pub type SymbolId = usize;
24
25/// A source file's index within a [`SymbolTable`]. `0` is always the main
26/// script; imported libraries get the ids that follow.
27pub type FileId = usize;
28
29/// What opened a scope — for display and for scope-aware queries.
30#[derive(Debug, Clone, Copy, PartialEq, Eq)]
31pub enum ScopeKind {
32    /// The whole script.
33    Global,
34    /// A function/method body; its parameters are declared here.
35    Function,
36    /// A loop or `if`/`else` block.
37    Block,
38}
39
40/// A declared name and what is known about it at its declaration.
41#[derive(Debug, Clone, PartialEq)]
42pub struct Symbol {
43    pub name: String,
44    pub kind: SymbolKind,
45    pub file: FileId,
46    pub decl: Option<(u32, u32)>,
47    pub scope: ScopeId,
48    pub params: Vec<String>,
49    pub type_annotation: Option<String>,
50    /// The type/enum this symbol is a member (field or enum case) of. A member
51    /// is reached as `owner.member`, so it is not in any scope's resolution list.
52    pub container: Option<SymbolId>,
53    /// For a variable of a user-defined type, that type's symbol, so `v.field`
54    /// resolves. Set from an annotation or a `Type.new()` initializer.
55    pub type_ref: Option<SymbolId>,
56    pub exported: bool,
57    /// For an import alias, the imported library's global scope.
58    pub module: Option<ScopeId>,
59}
60
61impl Symbol {
62    pub(crate) fn new(
63        name: &str,
64        kind: SymbolKind,
65        decl: Option<(u32, u32)>,
66        scope: ScopeId,
67    ) -> Self {
68        Symbol {
69            name: name.to_string(),
70            kind,
71            file: 0,
72            decl,
73            scope,
74            params: Vec::new(),
75            type_annotation: None,
76            container: None,
77            type_ref: None,
78            exported: false,
79            module: None,
80        }
81    }
82
83    pub(crate) fn with_params(mut self, params: Vec<String>) -> Self {
84        self.params = params;
85        self
86    }
87
88    pub(crate) fn with_type(mut self, type_annotation: Option<String>) -> Self {
89        self.type_annotation = type_annotation;
90        self
91    }
92
93    pub(crate) fn with_type_ref(mut self, type_ref: Option<SymbolId>) -> Self {
94        self.type_ref = type_ref;
95        self
96    }
97}
98
99#[derive(Debug, Clone)]
100struct ScopeData {
101    parent: Option<ScopeId>,
102    kind: ScopeKind,
103    file: FileId,
104    symbols: Vec<SymbolId>,
105}
106
107/// A use of a symbol — the index behind find-references / rename.
108#[derive(Debug, Clone)]
109struct Occurrence {
110    file: FileId,
111    line: u32,
112    column: u32,
113    symbol: SymbolId,
114}
115
116/// The scope tree of a program, the symbols each scope declares, every use of
117/// those symbols, and the files they live in.
118#[derive(Debug, Clone)]
119pub struct SymbolTable {
120    symbols: Vec<Symbol>,
121    scopes: Vec<ScopeData>,
122    occurrences: Vec<Occurrence>,
123    files: Vec<String>,
124    file_roots: Vec<ScopeId>,
125}
126
127impl SymbolTable {
128    /// The main file's global scope, always present.
129    pub const GLOBAL: ScopeId = 0;
130    /// The main file's id, always present.
131    pub const MAIN: FileId = 0;
132
133    pub(crate) fn new() -> Self {
134        SymbolTable {
135            symbols: Vec::new(),
136            scopes: vec![ScopeData {
137                parent: None,
138                kind: ScopeKind::Global,
139                file: Self::MAIN,
140                symbols: Vec::new(),
141            }],
142            occurrences: Vec::new(),
143            files: vec![String::new()],
144            file_roots: vec![Self::GLOBAL],
145        }
146    }
147
148    pub(crate) fn add_file(&mut self, path: &str) -> (FileId, ScopeId) {
149        let file = self.files.len();
150        self.files.push(path.to_string());
151        let root = self.scopes.len();
152        self.scopes.push(ScopeData {
153            parent: None,
154            kind: ScopeKind::Global,
155            file,
156            symbols: Vec::new(),
157        });
158        self.file_roots.push(root);
159        (file, root)
160    }
161
162    pub(crate) fn file_by_path(&self, path: &str) -> Option<FileId> {
163        self.files.iter().position(|p| p == path)
164    }
165
166    pub fn file_path(&self, file: FileId) -> &str {
167        &self.files[file]
168    }
169
170    pub fn file_root(&self, file: FileId) -> ScopeId {
171        self.file_roots[file]
172    }
173
174    pub fn files(&self) -> impl Iterator<Item = (FileId, &str)> {
175        self.files
176            .iter()
177            .enumerate()
178            .map(|(id, path)| (id, path.as_str()))
179    }
180
181    pub fn scope_file(&self, scope: ScopeId) -> FileId {
182        self.scopes[scope].file
183    }
184
185    pub(crate) fn open_scope(&mut self, parent: ScopeId, kind: ScopeKind) -> ScopeId {
186        let id = self.scopes.len();
187        let file = self.scopes[parent].file;
188        self.scopes.push(ScopeData {
189            parent: Some(parent),
190            kind,
191            file,
192            symbols: Vec::new(),
193        });
194        id
195    }
196
197    pub(crate) fn set_module(&mut self, id: SymbolId, scope: ScopeId) {
198        self.symbols[id].module = Some(scope);
199    }
200
201    pub(crate) fn mark_exported(&mut self, id: SymbolId) {
202        self.symbols[id].exported = true;
203    }
204
205    /// Record a symbol reachable by bare name in its scope.
206    pub(crate) fn declare(&mut self, symbol: Symbol) -> SymbolId {
207        let scope = symbol.scope;
208        let id = self.symbols.len();
209        self.symbols.push(symbol);
210        self.scopes[scope].symbols.push(id);
211        id
212    }
213
214    /// Whether `name` is already declared directly in `scope` (ignoring parents)
215    /// — a redeclaration in the same scope.
216    pub(crate) fn declared_locally(&self, scope: ScopeId, name: &str) -> bool {
217        self.scopes[scope]
218            .symbols
219            .iter()
220            .any(|&id| self.symbols[id].name == name)
221    }
222
223    /// Like [`Self::declared_locally`], but only counts a symbol in the same
224    /// namespace — so a type and a value may share a name.
225    pub(crate) fn declared_locally_in(
226        &self,
227        scope: ScopeId,
228        name: &str,
229        namespace: Namespace,
230    ) -> bool {
231        self.scopes[scope].symbols.iter().any(|&id| {
232            let symbol = &self.symbols[id];
233            symbol.name == name && symbol.kind.namespace() == namespace
234        })
235    }
236
237    /// Record that the name at `(file, pos)` refers to `symbol` — one entry in
238    /// the occurrence index. `None` positions (unknown) are dropped.
239    pub(crate) fn record_use(&mut self, file: FileId, pos: Option<(u32, u32)>, symbol: SymbolId) {
240        if let Some((line, column)) = pos {
241            self.occurrences.push(Occurrence {
242                file,
243                line,
244                column,
245                symbol,
246            });
247        }
248    }
249
250    /// Record a member (field or enum case) of `owner`. Not reachable by bare
251    /// name, so not added to any scope's resolution list.
252    pub(crate) fn declare_member(
253        &mut self,
254        owner: SymbolId,
255        name: &str,
256        decl: Option<(u32, u32)>,
257        type_annotation: Option<String>,
258    ) {
259        let scope = self.symbols[owner].scope;
260        let file = self.symbols[owner].file;
261        let mut member = Symbol::new(name, SymbolKind::Var, decl, scope);
262        member.file = file;
263        member.type_annotation = type_annotation;
264        member.container = Some(owner);
265        self.symbols.push(member);
266    }
267
268    // --- Queries ---
269
270    /// Every symbol declared anywhere, in declaration order.
271    pub fn symbols(&self) -> &[Symbol] {
272        &self.symbols
273    }
274
275    /// The kind of a scope.
276    pub fn scope_kind(&self, scope: ScopeId) -> ScopeKind {
277        self.scopes[scope].kind
278    }
279
280    /// The symbols reachable by bare name directly in `scope`.
281    pub fn symbols_in(&self, scope: ScopeId) -> impl Iterator<Item = &Symbol> {
282        self.scopes[scope]
283            .symbols
284            .iter()
285            .map(|&id| &self.symbols[id])
286    }
287
288    /// The members (fields, enum cases) of a type or enum symbol — the
289    /// candidates for completion after `owner.`.
290    pub fn members_of(&self, owner: SymbolId) -> impl Iterator<Item = &Symbol> {
291        self.symbols
292            .iter()
293            .filter(move |s| s.container == Some(owner))
294    }
295
296    /// The member of `owner` named `name`, if any — resolves `owner.name`.
297    pub fn member_id(&self, owner: SymbolId, name: &str) -> Option<SymbolId> {
298        self.symbols
299            .iter()
300            .position(|s| s.container == Some(owner) && s.name == name)
301    }
302
303    /// An `export`ed symbol named `name` declared directly in `scope` — how a
304    /// member access resolves against an imported library's global scope.
305    pub fn exported_id(&self, scope: ScopeId, name: &str) -> Option<SymbolId> {
306        self.scopes[scope]
307            .symbols
308            .iter()
309            .rev()
310            .find(|&&id| self.symbols[id].exported && self.symbols[id].name == name)
311            .copied()
312    }
313
314    /// A symbol's declaration location as `(file, line, column)` — go-to-def,
315    /// including into an imported library file.
316    pub fn declaration_location(&self, id: SymbolId) -> Option<(FileId, u32, u32)> {
317        let symbol = &self.symbols[id];
318        symbol.decl.map(|(line, col)| (symbol.file, line, col))
319    }
320
321    /// The symbol at index `id`.
322    pub fn symbol(&self, id: SymbolId) -> &Symbol {
323        &self.symbols[id]
324    }
325
326    /// Resolve `name` from `scope` outward, innermost first.
327    pub fn resolve(&self, scope: ScopeId, name: &str) -> Option<&Symbol> {
328        self.resolve_id(scope, name).map(|id| &self.symbols[id])
329    }
330
331    /// Like [`resolve`](Self::resolve) but returns the symbol's id, so a use can
332    /// be recorded against it.
333    pub fn resolve_id(&self, scope: ScopeId, name: &str) -> Option<SymbolId> {
334        let mut current = Some(scope);
335        while let Some(id) = current {
336            let data = &self.scopes[id];
337            if let Some(&sym) = data
338                .symbols
339                .iter()
340                .rev()
341                .find(|&&s| self.symbols[s].name == name)
342            {
343                return Some(sym);
344            }
345            current = data.parent;
346        }
347        None
348    }
349
350    pub fn references(&self, symbol: SymbolId) -> impl Iterator<Item = (FileId, u32, u32)> + '_ {
351        self.occurrences
352            .iter()
353            .filter(move |o| o.symbol == symbol)
354            .map(|o| (o.file, o.line, o.column))
355    }
356
357    pub fn occurrences_in_file(
358        &self,
359        file: FileId,
360    ) -> impl Iterator<Item = (u32, u32, SymbolId)> + '_ {
361        self.occurrences
362            .iter()
363            .filter(move |o| o.file == file)
364            .map(|o| (o.line, o.column, o.symbol))
365    }
366
367    /// The symbol referenced or declared at `(file, line, column)` — hover and
368    /// go-to-definition, whether the cursor sits on a use or the declaration.
369    pub fn symbol_at(&self, file: FileId, line: u32, column: u32) -> Option<SymbolId> {
370        if let Some(occ) = self
371            .occurrences
372            .iter()
373            .find(|o| o.file == file && o.line == line && o.column == column)
374        {
375            return Some(occ.symbol);
376        }
377        self.symbols
378            .iter()
379            .position(|s| s.file == file && s.decl == Some((line, column)))
380    }
381
382    /// The symbol *declared* at `(file, line, column)`, if any.
383    pub fn find_declaration_at(&self, file: FileId, line: u32, column: u32) -> Option<&Symbol> {
384        self.symbols
385            .iter()
386            .find(|s| s.file == file && s.decl == Some((line, column)))
387    }
388}
389
390#[cfg(test)]
391mod tests {
392    use crate::{analyze_with_symbols, Diagnostic, SymbolKind, SymbolTable};
393    use pine_core::{DefaultPineOutput, FileResolver};
394    use pine_interpreter::Value;
395    use pine_parser::Parser;
396    use std::collections::HashMap;
397
398    const MAIN: crate::FileId = SymbolTable::MAIN;
399
400    fn table(source: &str) -> SymbolTable {
401        analyze(source, None).1
402    }
403
404    /// Analyze `source` (the main file) with an optional in-memory loader, and
405    /// return diagnostics + the symbol table.
406    fn analyze(source: &str, loader: Option<&FileResolver>) -> (Vec<Diagnostic>, SymbolTable) {
407        let program = Parser::parse_source(source).unwrap();
408        let builtins: HashMap<String, Value<DefaultPineOutput>> = HashMap::new();
409        analyze_with_symbols(
410            &program,
411            &builtins,
412            loader.map(|l| l as &dyn crate::LibraryLoader),
413        )
414    }
415
416    /// An in-memory loader over `(path, source)` library files.
417    fn libs(files: &[(&str, &str)]) -> FileResolver {
418        let mut resolver = FileResolver::new();
419        for (path, source) in files {
420            resolver.add(path, source);
421        }
422        resolver
423    }
424
425    #[test]
426    fn records_kinds_locations_and_params() {
427        // `sum` is a function (line 3), its parameters live in a child scope,
428        // and `total` is a global variable (line 4).
429        let table =
430            table("//@version=5\nindicator(\"t\")\nsum(a, b) => a + b\ntotal = sum(1, 2)\n");
431
432        let sum = table.resolve(SymbolTable::GLOBAL, "sum").unwrap();
433        assert_eq!(sum.kind, SymbolKind::Function);
434        assert_eq!(sum.params, vec!["a", "b"]);
435        assert_eq!(sum.decl, Some((3, 1)));
436
437        let total = table.resolve(SymbolTable::GLOBAL, "total").unwrap();
438        assert_eq!(total.kind, SymbolKind::Var);
439        assert_eq!(total.decl, Some((4, 1)));
440
441        // The parameters are not visible at global scope, only inside the body.
442        assert!(table.resolve(SymbolTable::GLOBAL, "a").is_none());
443        assert!(table.symbols().iter().any(|s| s.name == "a"));
444    }
445
446    #[test]
447    fn records_type_members_and_lookups() {
448        let table = table(
449            "//@version=5\nindicator(\"t\")\ntype Point\n    float x\n    float y = 0.0\np = Point.new()\n",
450        );
451
452        let point = table.resolve(SymbolTable::GLOBAL, "Point").unwrap();
453        assert_eq!(point.kind, SymbolKind::Type);
454
455        // Members are reached through the owner, not by bare name.
456        assert!(table.resolve(SymbolTable::GLOBAL, "x").is_none());
457        let point_id = table
458            .symbols()
459            .iter()
460            .position(|s| s.name == "Point")
461            .unwrap();
462        let members: Vec<_> = table
463            .members_of(point_id)
464            .map(|m| m.name.as_str())
465            .collect();
466        assert_eq!(members, vec!["x", "y"]);
467
468        // `find_declaration_at` maps a declaration position back to its symbol.
469        let x = table.find_declaration_at(MAIN, 4, 11).unwrap();
470        assert_eq!(x.name, "x");
471        assert_eq!(x.type_annotation.as_deref(), Some("float"));
472    }
473
474    #[test]
475    fn records_uses_for_references_and_hover() {
476        // `x` is declared on line 3 and used twice on line 4 (`y = x + x`).
477        let table = table("//@version=5\nindicator(\"t\")\nx = 1\ny = x + x\n");
478        let x_id = table.symbols().iter().position(|s| s.name == "x").unwrap();
479
480        // find-references: both uses, not the declaration.
481        let mut refs: Vec<_> = table.references(x_id).collect();
482        refs.sort();
483        assert_eq!(refs, vec![(MAIN, 4, 5), (MAIN, 4, 9)]);
484
485        // hover / go-to-definition maps a cursor on a use back to the symbol…
486        assert_eq!(table.symbol_at(MAIN, 4, 5), Some(x_id));
487        assert_eq!(table.symbol_at(MAIN, 4, 9), Some(x_id));
488        // …and a cursor on the declaration resolves to it too.
489        assert_eq!(table.symbol_at(MAIN, 3, 1), Some(x_id));
490
491        // A builtin use (`indicator`) is not a user symbol, so no occurrence.
492        assert!(table.symbol_at(MAIN, 2, 1).is_none());
493    }
494
495    #[test]
496    fn resolves_members_of_a_typed_variable() {
497        // `p` is a Point (via the `.new()` constructor); `p.x` on line 6 refers
498        // to the field `x` declared on line 4.
499        let table = table(
500            "//@version=5\nindicator(\"t\")\ntype Point\n    float x\n    float y\np = Point.new()\nv = p.x\n",
501        );
502
503        let point_id = table
504            .symbols()
505            .iter()
506            .position(|s| s.name == "Point")
507            .unwrap();
508        let x_id = table.member_id(point_id, "x").unwrap();
509
510        // The `x` in `p.x` (line 7) resolves to the field, not a bare name.
511        assert!(table.resolve(SymbolTable::GLOBAL, "x").is_none());
512        let refs: Vec<_> = table.references(x_id).collect();
513        assert_eq!(refs, vec![(MAIN, 7, 7)]);
514        assert_eq!(table.symbol_at(MAIN, 7, 7), Some(x_id));
515    }
516
517    #[test]
518    fn resolves_enum_cases() {
519        // `Signal.buy` refers to the enum case declared on line 4.
520        let table = table(
521            "//@version=5\nindicator(\"t\")\nenum Signal\n    buy\n    sell\ns = Signal.buy\n",
522        );
523
524        let signal_id = table
525            .symbols()
526            .iter()
527            .position(|s| s.name == "Signal")
528            .unwrap();
529        let buy_id = table.member_id(signal_id, "buy").unwrap();
530
531        assert_eq!(table.symbol_at(MAIN, 6, 12), Some(buy_id));
532    }
533
534    #[test]
535    fn imports_record_the_alias_but_not_its_members_without_a_loader() {
536        // Without a loader, `import foo/bar/1 as lib` then `x = lib.calc(1)`.
537        let table =
538            table("//@version=5\nindicator(\"t\")\nimport foo/bar/1 as lib\nx = lib.calc(1)\n");
539
540        // The alias is a symbol, so go-to-definition / find-references on `lib`
541        // work: it is declared on line 3 and used once (line 4, `lib.calc`).
542        let lib = table.resolve(SymbolTable::GLOBAL, "lib").unwrap();
543        assert_eq!(lib.kind, SymbolKind::Import);
544        let lib_id = table
545            .symbols()
546            .iter()
547            .position(|s| s.name == "lib")
548            .unwrap();
549        assert_eq!(
550            table.references(lib_id).collect::<Vec<_>>(),
551            vec![(MAIN, 4, 5)]
552        );
553
554        // The member `calc` is a library export — with no loader the library is
555        // not analyzed, so it does not resolve. No occurrence, no guess.
556        assert_eq!(table.symbol_at(MAIN, 4, 9), None);
557    }
558
559    #[test]
560    fn resolves_a_library_export_across_files() {
561        let loader = libs(&[("lib", "//@version=5\nexport add(a, b) => a + b\n")]);
562        let (_diags, table) = analyze(
563            "//@version=5\nindicator(\"t\")\nimport lib as l\nx = l.add(1, 2)\n",
564            Some(&loader),
565        );
566
567        // `l.add` resolves to the library's exported function.
568        let add_id = table
569            .symbols()
570            .iter()
571            .position(|s| s.name == "add" && s.exported)
572            .unwrap();
573        // Its declaration is in the library file, go-to-def lands there.
574        let (file, _, _) = table.declaration_location(add_id).unwrap();
575        assert_ne!(file, MAIN);
576        assert_eq!(table.file_path(file), "lib");
577        // Find-references reports the single main-file use, which maps back.
578        let refs: Vec<_> = table.references(add_id).collect();
579        assert_eq!(refs.len(), 1);
580        let (rf, rl, rc) = refs[0];
581        assert_eq!(rf, MAIN);
582        assert_eq!(table.symbol_at(rf, rl, rc), Some(add_id));
583    }
584
585    #[test]
586    fn resolves_a_field_of_a_variable_typed_from_a_library() {
587        let loader = libs(&[(
588            "geo",
589            "//@version=5\nexport type Point\n    float x\n    float y\n",
590        )]);
591        let (_diags, table) = analyze(
592            "//@version=5\nindicator(\"t\")\nimport geo as g\np = g.Point.new()\nv = p.x\n",
593            Some(&loader),
594        );
595
596        let point_id = table
597            .symbols()
598            .iter()
599            .position(|s| s.name == "Point" && s.exported)
600            .unwrap();
601        let x_id = table.member_id(point_id, "x").unwrap();
602        // `p.x` (main) resolves to the library field — one use, in the main file.
603        let refs: Vec<_> = table.references(x_id).collect();
604        assert_eq!(refs.len(), 1);
605        assert_eq!(refs[0].0, MAIN);
606    }
607
608    #[test]
609    fn resolves_an_enum_case_from_a_library() {
610        let loader = libs(&[(
611            "sig",
612            "//@version=5\nexport enum Direction\n    up\n    down\n",
613        )]);
614        let (_diags, table) = analyze(
615            "//@version=5\nindicator(\"t\")\nimport sig as s\nd = s.Direction.up\n",
616            Some(&loader),
617        );
618
619        let dir_id = table
620            .symbols()
621            .iter()
622            .position(|s| s.name == "Direction" && s.exported)
623            .unwrap();
624        let up_id = table.member_id(dir_id, "up").unwrap();
625        assert_eq!(table.references(up_id).count(), 1);
626    }
627
628    #[test]
629    fn a_library_use_of_its_own_import_resolves() {
630        // main -> a -> b. `a`'s exported `compute` body calls `bb.helper()`
631        // (b's export, reached through a's own import). That inner use resolves
632        // to b's `helper`, recorded in file `a`.
633        let loader = libs(&[
634            ("b", "//@version=5\nexport helper() => 42\n"),
635            (
636                "a",
637                "//@version=5\nimport b as bb\nexport compute() => bb.helper()\n",
638            ),
639        ]);
640        let (_diags, table) = analyze(
641            "//@version=5\nindicator(\"t\")\nimport a as x\ny = x.compute()\n",
642            Some(&loader),
643        );
644
645        // main's `x.compute` resolves to a's export.
646        let compute = table
647            .symbols()
648            .iter()
649            .position(|s| s.name == "compute")
650            .unwrap();
651        assert_eq!(table.references(compute).count(), 1);
652
653        // b's `helper` is used inside library `a`, not the main file.
654        let helper = table
655            .symbols()
656            .iter()
657            .position(|s| s.name == "helper")
658            .unwrap();
659        let (decl_file, _, _) = table.declaration_location(helper).unwrap();
660        let refs: Vec<_> = table.references(helper).collect();
661        assert_eq!(refs.len(), 1);
662        let (use_file, _, _) = refs[0];
663        assert_ne!(use_file, MAIN);
664        assert_eq!(table.file_path(use_file), "a");
665        assert_eq!(table.file_path(decl_file), "b");
666    }
667
668    #[test]
669    fn import_cycles_terminate() {
670        // Mutual: a imports b, b imports a. Terminates via the cache; a's export
671        // still resolves from the main file.
672        let loader = libs(&[
673            ("a", "//@version=5\nimport b as bb\nexport fa() => 1\n"),
674            ("b", "//@version=5\nimport a as aa\nexport fb() => 2\n"),
675        ]);
676        let (_diags, table) = analyze(
677            "//@version=5\nindicator(\"t\")\nimport a as x\ny = x.fa()\n",
678            Some(&loader),
679        );
680        let fa_id = table.symbols().iter().position(|s| s.name == "fa").unwrap();
681        assert_eq!(table.references(fa_id).count(), 1);
682
683        // Self-import terminates too.
684        let loader = libs(&[(
685            "selfimp",
686            "//@version=5\nimport selfimp as me\nexport g() => 1\n",
687        )]);
688        let (_diags, table) = analyze(
689            "//@version=5\nindicator(\"t\")\nimport selfimp as s\ny = s.g()\n",
690            Some(&loader),
691        );
692        let g_id = table.symbols().iter().position(|s| s.name == "g").unwrap();
693        assert_eq!(table.references(g_id).count(), 1);
694    }
695
696    #[test]
697    fn a_use_binds_to_the_innermost_shadowing_declaration() {
698        // A global `x` (line 3) and a parameter `x` (line 4) that shadows it. The
699        // use of `x` inside the function body must bind to the parameter, not the
700        // global.
701        let table = table("//@version=5\nindicator(\"t\")\nx = 1\nf(x) => x + 1\ny = x + 1\n");
702
703        let symbols = table.symbols();
704        // Two distinct `x` symbols: the global var and the parameter.
705        let global_x = symbols
706            .iter()
707            .position(|s| s.name == "x" && s.scope == SymbolTable::GLOBAL)
708            .unwrap();
709        let param_x = symbols
710            .iter()
711            .position(|s| s.name == "x" && s.scope != SymbolTable::GLOBAL)
712            .unwrap();
713        assert_ne!(global_x, param_x);
714
715        // The `x` in the body (line 4, `f(x) => x + 1`) binds to the parameter;
716        // the `x` in `y = x + 1` (line 5) binds to the global.
717        assert_eq!(table.symbol_at(MAIN, 4, 9), Some(param_x));
718        assert_eq!(table.symbol_at(MAIN, 5, 5), Some(global_x));
719
720        // Find-references keeps them apart.
721        assert_eq!(
722            table.references(param_x).collect::<Vec<_>>(),
723            vec![(MAIN, 4, 9)]
724        );
725        assert_eq!(
726            table.references(global_x).collect::<Vec<_>>(),
727            vec![(MAIN, 5, 5)]
728        );
729    }
730}