Skip to main content

php_lsp/
references.rs

1use std::collections::{HashMap, HashSet};
2use std::ops::ControlFlow;
3use std::sync::Arc;
4
5use php_ast::visitor::{Visitor, walk_stmt};
6use php_ast::{
7    ClassMemberKind, EnumMemberKind, ExprKind, NamespaceBody, Span, Stmt, StmtKind, UseKind,
8};
9use rayon::prelude::*;
10use tower_lsp::lsp_types::{Location, Position, Range, Url};
11
12use crate::ast::{ParsedDoc, str_offset_in_range};
13use crate::util::utf16_code_units;
14use crate::walk::{
15    all_class_ref_names_in_stmts, class_refs_in_stmts, constant_refs_in_stmts,
16    fqn_new_class_refs_in_stmts, function_refs_in_stmts, global_constant_refs_in_stmts,
17    method_refs_in_stmts, new_refs_in_stmts, property_refs_in_stmts, refs_in_stmts,
18    refs_in_stmts_with_use,
19};
20
21/// Callback signature for the mir-codebase reference-lookup fast path:
22/// `(key) -> Vec<(file_uri, start_byte, end_byte)>`.
23pub type RefLookup<'a> = dyn Fn(&str) -> Vec<(Arc<str>, u32, u16, u16)> + 'a;
24
25/// What kind of symbol the cursor is on.  Used to dispatch to the
26/// appropriate semantic walker so that, e.g., searching for `get` as a
27/// *method* doesn't return free-function calls named `get`.
28#[derive(Debug, Clone, Copy, PartialEq, Eq)]
29pub enum SymbolKind {
30    /// A free (top-level) function.
31    Function,
32    /// An instance or static method (`->name`, `?->name`, `::name`).
33    Method,
34    /// A class, interface, trait, or enum name used as a type.
35    Class,
36    /// A class / trait property (`->name`, `?->name`, promoted or declared).
37    Property,
38    /// A class, interface, enum, or trait constant (`Class::CONST`, `self::CONST`).
39    Constant,
40}
41
42fn class_has_ancestor(
43    codebase: &mir_analyzer::db::MirDbStorage,
44    class_fqcn: &str,
45    target_fqcn: &str,
46) -> bool {
47    mir_analyzer::db::extends_or_implements(codebase, class_fqcn, target_fqcn)
48}
49
50/// Find all locations where `word` is referenced across the given documents.
51/// If `include_declaration` is true, also includes the declaration site.
52/// Pass `kind` to restrict results to a particular symbol category; `None`
53/// falls back to the original word-based walker (better some results than none).
54pub fn find_references(
55    word: &str,
56    all_docs: &[(Url, Arc<ParsedDoc>)],
57    include_declaration: bool,
58    kind: Option<SymbolKind>,
59) -> Vec<Location> {
60    find_references_inner(word, all_docs, include_declaration, false, kind, None)
61}
62
63/// Like [`find_references`] but narrows scanning to docs whose namespace +
64/// `use` imports would resolve `word` to `target_fqn`. Used by
65/// `textDocument/references` for the AST fallback so it doesn't match
66/// same-short-name symbols in unrelated namespaces.
67pub fn find_references_with_target(
68    word: &str,
69    all_docs: &[(Url, Arc<ParsedDoc>)],
70    include_declaration: bool,
71    kind: Option<SymbolKind>,
72    target_fqn: &str,
73) -> Vec<Location> {
74    // Default: include `use` statement spans so callers that pass
75    // `kind=None` (notably the rename handler) get their use-import edits.
76    // For typed kinds we want the kind-specific walker (so a Method search
77    // doesn't pick up free functions sharing the name); the general walker
78    // would falsely widen those results.
79    let include_use = kind.is_none();
80    find_references_inner(
81        word,
82        all_docs,
83        include_declaration,
84        include_use,
85        kind,
86        Some(target_fqn),
87    )
88}
89
90/// Like `find_references` but also includes `use` statement spans.
91/// Used by rename so that `use Foo;` statements are also updated.
92/// Always uses the general walker (rename must update all occurrence kinds).
93pub fn find_references_with_use(
94    word: &str,
95    all_docs: &[(Url, Arc<ParsedDoc>)],
96    include_declaration: bool,
97) -> Vec<Location> {
98    find_references_inner(word, all_docs, include_declaration, true, None, None)
99}
100
101/// Find only `new ClassName(...)` instantiation sites across all docs.
102///
103/// Used by the `__construct` references handler — `SymbolKind::Class` (the normal
104/// class-kind path) is too broad because mir's `ClassReference` key covers type
105/// hints, `instanceof`, `extends`, and `implements` in addition to `new` calls.
106/// This function walks the AST using `new_refs_in_stmts` which only emits spans
107/// for `ExprKind::New` nodes, giving the caller exactly the call sites.
108///
109/// `class_fqn` is the fully-qualified name (e.g. `"Alpha\\Widget"`) used to
110/// filter files where the short name resolves to a different class. Pass `None`
111/// for global-namespace classes.
112pub fn find_constructor_references(
113    short_name: &str,
114    all_docs: &[(Url, Arc<ParsedDoc>)],
115    class_fqn: Option<&str>,
116) -> Vec<Location> {
117    all_docs
118        .par_iter()
119        .flat_map_iter(|(uri, doc)| {
120            // Skip files that can't reference the target unless they may use the FQN
121            // directly (without a `use` statement). FQN-qualified identifiers in the
122            // AST are disambiguated inside `new_refs_in_stmts` via `class_fqn`.
123            if let Some(fqn) = class_fqn
124                && !doc_can_reference_target(doc, short_name, fqn)
125                && !doc.view().source().contains(fqn.trim_start_matches('\\'))
126            {
127                return Vec::new();
128            }
129            let mut spans = Vec::new();
130            new_refs_in_stmts(&doc.program().stmts, short_name, class_fqn, &mut spans);
131            let sv = doc.view();
132            spans
133                .into_iter()
134                .map(|span| {
135                    let start = sv.position_of(span.start);
136                    let end = sv.position_of(span.end);
137                    Location {
138                        uri: uri.clone(),
139                        range: Range { start, end },
140                    }
141                })
142                .collect::<Vec<_>>()
143        })
144        .collect()
145}
146
147/// Fast path: look up pre-computed reference locations from the mir codebase index.
148///
149/// Handles `Function`, `Class`, and (partially) `Method` kinds.  For `Function` and
150/// `Class` the mir analyzer records every call-site / instantiation via
151/// `mark_*_referenced_at` and the index is authoritative.
152///
153/// For `Method`, the index is used as a pre-filter: only files that contain a tracked
154/// call site for the method are scanned with the AST walker.  This fast path is
155/// activated for two cases where the tracked set is reliably complete or narrows the
156/// search scope without missing real references:
157///   • `private` methods — PHP semantics guarantee that private methods are only
158///     callable from within the class body, so mir always resolves the receiver type.
159///   • methods on `final` classes — no subclassing means call sites on the concrete
160///     type are unambiguous; the codebase set covers all statically-typed callers.
161///
162/// Returns `None` for public/protected methods on non-final classes and for `None`
163/// kind (caller should use the general AST walker instead).  Also returns `None` when
164/// no matching symbol is found in the codebase.
165pub fn find_references_codebase(
166    word: &str,
167    all_docs: &[(Url, Arc<ParsedDoc>)],
168    include_declaration: bool,
169    kind: Option<SymbolKind>,
170    codebase: &mir_analyzer::db::MirDbStorage,
171    lookup_refs: &RefLookup<'_>,
172) -> Option<Vec<Location>> {
173    find_references_codebase_with_target(
174        word,
175        all_docs,
176        include_declaration,
177        kind,
178        None,
179        codebase,
180        lookup_refs,
181    )
182}
183
184/// Like [`find_references_codebase`] but accepts an exact FQN (for Function/Class)
185/// or owning FQCN (for Method) to avoid short-name collisions across namespaces
186/// and unrelated classes. When `target_fqn` is `None`, behaves identically to
187/// `find_references_codebase`.
188pub fn find_references_codebase_with_target(
189    _word: &str,
190    _all_docs: &[(Url, Arc<ParsedDoc>)],
191    _include_declaration: bool,
192    kind: Option<SymbolKind>,
193    _target_fqn: Option<&str>,
194    _codebase: &mir_analyzer::db::MirDbStorage,
195    _lookup_refs: &RefLookup<'_>,
196) -> Option<Vec<Location>> {
197    match kind {
198        Some(SymbolKind::Function) => {
199            // For now, fall back to the AST walker for functions.
200            // In the future, we could query the MirDb for function info.
201            None
202        }
203
204        // The mir index records ClassReference only for `new Foo()` expressions, not
205        // for type hints, `extends`, `implements`, or `instanceof`. Using the index
206        // would silently drop those sites when any `new` call exists. Always fall
207        // through to the AST walker (class_refs_in_stmts) which covers all sites.
208        Some(SymbolKind::Class) => None,
209
210        Some(SymbolKind::Method) => {
211            // For now, fall back to the AST walker for methods.
212            // In the future, we could use the MirDb to optimize this.
213            None
214        }
215
216        // General walker already handles None kind; codebase index adds no value.
217        None => None,
218
219        // Properties and constants aren't tracked in the mir codebase index; fall
220        // through to the AST walker.
221        Some(SymbolKind::Property) | Some(SymbolKind::Constant) => None,
222    }
223}
224
225fn find_references_inner(
226    word: &str,
227    all_docs: &[(Url, Arc<ParsedDoc>)],
228    include_declaration: bool,
229    include_use: bool,
230    kind: Option<SymbolKind>,
231    target_fqn: Option<&str>,
232) -> Vec<Location> {
233    // Each document is scanned independently: substring pre-filter, AST walk,
234    // then span → position translation. Rayon parallelizes across docs; the
235    // per-doc work is CPU-bound and 100% independent, so this scales linearly
236    // with cores on large workspaces (Laravel: ~1,600 files).
237    // Per-file namespace pre-filter only applies to Function and Class kinds,
238    // where the target FQN refers to the symbol itself. For methods the
239    // target is the *owning* FQCN, which can't be compared against the
240    // method name via namespace resolution.
241    let namespace_filter_active =
242        matches!(kind, Some(SymbolKind::Function) | Some(SymbolKind::Class));
243    all_docs
244        .par_iter()
245        .flat_map_iter(|(uri, doc)| {
246            if namespace_filter_active
247                && let Some(target) = target_fqn
248                && !doc_can_reference_target(doc, word, target)
249            {
250                return Vec::new();
251            }
252            scan_doc(
253                word,
254                uri,
255                doc,
256                include_declaration,
257                include_use,
258                kind,
259                target_fqn,
260            )
261        })
262        .collect()
263}
264
265/// Return true when this doc's namespace + `use` imports could plausibly
266/// refer to `target_fqn` under the short name `word`.  Used as a pre-filter
267/// so the AST walker doesn't emit refs in files whose namespace would resolve
268/// `word` to a different FQN.
269fn doc_can_reference_target(doc: &ParsedDoc, word: &str, target_fqn: &str) -> bool {
270    let target = target_fqn.trim_start_matches('\\');
271    let imports = collect_file_imports(doc);
272    let resolved = crate::moniker::resolve_fqn(doc, word, &imports);
273    // PHP falls back to the global namespace for unqualified *function* calls
274    // when the namespaced version doesn't exist.  We don't know at this point
275    // which symbol category the target is, so accept either an exact match
276    // or a global-namespace fallback match.
277    resolved == target
278        || (resolved == word && !target.contains('\\'))
279        || (resolved == word && target == format!("\\{word}"))
280}
281
282struct ImportsVisitor {
283    only_kind: Option<UseKind>,
284    out: HashMap<String, String>,
285}
286
287impl<'arena, 'src> Visitor<'arena, 'src> for ImportsVisitor {
288    fn visit_stmt(&mut self, stmt: &Stmt<'arena, 'src>) -> ControlFlow<()> {
289        match &stmt.kind {
290            StmtKind::Use(u) if self.only_kind.is_none_or(|k| u.kind == k) => {
291                for item in u.uses.iter() {
292                    let fqn = item.name.to_string_repr().into_owned();
293                    let short = item
294                        .alias
295                        .map(|a| a.to_string())
296                        .unwrap_or_else(|| fqn.rsplit('\\').next().unwrap_or(&fqn).to_string());
297                    self.out.insert(short, fqn);
298                }
299                ControlFlow::Continue(())
300            }
301            // walk_stmt recurses into NamespaceBody::Braced automatically.
302            StmtKind::Namespace(_) => walk_stmt(self, stmt),
303            _ => ControlFlow::Continue(()),
304        }
305    }
306}
307
308/// Build a local-name → FQN map from a doc's `use` statements.  Mirrors
309/// `Backend::file_imports` but self-contained so the reference walker can
310/// run without a persistent codebase. Includes all use kinds (class, function,
311/// const) — callers that only want class imports should use `collect_class_imports`.
312pub(crate) fn collect_file_imports(doc: &ParsedDoc) -> HashMap<String, String> {
313    collect_imports_filtered(doc, None)
314}
315
316/// Like `collect_file_imports` but restricted to `use ClassName` statements
317/// (`UseKind::Normal`). Use this wherever the import map is fed into class
318/// resolution — mixing in `use function` / `use const` entries causes the
319/// resolver to map a function/const short name to the wrong FQN when the same
320/// short name appears as a type hint or class reference.
321///
322/// TODO: upstream fix — have mir's FileAnalyzer auto-load via its ClassResolver
323/// so lsp no longer needs to pre-collect class dependencies manually.
324pub(crate) fn collect_class_imports(doc: &ParsedDoc) -> HashMap<String, String> {
325    collect_imports_filtered(doc, Some(UseKind::Normal))
326}
327
328fn collect_imports_filtered(
329    doc: &ParsedDoc,
330    only_kind: Option<UseKind>,
331) -> HashMap<String, String> {
332    let mut v = ImportsVisitor {
333        only_kind,
334        out: HashMap::new(),
335    };
336    for stmt in doc.program().stmts.iter() {
337        let _ = v.visit_stmt(stmt);
338    }
339    v.out
340}
341
342/// Collect every FQN class name (e.g. `\App\Model\Entity`) referenced in a
343/// `new` expression that has no corresponding `use` import (i.e. written with
344/// a leading `\`).  Returns de-duplicated strings with the leading `\` stripped,
345/// ready for `session.lazy_load_class`.
346pub(crate) fn collect_fqn_new_class_refs(doc: &ParsedDoc) -> Vec<String> {
347    fqn_new_class_refs_in_stmts(&doc.program().stmts)
348}
349
350/// Collect every class-typed reference in `doc` (extends, implements, new,
351/// instanceof, type hints, static calls, catch types), resolved to an FQN via
352/// the current namespace and `use` imports. Used to lazy-load same-namespace
353/// dependencies that have no explicit `use` statement (and so are missed by
354/// `collect_file_imports`) before semantic analysis runs.
355///
356/// Returns de-duplicated FQNs with any leading `\` stripped.
357pub(crate) fn collect_referenced_class_fqns(doc: &ParsedDoc) -> Vec<String> {
358    let imports = collect_class_imports(doc);
359    let names = all_class_ref_names_in_stmts(&doc.program().stmts);
360    let locals = collect_local_type_decl_fqns(doc);
361    let mut out: Vec<String> = names
362        .into_iter()
363        .map(|name| {
364            // A leading `\` marks an already-fully-qualified reference like
365            // `new \App\Model\Entity()` — strip the slash and use as-is.
366            // `resolve_fqn` would otherwise prepend the current namespace.
367            if let Some(stripped) = name.strip_prefix('\\') {
368                return stripped.to_string();
369            }
370            let fqn = crate::moniker::resolve_fqn(doc, &name, &imports);
371            fqn.trim_start_matches('\\').to_string()
372        })
373        // Skip references that resolve to a type declared in this very file —
374        // mir already has them via `session.ingest_file`, and asking it to
375        // lazy-load them can recurse back through analysis.
376        .filter(|fqn| !locals.contains(fqn))
377        .collect();
378    out.sort_unstable();
379    out.dedup();
380    out
381}
382
383/// FQNs of every top-level type declared in `doc` (class, interface, trait,
384/// enum), applying the file's `namespace` declaration. Used to suppress
385/// self-references in the lazy-load list.
386fn collect_local_type_decl_fqns(doc: &ParsedDoc) -> HashSet<String> {
387    use php_ast::NamespaceBody;
388    let mut out = HashSet::new();
389    fn name_of(kind: &StmtKind<'_, '_>) -> Option<String> {
390        match kind {
391            StmtKind::Class(c) => c.name.as_ref().map(|n| n.to_string()),
392            StmtKind::Interface(i) => Some(i.name.to_string()),
393            StmtKind::Trait(t) => Some(t.name.to_string()),
394            StmtKind::Enum(e) => Some(e.name.to_string()),
395            _ => None,
396        }
397    }
398    let mut current_ns: Option<String> = None;
399    for stmt in doc.program().stmts.iter() {
400        match &stmt.kind {
401            StmtKind::Namespace(ns) => {
402                let ns_name = ns.name.as_ref().map(|n| n.to_string_repr().to_string());
403                match &ns.body {
404                    NamespaceBody::Braced(inner) => {
405                        let prefix = ns_name
406                            .as_deref()
407                            .map(|n| format!("{n}\\"))
408                            .unwrap_or_default();
409                        for s in inner.stmts.iter() {
410                            if let Some(n) = name_of(&s.kind) {
411                                out.insert(format!("{prefix}{n}"));
412                            }
413                        }
414                    }
415                    NamespaceBody::Simple => {
416                        current_ns = ns_name;
417                    }
418                }
419            }
420            k => {
421                if let Some(n) = name_of(k) {
422                    let fqn = match &current_ns {
423                        Some(ns) => format!("{ns}\\{n}"),
424                        None => n,
425                    };
426                    out.insert(fqn);
427                }
428            }
429        }
430    }
431    out
432}
433
434fn scan_doc(
435    word: &str,
436    uri: &Url,
437    doc: &Arc<ParsedDoc>,
438    include_declaration: bool,
439    include_use: bool,
440    kind: Option<SymbolKind>,
441    target_fqn: Option<&str>,
442) -> Vec<Location> {
443    let source = doc.source();
444    // Substring pre-filter: every walker below pushes a span only when an
445    // identifier's bytes equal `word`, so if `word` does not appear in the
446    // source it cannot produce any reference. `str::contains` is memchr-fast
447    // and skips the full AST traversal for the vast majority of files.
448    if !source.contains(word) {
449        return Vec::new();
450    }
451    let stmts = &doc.program().stmts;
452    let mut spans = Vec::new();
453
454    if include_use {
455        // Rename path: general walker covers call sites, `use` imports, and declarations.
456        refs_in_stmts_with_use(source, stmts, word, &mut spans);
457        if !include_declaration {
458            let mut decl_spans = Vec::new();
459            collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
460            let decl_set: HashSet<(u32, u32)> =
461                decl_spans.iter().map(|s| (s.start, s.end)).collect();
462            spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
463        }
464    } else {
465        match kind {
466            Some(SymbolKind::Function) => function_refs_in_stmts(stmts, word, &mut spans),
467            Some(SymbolKind::Method) => method_refs_in_stmts(stmts, word, &mut spans),
468            Some(SymbolKind::Class) => class_refs_in_stmts(stmts, word, &mut spans),
469            // Property walker emits both access sites *and* declaration spans
470            // (used by rename). Strip decls here when the caller doesn't want them.
471            Some(SymbolKind::Property) => {
472                property_refs_in_stmts(source, stmts, word, &mut spans);
473                if !include_declaration {
474                    let mut decl_spans = Vec::new();
475                    collect_declaration_spans(
476                        source,
477                        stmts,
478                        word,
479                        Some(SymbolKind::Property),
480                        &mut decl_spans,
481                    );
482                    let decl_set: HashSet<(u32, u32)> =
483                        decl_spans.iter().map(|s| (s.start, s.end)).collect();
484                    spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
485                }
486            }
487            // Constant walker emits both declaration spans and access spans.
488            Some(SymbolKind::Constant) => {
489                // Class constants: target_fqn = owning class short name (no backslash).
490                // Global/namespace constants: target_fqn = None (root) or
491                //   "Namespace\\ConstName" (namespaced, has backslash). Route to the
492                //   bare-identifier walker instead of the `::` class-const walker.
493                let is_global = target_fqn.is_none_or(|fqn| fqn.contains('\\'));
494                if is_global {
495                    global_constant_refs_in_stmts(source, stmts, word, target_fqn, &mut spans);
496                } else {
497                    // target_fqn = class short name for class constants.
498                    constant_refs_in_stmts(source, stmts, word, target_fqn, &mut spans);
499                }
500                if !include_declaration {
501                    let mut decl_spans = Vec::new();
502                    collect_declaration_spans(
503                        source,
504                        stmts,
505                        word,
506                        Some(SymbolKind::Constant),
507                        &mut decl_spans,
508                    );
509                    let decl_set: HashSet<(u32, u32)> =
510                        decl_spans.iter().map(|s| (s.start, s.end)).collect();
511                    spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
512                }
513            }
514            // General walker already includes declarations; filter them out if unwanted.
515            None => {
516                refs_in_stmts(source, stmts, word, &mut spans);
517                if !include_declaration {
518                    let mut decl_spans = Vec::new();
519                    collect_declaration_spans(source, stmts, word, None, &mut decl_spans);
520                    let decl_set: HashSet<(u32, u32)> =
521                        decl_spans.iter().map(|s| (s.start, s.end)).collect();
522                    spans.retain(|span| !decl_set.contains(&(span.start, span.end)));
523                }
524            }
525        }
526        // Typed walkers (except Property, which already includes decls) don't emit
527        // declaration spans, so add them separately when wanted. Pass `kind` so only
528        // declarations of the matching category are appended — a Method search must
529        // not return a free-function declaration with the same name.
530        if include_declaration
531            && matches!(
532                kind,
533                Some(SymbolKind::Function) | Some(SymbolKind::Method) | Some(SymbolKind::Class)
534            )
535        {
536            collect_declaration_spans(source, stmts, word, kind, &mut spans);
537        }
538    }
539
540    let sv = doc.view();
541    let word_utf16_len: u32 = utf16_code_units(word);
542    spans
543        .into_iter()
544        .map(|span| {
545            let start = sv.position_of(span.start);
546            let end = Position {
547                line: start.line,
548                character: start.character + word_utf16_len,
549            };
550            Location {
551                uri: uri.clone(),
552                range: Range { start, end },
553            }
554        })
555        .collect()
556}
557
558/// Build a span covering exactly the declared name (not the keyword before it).
559/// Uses the stmt_span to search within the statement's context, avoiding false
560/// matches from earlier occurrences of the same name in the file.
561fn declaration_name_span(source: &str, name: &str, stmt_span: Span) -> Span {
562    let start = str_offset_in_range(source, stmt_span, name).unwrap_or(stmt_span.start);
563    Span {
564        start,
565        end: start + name.len() as u32,
566    }
567}
568
569/// Collect every span where `word` is *declared* within `stmts`.
570///
571/// When `kind` is `Some`, only declarations of the matching category are collected:
572/// - `Function` → free (`StmtKind::Function`) declarations only
573/// - `Method`   → method declarations inside classes / traits / enums only
574/// - `Class`    → class / interface / trait / enum type declarations only
575///
576/// `None` collects every declaration kind (used by `is_declaration_span`).
577fn collect_declaration_spans(
578    source: &str,
579    stmts: &[Stmt<'_, '_>],
580    word: &str,
581    kind: Option<SymbolKind>,
582    out: &mut Vec<Span>,
583) {
584    let want_free = matches!(kind, None | Some(SymbolKind::Function));
585    let want_method = matches!(kind, None | Some(SymbolKind::Method));
586    let want_type = matches!(kind, None | Some(SymbolKind::Class));
587    let want_property = matches!(kind, None | Some(SymbolKind::Property));
588    let want_constant = matches!(kind, None | Some(SymbolKind::Constant));
589
590    for stmt in stmts {
591        match &stmt.kind {
592            StmtKind::Function(f) if want_free && f.name == word => {
593                out.push(declaration_name_span(
594                    source,
595                    &f.name.to_string(),
596                    stmt.span,
597                ));
598            }
599            StmtKind::Class(c) => {
600                if want_type
601                    && let Some(name) = c.name
602                    && name == word
603                {
604                    out.push(declaration_name_span(source, &name.to_string(), stmt.span));
605                }
606                if want_method || want_property || want_constant {
607                    for member in c.body.members.iter() {
608                        match &member.kind {
609                            ClassMemberKind::Method(m) if want_method && m.name == word => {
610                                // Scope the name search to the member span,
611                                // not the whole class — otherwise a class
612                                // named the same as one of its members
613                                // (`class get { function get() {} }`) resolves
614                                // both decls to the class name's position.
615                                out.push(declaration_name_span(
616                                    source,
617                                    &m.name.to_string(),
618                                    member.span,
619                                ));
620                            }
621                            ClassMemberKind::Method(m)
622                                if want_property && m.name == "__construct" =>
623                            {
624                                // Promoted constructor params act as property declarations.
625                                for p in m.params.iter() {
626                                    if p.visibility.is_some() && p.name == word {
627                                        out.push(declaration_name_span(
628                                            source,
629                                            &p.name.to_string(),
630                                            p.span,
631                                        ));
632                                    }
633                                }
634                            }
635                            ClassMemberKind::Property(p) if want_property && p.name == word => {
636                                out.push(declaration_name_span(
637                                    source,
638                                    &p.name.to_string(),
639                                    member.span,
640                                ));
641                            }
642                            ClassMemberKind::ClassConst(c) if want_constant && c.name == word => {
643                                out.push(declaration_name_span(
644                                    source,
645                                    &c.name.to_string(),
646                                    member.span,
647                                ));
648                            }
649                            _ => {}
650                        }
651                    }
652                }
653            }
654            StmtKind::Interface(i) => {
655                if want_type && i.name == word {
656                    out.push(declaration_name_span(
657                        source,
658                        &i.name.to_string(),
659                        stmt.span,
660                    ));
661                }
662                if want_method || want_constant {
663                    for member in i.body.members.iter() {
664                        match &member.kind {
665                            ClassMemberKind::Method(m) if want_method && m.name == word => {
666                                out.push(declaration_name_span(
667                                    source,
668                                    &m.name.to_string(),
669                                    member.span,
670                                ));
671                            }
672                            ClassMemberKind::ClassConst(c) if want_constant && c.name == word => {
673                                out.push(declaration_name_span(
674                                    source,
675                                    &c.name.to_string(),
676                                    member.span,
677                                ));
678                            }
679                            _ => {}
680                        }
681                    }
682                }
683            }
684            StmtKind::Trait(t) => {
685                if want_type && t.name == word {
686                    out.push(declaration_name_span(
687                        source,
688                        &t.name.to_string(),
689                        stmt.span,
690                    ));
691                }
692                if want_method || want_property || want_constant {
693                    for member in t.body.members.iter() {
694                        match &member.kind {
695                            ClassMemberKind::Method(m) if want_method && m.name == word => {
696                                out.push(declaration_name_span(
697                                    source,
698                                    &m.name.to_string(),
699                                    member.span,
700                                ));
701                            }
702                            ClassMemberKind::Property(p) if want_property && p.name == word => {
703                                out.push(declaration_name_span(
704                                    source,
705                                    &p.name.to_string(),
706                                    member.span,
707                                ));
708                            }
709                            ClassMemberKind::ClassConst(c) if want_constant && c.name == word => {
710                                out.push(declaration_name_span(
711                                    source,
712                                    &c.name.to_string(),
713                                    member.span,
714                                ));
715                            }
716                            _ => {}
717                        }
718                    }
719                }
720            }
721            StmtKind::Enum(e) => {
722                if want_type && e.name == word {
723                    out.push(declaration_name_span(
724                        source,
725                        &e.name.to_string(),
726                        stmt.span,
727                    ));
728                }
729                for member in e.body.members.iter() {
730                    match &member.kind {
731                        EnumMemberKind::Method(m) if want_method && m.name == word => {
732                            out.push(declaration_name_span(
733                                source,
734                                &m.name.to_string(),
735                                member.span,
736                            ));
737                        }
738                        EnumMemberKind::Case(c) if want_type && c.name == word => {
739                            out.push(declaration_name_span(
740                                source,
741                                &c.name.to_string(),
742                                member.span,
743                            ));
744                        }
745                        EnumMemberKind::ClassConst(c) if want_constant && c.name == word => {
746                            out.push(declaration_name_span(
747                                source,
748                                &c.name.to_string(),
749                                member.span,
750                            ));
751                        }
752                        _ => {}
753                    }
754                }
755            }
756            StmtKind::Const(items) if want_constant => {
757                for item in items.iter() {
758                    if item.name == word {
759                        let name = item.name.to_string();
760                        out.push(declaration_name_span(source, &name, item.span));
761                    }
762                }
763            }
764            StmtKind::Expression(expr) if want_constant => {
765                // `define('NAME', value)` acts as a global constant declaration.
766                if let ExprKind::FunctionCall(f) = &expr.kind
767                    && let ExprKind::Identifier(id) = &f.name.kind
768                    && id.as_str() == "define"
769                    && let Some(first_arg) = f.args.first()
770                    && let ExprKind::String(s) = &first_arg.value.kind
771                    && *s == word
772                {
773                    let start = first_arg.value.span.start + 1;
774                    out.push(Span {
775                        start,
776                        end: start + s.len() as u32,
777                    });
778                }
779            }
780            StmtKind::Namespace(ns) => {
781                if let NamespaceBody::Braced(inner) = &ns.body {
782                    collect_declaration_spans(source, &inner.stmts, word, kind, out);
783                }
784            }
785            _ => {}
786        }
787    }
788}