1use std::sync::Arc;
4
5use php_ast::{ExprKind, NamespaceBody, Stmt, StmtKind};
6use tower_lsp::lsp_types::{Location, Url};
7
8use crate::document::ast::{ParsedDoc, SourceView};
9
10#[inline]
27fn name_matches(repr: &str, word: &str, fqn: Option<&str>) -> bool {
28 repr == word
29 || fqn.is_some_and(|f| repr.trim_start_matches('\\') == f)
30 || (fqn.is_none() && !word.contains('\\') && repr.trim_start_matches('\\') == word)
31}
32
33pub fn find_implementations(
41 word: &str,
42 fqn: Option<&str>,
43 all_docs: &[(Url, Arc<ParsedDoc>)],
44) -> Vec<Location> {
45 let mut locations = Vec::new();
46 for (uri, doc) in all_docs {
47 let sv = doc.view();
48 collect_implementations(&doc.program().stmts, word, fqn, sv, uri, &mut locations);
49 }
50 locations
51}
52
53pub fn find_method_implementations_from_workspace(
61 method_name: &str,
62 declaring_class: &str,
63 wi: &crate::db::workspace_index::WorkspaceIndexData,
64) -> Vec<tower_lsp::lsp_types::Location> {
65 let mut locations = Vec::new();
66 if let Some(refs) = wi.subtypes_of.get(declaring_class) {
67 for &class_ref in refs {
68 if let Some((uri, cls)) = wi.at(class_ref)
69 && let Some(method) = cls
70 .methods
71 .iter()
72 .find(|m| m.name.as_ref() == method_name && !m.is_abstract)
73 {
74 locations.push(tower_lsp::lsp_types::Location {
75 uri: uri.clone(),
76 range: crate::text::zero_width_range(method.start_line),
77 });
78 }
79 }
80 }
81 locations.sort_by(|a, b| {
82 a.uri
83 .as_str()
84 .cmp(b.uri.as_str())
85 .then(a.range.start.line.cmp(&b.range.start.line))
86 });
87 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
88 locations
89}
90
91pub 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
242fn collect_anon_class_in_expr(
245 expr: &php_ast::Expr<'_, '_>,
246 word: &str,
247 fqn: Option<&str>,
248 sv: SourceView<'_>,
249 stmt_span: php_ast::Span,
250 uri: &Url,
251 out: &mut Vec<Location>,
252) {
253 match &expr.kind {
254 ExprKind::AnonymousClass(c) => {
255 let extends_match = c
256 .extends
257 .as_ref()
258 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
259 .unwrap_or(false);
260 let implements_match = c
261 .implements
262 .iter()
263 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
264 if extends_match || implements_match {
265 out.push(Location {
267 uri: uri.clone(),
268 range: sv.name_range_in_span("class", stmt_span),
269 });
270 }
271 }
272 ExprKind::New(n) => {
273 collect_anon_class_in_expr(n.class, word, fqn, sv, stmt_span, uri, out);
274 }
275 ExprKind::Assign(a) => {
276 collect_anon_class_in_expr(a.value, word, fqn, sv, stmt_span, uri, out);
277 }
278 _ => {}
279 }
280}