Skip to main content

php_ast/
symbol_table.rs

1/// Extracts top-level symbol declarations from a parsed AST.
2///
3/// Walks the AST once and collects all function, class, interface, trait, enum,
4/// and constant declarations along with their namespace context and `use` imports.
5/// This provides the foundation for name resolution and "go to definition" in
6/// LSP-like tools.
7///
8/// # Example
9///
10/// ```
11/// use php_ast::symbol_table::SymbolTable;
12/// use php_ast::ast::Program;
13///
14/// // After parsing:
15/// // let result = php_rs_parser::parse(&arena, source);
16/// // let symbols = SymbolTable::build(&result.program);
17/// // let classes = symbols.classes();
18/// ```
19use std::borrow::Cow;
20use std::ops::ControlFlow;
21
22use crate::ast::*;
23use crate::visitor::{walk_enum_member, walk_stmt, Visitor};
24use crate::Span;
25
26/// The kind of a declared symbol.
27#[derive(Debug, Clone, Copy, PartialEq, Eq)]
28pub enum SymbolKind {
29    Function,
30    Class,
31    Interface,
32    Trait,
33    Enum,
34    Constant,
35    Method,
36    Property,
37    ClassConstant,
38    EnumCase,
39}
40
41/// A single symbol declaration extracted from the AST.
42#[derive(Debug, Clone)]
43pub struct Symbol<'src> {
44    /// The symbol's short name (e.g. `"MyClass"`, `"doStuff"`).
45    pub name: Cow<'src, str>,
46    /// Fully qualified name including namespace (e.g. `"App\\Models\\User"`).
47    pub fqn: String,
48    pub kind: SymbolKind,
49    pub span: Span,
50    /// Visibility, if applicable (methods, properties, class constants).
51    pub visibility: Option<Visibility>,
52    /// The containing symbol's FQN, if this is a member (method, property, etc.).
53    pub parent: Option<String>,
54}
55
56/// A `use` import declaration.
57#[derive(Debug, Clone)]
58pub struct UseImport<'src> {
59    /// The imported name (e.g. `"App\\Models\\User"`).
60    pub name: Cow<'src, str>,
61    /// The local alias, or `None` if unaliased.
62    pub alias: Option<&'src str>,
63    pub kind: UseKind,
64    pub span: Span,
65}
66
67impl<'src> UseImport<'src> {
68    /// The local name this import introduces (alias or last segment).
69    pub fn local_name(&self) -> &str {
70        if let Some(alias) = self.alias {
71            return alias;
72        }
73        self.name.rsplit('\\').next().unwrap_or(self.name.as_ref())
74    }
75}
76
77/// Collected symbols and imports from a PHP source file.
78pub struct SymbolTable<'src> {
79    symbols: Vec<Symbol<'src>>,
80    imports: Vec<UseImport<'src>>,
81}
82
83impl<'src> SymbolTable<'src> {
84    /// Walk the program AST and extract all declarations.
85    pub fn build<'arena>(program: &Program<'arena, 'src>) -> Self {
86        let mut collector = SymbolCollector {
87            symbols: Vec::new(),
88            imports: Vec::new(),
89            namespace: String::new(),
90        };
91        let _ = collector.visit_program(program);
92        Self {
93            symbols: collector.symbols,
94            imports: collector.imports,
95        }
96    }
97
98    /// All collected symbols.
99    pub fn symbols(&self) -> &[Symbol<'src>] {
100        &self.symbols
101    }
102
103    /// All `use` imports.
104    pub fn imports(&self) -> &[UseImport<'src>] {
105        &self.imports
106    }
107
108    /// Iterate symbols of a specific kind.
109    pub fn symbols_of_kind(&self, kind: SymbolKind) -> impl Iterator<Item = &Symbol<'src>> {
110        self.symbols.iter().filter(move |s| s.kind == kind)
111    }
112
113    /// Find a symbol by its fully qualified name.
114    pub fn find_by_fqn(&self, fqn: &str) -> Option<&Symbol<'src>> {
115        self.symbols.iter().find(|s| s.fqn == fqn)
116    }
117
118    /// Find all symbols at a given byte offset (i.e. whose span contains the offset).
119    pub fn symbols_at(&self, offset: u32) -> Vec<&Symbol<'src>> {
120        self.symbols
121            .iter()
122            .filter(|s| s.span.start <= offset && offset < s.span.end)
123            .collect()
124    }
125
126    /// Resolve a simple (unqualified) name using the imports and current namespace.
127    /// Returns the FQN if found, `None` otherwise.
128    pub fn resolve_name(&self, name: &str) -> Option<&str> {
129        // Check use imports first
130        for import in &self.imports {
131            if import.local_name() == name {
132                return Some(&import.name);
133            }
134        }
135        // Check if it's a top-level symbol in the file
136        for sym in &self.symbols {
137            if sym.name == name && sym.parent.is_none() {
138                return Some(&sym.fqn);
139            }
140        }
141        None
142    }
143
144    /// Convenience: all functions.
145    pub fn functions(&self) -> impl Iterator<Item = &Symbol<'src>> {
146        self.symbols_of_kind(SymbolKind::Function)
147    }
148
149    /// Convenience: all classes.
150    pub fn classes(&self) -> impl Iterator<Item = &Symbol<'src>> {
151        self.symbols_of_kind(SymbolKind::Class)
152    }
153
154    /// Convenience: all interfaces.
155    pub fn interfaces(&self) -> impl Iterator<Item = &Symbol<'src>> {
156        self.symbols_of_kind(SymbolKind::Interface)
157    }
158
159    /// Convenience: all traits.
160    pub fn traits(&self) -> impl Iterator<Item = &Symbol<'src>> {
161        self.symbols_of_kind(SymbolKind::Trait)
162    }
163
164    /// Convenience: all enums.
165    pub fn enums(&self) -> impl Iterator<Item = &Symbol<'src>> {
166        self.symbols_of_kind(SymbolKind::Enum)
167    }
168
169    /// Convenience: all constants.
170    pub fn constants(&self) -> impl Iterator<Item = &Symbol<'src>> {
171        self.symbols_of_kind(SymbolKind::Constant)
172    }
173
174    /// Get all members (methods, properties, class constants, enum cases) of a given parent FQN.
175    pub fn members_of<'a>(&'a self, parent_fqn: &'a str) -> impl Iterator<Item = &'a Symbol<'src>> {
176        self.symbols
177            .iter()
178            .filter(move |s| s.parent.as_deref() == Some(parent_fqn))
179    }
180}
181
182struct SymbolCollector<'src> {
183    symbols: Vec<Symbol<'src>>,
184    imports: Vec<UseImport<'src>>,
185    namespace: String,
186}
187
188impl<'src> SymbolCollector<'src> {
189    fn fqn(&self, name: &str) -> String {
190        if self.namespace.is_empty() {
191            name.to_string()
192        } else {
193            format!("{}\\{}", self.namespace, name)
194        }
195    }
196
197    fn add_class_members<'arena>(
198        &mut self,
199        parent_fqn: &str,
200        members: &[ClassMember<'arena, 'src>],
201    ) {
202        for member in members {
203            match &member.kind {
204                ClassMemberKind::Method(method) => {
205                    self.symbols.push(Symbol {
206                        name: Cow::Borrowed(method.name),
207                        fqn: format!("{}::{}", parent_fqn, method.name),
208                        kind: SymbolKind::Method,
209                        span: member.span,
210                        visibility: method.visibility,
211                        parent: Some(parent_fqn.to_string()),
212                    });
213                }
214                ClassMemberKind::Property(prop) => {
215                    self.symbols.push(Symbol {
216                        name: Cow::Borrowed(prop.name),
217                        fqn: format!("{}::${}", parent_fqn, prop.name),
218                        kind: SymbolKind::Property,
219                        span: member.span,
220                        visibility: prop.visibility,
221                        parent: Some(parent_fqn.to_string()),
222                    });
223                }
224                ClassMemberKind::ClassConst(cc) => {
225                    self.symbols.push(Symbol {
226                        name: Cow::Borrowed(cc.name),
227                        fqn: format!("{}::{}", parent_fqn, cc.name),
228                        kind: SymbolKind::ClassConstant,
229                        span: member.span,
230                        visibility: cc.visibility,
231                        parent: Some(parent_fqn.to_string()),
232                    });
233                }
234                ClassMemberKind::TraitUse(_) => {}
235            }
236        }
237    }
238}
239
240impl<'arena, 'src> Visitor<'arena, 'src> for SymbolCollector<'src> {
241    fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
242        match &stmt.kind {
243            StmtKind::Namespace(ns) => {
244                if let Some(name) = &ns.name {
245                    self.namespace = name.join_parts().into_owned();
246                } else {
247                    self.namespace.clear();
248                }
249                match &ns.body {
250                    NamespaceBody::Braced(stmts) => {
251                        for s in stmts.iter() {
252                            self.visit_stmt(s)?;
253                        }
254                        // Restore after braced block — braced namespaces are self-contained
255                        self.namespace.clear();
256                    }
257                    NamespaceBody::Simple => {
258                        // Unbraced: namespace applies to all subsequent siblings
259                        // until the next namespace declaration — don't restore
260                    }
261                }
262                return ControlFlow::Continue(());
263            }
264            StmtKind::Use(use_decl) => {
265                for item in use_decl.uses.iter() {
266                    self.imports.push(UseImport {
267                        name: item.name.join_parts(),
268                        alias: item.alias,
269                        kind: item.kind.unwrap_or(use_decl.kind),
270                        span: item.name.span(),
271                    });
272                }
273                return ControlFlow::Continue(());
274            }
275            StmtKind::Function(func) => {
276                self.symbols.push(Symbol {
277                    name: Cow::Borrowed(func.name),
278                    fqn: self.fqn(func.name),
279                    kind: SymbolKind::Function,
280                    span: stmt.span,
281                    visibility: None,
282                    parent: None,
283                });
284            }
285            StmtKind::Class(class) => {
286                if let Some(name) = class.name {
287                    let fqn = self.fqn(name);
288                    self.symbols.push(Symbol {
289                        name: Cow::Borrowed(name),
290                        fqn: fqn.clone(),
291                        kind: SymbolKind::Class,
292                        span: stmt.span,
293                        visibility: None,
294                        parent: None,
295                    });
296                    self.add_class_members(&fqn, &class.members);
297                }
298            }
299            StmtKind::Interface(iface) => {
300                let fqn = self.fqn(iface.name);
301                self.symbols.push(Symbol {
302                    name: Cow::Borrowed(iface.name),
303                    fqn: fqn.clone(),
304                    kind: SymbolKind::Interface,
305                    span: stmt.span,
306                    visibility: None,
307                    parent: None,
308                });
309                self.add_class_members(&fqn, &iface.members);
310            }
311            StmtKind::Trait(trait_decl) => {
312                let fqn = self.fqn(trait_decl.name);
313                self.symbols.push(Symbol {
314                    name: Cow::Borrowed(trait_decl.name),
315                    fqn: fqn.clone(),
316                    kind: SymbolKind::Trait,
317                    span: stmt.span,
318                    visibility: None,
319                    parent: None,
320                });
321                self.add_class_members(&fqn, &trait_decl.members);
322            }
323            StmtKind::Enum(enum_decl) => {
324                let fqn = self.fqn(enum_decl.name);
325                self.symbols.push(Symbol {
326                    name: Cow::Borrowed(enum_decl.name),
327                    fqn: fqn.clone(),
328                    kind: SymbolKind::Enum,
329                    span: stmt.span,
330                    visibility: None,
331                    parent: None,
332                });
333                for member in enum_decl.members.iter() {
334                    match &member.kind {
335                        EnumMemberKind::Case(case) => {
336                            self.symbols.push(Symbol {
337                                name: Cow::Borrowed(case.name),
338                                fqn: format!("{}::{}", fqn, case.name),
339                                kind: SymbolKind::EnumCase,
340                                span: member.span,
341                                visibility: None,
342                                parent: Some(fqn.clone()),
343                            });
344                        }
345                        EnumMemberKind::Method(method) => {
346                            self.symbols.push(Symbol {
347                                name: Cow::Borrowed(method.name),
348                                fqn: format!("{}::{}", fqn, method.name),
349                                kind: SymbolKind::Method,
350                                span: member.span,
351                                visibility: method.visibility,
352                                parent: Some(fqn.clone()),
353                            });
354                        }
355                        EnumMemberKind::ClassConst(cc) => {
356                            self.symbols.push(Symbol {
357                                name: Cow::Borrowed(cc.name),
358                                fqn: format!("{}::{}", fqn, cc.name),
359                                kind: SymbolKind::ClassConstant,
360                                span: member.span,
361                                visibility: cc.visibility,
362                                parent: Some(fqn.clone()),
363                            });
364                        }
365                        EnumMemberKind::TraitUse(_) => {}
366                    }
367                    walk_enum_member(self, member)?;
368                }
369                return ControlFlow::Continue(());
370            }
371            StmtKind::Const(items) => {
372                for item in items.iter() {
373                    self.symbols.push(Symbol {
374                        name: Cow::Borrowed(item.name),
375                        fqn: self.fqn(item.name),
376                        kind: SymbolKind::Constant,
377                        span: item.span,
378                        visibility: None,
379                        parent: None,
380                    });
381                }
382            }
383            _ => {}
384        }
385        walk_stmt(self, stmt)
386    }
387
388    // Don't recurse into expressions — we only want declarations
389    fn visit_expr(&mut self, _expr: &Expr<'arena, 'src>) -> ControlFlow<()> {
390        ControlFlow::Continue(())
391    }
392}
393
394#[cfg(test)]
395mod tests {
396    use super::*;
397    use crate::ast::ArenaVec;
398
399    // Helper to build a simple program with statements
400    fn build_table<'arena, 'src>(
401        _arena: &'arena bumpalo::Bump,
402        stmts: ArenaVec<'arena, Stmt<'arena, 'src>>,
403    ) -> SymbolTable<'src> {
404        let program = Program {
405            stmts,
406            span: Span::DUMMY,
407        };
408        SymbolTable::build(&program)
409    }
410
411    #[test]
412    fn collects_function() {
413        let arena = bumpalo::Bump::new();
414        let func = arena.alloc(FunctionDecl {
415            name: "doStuff",
416            params: ArenaVec::new_in(&arena),
417            body: ArenaVec::new_in(&arena),
418            return_type: None,
419            by_ref: false,
420            attributes: ArenaVec::new_in(&arena),
421            doc_comment: None,
422        });
423        let mut stmts = ArenaVec::new_in(&arena);
424        stmts.push(Stmt {
425            kind: StmtKind::Function(func),
426            span: Span::new(0, 30),
427        });
428
429        let table = build_table(&arena, stmts);
430        assert_eq!(table.symbols().len(), 1);
431        assert_eq!(table.symbols()[0].name, "doStuff");
432        assert_eq!(table.symbols()[0].fqn, "doStuff");
433        assert_eq!(table.symbols()[0].kind, SymbolKind::Function);
434    }
435
436    #[test]
437    fn collects_namespaced_class_with_members() {
438        let arena = bumpalo::Bump::new();
439
440        // Method
441        let method = MethodDecl {
442            name: "getName",
443            visibility: Some(Visibility::Public),
444            is_static: false,
445            is_abstract: false,
446            is_final: false,
447            by_ref: false,
448            params: ArenaVec::new_in(&arena),
449            return_type: None,
450            body: Some(ArenaVec::new_in(&arena)),
451            attributes: ArenaVec::new_in(&arena),
452            doc_comment: None,
453        };
454        let mut members = ArenaVec::new_in(&arena);
455        members.push(ClassMember {
456            kind: ClassMemberKind::Method(method),
457            span: Span::new(40, 60),
458        });
459
460        let class = arena.alloc(ClassDecl {
461            name: Some("User"),
462            modifiers: ClassModifiers::default(),
463            extends: None,
464            implements: ArenaVec::new_in(&arena),
465            members,
466            attributes: ArenaVec::new_in(&arena),
467            doc_comment: None,
468        });
469
470        let ns = arena.alloc(NamespaceDecl {
471            name: Some(Name::Simple {
472                value: "App",
473                span: Span::DUMMY,
474            }),
475            body: NamespaceBody::Braced({
476                let mut inner = ArenaVec::new_in(&arena);
477                inner.push(Stmt {
478                    kind: StmtKind::Class(class),
479                    span: Span::new(20, 80),
480                });
481                inner
482            }),
483        });
484
485        let mut stmts = ArenaVec::new_in(&arena);
486        stmts.push(Stmt {
487            kind: StmtKind::Namespace(ns),
488            span: Span::new(0, 100),
489        });
490
491        let table = build_table(&arena, stmts);
492        assert_eq!(table.symbols().len(), 2);
493        assert_eq!(table.symbols()[0].fqn, "App\\User");
494        assert_eq!(table.symbols()[0].kind, SymbolKind::Class);
495        assert_eq!(table.symbols()[1].fqn, "App\\User::getName");
496        assert_eq!(table.symbols()[1].kind, SymbolKind::Method);
497        assert_eq!(table.symbols()[1].parent.as_deref(), Some("App\\User"));
498
499        // members_of
500        let members: Vec<_> = table.members_of("App\\User").collect();
501        assert_eq!(members.len(), 1);
502        assert_eq!(members[0].name, "getName");
503    }
504
505    #[test]
506    fn collects_use_imports() {
507        let arena = bumpalo::Bump::new();
508
509        let mut uses = ArenaVec::new_in(&arena);
510        uses.push(UseItem {
511            name: Name::Simple {
512                value: "Foo",
513                span: Span::new(4, 7),
514            },
515            alias: Some("Bar"),
516            kind: None,
517            span: Span::new(4, 7),
518        });
519
520        let use_decl = arena.alloc(UseDecl {
521            kind: UseKind::Normal,
522            uses,
523        });
524
525        let mut stmts = ArenaVec::new_in(&arena);
526        stmts.push(Stmt {
527            kind: StmtKind::Use(use_decl),
528            span: Span::new(0, 15),
529        });
530
531        let table = build_table(&arena, stmts);
532        assert_eq!(table.imports().len(), 1);
533        assert_eq!(table.imports()[0].name, "Foo");
534        assert_eq!(table.imports()[0].alias, Some("Bar"));
535        assert_eq!(table.imports()[0].local_name(), "Bar");
536    }
537
538    #[test]
539    fn resolve_name_from_imports() {
540        let arena = bumpalo::Bump::new();
541
542        let mut uses = ArenaVec::new_in(&arena);
543        uses.push(UseItem {
544            name: Name::Simple {
545                value: "User",
546                span: Span::DUMMY,
547            },
548            alias: None,
549            kind: None,
550            span: Span::DUMMY,
551        });
552
553        let use_decl = arena.alloc(UseDecl {
554            kind: UseKind::Normal,
555            uses,
556        });
557
558        let mut stmts = ArenaVec::new_in(&arena);
559        stmts.push(Stmt {
560            kind: StmtKind::Use(use_decl),
561            span: Span::DUMMY,
562        });
563
564        let table = build_table(&arena, stmts);
565        assert_eq!(table.resolve_name("User"), Some("User"));
566        assert_eq!(table.resolve_name("Unknown"), None);
567    }
568
569    #[test]
570    fn symbols_at_offset() {
571        let arena = bumpalo::Bump::new();
572        let func = arena.alloc(FunctionDecl {
573            name: "foo",
574            params: ArenaVec::new_in(&arena),
575            body: ArenaVec::new_in(&arena),
576            return_type: None,
577            by_ref: false,
578            attributes: ArenaVec::new_in(&arena),
579            doc_comment: None,
580        });
581        let mut stmts = ArenaVec::new_in(&arena);
582        stmts.push(Stmt {
583            kind: StmtKind::Function(func),
584            span: Span::new(10, 50),
585        });
586
587        let table = build_table(&arena, stmts);
588        assert_eq!(table.symbols_at(25).len(), 1);
589        assert_eq!(table.symbols_at(5).len(), 0);
590        assert_eq!(table.symbols_at(50).len(), 0);
591    }
592
593    #[test]
594    fn collects_enum_cases() {
595        let arena = bumpalo::Bump::new();
596
597        let mut members = ArenaVec::new_in(&arena);
598        members.push(EnumMember {
599            kind: EnumMemberKind::Case(EnumCase {
600                name: "Hearts",
601                value: None,
602                attributes: ArenaVec::new_in(&arena),
603                doc_comment: None,
604            }),
605            span: Span::new(30, 45),
606        });
607
608        let enum_decl = arena.alloc(EnumDecl {
609            name: "Suit",
610            scalar_type: None,
611            implements: ArenaVec::new_in(&arena),
612            members,
613            attributes: ArenaVec::new_in(&arena),
614            doc_comment: None,
615        });
616
617        let mut stmts = ArenaVec::new_in(&arena);
618        stmts.push(Stmt {
619            kind: StmtKind::Enum(enum_decl),
620            span: Span::new(0, 60),
621        });
622
623        let table = build_table(&arena, stmts);
624        assert_eq!(table.enums().count(), 1);
625        let cases: Vec<_> = table.symbols_of_kind(SymbolKind::EnumCase).collect();
626        assert_eq!(cases.len(), 1);
627        assert_eq!(cases[0].fqn, "Suit::Hearts");
628        assert_eq!(cases[0].parent.as_deref(), Some("Suit"));
629    }
630
631    #[test]
632    fn unbraced_namespace_applies_to_siblings() {
633        let arena = bumpalo::Bump::new();
634
635        // namespace App\Models;
636        let ns = arena.alloc(NamespaceDecl {
637            name: Some(Name::Complex {
638                parts: {
639                    let mut v = ArenaVec::new_in(&arena);
640                    v.push("App");
641                    v.push("Models");
642                    v
643                },
644                kind: crate::ast::NameKind::Qualified,
645                span: Span::DUMMY,
646            }),
647            body: NamespaceBody::Simple,
648        });
649
650        // class User {}
651        let class = arena.alloc(ClassDecl {
652            name: Some("User"),
653            modifiers: ClassModifiers::default(),
654            extends: None,
655            implements: ArenaVec::new_in(&arena),
656            members: ArenaVec::new_in(&arena),
657            attributes: ArenaVec::new_in(&arena),
658            doc_comment: None,
659        });
660
661        // function helper() {}
662        let func = arena.alloc(FunctionDecl {
663            name: "helper",
664            params: ArenaVec::new_in(&arena),
665            body: ArenaVec::new_in(&arena),
666            return_type: None,
667            by_ref: false,
668            attributes: ArenaVec::new_in(&arena),
669            doc_comment: None,
670        });
671
672        let mut stmts = ArenaVec::new_in(&arena);
673        stmts.push(Stmt {
674            kind: StmtKind::Namespace(ns),
675            span: Span::new(0, 25),
676        });
677        stmts.push(Stmt {
678            kind: StmtKind::Class(class),
679            span: Span::new(26, 50),
680        });
681        stmts.push(Stmt {
682            kind: StmtKind::Function(func),
683            span: Span::new(51, 80),
684        });
685
686        let table = build_table(&arena, stmts);
687        assert_eq!(table.symbols().len(), 2);
688        assert_eq!(table.symbols()[0].fqn, "App\\Models\\User");
689        assert_eq!(table.symbols()[1].fqn, "App\\Models\\helper");
690    }
691
692    #[test]
693    fn multiple_unbraced_namespaces_switch_context() {
694        // namespace A; class Foo {} namespace B; class Bar {}
695        let arena = bumpalo::Bump::new();
696
697        let ns_a = arena.alloc(NamespaceDecl {
698            name: Some(Name::Simple {
699                value: "A",
700                span: Span::DUMMY,
701            }),
702            body: NamespaceBody::Simple,
703        });
704        let class_foo = arena.alloc(ClassDecl {
705            name: Some("Foo"),
706            modifiers: ClassModifiers::default(),
707            extends: None,
708            implements: ArenaVec::new_in(&arena),
709            members: ArenaVec::new_in(&arena),
710            attributes: ArenaVec::new_in(&arena),
711            doc_comment: None,
712        });
713        let ns_b = arena.alloc(NamespaceDecl {
714            name: Some(Name::Simple {
715                value: "B",
716                span: Span::DUMMY,
717            }),
718            body: NamespaceBody::Simple,
719        });
720        let class_bar = arena.alloc(ClassDecl {
721            name: Some("Bar"),
722            modifiers: ClassModifiers::default(),
723            extends: None,
724            implements: ArenaVec::new_in(&arena),
725            members: ArenaVec::new_in(&arena),
726            attributes: ArenaVec::new_in(&arena),
727            doc_comment: None,
728        });
729
730        let mut stmts = ArenaVec::new_in(&arena);
731        stmts.push(Stmt {
732            kind: StmtKind::Namespace(ns_a),
733            span: Span::DUMMY,
734        });
735        stmts.push(Stmt {
736            kind: StmtKind::Class(class_foo),
737            span: Span::DUMMY,
738        });
739        stmts.push(Stmt {
740            kind: StmtKind::Namespace(ns_b),
741            span: Span::DUMMY,
742        });
743        stmts.push(Stmt {
744            kind: StmtKind::Class(class_bar),
745            span: Span::DUMMY,
746        });
747
748        let table = build_table(&arena, stmts);
749        assert_eq!(table.symbols().len(), 2);
750        assert_eq!(table.symbols()[0].fqn, "A\\Foo");
751        assert_eq!(table.symbols()[1].fqn, "B\\Bar");
752    }
753
754    #[test]
755    fn braced_namespace_does_not_leak_to_siblings() {
756        // namespace A { class Foo {} } class Bar {}
757        let arena = bumpalo::Bump::new();
758
759        let class_foo = arena.alloc(ClassDecl {
760            name: Some("Foo"),
761            modifiers: ClassModifiers::default(),
762            extends: None,
763            implements: ArenaVec::new_in(&arena),
764            members: ArenaVec::new_in(&arena),
765            attributes: ArenaVec::new_in(&arena),
766            doc_comment: None,
767        });
768        let ns = arena.alloc(NamespaceDecl {
769            name: Some(Name::Simple {
770                value: "A",
771                span: Span::DUMMY,
772            }),
773            body: NamespaceBody::Braced({
774                let mut inner = ArenaVec::new_in(&arena);
775                inner.push(Stmt {
776                    kind: StmtKind::Class(class_foo),
777                    span: Span::DUMMY,
778                });
779                inner
780            }),
781        });
782        let class_bar = arena.alloc(ClassDecl {
783            name: Some("Bar"),
784            modifiers: ClassModifiers::default(),
785            extends: None,
786            implements: ArenaVec::new_in(&arena),
787            members: ArenaVec::new_in(&arena),
788            attributes: ArenaVec::new_in(&arena),
789            doc_comment: None,
790        });
791
792        let mut stmts = ArenaVec::new_in(&arena);
793        stmts.push(Stmt {
794            kind: StmtKind::Namespace(ns),
795            span: Span::DUMMY,
796        });
797        stmts.push(Stmt {
798            kind: StmtKind::Class(class_bar),
799            span: Span::DUMMY,
800        });
801
802        let table = build_table(&arena, stmts);
803        assert_eq!(table.symbols().len(), 2);
804        assert_eq!(table.symbols()[0].fqn, "A\\Foo");
805        // Bar is outside the braced namespace — should be global
806        assert_eq!(table.symbols()[1].fqn, "Bar");
807    }
808
809    #[test]
810    fn unbraced_namespace_no_name_clears_namespace() {
811        // namespace A; class Foo {} namespace; class Bar {}
812        // (namespace with no name resets to global)
813        let arena = bumpalo::Bump::new();
814
815        let ns_a = arena.alloc(NamespaceDecl {
816            name: Some(Name::Simple {
817                value: "A",
818                span: Span::DUMMY,
819            }),
820            body: NamespaceBody::Simple,
821        });
822        let class_foo = arena.alloc(ClassDecl {
823            name: Some("Foo"),
824            modifiers: ClassModifiers::default(),
825            extends: None,
826            implements: ArenaVec::new_in(&arena),
827            members: ArenaVec::new_in(&arena),
828            attributes: ArenaVec::new_in(&arena),
829            doc_comment: None,
830        });
831        let ns_global = arena.alloc(NamespaceDecl {
832            name: None,
833            body: NamespaceBody::Simple,
834        });
835        let class_bar = arena.alloc(ClassDecl {
836            name: Some("Bar"),
837            modifiers: ClassModifiers::default(),
838            extends: None,
839            implements: ArenaVec::new_in(&arena),
840            members: ArenaVec::new_in(&arena),
841            attributes: ArenaVec::new_in(&arena),
842            doc_comment: None,
843        });
844
845        let mut stmts = ArenaVec::new_in(&arena);
846        stmts.push(Stmt {
847            kind: StmtKind::Namespace(ns_a),
848            span: Span::DUMMY,
849        });
850        stmts.push(Stmt {
851            kind: StmtKind::Class(class_foo),
852            span: Span::DUMMY,
853        });
854        stmts.push(Stmt {
855            kind: StmtKind::Namespace(ns_global),
856            span: Span::DUMMY,
857        });
858        stmts.push(Stmt {
859            kind: StmtKind::Class(class_bar),
860            span: Span::DUMMY,
861        });
862
863        let table = build_table(&arena, stmts);
864        assert_eq!(table.symbols().len(), 2);
865        assert_eq!(table.symbols()[0].fqn, "A\\Foo");
866        assert_eq!(table.symbols()[1].fqn, "Bar");
867    }
868}