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 ¤t_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}