1use std::collections::HashSet;
4use std::sync::Arc;
5
6use php_ast::{ExprKind, NamespaceBody, Stmt, StmtKind};
7use tower_lsp::lsp_types::{Location, Url};
8
9use crate::document::ast::{ParsedDoc, SourceView};
10
11#[inline]
28pub(crate) fn name_matches(repr: &str, word: &str, fqn: Option<&str>) -> bool {
29 repr == word
30 || fqn.is_some_and(|f| repr.trim_start_matches('\\') == f)
31 || (fqn.is_none() && !word.contains('\\') && repr.trim_start_matches('\\') == word)
32}
33
34pub fn find_implementations(
42 word: &str,
43 fqn: Option<&str>,
44 all_docs: &[(Url, Arc<ParsedDoc>)],
45) -> Vec<Location> {
46 let mut locations = Vec::new();
47 for (uri, doc) in all_docs {
48 let sv = doc.view();
49 collect_implementations(&doc.program().stmts, word, fqn, sv, uri, &mut locations);
50 }
51 locations
52}
53
54pub fn find_method_implementations_from_workspace(
62 method_name: &str,
63 declaring_class: &str,
64 wi: &crate::db::workspace_index::WorkspaceIndexData,
65) -> Vec<tower_lsp::lsp_types::Location> {
66 let mut locations = Vec::new();
67 if let Some(refs) = wi.subtypes_of.get(declaring_class) {
68 for &class_ref in refs {
69 if let Some((uri, cls)) = wi.at(class_ref)
70 && let Some(method) = cls
71 .methods
72 .iter()
73 .find(|m| m.name.as_ref() == method_name && !m.is_abstract)
74 {
75 locations.push(tower_lsp::lsp_types::Location {
76 uri: uri.clone(),
77 range: crate::text::zero_width_range(method.start_line),
78 });
79 }
80 }
81 }
82 locations.sort_by(|a, b| {
83 a.uri
84 .as_str()
85 .cmp(b.uri.as_str())
86 .then(a.range.start.line.cmp(&b.range.start.line))
87 });
88 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
89 locations
90}
91
92pub fn find_implementations_from_workspace(
98 word: &str,
99 fqn: Option<&str>,
100 wi: &crate::db::workspace_index::WorkspaceIndexData,
101) -> Vec<Location> {
102 let mut locations = Vec::new();
103 let mut push_refs = |key: &str| {
104 if let Some(refs) = wi.subtypes_of.get(key) {
105 for r in refs {
106 if let Some((uri, cls)) = wi.at(*r) {
107 let file_idx = wi.files.get(r.file as usize).map(|(_, idx)| idx.as_ref());
108 let extends_match = cls.parent.as_deref().is_some_and(|p| match fqn {
116 Some(f) => file_idx.is_some_and(|idx| resolves_to_fqn(p, f, idx)),
117 None => name_matches(p, word, None),
118 });
119 let implements_match = cls.implements.iter().any(|iface| match fqn {
120 Some(f) => {
121 file_idx.is_some_and(|idx| resolves_to_fqn(iface.as_ref(), f, idx))
122 }
123 None => name_matches(iface.as_ref(), word, None),
124 });
125 if extends_match || implements_match {
126 let pos = tower_lsp::lsp_types::Position {
127 line: cls.start_line,
128 character: 0,
129 };
130 locations.push(Location {
131 uri: uri.clone(),
132 range: tower_lsp::lsp_types::Range {
133 start: pos,
134 end: pos,
135 },
136 });
137 }
138 }
139 }
140 }
141 };
142 push_refs(word);
143 if let Some(f) = fqn
144 && f != word
145 {
146 push_refs(f);
147 let trimmed = f.trim_start_matches('\\');
149 if trimmed != f {
150 push_refs(trimmed);
151 }
152 }
153 locations.sort_by(|a, b| {
156 a.uri
157 .as_str()
158 .cmp(b.uri.as_str())
159 .then(a.range.start.line.cmp(&b.range.start.line))
160 });
161 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
162 locations
163}
164
165fn collect_implementations(
166 stmts: &[Stmt<'_, '_>],
167 word: &str,
168 fqn: Option<&str>,
169 sv: SourceView<'_>,
170 uri: &Url,
171 out: &mut Vec<Location>,
172) {
173 for stmt in stmts {
174 match &stmt.kind {
175 StmtKind::Class(c) => {
176 let extends_match = c
177 .extends
178 .as_ref()
179 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
180 .unwrap_or(false);
181
182 let implements_match = c
183 .implements
184 .iter()
185 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
186
187 if extends_match || implements_match {
188 let range = if let Some(class_name) = c.name {
189 sv.name_range_in_span(class_name.or_error(), stmt.span)
190 } else {
191 sv.name_range_in_span("class", stmt.span)
193 };
194 out.push(Location {
195 uri: uri.clone(),
196 range,
197 });
198 }
199 }
200 StmtKind::Enum(e) => {
201 let implements_match = e
202 .implements
203 .iter()
204 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
205 if implements_match {
206 out.push(Location {
207 uri: uri.clone(),
208 range: sv.name_range_in_span(e.name.or_error(), stmt.span),
209 });
210 }
211 }
212 StmtKind::Interface(i) => {
213 let extends_match = i
214 .extends
215 .iter()
216 .any(|base| name_matches(base.to_string_repr().as_ref(), word, fqn));
217 if extends_match {
218 out.push(Location {
219 uri: uri.clone(),
220 range: sv.name_range_in_span(i.name.or_error(), stmt.span),
221 });
222 }
223 }
224 StmtKind::Expression(expr) => {
225 collect_anon_class_in_expr(expr, word, fqn, sv, stmt.span, uri, out);
226 }
227 StmtKind::Namespace(ns) => {
228 if let NamespaceBody::Braced(inner) = &ns.body {
229 collect_implementations(&inner.stmts, word, fqn, sv, uri, out);
230 }
231 }
232 _ => {}
233 }
234 }
235}
236
237pub(crate) fn alias_resolves_to(
240 name: &str,
241 fqn: &str,
242 use_imports: &[(Box<str>, Box<str>)],
243) -> bool {
244 use_imports.iter().any(|(alias, resolved)| {
245 alias.as_ref() == name
246 && (resolved.as_ref() == fqn || resolved.trim_start_matches('\\') == fqn)
247 })
248}
249
250pub(crate) fn resolves_to_fqn(
267 written: &str,
268 target_fqn: &str,
269 idx: &crate::index::file_index::FileIndex,
270) -> bool {
271 if written.contains('\\') {
272 return written.trim_start_matches('\\') == target_fqn;
273 }
274 if let Some((_, resolved)) = idx
275 .use_imports
276 .iter()
277 .find(|(alias, _)| alias.as_ref() == written)
278 {
279 return resolved.as_ref() == target_fqn || resolved.trim_start_matches('\\') == target_fqn;
280 }
281 match idx.namespace.as_deref() {
282 Some(ns) => format!("{ns}\\{written}") == target_fqn,
283 None => written == target_fqn,
284 }
285}
286
287pub fn find_implementations_mir_backed(
296 word: &str,
297 fqn: Option<&str>,
298 wi: &crate::db::workspace_index::WorkspaceIndexData,
299 subtype_urls: &[Url],
300) -> Vec<Location> {
301 if subtype_urls.is_empty() {
302 return find_implementations_from_workspace(word, fqn, wi);
303 }
304 let url_set: HashSet<&Url> = subtype_urls.iter().collect();
305 let mut locations = Vec::new();
306 for (uri, idx) in &wi.files {
307 if !url_set.contains(uri) {
308 continue;
309 }
310 for cls in &idx.classes {
311 let extends_match = cls
312 .parent
313 .as_deref()
314 .map(|p| {
315 name_matches(p, word, fqn)
316 || fqn.is_some_and(|f| alias_resolves_to(p, f, &idx.use_imports))
317 })
318 .unwrap_or(false);
319 let implements_match = cls.implements.iter().any(|iface| {
320 name_matches(iface.as_ref(), word, fqn)
321 || fqn.is_some_and(|f| alias_resolves_to(iface.as_ref(), f, &idx.use_imports))
322 });
323 if extends_match || implements_match {
324 let pos = tower_lsp::lsp_types::Position {
325 line: cls.start_line,
326 character: 0,
327 };
328 locations.push(Location {
329 uri: uri.clone(),
330 range: tower_lsp::lsp_types::Range {
331 start: pos,
332 end: pos,
333 },
334 });
335 }
336 }
337 }
338 locations.sort_by(|a, b| {
339 a.uri
340 .as_str()
341 .cmp(b.uri.as_str())
342 .then(a.range.start.line.cmp(&b.range.start.line))
343 });
344 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
345 locations
346}
347
348pub fn find_method_implementations_mir_backed(
355 method_name: &str,
356 declaring_class: &str,
357 wi: &crate::db::workspace_index::WorkspaceIndexData,
358 subtype_urls: &[Url],
359) -> Vec<tower_lsp::lsp_types::Location> {
360 if subtype_urls.is_empty() {
361 return find_method_implementations_from_workspace(method_name, declaring_class, wi);
362 }
363 let url_set: HashSet<&Url> = subtype_urls.iter().collect();
364 let mut locations = Vec::new();
365 for (uri, idx) in &wi.files {
366 if !url_set.contains(uri) {
367 continue;
368 }
369 for cls in &idx.classes {
370 if let Some(method) = cls
371 .methods
372 .iter()
373 .find(|m| m.name.as_ref() == method_name && !m.is_abstract)
374 {
375 locations.push(tower_lsp::lsp_types::Location {
376 uri: uri.clone(),
377 range: crate::text::zero_width_range(method.start_line),
378 });
379 }
380 }
381 }
382 locations.sort_by(|a, b| {
383 a.uri
384 .as_str()
385 .cmp(b.uri.as_str())
386 .then(a.range.start.line.cmp(&b.range.start.line))
387 });
388 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
389 locations
390}
391
392fn collect_anon_class_in_expr(
395 expr: &php_ast::Expr<'_, '_>,
396 word: &str,
397 fqn: Option<&str>,
398 sv: SourceView<'_>,
399 stmt_span: php_ast::Span,
400 uri: &Url,
401 out: &mut Vec<Location>,
402) {
403 match &expr.kind {
404 ExprKind::AnonymousClass(c) => {
405 let extends_match = c
406 .extends
407 .as_ref()
408 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
409 .unwrap_or(false);
410 let implements_match = c
411 .implements
412 .iter()
413 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
414 if extends_match || implements_match {
415 out.push(Location {
417 uri: uri.clone(),
418 range: sv.name_range_in_span("class", stmt_span),
419 });
420 }
421 }
422 ExprKind::New(n) => {
423 collect_anon_class_in_expr(n.class, word, fqn, sv, stmt_span, uri, out);
424 }
425 ExprKind::Assign(a) => {
426 collect_anon_class_in_expr(a.value, word, fqn, sv, stmt_span, uri, out);
427 }
428 _ => {}
429 }
430}