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 extends_match = cls
110 .parent
111 .as_deref()
112 .map(|p| name_matches(p, word, fqn))
113 .unwrap_or(false);
114 let implements_match = cls.implements.iter().any(|iface| {
115 if name_matches(iface.as_ref(), word, fqn) {
116 return true;
117 }
118 if let Some((_, file_idx)) = wi.files.get(r.file as usize) {
122 file_idx.use_imports.iter().any(|(alias, resolved_fqn)| {
123 alias.as_ref() == iface.as_ref()
124 && crate::text::fqn_short_name(resolved_fqn) == word
125 })
126 } else {
127 false
128 }
129 });
130 if extends_match || implements_match {
131 let pos = tower_lsp::lsp_types::Position {
132 line: cls.start_line,
133 character: 0,
134 };
135 locations.push(Location {
136 uri: uri.clone(),
137 range: tower_lsp::lsp_types::Range {
138 start: pos,
139 end: pos,
140 },
141 });
142 }
143 }
144 }
145 }
146 };
147 push_refs(word);
148 if let Some(f) = fqn
149 && f != word
150 {
151 push_refs(f);
152 let trimmed = f.trim_start_matches('\\');
154 if trimmed != f {
155 push_refs(trimmed);
156 }
157 }
158 locations.sort_by(|a, b| {
161 a.uri
162 .as_str()
163 .cmp(b.uri.as_str())
164 .then(a.range.start.line.cmp(&b.range.start.line))
165 });
166 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
167 locations
168}
169
170fn collect_implementations(
171 stmts: &[Stmt<'_, '_>],
172 word: &str,
173 fqn: Option<&str>,
174 sv: SourceView<'_>,
175 uri: &Url,
176 out: &mut Vec<Location>,
177) {
178 for stmt in stmts {
179 match &stmt.kind {
180 StmtKind::Class(c) => {
181 let extends_match = c
182 .extends
183 .as_ref()
184 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
185 .unwrap_or(false);
186
187 let implements_match = c
188 .implements
189 .iter()
190 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
191
192 if extends_match || implements_match {
193 let range = if let Some(class_name) = c.name {
194 sv.name_range_in_span(class_name.or_error(), stmt.span)
195 } else {
196 sv.name_range_in_span("class", stmt.span)
198 };
199 out.push(Location {
200 uri: uri.clone(),
201 range,
202 });
203 }
204 }
205 StmtKind::Enum(e) => {
206 let implements_match = e
207 .implements
208 .iter()
209 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
210 if implements_match {
211 out.push(Location {
212 uri: uri.clone(),
213 range: sv.name_range_in_span(e.name.or_error(), stmt.span),
214 });
215 }
216 }
217 StmtKind::Interface(i) => {
218 let extends_match = i
219 .extends
220 .iter()
221 .any(|base| name_matches(base.to_string_repr().as_ref(), word, fqn));
222 if extends_match {
223 out.push(Location {
224 uri: uri.clone(),
225 range: sv.name_range_in_span(i.name.or_error(), stmt.span),
226 });
227 }
228 }
229 StmtKind::Expression(expr) => {
230 collect_anon_class_in_expr(expr, word, fqn, sv, stmt.span, uri, out);
231 }
232 StmtKind::Namespace(ns) => {
233 if let NamespaceBody::Braced(inner) = &ns.body {
234 collect_implementations(&inner.stmts, word, fqn, sv, uri, out);
235 }
236 }
237 _ => {}
238 }
239 }
240}
241
242pub(crate) fn alias_resolves_to(
245 name: &str,
246 fqn: &str,
247 use_imports: &[(Box<str>, Box<str>)],
248) -> bool {
249 use_imports.iter().any(|(alias, resolved)| {
250 alias.as_ref() == name
251 && (resolved.as_ref() == fqn || resolved.trim_start_matches('\\') == fqn)
252 })
253}
254
255pub fn find_implementations_mir_backed(
264 word: &str,
265 fqn: Option<&str>,
266 wi: &crate::db::workspace_index::WorkspaceIndexData,
267 subtype_urls: &[Url],
268) -> Vec<Location> {
269 if subtype_urls.is_empty() {
270 return find_implementations_from_workspace(word, fqn, wi);
271 }
272 let url_set: HashSet<&Url> = subtype_urls.iter().collect();
273 let mut locations = Vec::new();
274 for (uri, idx) in &wi.files {
275 if !url_set.contains(uri) {
276 continue;
277 }
278 for cls in &idx.classes {
279 let extends_match = cls
280 .parent
281 .as_deref()
282 .map(|p| {
283 name_matches(p, word, fqn)
284 || fqn.is_some_and(|f| alias_resolves_to(p, f, &idx.use_imports))
285 })
286 .unwrap_or(false);
287 let implements_match = cls.implements.iter().any(|iface| {
288 name_matches(iface.as_ref(), word, fqn)
289 || fqn.is_some_and(|f| alias_resolves_to(iface.as_ref(), f, &idx.use_imports))
290 });
291 if extends_match || implements_match {
292 let pos = tower_lsp::lsp_types::Position {
293 line: cls.start_line,
294 character: 0,
295 };
296 locations.push(Location {
297 uri: uri.clone(),
298 range: tower_lsp::lsp_types::Range {
299 start: pos,
300 end: pos,
301 },
302 });
303 }
304 }
305 }
306 locations.sort_by(|a, b| {
307 a.uri
308 .as_str()
309 .cmp(b.uri.as_str())
310 .then(a.range.start.line.cmp(&b.range.start.line))
311 });
312 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
313 locations
314}
315
316pub fn find_method_implementations_mir_backed(
323 method_name: &str,
324 declaring_class: &str,
325 wi: &crate::db::workspace_index::WorkspaceIndexData,
326 subtype_urls: &[Url],
327) -> Vec<tower_lsp::lsp_types::Location> {
328 if subtype_urls.is_empty() {
329 return find_method_implementations_from_workspace(method_name, declaring_class, wi);
330 }
331 let url_set: HashSet<&Url> = subtype_urls.iter().collect();
332 let mut locations = Vec::new();
333 for (uri, idx) in &wi.files {
334 if !url_set.contains(uri) {
335 continue;
336 }
337 for cls in &idx.classes {
338 if let Some(method) = cls
339 .methods
340 .iter()
341 .find(|m| m.name.as_ref() == method_name && !m.is_abstract)
342 {
343 locations.push(tower_lsp::lsp_types::Location {
344 uri: uri.clone(),
345 range: crate::text::zero_width_range(method.start_line),
346 });
347 }
348 }
349 }
350 locations.sort_by(|a, b| {
351 a.uri
352 .as_str()
353 .cmp(b.uri.as_str())
354 .then(a.range.start.line.cmp(&b.range.start.line))
355 });
356 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
357 locations
358}
359
360fn collect_anon_class_in_expr(
363 expr: &php_ast::Expr<'_, '_>,
364 word: &str,
365 fqn: Option<&str>,
366 sv: SourceView<'_>,
367 stmt_span: php_ast::Span,
368 uri: &Url,
369 out: &mut Vec<Location>,
370) {
371 match &expr.kind {
372 ExprKind::AnonymousClass(c) => {
373 let extends_match = c
374 .extends
375 .as_ref()
376 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
377 .unwrap_or(false);
378 let implements_match = c
379 .implements
380 .iter()
381 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
382 if extends_match || implements_match {
383 out.push(Location {
385 uri: uri.clone(),
386 range: sv.name_range_in_span("class", stmt_span),
387 });
388 }
389 }
390 ExprKind::New(n) => {
391 collect_anon_class_in_expr(n.class, word, fqn, sv, stmt_span, uri, out);
392 }
393 ExprKind::Assign(a) => {
394 collect_anon_class_in_expr(a.value, word, fqn, sv, stmt_span, uri, out);
395 }
396 _ => {}
397 }
398}