php-lsp 0.25.3

A PHP Language Server Protocol implementation
Documentation
use crate::document::ast::format_type_hint;
use crate::types::resolve::Declaration;
use php_ast::{MethodDecl, Param, Visibility};

/// Format an expression literal value.
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,
    }
}

/// Format a class/interface/enum constant declaration for hover display.
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(", ")
}

/// Format a default parameter value for display in signatures.
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)
}

/// Format a method/function-style member signature, e.g.
/// `public static function foo(int $x): void`.
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)
}

/// Render the hover signature for a resolved declaration. Returns `None` for
/// kinds rendered elsewhere (properties via mir-primary path).
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(" ") + " "
    }
}