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(
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(
221 range: tower_lsp::lsp_types::Range,
222 name: &str,
223 uri: &Url,
224 all_docs: &[(Url, Arc<ParsedDoc>)],
225 kind: Option<SymbolKind>,
226) -> CodeLens {
227 let locations = find_references(name, all_docs, false, kind);
228 let count = locations.len();
229 let label = match count {
230 0 => "0 references".to_string(),
231 1 => "1 reference".to_string(),
232 n => format!("{n} references"),
233 };
234 CodeLens {
235 range,
236 command: Some(Command {
237 title: label,
238 command: "editor.action.showReferences".to_string(),
239 arguments: Some(vec![json!(uri), json!(range.start), json!(locations)]),
240 }),
241 data: None,
242 }
243}
244
245fn impl_count_lens(
246 range: tower_lsp::lsp_types::Range,
247 uri: &Url,
248 locations: Vec<tower_lsp::lsp_types::Location>,
249) -> CodeLens {
250 let count = locations.len();
251 let label = match count {
252 0 => "0 implementations".to_string(),
253 1 => "1 implementation".to_string(),
254 n => format!("{n} implementations"),
255 };
256 CodeLens {
257 range,
258 command: Some(Command {
259 title: label,
260 command: "editor.action.showReferences".to_string(),
261 arguments: Some(vec![json!(uri), json!(range.start), json!(locations)]),
262 }),
263 data: None,
264 }
265}
266
267fn overrides_lens(
268 range: tower_lsp::lsp_types::Range,
269 uri: &Url,
270 parent_class: &str,
271 method_name: &str,
272 parent_location: tower_lsp::lsp_types::Location,
273) -> CodeLens {
274 CodeLens {
275 range,
276 command: Some(Command {
277 title: format!("overrides {}::{}", parent_class, method_name),
278 command: "editor.action.showReferences".to_string(),
279 arguments: Some(vec![
280 json!(uri),
281 json!(range.start),
282 json!(vec![parent_location]),
283 ]),
284 }),
285 data: None,
286 }
287}
288
289fn run_test_lens(
290 range: tower_lsp::lsp_types::Range,
291 uri: &Url,
292 class: &str,
293 method: &str,
294) -> CodeLens {
295 CodeLens {
296 range,
297 command: Some(Command {
298 title: "▶ Run test".to_string(),
299 command: "php-lsp.runTest".to_string(),
300 arguments: Some(vec![
301 serde_json::json!(uri.to_string()),
302 serde_json::json!(format!("{class}::{method}")),
303 ]),
304 }),
305 data: None,
306 }
307}
308
309fn trait_usage_locations(
311 trait_name: &str,
312 all_docs: &[(Url, Arc<ParsedDoc>)],
313) -> Vec<tower_lsp::lsp_types::Location> {
314 let mut out = Vec::new();
315 for (uri, doc) in all_docs {
316 let sv = doc.view();
317 collect_trait_usages_in_stmts(trait_name, &doc.program().stmts, sv, uri, &mut out);
318 }
319 out
320}
321
322fn collect_trait_usages_in_stmts(
323 trait_name: &str,
324 stmts: &[php_ast::Stmt<'_, '_>],
325 sv: SourceView<'_>,
326 uri: &Url,
327 out: &mut Vec<tower_lsp::lsp_types::Location>,
328) {
329 for stmt in stmts {
330 match &stmt.kind {
331 StmtKind::Class(c) => {
332 let uses_trait = c.body.members.iter().any(|m| {
333 if let ClassMemberKind::TraitUse(t) = &m.kind {
334 t.traits
335 .iter()
336 .any(|name| name.to_string_repr().as_ref() == trait_name)
337 } else {
338 false
339 }
340 });
341 if uses_trait && let Some(class_name) = c.name {
342 out.push(tower_lsp::lsp_types::Location {
343 uri: uri.clone(),
344 range: sv.name_range_in_span(class_name.or_error(), stmt.span),
345 });
346 }
347 }
348 StmtKind::Namespace(ns) => {
349 if let NamespaceBody::Braced(inner) = &ns.body {
350 collect_trait_usages_in_stmts(trait_name, &inner.stmts, sv, uri, out);
351 }
352 }
353 _ => {}
354 }
355 }
356}
357
358fn collect_direct_supertypes(
362 c: &php_ast::ClassDecl<'_, '_>,
363 all_docs: &[(Url, Arc<ParsedDoc>)],
364) -> Vec<String> {
365 let mut out: Vec<String> = Vec::new();
366 if let Some(extends) = &c.extends {
367 let parent_short = extends.to_string_repr().into_owned();
368 let resolved = all_docs
369 .iter()
370 .find_map(|(_, doc)| parent_class_name(doc, &parent_short))
371 .unwrap_or(parent_short);
372 out.push(resolved);
373 }
374 for member in c.body.members.iter() {
375 if let ClassMemberKind::TraitUse(t) = &member.kind {
376 for name in t.traits.iter() {
377 let s = name.to_string_repr().into_owned();
378 if !out.contains(&s) {
379 out.push(s);
380 }
381 }
382 }
383 }
384 out
385}
386
387fn parent_method_location(
390 parent_name: &str,
391 method_name: &str,
392 all_docs: &[(Url, Arc<ParsedDoc>)],
393) -> Option<tower_lsp::lsp_types::Location> {
394 for (uri, doc) in all_docs {
395 let sv = doc.view();
396 if let Some(range) =
397 find_method_name_range(&doc.program().stmts, parent_name, method_name, sv)
398 {
399 return Some(tower_lsp::lsp_types::Location {
400 uri: uri.clone(),
401 range,
402 });
403 }
404 }
405 None
406}
407
408fn find_method_name_range(
409 stmts: &[php_ast::Stmt<'_, '_>],
410 parent_name: &str,
411 method_name: &str,
412 sv: SourceView<'_>,
413) -> Option<tower_lsp::lsp_types::Range> {
414 for stmt in stmts {
415 match &stmt.kind {
416 StmtKind::Class(c) if c.name.as_ref().and_then(|n| n.as_str()) == Some(parent_name) => {
417 for member in c.body.members.iter() {
418 if let ClassMemberKind::Method(m) = &member.kind
419 && m.name == method_name
420 {
421 return Some(sv.name_range(m.name.as_str().unwrap_or_default()));
422 }
423 }
424 }
425 StmtKind::Trait(t) if t.name == parent_name => {
426 for member in t.body.members.iter() {
427 if let ClassMemberKind::Method(m) = &member.kind
428 && m.name == method_name
429 {
430 return Some(sv.name_range(m.name.as_str().unwrap_or_default()));
431 }
432 }
433 }
434 StmtKind::Namespace(ns) => {
435 if let NamespaceBody::Braced(inner) = &ns.body
436 && let Some(r) =
437 find_method_name_range(&inner.stmts, parent_name, method_name, sv)
438 {
439 return Some(r);
440 }
441 }
442 _ => {}
443 }
444 }
445 None
446}
447
448fn is_test_method(source: &str, m: &php_ast::MethodDecl<'_, '_>) -> bool {
452 if m.name
453 .as_str()
454 .map(|s| s.starts_with("test"))
455 .unwrap_or(false)
456 {
457 return true;
458 }
459 let has_test_attr = m.attributes.iter().any(|attr| {
460 let span = attr.name.span();
461 let attr_name = source
462 .get(span.start as usize..span.end as usize)
463 .unwrap_or("");
464 attr_name == "Test" || attr_name.ends_with("\\Test")
465 });
466 if has_test_attr {
467 return true;
468 }
469 m.doc_comment
470 .as_ref()
471 .is_some_and(|c| c.text.contains("@test"))
472}