1use php_ast::{MethodDecl, Param, Visibility};
2use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind};
3
4use crate::ast::format_type_hint;
5use crate::resolve::Declaration;
6use crate::util::{is_php_builtin, php_doc_url};
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
246pub fn signature_for_symbol_from_index(
248 name: &str,
249 indexes: &[(
250 tower_lsp::lsp_types::Url,
251 std::sync::Arc<crate::file_index::FileIndex>,
252 )],
253) -> Option<String> {
254 for (_, idx) in indexes {
255 for f in &idx.functions {
256 if f.name.as_ref() == name {
257 let params_str = f
258 .params
259 .iter()
260 .map(|p| {
261 let mut s = String::new();
262 if let Some(t) = &p.type_hint {
263 s.push_str(&format!("{} ", t));
264 }
265 if p.variadic {
266 s.push_str("...");
267 }
268 s.push_str(&format!("${}", p.name));
269 s
270 })
271 .collect::<Vec<_>>()
272 .join(", ");
273 let ret = f
274 .return_type
275 .as_deref()
276 .map(|r| format!(": {}", r))
277 .unwrap_or_default();
278 return Some(format!("function {}({}){}", name, params_str, ret));
279 }
280 }
281 for cls in &idx.classes {
282 for m in &cls.methods {
283 if m.name.as_ref() == name {
284 let params_str = m
285 .params
286 .iter()
287 .map(|p| {
288 let mut s = String::new();
289 if let Some(t) = &p.type_hint {
290 s.push_str(&format!("{} ", t));
291 }
292 if p.variadic {
293 s.push_str("...");
294 }
295 s.push_str(&format!("${}", p.name));
296 s
297 })
298 .collect::<Vec<_>>()
299 .join(", ");
300 let ret = m
301 .return_type
302 .as_deref()
303 .map(|r| format!(": {}", r))
304 .unwrap_or_default();
305 return Some(format!("function {}({}){}", name, params_str, ret));
306 }
307 }
308 }
309 }
310 None
311}
312
313pub fn docs_for_symbol_from_index(
315 name: &str,
316 indexes: &[(
317 tower_lsp::lsp_types::Url,
318 std::sync::Arc<crate::file_index::FileIndex>,
319 )],
320) -> Option<String> {
321 if let Some(sig) = signature_for_symbol_from_index(name, indexes) {
322 let mut value = wrap_php(&sig);
323 for (_, idx) in indexes {
324 for f in &idx.functions {
325 if f.name.as_ref() == name {
326 if let Some(raw) = &f.doc {
327 let db = crate::docblock::parse_docblock(raw);
328 let md = db.to_markdown();
329 if !md.is_empty() {
330 value.push_str("\n\n---\n\n");
331 value.push_str(&md);
332 }
333 }
334 break;
335 }
336 }
337 for cls in &idx.classes {
338 for m in &cls.methods {
339 if m.name.as_ref() == name {
340 if let Some(raw) = &m.doc {
341 let db = crate::docblock::parse_docblock(raw);
342 let md = db.to_markdown();
343 if !md.is_empty() {
344 value.push_str("\n\n---\n\n");
345 value.push_str(&md);
346 }
347 }
348 break;
349 }
350 }
351 }
352 }
353 if is_php_builtin(name) {
354 value.push_str(&format!(
355 "\n\n[php.net documentation]({})",
356 php_doc_url(name)
357 ));
358 }
359 return Some(value);
360 }
361 if is_php_builtin(name) {
362 return Some(format!(
363 "```php\nfunction {}()\n```\n\n[php.net documentation]({})",
364 name,
365 php_doc_url(name)
366 ));
367 }
368 None
369}
370
371pub fn class_hover_from_index(
373 word: &str,
374 indexes: &[(
375 tower_lsp::lsp_types::Url,
376 std::sync::Arc<crate::file_index::FileIndex>,
377 )],
378) -> Option<Hover> {
379 use crate::file_index::ClassKind;
380
381 for (_, idx) in indexes {
382 for cls in &idx.classes {
383 if cls.name.as_ref() == word || cls.fqn.as_ref().trim_start_matches('\\') == word {
384 let kw = match cls.kind {
385 ClassKind::Interface => "interface",
386 ClassKind::Trait => "trait",
387 ClassKind::Enum => "enum",
388 ClassKind::Class => {
389 if cls.is_abstract {
390 "abstract class"
391 } else {
392 "class"
393 }
394 }
395 };
396 let mut sig = format!("{} {}", kw, &cls.name.to_string());
397 if let Some(parent) = &cls.parent {
398 sig.push_str(&format!(" extends {}", parent));
399 }
400 if !cls.implements.is_empty() {
401 let list: Vec<&str> = cls.implements.iter().map(|s| s.as_ref()).collect();
402 sig.push_str(&format!(" implements {}", list.join(", ")));
403 }
404 return Some(Hover {
405 contents: HoverContents::Markup(MarkupContent {
406 kind: MarkupKind::Markdown,
407 value: wrap_php(&sig),
408 }),
409 range: None,
410 });
411 }
412 }
413 }
414 None
415}