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