Skip to main content

php_lsp/hover/
formatting.rs

1use crate::document::ast::format_type_hint;
2use crate::types::resolve::Declaration;
3use php_ast::{MethodDecl, Param, Visibility};
4
5/// Format an expression literal value.
6pub(crate) fn format_expr_literal(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
7    use php_ast::ExprKind;
8    match &expr.kind {
9        ExprKind::Int(n) => Some(n.to_string()),
10        ExprKind::Float(f) => Some(f.to_string()),
11        ExprKind::Bool(b) => Some(if *b { "true" } else { "false" }.to_string()),
12        ExprKind::String(s) => Some(format!("'{}'", s)),
13        _ => None,
14    }
15}
16
17/// Format a class/interface/enum constant declaration for hover display.
18pub(crate) fn format_class_const(c: &php_ast::ClassConstDecl<'_, '_>) -> String {
19    use php_ast::ExprKind;
20    let type_str = c
21        .type_hint
22        .as_ref()
23        .map(|t| format!("{} ", format_type_hint(t)))
24        .or_else(|| match &c.value.kind {
25            ExprKind::Int(_) => Some("int ".to_string()),
26            ExprKind::String(_) => Some("string ".to_string()),
27            ExprKind::Float(_) => Some("float ".to_string()),
28            ExprKind::Bool(_) => Some("bool ".to_string()),
29            _ => None,
30        })
31        .unwrap_or_default();
32    let value_str = format_expr_literal(&c.value)
33        .map(|v| format!(" = {v}"))
34        .unwrap_or_default();
35    format!("const {}{}{}", type_str, c.name, value_str)
36}
37
38pub fn format_params_str(params: &[Param<'_, '_>]) -> String {
39    format_params(params)
40}
41
42pub(crate) fn format_params(params: &[Param<'_, '_>]) -> String {
43    params
44        .iter()
45        .map(|p| {
46            let mut s = String::new();
47            if p.by_ref {
48                s.push('&');
49            }
50            if let Some(t) = &p.type_hint {
51                s.push_str(&format!("{} ", format_type_hint(t)));
52            }
53            if p.variadic {
54                s.push_str("...");
55            }
56            s.push_str(&format!("${}", p.name));
57            if let Some(default) = &p.default {
58                s.push_str(&format!(" = {}", format_default_value(default)));
59            }
60            s
61        })
62        .collect::<Vec<_>>()
63        .join(", ")
64}
65
66/// Format a default parameter value for display in signatures.
67pub(crate) fn format_default_value(expr: &php_ast::Expr<'_, '_>) -> String {
68    use php_ast::ExprKind;
69    match &expr.kind {
70        ExprKind::Int(n) => n.to_string(),
71        ExprKind::Float(f) => f.to_string(),
72        ExprKind::String(s) => format!("'{}'", s),
73        ExprKind::Bool(b) => {
74            if *b {
75                "true".to_string()
76            } else {
77                "false".to_string()
78            }
79        }
80        ExprKind::Null => "null".to_string(),
81        ExprKind::Array(items) => {
82            if items.is_empty() {
83                "[]".to_string()
84            } else {
85                "[...]".to_string()
86            }
87        }
88        _ => "...".to_string(),
89    }
90}
91
92pub(crate) fn wrap_php(sig: &str) -> String {
93    format!("```php\n{}\n```", sig)
94}
95
96/// Format a method/function-style member signature, e.g.
97/// `public static function foo(int $x): void`.
98pub(crate) fn method_signature(m: &MethodDecl<'_, '_>) -> String {
99    let prefix = format_method_prefix(
100        m.visibility.as_ref(),
101        m.is_static,
102        m.is_abstract,
103        m.is_final,
104    );
105    let params = format_params(&m.params);
106    let ret = m
107        .return_type
108        .as_ref()
109        .map(|r| format!(": {}", format_type_hint(r)))
110        .unwrap_or_default();
111    format!("{}function {}({}){}", prefix, m.name, params, ret)
112}
113
114/// Render the hover signature for a resolved declaration. Returns `None` for
115/// kinds rendered elsewhere (properties via mir-primary path).
116pub(crate) fn declaration_signature(decl: &Declaration<'_>, word: &str) -> Option<String> {
117    let sig = match decl {
118        Declaration::Function { decl: f, .. } => {
119            let params = format_params(&f.params);
120            let ret = f
121                .return_type
122                .as_ref()
123                .map(|r| format!(": {}", format_type_hint(r)))
124                .unwrap_or_default();
125            format!("function {}({}){}", word, params, ret)
126        }
127        Declaration::Class { decl: c, .. } => {
128            let kw = if c.modifiers.is_abstract {
129                "abstract class"
130            } else if c.modifiers.is_final {
131                "final class"
132            } else if c.modifiers.is_readonly {
133                "readonly class"
134            } else {
135                "class"
136            };
137            let mut sig = format!("{} {}", kw, word);
138            if let Some(ext) = &c.extends {
139                sig.push_str(&format!(" extends {}", ext.to_string_repr()));
140            }
141            if !c.implements.is_empty() {
142                let ifaces: Vec<String> = c
143                    .implements
144                    .iter()
145                    .map(|i| i.to_string_repr().into_owned())
146                    .collect();
147                sig.push_str(&format!(" implements {}", ifaces.join(", ")));
148            }
149            sig
150        }
151        Declaration::Interface { .. } => format!("interface {}", word),
152        Declaration::Trait { .. } => format!("trait {}", word),
153        Declaration::Enum { decl: e, .. } => {
154            let mut sig = if let Some(scalar) = &e.scalar_type {
155                format!("enum {}: {}", word, scalar.to_string_repr())
156            } else {
157                format!("enum {}", word)
158            };
159            if !e.implements.is_empty() {
160                let ifaces: Vec<String> = e
161                    .implements
162                    .iter()
163                    .map(|i| i.to_string_repr().into_owned())
164                    .collect();
165                sig.push_str(&format!(" implements {}", ifaces.join(", ")));
166            }
167            sig
168        }
169        Declaration::Method { method, .. } => method_signature(method),
170        Declaration::ClassConst { konst, .. } => format_class_const(konst),
171        Declaration::EnumCase {
172            case, enum_name, ..
173        } => {
174            let value_str = case
175                .value
176                .as_ref()
177                .and_then(format_expr_literal)
178                .map(|v| format!(" = {v}"))
179                .unwrap_or_default();
180            format!("case {}::{}{}", enum_name, case.name, value_str)
181        }
182        Declaration::Property { .. } | Declaration::PromotedParam { .. } => return None,
183    };
184    Some(sig)
185}
186
187fn visibility_str(v: &Visibility) -> &'static str {
188    match v {
189        Visibility::Public => "public",
190        Visibility::Protected => "protected",
191        Visibility::Private => "private",
192    }
193}
194
195pub(crate) fn format_method_prefix(
196    visibility: Option<&Visibility>,
197    is_static: bool,
198    is_abstract: bool,
199    is_final: bool,
200) -> String {
201    let mut parts: Vec<&str> = Vec::new();
202    if let Some(v) = visibility {
203        parts.push(visibility_str(v));
204    }
205    if is_abstract {
206        parts.push("abstract");
207    }
208    if is_final {
209        parts.push("final");
210    }
211    if is_static {
212        parts.push("static");
213    }
214    if parts.is_empty() {
215        String::new()
216    } else {
217        parts.join(" ") + " "
218    }
219}
220
221pub(crate) fn format_prop_prefix(
222    visibility: Option<&Visibility>,
223    is_static: bool,
224    is_readonly: bool,
225) -> String {
226    let mut parts: Vec<&str> = Vec::new();
227    if let Some(v) = visibility {
228        parts.push(visibility_str(v));
229    }
230    if is_static {
231        parts.push("static");
232    }
233    if is_readonly {
234        parts.push("readonly");
235    }
236    if parts.is_empty() {
237        String::new()
238    } else {
239        parts.join(" ") + " "
240    }
241}