1use php_ast::{MethodDecl, Param, Visibility};
2use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind};
3
4use crate::document::ast::format_type_hint;
5use crate::lang::php_names::{is_php_builtin, php_doc_url};
6use crate::types::resolve::Declaration;
7
8pub(crate) fn format_expr_literal(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
10 use php_ast::ExprKind;
11 match &expr.kind {
12 ExprKind::Int(n) => Some(n.to_string()),
13 ExprKind::Float(f) => Some(f.to_string()),
14 ExprKind::Bool(b) => Some(if *b { "true" } else { "false" }.to_string()),
15 ExprKind::String(s) => Some(format!("'{}'", s)),
16 _ => None,
17 }
18}
19
20pub(crate) fn format_class_const(c: &php_ast::ClassConstDecl<'_, '_>) -> String {
22 use php_ast::ExprKind;
23 let type_str = c
24 .type_hint
25 .as_ref()
26 .map(|t| format!("{} ", format_type_hint(t)))
27 .or_else(|| match &c.value.kind {
28 ExprKind::Int(_) => Some("int ".to_string()),
29 ExprKind::String(_) => Some("string ".to_string()),
30 ExprKind::Float(_) => Some("float ".to_string()),
31 ExprKind::Bool(_) => Some("bool ".to_string()),
32 _ => None,
33 })
34 .unwrap_or_default();
35 let value_str = format_expr_literal(&c.value)
36 .map(|v| format!(" = {v}"))
37 .unwrap_or_default();
38 format!("const {}{}{}", type_str, c.name, value_str)
39}
40
41pub fn format_params_str(params: &[Param<'_, '_>]) -> String {
42 format_params(params)
43}
44
45pub(crate) fn format_params(params: &[Param<'_, '_>]) -> String {
46 params
47 .iter()
48 .map(|p| {
49 let mut s = String::new();
50 if p.by_ref {
51 s.push('&');
52 }
53 if let Some(t) = &p.type_hint {
54 s.push_str(&format!("{} ", format_type_hint(t)));
55 }
56 if p.variadic {
57 s.push_str("...");
58 }
59 s.push_str(&format!("${}", p.name));
60 if let Some(default) = &p.default {
61 s.push_str(&format!(" = {}", format_default_value(default)));
62 }
63 s
64 })
65 .collect::<Vec<_>>()
66 .join(", ")
67}
68
69pub(crate) fn format_default_value(expr: &php_ast::Expr<'_, '_>) -> String {
71 use php_ast::ExprKind;
72 match &expr.kind {
73 ExprKind::Int(n) => n.to_string(),
74 ExprKind::Float(f) => f.to_string(),
75 ExprKind::String(s) => format!("'{}'", s),
76 ExprKind::Bool(b) => {
77 if *b {
78 "true".to_string()
79 } else {
80 "false".to_string()
81 }
82 }
83 ExprKind::Null => "null".to_string(),
84 ExprKind::Array(items) => {
85 if items.is_empty() {
86 "[]".to_string()
87 } else {
88 "[...]".to_string()
89 }
90 }
91 _ => "...".to_string(),
92 }
93}
94
95pub(crate) fn wrap_php(sig: &str) -> String {
96 format!("```php\n{}\n```", sig)
97}
98
99pub(crate) fn method_signature(m: &MethodDecl<'_, '_>) -> String {
102 let prefix = format_method_prefix(
103 m.visibility.as_ref(),
104 m.is_static,
105 m.is_abstract,
106 m.is_final,
107 );
108 let params = format_params(&m.params);
109 let ret = m
110 .return_type
111 .as_ref()
112 .map(|r| format!(": {}", format_type_hint(r)))
113 .unwrap_or_default();
114 format!("{}function {}({}){}", prefix, m.name, params, ret)
115}
116
117pub(crate) fn declaration_signature(decl: &Declaration<'_>, word: &str) -> Option<String> {
120 let sig = match decl {
121 Declaration::Function { decl: f, .. } => {
122 let params = format_params(&f.params);
123 let ret = f
124 .return_type
125 .as_ref()
126 .map(|r| format!(": {}", format_type_hint(r)))
127 .unwrap_or_default();
128 format!("function {}({}){}", word, params, ret)
129 }
130 Declaration::Class { decl: c, .. } => {
131 let kw = if c.modifiers.is_abstract {
132 "abstract class"
133 } else if c.modifiers.is_final {
134 "final class"
135 } else if c.modifiers.is_readonly {
136 "readonly class"
137 } else {
138 "class"
139 };
140 let mut sig = format!("{} {}", kw, word);
141 if let Some(ext) = &c.extends {
142 sig.push_str(&format!(" extends {}", ext.to_string_repr()));
143 }
144 if !c.implements.is_empty() {
145 let ifaces: Vec<String> = c
146 .implements
147 .iter()
148 .map(|i| i.to_string_repr().into_owned())
149 .collect();
150 sig.push_str(&format!(" implements {}", ifaces.join(", ")));
151 }
152 sig
153 }
154 Declaration::Interface { .. } => format!("interface {}", word),
155 Declaration::Trait { .. } => format!("trait {}", word),
156 Declaration::Enum { decl: e, .. } => {
157 let mut sig = if let Some(scalar) = &e.scalar_type {
158 format!("enum {}: {}", word, scalar.to_string_repr())
159 } else {
160 format!("enum {}", word)
161 };
162 if !e.implements.is_empty() {
163 let ifaces: Vec<String> = e
164 .implements
165 .iter()
166 .map(|i| i.to_string_repr().into_owned())
167 .collect();
168 sig.push_str(&format!(" implements {}", ifaces.join(", ")));
169 }
170 sig
171 }
172 Declaration::Method { method, .. } => method_signature(method),
173 Declaration::ClassConst { konst, .. } => format_class_const(konst),
174 Declaration::EnumCase {
175 case, enum_name, ..
176 } => {
177 let value_str = case
178 .value
179 .as_ref()
180 .and_then(format_expr_literal)
181 .map(|v| format!(" = {v}"))
182 .unwrap_or_default();
183 format!("case {}::{}{}", enum_name, case.name, value_str)
184 }
185 Declaration::Property { .. } | Declaration::PromotedParam { .. } => return None,
186 };
187 Some(sig)
188}
189
190fn visibility_str(v: &Visibility) -> &'static str {
191 match v {
192 Visibility::Public => "public",
193 Visibility::Protected => "protected",
194 Visibility::Private => "private",
195 }
196}
197
198pub(crate) fn format_method_prefix(
199 visibility: Option<&Visibility>,
200 is_static: bool,
201 is_abstract: bool,
202 is_final: bool,
203) -> String {
204 let mut parts: Vec<&str> = Vec::new();
205 if let Some(v) = visibility {
206 parts.push(visibility_str(v));
207 }
208 if is_abstract {
209 parts.push("abstract");
210 }
211 if is_final {
212 parts.push("final");
213 }
214 if is_static {
215 parts.push("static");
216 }
217 if parts.is_empty() {
218 String::new()
219 } else {
220 parts.join(" ") + " "
221 }
222}
223
224pub(crate) fn format_prop_prefix(
225 visibility: Option<&Visibility>,
226 is_static: bool,
227 is_readonly: bool,
228) -> String {
229 let mut parts: Vec<&str> = Vec::new();
230 if let Some(v) = visibility {
231 parts.push(visibility_str(v));
232 }
233 if is_static {
234 parts.push("static");
235 }
236 if is_readonly {
237 parts.push("readonly");
238 }
239 if parts.is_empty() {
240 String::new()
241 } else {
242 parts.join(" ") + " "
243 }
244}
245
246fn class_matches_hint(cls: &crate::index::file_index::ClassDef, class_hint: Option<&str>) -> bool {
251 let Some(hint) = class_hint else { return true };
252 let hint = hint.trim_start_matches('\\');
253 cls.fqn.trim_start_matches('\\').eq_ignore_ascii_case(hint)
254 || cls.name.eq_ignore_ascii_case(hint)
255}
256
257pub fn signature_for_symbol_from_index(
258 name: &str,
259 indexes: &[(
260 tower_lsp::lsp_types::Url,
261 std::sync::Arc<crate::index::file_index::FileIndex>,
262 )],
263) -> Option<String> {
264 signature_for_symbol_from_index_scoped(name, indexes, None)
265}
266
267pub fn signature_for_symbol_from_index_scoped(
274 name: &str,
275 indexes: &[(
276 tower_lsp::lsp_types::Url,
277 std::sync::Arc<crate::index::file_index::FileIndex>,
278 )],
279 class_hint: Option<&str>,
280) -> Option<String> {
281 for (_, idx) in indexes {
282 if class_hint.is_none() {
283 for f in &idx.functions {
284 if f.name.as_ref() == name {
285 let params_str = f
286 .params
287 .iter()
288 .map(|p| {
289 let mut s = String::new();
290 if let Some(t) = &p.type_hint {
291 s.push_str(&format!("{} ", t));
292 }
293 if p.variadic {
294 s.push_str("...");
295 }
296 s.push_str(&format!("${}", p.name));
297 s
298 })
299 .collect::<Vec<_>>()
300 .join(", ");
301 let ret = f
302 .return_type
303 .as_deref()
304 .map(|r| format!(": {}", r))
305 .unwrap_or_default();
306 return Some(format!("function {}({}){}", name, params_str, ret));
307 }
308 }
309 }
310 for cls in &idx.classes {
311 if !class_matches_hint(cls, class_hint) {
312 continue;
313 }
314 for m in &cls.methods {
315 if m.name.as_ref() == name {
316 let params_str = m
317 .params
318 .iter()
319 .map(|p| {
320 let mut s = String::new();
321 if let Some(t) = &p.type_hint {
322 s.push_str(&format!("{} ", t));
323 }
324 if p.variadic {
325 s.push_str("...");
326 }
327 s.push_str(&format!("${}", p.name));
328 s
329 })
330 .collect::<Vec<_>>()
331 .join(", ");
332 let ret = m
333 .return_type
334 .as_deref()
335 .map(|r| format!(": {}", r))
336 .unwrap_or_default();
337 return Some(format!("function {}({}){}", name, params_str, ret));
338 }
339 }
340 }
341 }
342 None
343}
344
345pub fn docs_for_symbol_from_index(
347 name: &str,
348 indexes: &[(
349 tower_lsp::lsp_types::Url,
350 std::sync::Arc<crate::index::file_index::FileIndex>,
351 )],
352) -> Option<String> {
353 docs_for_symbol_from_index_scoped(name, indexes, None)
354}
355
356pub fn docs_for_symbol_from_index_scoped(
361 name: &str,
362 indexes: &[(
363 tower_lsp::lsp_types::Url,
364 std::sync::Arc<crate::index::file_index::FileIndex>,
365 )],
366 class_hint: Option<&str>,
367) -> Option<String> {
368 if let Some(sig) = signature_for_symbol_from_index_scoped(name, indexes, class_hint) {
369 let mut value = wrap_php(&sig);
370 for (_, idx) in indexes {
371 if class_hint.is_none() {
372 for f in &idx.functions {
373 if f.name.as_ref() == name {
374 if let Some(raw) = &f.docblock {
375 let db = crate::lang::docblock::parse_docblock(raw);
376 let md = db.to_markdown();
377 if !md.is_empty() {
378 value.push_str("\n\n---\n\n");
379 value.push_str(&md);
380 }
381 }
382 break;
383 }
384 }
385 }
386 for cls in &idx.classes {
387 if !class_matches_hint(cls, class_hint) {
388 continue;
389 }
390 for m in &cls.methods {
391 if m.name.as_ref() == name {
392 if let Some(raw) = &m.docblock {
393 let db = crate::lang::docblock::parse_docblock(raw);
394 let md = db.to_markdown();
395 if !md.is_empty() {
396 value.push_str("\n\n---\n\n");
397 value.push_str(&md);
398 }
399 }
400 break;
401 }
402 }
403 }
404 }
405 if class_hint.is_none() && is_php_builtin(name) {
406 value.push_str(&format!(
407 "\n\n[php.net documentation]({})",
408 php_doc_url(name)
409 ));
410 }
411 return Some(value);
412 }
413 if class_hint.is_none() && is_php_builtin(name) {
414 return Some(format!(
415 "```php\nfunction {}()\n```\n\n[php.net documentation]({})",
416 name,
417 php_doc_url(name)
418 ));
419 }
420 None
421}
422
423pub fn method_hover_from_index(
427 class_name: &str,
428 method_name: &str,
429 indexes: &[(
430 tower_lsp::lsp_types::Url,
431 std::sync::Arc<crate::index::file_index::FileIndex>,
432 )],
433) -> Option<Hover> {
434 for (_, idx) in indexes {
435 for cls in &idx.classes {
436 if cls.name.as_ref() != class_name
437 && crate::text::fqn_short_name(cls.fqn.as_ref()) != class_name
438 {
439 continue;
440 }
441 for m in &cls.methods {
442 if m.name.as_ref() != method_name {
443 continue;
444 }
445 let params_str = m
446 .params
447 .iter()
448 .map(|p| {
449 let mut s = String::new();
450 if let Some(t) = &p.type_hint {
451 s.push_str(&format!("{} ", t));
452 }
453 if p.variadic {
454 s.push_str("...");
455 }
456 s.push_str(&format!("${}", p.name));
457 s
458 })
459 .collect::<Vec<_>>()
460 .join(", ");
461 let ret = m
462 .return_type
463 .as_deref()
464 .map(|r| format!(": {}", r))
465 .unwrap_or_default();
466 let sig = format!("{}::{}({}){}", class_name, method_name, params_str, ret);
467 let mut value = wrap_php(&sig);
468 if let Some(raw) = &m.docblock {
469 let db = crate::lang::docblock::parse_docblock(raw);
470 let md = db.to_markdown();
471 if !md.is_empty() {
472 value.push_str("\n\n---\n\n");
473 value.push_str(&md);
474 }
475 }
476 return Some(Hover {
477 contents: HoverContents::Markup(MarkupContent {
478 kind: MarkupKind::Markdown,
479 value,
480 }),
481 range: None,
482 });
483 }
484 }
485 }
486 None
487}
488
489pub fn class_hover_from_index(
499 word: &str,
500 fqn: Option<&str>,
501 indexes: &[(
502 tower_lsp::lsp_types::Url,
503 std::sync::Arc<crate::index::file_index::FileIndex>,
504 )],
505) -> Option<Hover> {
506 let mut first_match: Option<&crate::index::file_index::ClassDef> = None;
507 for (_, idx) in indexes {
508 for cls in &idx.classes {
509 if cls.name.as_ref() == word || cls.fqn.as_ref().trim_start_matches('\\') == word {
510 if let Some(target) = fqn {
511 if cls.fqn.as_ref().trim_start_matches('\\') == target {
512 return Some(class_hover_for(cls));
513 }
514 if first_match.is_none() {
515 first_match = Some(cls);
516 }
517 continue;
518 }
519 return Some(class_hover_for(cls));
520 }
521 }
522 }
523 first_match.map(class_hover_for)
524}
525
526fn class_hover_for(cls: &crate::index::file_index::ClassDef) -> Hover {
527 use crate::index::file_index::ClassKind;
528
529 let kw = match cls.kind {
530 ClassKind::Interface => "interface",
531 ClassKind::Trait => "trait",
532 ClassKind::Enum => "enum",
533 ClassKind::Class => {
534 if cls.is_abstract {
535 "abstract class"
536 } else if cls.is_readonly {
537 "readonly class"
538 } else {
539 "class"
540 }
541 }
542 };
543 let mut sig = format!("{} {}", kw, &cls.name.to_string());
544 if let Some(parent) = &cls.parent {
545 sig.push_str(&format!(" extends {}", parent));
546 }
547 if !cls.implements.is_empty() {
548 let list: Vec<&str> = cls.implements.iter().map(|s| s.as_ref()).collect();
549 sig.push_str(&format!(" implements {}", list.join(", ")));
550 }
551 Hover {
552 contents: HoverContents::Markup(MarkupContent {
553 kind: MarkupKind::Markdown,
554 value: wrap_php(&sig),
555 }),
556 range: None,
557 }
558}