use php_ast::{MethodDecl, Param, Visibility};
use tower_lsp::lsp_types::{Hover, HoverContents, MarkupContent, MarkupKind};
use crate::ast::format_type_hint;
use crate::resolve::Declaration;
use crate::util::{is_php_builtin, php_doc_url};
pub(crate) fn format_expr_literal(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
use php_ast::ExprKind;
match &expr.kind {
ExprKind::Int(n) => Some(n.to_string()),
ExprKind::Float(f) => Some(f.to_string()),
ExprKind::Bool(b) => Some(if *b { "true" } else { "false" }.to_string()),
ExprKind::String(s) => Some(format!("'{}'", s)),
_ => None,
}
}
pub(crate) fn format_class_const(c: &php_ast::ClassConstDecl<'_, '_>) -> String {
use php_ast::ExprKind;
let type_str = c
.type_hint
.as_ref()
.map(|t| format!("{} ", format_type_hint(t)))
.or_else(|| match &c.value.kind {
ExprKind::Int(_) => Some("int ".to_string()),
ExprKind::String(_) => Some("string ".to_string()),
ExprKind::Float(_) => Some("float ".to_string()),
ExprKind::Bool(_) => Some("bool ".to_string()),
_ => None,
})
.unwrap_or_default();
let value_str = format_expr_literal(&c.value)
.map(|v| format!(" = {v}"))
.unwrap_or_default();
format!("const {}{}{}", type_str, c.name, value_str)
}
pub fn format_params_str(params: &[Param<'_, '_>]) -> String {
format_params(params)
}
pub(crate) fn format_params(params: &[Param<'_, '_>]) -> String {
params
.iter()
.map(|p| {
let mut s = String::new();
if p.by_ref {
s.push('&');
}
if let Some(t) = &p.type_hint {
s.push_str(&format!("{} ", format_type_hint(t)));
}
if p.variadic {
s.push_str("...");
}
s.push_str(&format!("${}", p.name));
if let Some(default) = &p.default {
s.push_str(&format!(" = {}", format_default_value(default)));
}
s
})
.collect::<Vec<_>>()
.join(", ")
}
pub(crate) fn format_default_value(expr: &php_ast::Expr<'_, '_>) -> String {
use php_ast::ExprKind;
match &expr.kind {
ExprKind::Int(n) => n.to_string(),
ExprKind::Float(f) => f.to_string(),
ExprKind::String(s) => format!("'{}'", s),
ExprKind::Bool(b) => {
if *b {
"true".to_string()
} else {
"false".to_string()
}
}
ExprKind::Null => "null".to_string(),
ExprKind::Array(items) => {
if items.is_empty() {
"[]".to_string()
} else {
"[...]".to_string()
}
}
_ => "...".to_string(),
}
}
pub(crate) fn wrap_php(sig: &str) -> String {
format!("```php\n{}\n```", sig)
}
pub(crate) fn method_signature(m: &MethodDecl<'_, '_>) -> String {
let prefix = format_method_prefix(
m.visibility.as_ref(),
m.is_static,
m.is_abstract,
m.is_final,
);
let params = format_params(&m.params);
let ret = m
.return_type
.as_ref()
.map(|r| format!(": {}", format_type_hint(r)))
.unwrap_or_default();
format!("{}function {}({}){}", prefix, m.name, params, ret)
}
pub(crate) fn declaration_signature(decl: &Declaration<'_>, word: &str) -> Option<String> {
let sig = match decl {
Declaration::Function { decl: f, .. } => {
let params = format_params(&f.params);
let ret = f
.return_type
.as_ref()
.map(|r| format!(": {}", format_type_hint(r)))
.unwrap_or_default();
format!("function {}({}){}", word, params, ret)
}
Declaration::Class { decl: c, .. } => {
let kw = if c.modifiers.is_abstract {
"abstract class"
} else if c.modifiers.is_final {
"final class"
} else if c.modifiers.is_readonly {
"readonly class"
} else {
"class"
};
let mut sig = format!("{} {}", kw, word);
if let Some(ext) = &c.extends {
sig.push_str(&format!(" extends {}", ext.to_string_repr()));
}
if !c.implements.is_empty() {
let ifaces: Vec<String> = c
.implements
.iter()
.map(|i| i.to_string_repr().into_owned())
.collect();
sig.push_str(&format!(" implements {}", ifaces.join(", ")));
}
sig
}
Declaration::Interface { .. } => format!("interface {}", word),
Declaration::Trait { .. } => format!("trait {}", word),
Declaration::Enum { decl: e, .. } => {
let mut sig = if let Some(scalar) = &e.scalar_type {
format!("enum {}: {}", word, scalar.to_string_repr())
} else {
format!("enum {}", word)
};
if !e.implements.is_empty() {
let ifaces: Vec<String> = e
.implements
.iter()
.map(|i| i.to_string_repr().into_owned())
.collect();
sig.push_str(&format!(" implements {}", ifaces.join(", ")));
}
sig
}
Declaration::Method { method, .. } => method_signature(method),
Declaration::ClassConst { konst, .. } => format_class_const(konst),
Declaration::EnumCase {
case, enum_name, ..
} => {
let value_str = case
.value
.as_ref()
.and_then(format_expr_literal)
.map(|v| format!(" = {v}"))
.unwrap_or_default();
format!("case {}::{}{}", enum_name, case.name, value_str)
}
Declaration::Property { .. } | Declaration::PromotedParam { .. } => return None,
};
Some(sig)
}
fn visibility_str(v: &Visibility) -> &'static str {
match v {
Visibility::Public => "public",
Visibility::Protected => "protected",
Visibility::Private => "private",
}
}
pub(crate) fn format_method_prefix(
visibility: Option<&Visibility>,
is_static: bool,
is_abstract: bool,
is_final: bool,
) -> String {
let mut parts: Vec<&str> = Vec::new();
if let Some(v) = visibility {
parts.push(visibility_str(v));
}
if is_abstract {
parts.push("abstract");
}
if is_final {
parts.push("final");
}
if is_static {
parts.push("static");
}
if parts.is_empty() {
String::new()
} else {
parts.join(" ") + " "
}
}
pub(crate) fn format_prop_prefix(
visibility: Option<&Visibility>,
is_static: bool,
is_readonly: bool,
) -> String {
let mut parts: Vec<&str> = Vec::new();
if let Some(v) = visibility {
parts.push(visibility_str(v));
}
if is_static {
parts.push("static");
}
if is_readonly {
parts.push("readonly");
}
if parts.is_empty() {
String::new()
} else {
parts.join(" ") + " "
}
}
pub fn signature_for_symbol_from_index(
name: &str,
indexes: &[(
tower_lsp::lsp_types::Url,
std::sync::Arc<crate::file_index::FileIndex>,
)],
) -> Option<String> {
for (_, idx) in indexes {
for f in &idx.functions {
if f.name.as_ref() == name {
let params_str = f
.params
.iter()
.map(|p| {
let mut s = String::new();
if let Some(t) = &p.type_hint {
s.push_str(&format!("{} ", t));
}
if p.variadic {
s.push_str("...");
}
s.push_str(&format!("${}", p.name));
s
})
.collect::<Vec<_>>()
.join(", ");
let ret = f
.return_type
.as_deref()
.map(|r| format!(": {}", r))
.unwrap_or_default();
return Some(format!("function {}({}){}", name, params_str, ret));
}
}
for cls in &idx.classes {
for m in &cls.methods {
if m.name.as_ref() == name {
let params_str = m
.params
.iter()
.map(|p| {
let mut s = String::new();
if let Some(t) = &p.type_hint {
s.push_str(&format!("{} ", t));
}
if p.variadic {
s.push_str("...");
}
s.push_str(&format!("${}", p.name));
s
})
.collect::<Vec<_>>()
.join(", ");
let ret = m
.return_type
.as_deref()
.map(|r| format!(": {}", r))
.unwrap_or_default();
return Some(format!("function {}({}){}", name, params_str, ret));
}
}
}
}
None
}
pub fn docs_for_symbol_from_index(
name: &str,
indexes: &[(
tower_lsp::lsp_types::Url,
std::sync::Arc<crate::file_index::FileIndex>,
)],
) -> Option<String> {
if let Some(sig) = signature_for_symbol_from_index(name, indexes) {
let mut value = wrap_php(&sig);
for (_, idx) in indexes {
for f in &idx.functions {
if f.name.as_ref() == name {
if let Some(raw) = &f.doc {
let db = crate::docblock::parse_docblock(raw);
let md = db.to_markdown();
if !md.is_empty() {
value.push_str("\n\n---\n\n");
value.push_str(&md);
}
}
break;
}
}
for cls in &idx.classes {
for m in &cls.methods {
if m.name.as_ref() == name {
if let Some(raw) = &m.doc {
let db = crate::docblock::parse_docblock(raw);
let md = db.to_markdown();
if !md.is_empty() {
value.push_str("\n\n---\n\n");
value.push_str(&md);
}
}
break;
}
}
}
}
if is_php_builtin(name) {
value.push_str(&format!(
"\n\n[php.net documentation]({})",
php_doc_url(name)
));
}
return Some(value);
}
if is_php_builtin(name) {
return Some(format!(
"```php\nfunction {}()\n```\n\n[php.net documentation]({})",
name,
php_doc_url(name)
));
}
None
}
pub fn class_hover_from_index(
word: &str,
indexes: &[(
tower_lsp::lsp_types::Url,
std::sync::Arc<crate::file_index::FileIndex>,
)],
) -> Option<Hover> {
use crate::file_index::ClassKind;
for (_, idx) in indexes {
for cls in &idx.classes {
if cls.name.as_ref() == word || cls.fqn.as_ref().trim_start_matches('\\') == word {
let kw = match cls.kind {
ClassKind::Interface => "interface",
ClassKind::Trait => "trait",
ClassKind::Enum => "enum",
ClassKind::Class => {
if cls.is_abstract {
"abstract class"
} else {
"class"
}
}
};
let mut sig = format!("{} {}", kw, &cls.name.to_string());
if let Some(parent) = &cls.parent {
sig.push_str(&format!(" extends {}", parent));
}
if !cls.implements.is_empty() {
let list: Vec<&str> = cls.implements.iter().map(|s| s.as_ref()).collect();
sig.push_str(&format!(" implements {}", list.join(", ")));
}
return Some(Hover {
contents: HoverContents::Markup(MarkupContent {
kind: MarkupKind::Markdown,
value: wrap_php(&sig),
}),
range: None,
});
}
}
}
None
}