1use std::sync::Arc;
12
13use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
14use serde_json::json;
15use tower_lsp::lsp_types::{CodeLens, Command, Url};
16
17use crate::ast::{ParsedDoc, SourceView};
18use crate::navigation::implementation::find_implementations;
19use crate::navigation::references::{SymbolKind, find_references};
20use crate::type_map::parent_class_name;
21
22pub fn code_lenses(
24 uri: &Url,
25 doc: &ParsedDoc,
26 all_docs: &[(Url, Arc<ParsedDoc>)],
27) -> Vec<CodeLens> {
28 let sv = doc.view();
29 let mut lenses = Vec::new();
30 collect_lenses(&doc.program().stmts, sv, uri, all_docs, &mut lenses);
31 lenses
32}
33
34fn collect_lenses(
35 stmts: &[Stmt<'_, '_>],
36 sv: SourceView<'_>,
37 uri: &Url,
38 all_docs: &[(Url, Arc<ParsedDoc>)],
39 out: &mut Vec<CodeLens>,
40) {
41 for stmt in stmts {
42 match &stmt.kind {
43 StmtKind::Function(f) => {
44 let name = f.name.as_str().unwrap_or_default();
45 let range = sv.name_range(name);
46 out.push(ref_count_lens(range, name, uri, all_docs, None));
47 }
48 StmtKind::Class(c) => {
49 if let Some(class_name) = c.name {
50 let class_name_str = class_name.as_str().unwrap_or_default();
51 let class_range = sv.name_range(class_name_str);
52 out.push(ref_count_lens(
53 class_range,
54 class_name_str,
55 uri,
56 all_docs,
57 None,
58 ));
59
60 if c.modifiers.is_abstract {
62 let impls = find_implementations(class_name_str, None, all_docs);
63 out.push(impl_count_lens(class_range, uri, impls));
64 }
65
66 let parents = collect_direct_supertypes(c, all_docs);
69
70 for member in c.body.members.iter() {
71 match &member.kind {
72 ClassMemberKind::Method(m) => {
73 let method_name = m.name.as_str().unwrap_or_default();
74 let method_range = sv.name_range(method_name);
75 out.push(ref_count_lens(
76 method_range,
77 method_name,
78 uri,
79 all_docs,
80 None,
81 ));
82
83 if is_test_method(sv.source(), m) {
84 out.push(run_test_lens(
85 method_range,
86 uri,
87 class_name_str,
88 method_name,
89 ));
90 }
91
92 for parent_name in &parents {
95 if let Some(parent_loc) =
96 parent_method_location(parent_name, method_name, all_docs)
97 {
98 out.push(overrides_lens(
99 method_range,
100 uri,
101 parent_name,
102 method_name,
103 parent_loc,
104 ));
105 }
106 }
107
108 if m.name == "__construct" {
110 for p in m.params.iter() {
111 if p.visibility.is_some() {
112 let param_name = p.name.as_str().unwrap_or_default();
113 let prop_range = sv.name_range(param_name);
114 out.push(ref_count_lens(
115 prop_range,
116 param_name,
117 uri,
118 all_docs,
119 Some(SymbolKind::Property),
120 ));
121 }
122 }
123 }
124 }
125 ClassMemberKind::Property(p) => {
126 let prop_name = p.name.as_str().unwrap_or_default();
127 let prop_range = sv.name_range(prop_name);
128 out.push(ref_count_lens(
129 prop_range,
130 prop_name,
131 uri,
132 all_docs,
133 Some(SymbolKind::Property),
134 ));
135 }
136 _ => {}
137 }
138 }
139 }
140 }
141 StmtKind::Interface(i) => {
142 let name = i.name.as_str().unwrap_or_default();
143 let range = sv.name_range(name);
144 out.push(ref_count_lens(range, name, uri, all_docs, None));
145 let impls = find_implementations(name, None, all_docs);
147 out.push(impl_count_lens(range, uri, impls));
148 }
149 StmtKind::Trait(t) => {
150 let trait_name = t.name.as_str().unwrap_or_default();
151 let range = sv.name_range(trait_name);
152 out.push(ref_count_lens(range, trait_name, uri, all_docs, None));
153 let usages = trait_usage_locations(trait_name, all_docs);
155 out.push(impl_count_lens(range, uri, usages));
156 for member in t.body.members.iter() {
157 match &member.kind {
158 ClassMemberKind::Method(m) => {
159 let method_name = m.name.as_str().unwrap_or_default();
160 let method_range = sv.name_range(method_name);
161 out.push(ref_count_lens(
162 method_range,
163 method_name,
164 uri,
165 all_docs,
166 None,
167 ));
168 }
169 ClassMemberKind::Property(p) => {
170 let prop_name = p.name.as_str().unwrap_or_default();
171 let prop_range = sv.name_range(prop_name);
172 out.push(ref_count_lens(
173 prop_range,
174 prop_name,
175 uri,
176 all_docs,
177 Some(SymbolKind::Property),
178 ));
179 }
180 _ => {}
181 }
182 }
183 }
184 StmtKind::Enum(e) => {
185 let enum_name = e.name.as_str().unwrap_or_default();
186 let range = sv.name_range(enum_name);
187 out.push(ref_count_lens(range, enum_name, uri, all_docs, None));
188 for member in e.body.members.iter() {
189 match &member.kind {
190 EnumMemberKind::Method(m) => {
191 let method_name = m.name.as_str().unwrap_or_default();
192 let method_range = sv.name_range(method_name);
193 out.push(ref_count_lens(
194 method_range,
195 method_name,
196 uri,
197 all_docs,
198 None,
199 ));
200 }
201 EnumMemberKind::Case(c) => {
202 let case_name = c.name.as_str().unwrap_or_default();
203 let case_range = sv.name_range(case_name);
204 out.push(ref_count_lens(case_range, case_name, uri, all_docs, None));
205 }
206 _ => {}
207 }
208 }
209 }
210 StmtKind::Namespace(ns) => {
211 if let NamespaceBody::Braced(inner) = &ns.body {
212 collect_lenses(&inner.stmts, sv, uri, all_docs, out);
213 }
214 }
215 _ => {}
216 }
217 }
218}
219
220fn ref_count_lens(
223 range: tower_lsp::lsp_types::Range,
224 name: &str,
225 uri: &Url,
226 all_docs: &[(Url, Arc<ParsedDoc>)],
227 kind: Option<SymbolKind>,
228) -> CodeLens {
229 let locations = find_references(name, all_docs, false, kind);
230 let count = locations.len();
231 let label = match count {
232 0 => "0 references".to_string(),
233 1 => "1 reference".to_string(),
234 n => format!("{n} references"),
235 };
236 CodeLens {
237 range,
238 command: Some(Command {
239 title: label,
240 command: "editor.action.showReferences".to_string(),
241 arguments: Some(vec![json!(uri), json!(range.start), json!(locations)]),
242 }),
243 data: None,
244 }
245}
246
247fn impl_count_lens(
248 range: tower_lsp::lsp_types::Range,
249 uri: &Url,
250 locations: Vec<tower_lsp::lsp_types::Location>,
251) -> CodeLens {
252 let count = locations.len();
253 let label = match count {
254 0 => "0 implementations".to_string(),
255 1 => "1 implementation".to_string(),
256 n => format!("{n} implementations"),
257 };
258 CodeLens {
259 range,
260 command: Some(Command {
261 title: label,
262 command: "editor.action.showReferences".to_string(),
263 arguments: Some(vec![json!(uri), json!(range.start), json!(locations)]),
264 }),
265 data: None,
266 }
267}
268
269fn overrides_lens(
270 range: tower_lsp::lsp_types::Range,
271 uri: &Url,
272 parent_class: &str,
273 method_name: &str,
274 parent_location: tower_lsp::lsp_types::Location,
275) -> CodeLens {
276 CodeLens {
277 range,
278 command: Some(Command {
279 title: format!("overrides {}::{}", parent_class, method_name),
280 command: "editor.action.showReferences".to_string(),
281 arguments: Some(vec![
282 json!(uri),
283 json!(range.start),
284 json!(vec![parent_location]),
285 ]),
286 }),
287 data: None,
288 }
289}
290
291fn run_test_lens(
292 range: tower_lsp::lsp_types::Range,
293 uri: &Url,
294 class: &str,
295 method: &str,
296) -> CodeLens {
297 CodeLens {
298 range,
299 command: Some(Command {
300 title: "▶ Run test".to_string(),
301 command: "php-lsp.runTest".to_string(),
302 arguments: Some(vec![
303 serde_json::json!(uri.to_string()),
304 serde_json::json!(format!("{class}::{method}")),
305 ]),
306 }),
307 data: None,
308 }
309}
310
311fn trait_usage_locations(
315 trait_name: &str,
316 all_docs: &[(Url, Arc<ParsedDoc>)],
317) -> Vec<tower_lsp::lsp_types::Location> {
318 let mut out = Vec::new();
319 for (uri, doc) in all_docs {
320 let sv = doc.view();
321 collect_trait_usages_in_stmts(trait_name, &doc.program().stmts, sv, uri, &mut out);
322 }
323 out
324}
325
326fn collect_trait_usages_in_stmts(
327 trait_name: &str,
328 stmts: &[php_ast::Stmt<'_, '_>],
329 sv: SourceView<'_>,
330 uri: &Url,
331 out: &mut Vec<tower_lsp::lsp_types::Location>,
332) {
333 for stmt in stmts {
334 match &stmt.kind {
335 StmtKind::Class(c) => {
336 let uses_trait = c.body.members.iter().any(|m| {
337 if let ClassMemberKind::TraitUse(t) = &m.kind {
338 t.traits
339 .iter()
340 .any(|name| name.to_string_repr().as_ref() == trait_name)
341 } else {
342 false
343 }
344 });
345 if uses_trait && let Some(class_name) = c.name {
346 out.push(tower_lsp::lsp_types::Location {
347 uri: uri.clone(),
348 range: sv.name_range_in_span(class_name.or_error(), stmt.span),
349 });
350 }
351 }
352 StmtKind::Namespace(ns) => {
353 if let NamespaceBody::Braced(inner) = &ns.body {
354 collect_trait_usages_in_stmts(trait_name, &inner.stmts, sv, uri, out);
355 }
356 }
357 _ => {}
358 }
359 }
360}
361
362fn collect_direct_supertypes(
366 c: &php_ast::ClassDecl<'_, '_>,
367 all_docs: &[(Url, Arc<ParsedDoc>)],
368) -> Vec<String> {
369 let mut out: Vec<String> = Vec::new();
370 if let Some(extends) = &c.extends {
371 let parent_short = extends.to_string_repr().into_owned();
372 let resolved = all_docs
373 .iter()
374 .find_map(|(_, doc)| parent_class_name(doc, &parent_short))
375 .unwrap_or(parent_short);
376 out.push(resolved);
377 }
378 for member in c.body.members.iter() {
379 if let ClassMemberKind::TraitUse(t) = &member.kind {
380 for name in t.traits.iter() {
381 let s = name.to_string_repr().into_owned();
382 if !out.contains(&s) {
383 out.push(s);
384 }
385 }
386 }
387 }
388 out
389}
390
391fn parent_method_location(
394 parent_name: &str,
395 method_name: &str,
396 all_docs: &[(Url, Arc<ParsedDoc>)],
397) -> Option<tower_lsp::lsp_types::Location> {
398 for (uri, doc) in all_docs {
399 let sv = doc.view();
400 if let Some(range) =
401 find_method_name_range(&doc.program().stmts, parent_name, method_name, sv)
402 {
403 return Some(tower_lsp::lsp_types::Location {
404 uri: uri.clone(),
405 range,
406 });
407 }
408 }
409 None
410}
411
412fn find_method_name_range(
413 stmts: &[php_ast::Stmt<'_, '_>],
414 parent_name: &str,
415 method_name: &str,
416 sv: SourceView<'_>,
417) -> Option<tower_lsp::lsp_types::Range> {
418 for stmt in stmts {
419 match &stmt.kind {
420 StmtKind::Class(c) if c.name.as_ref().and_then(|n| n.as_str()) == Some(parent_name) => {
421 for member in c.body.members.iter() {
422 if let ClassMemberKind::Method(m) = &member.kind
423 && m.name == method_name
424 {
425 return Some(sv.name_range(m.name.as_str().unwrap_or_default()));
426 }
427 }
428 }
429 StmtKind::Trait(t) if t.name == parent_name => {
430 for member in t.body.members.iter() {
431 if let ClassMemberKind::Method(m) = &member.kind
432 && m.name == method_name
433 {
434 return Some(sv.name_range(m.name.as_str().unwrap_or_default()));
435 }
436 }
437 }
438 StmtKind::Namespace(ns) => {
439 if let NamespaceBody::Braced(inner) = &ns.body
440 && let Some(r) =
441 find_method_name_range(&inner.stmts, parent_name, method_name, sv)
442 {
443 return Some(r);
444 }
445 }
446 _ => {}
447 }
448 }
449 None
450}
451
452fn is_test_method(source: &str, m: &php_ast::MethodDecl<'_, '_>) -> bool {
456 if m.name
457 .as_str()
458 .map(|s| s.starts_with("test"))
459 .unwrap_or(false)
460 {
461 return true;
462 }
463 let has_test_attr = m.attributes.iter().any(|attr| {
464 let span = attr.name.span();
465 let attr_name = source
466 .get(span.start as usize..span.end as usize)
467 .unwrap_or("");
468 attr_name == "Test" || attr_name.ends_with("\\Test")
469 });
470 if has_test_attr {
471 return true;
472 }
473 m.doc_comment
474 .as_ref()
475 .is_some_and(|c| c.text.contains("@test"))
476}