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(
97 word: &str,
98 fqn: Option<&str>,
99 wi: &crate::db::workspace_index::WorkspaceIndexData,
100) -> Vec<Location> {
101 let mut locations = Vec::new();
102 let mut push_refs = |key: &str| {
103 if let Some(refs) = wi.subtypes_of.get(key) {
104 for r in refs {
105 if let Some((uri, cls)) = wi.at(*r) {
106 let extends_match = cls
109 .parent
110 .as_deref()
111 .map(|p| name_matches(p, word, fqn))
112 .unwrap_or(false);
113 let implements_match = cls.implements.iter().any(|iface| {
114 if name_matches(iface.as_ref(), word, fqn) {
115 return true;
116 }
117 if let Some((_, file_idx)) = wi.files.get(r.file as usize) {
121 file_idx.use_imports.iter().any(|(alias, resolved_fqn)| {
122 alias.as_ref() == iface.as_ref()
123 && crate::text::fqn_short_name(resolved_fqn) == word
124 })
125 } else {
126 false
127 }
128 });
129 if extends_match || implements_match {
130 let pos = tower_lsp::lsp_types::Position {
131 line: cls.start_line,
132 character: 0,
133 };
134 locations.push(Location {
135 uri: uri.clone(),
136 range: tower_lsp::lsp_types::Range {
137 start: pos,
138 end: pos,
139 },
140 });
141 }
142 }
143 }
144 }
145 };
146 push_refs(word);
147 if let Some(f) = fqn
148 && f != word
149 {
150 push_refs(f);
151 let trimmed = f.trim_start_matches('\\');
153 if trimmed != f {
154 push_refs(trimmed);
155 }
156 }
157 locations.sort_by(|a, b| {
160 a.uri
161 .as_str()
162 .cmp(b.uri.as_str())
163 .then(a.range.start.line.cmp(&b.range.start.line))
164 });
165 locations.dedup_by(|a, b| a.uri == b.uri && a.range.start.line == b.range.start.line);
166 locations
167}
168
169fn collect_implementations(
170 stmts: &[Stmt<'_, '_>],
171 word: &str,
172 fqn: Option<&str>,
173 sv: SourceView<'_>,
174 uri: &Url,
175 out: &mut Vec<Location>,
176) {
177 for stmt in stmts {
178 match &stmt.kind {
179 StmtKind::Class(c) => {
180 let extends_match = c
181 .extends
182 .as_ref()
183 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
184 .unwrap_or(false);
185
186 let implements_match = c
187 .implements
188 .iter()
189 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
190
191 if extends_match || implements_match {
192 let range = if let Some(class_name) = c.name {
193 sv.name_range_in_span(class_name.or_error(), stmt.span)
194 } else {
195 sv.name_range_in_span("class", stmt.span)
197 };
198 out.push(Location {
199 uri: uri.clone(),
200 range,
201 });
202 }
203 }
204 StmtKind::Enum(e) => {
205 let implements_match = e
206 .implements
207 .iter()
208 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
209 if implements_match {
210 out.push(Location {
211 uri: uri.clone(),
212 range: sv.name_range_in_span(e.name.or_error(), stmt.span),
213 });
214 }
215 }
216 StmtKind::Interface(i) => {
217 let extends_match = i
218 .extends
219 .iter()
220 .any(|base| name_matches(base.to_string_repr().as_ref(), word, fqn));
221 if extends_match {
222 out.push(Location {
223 uri: uri.clone(),
224 range: sv.name_range_in_span(i.name.or_error(), stmt.span),
225 });
226 }
227 }
228 StmtKind::Expression(expr) => {
229 collect_anon_class_in_expr(expr, word, fqn, sv, stmt.span, uri, out);
230 }
231 StmtKind::Namespace(ns) => {
232 if let NamespaceBody::Braced(inner) = &ns.body {
233 collect_implementations(&inner.stmts, word, fqn, sv, uri, out);
234 }
235 }
236 _ => {}
237 }
238 }
239}
240
241fn collect_anon_class_in_expr(
244 expr: &php_ast::Expr<'_, '_>,
245 word: &str,
246 fqn: Option<&str>,
247 sv: SourceView<'_>,
248 stmt_span: php_ast::Span,
249 uri: &Url,
250 out: &mut Vec<Location>,
251) {
252 match &expr.kind {
253 ExprKind::AnonymousClass(c) => {
254 let extends_match = c
255 .extends
256 .as_ref()
257 .map(|e| name_matches(e.to_string_repr().as_ref(), word, fqn))
258 .unwrap_or(false);
259 let implements_match = c
260 .implements
261 .iter()
262 .any(|iface| name_matches(iface.to_string_repr().as_ref(), word, fqn));
263 if extends_match || implements_match {
264 out.push(Location {
266 uri: uri.clone(),
267 range: sv.name_range_in_span("class", stmt_span),
268 });
269 }
270 }
271 ExprKind::New(n) => {
272 collect_anon_class_in_expr(n.class, word, fqn, sv, stmt_span, uri, out);
273 }
274 ExprKind::Assign(a) => {
275 collect_anon_class_in_expr(a.value, word, fqn, sv, stmt_span, uri, out);
276 }
277 _ => {}
278 }
279}