Skip to main content

php_lsp/analysis/
inlay_hints.rs

1use std::collections::HashMap;
2use std::sync::Arc;
3
4use php_ast::{
5    ClassMemberKind, EnumMemberKind, Expr, ExprKind, NamespaceBody, Param, Stmt, StmtKind,
6};
7use serde_json::json;
8use tower_lsp::lsp_types::{InlayHint, InlayHintKind, InlayHintLabel, Position, Range, Url};
9
10use crate::ast::{ParsedDoc, SourceView, format_type_hint};
11use crate::file_index::FileIndex;
12use crate::type_map::TypeMap;
13use crate::util::fqn_short_name;
14
15/// Resolve a foreach value/key variable's class (short name) for its type hint.
16/// mir-primary at the variable's byte offset; TypeMap fallback for binding
17/// sites mir resolves to `mixed` (e.g. element type it can't infer).
18fn foreach_var_class(
19    type_map: &TypeMap,
20    analysis: Option<&mir_analyzer::FileAnalysis>,
21    var_key: &str,
22    var_offset: u32,
23) -> Option<String> {
24    analysis
25        .and_then(|a| crate::type_query::type_at_offset(a, var_offset))
26        .and_then(crate::type_query::primary_class_name)
27        .map(|fqcn| fqn_short_name(&fqcn).to_string())
28        .or_else(|| type_map.get(var_key).map(str::to_owned))
29}
30
31#[derive(Clone)]
32struct FuncDef {
33    params: Vec<String>,
34    /// Whether the last parameter is variadic (`...$name`).
35    variadic_last: bool,
36    return_type: Option<String>,
37}
38
39/// Returns parameter-name inlay hints AND return-type hints for all
40/// function/method declarations and calls in `doc`.
41///
42/// `workspace_files` is the list of all indexed files; definitions not found
43/// in the current document fall back to this workspace index so that calls to
44/// cross-file functions/methods still get parameter-name hints.
45pub fn inlay_hints(
46    _source: &str,
47    doc: &ParsedDoc,
48    analysis: Option<&mir_analyzer::FileAnalysis>,
49    range: Range,
50    workspace_files: &[(Url, Arc<FileIndex>)],
51) -> Vec<InlayHint> {
52    let sv = doc.view();
53    let mut defs = collect_defs(&doc.program().stmts);
54    collect_defs_from_workspace(workspace_files, &mut defs);
55    let type_map = TypeMap::from_doc_with_meta(doc, None);
56    let mut hints = Vec::new();
57    hints_in_stmts(
58        sv,
59        &doc.program().stmts,
60        &defs,
61        &type_map,
62        analysis,
63        range,
64        &mut hints,
65    );
66    hints
67}
68
69// === Definition collection ===
70
71fn collect_defs(stmts: &[Stmt<'_, '_>]) -> HashMap<String, FuncDef> {
72    let mut map = HashMap::new();
73    collect_defs_stmts(stmts, &mut map);
74    map
75}
76
77/// Populate `map` with function and method definitions from the workspace index.
78/// Entries already present (from the current file's AST) are not overwritten so
79/// that the in-file definition always wins over a potentially stale index entry.
80fn collect_defs_from_workspace(
81    workspace_files: &[(Url, Arc<FileIndex>)],
82    map: &mut HashMap<String, FuncDef>,
83) {
84    for (_, idx) in workspace_files {
85        for func in &idx.functions {
86            let func_name = func.name.to_string();
87            if map.contains_key(&func_name) {
88                continue;
89            }
90            let params: Vec<String> = func.params.iter().map(|p| p.name.to_string()).collect();
91            let variadic_last = func.params.last().map(|p| p.variadic).unwrap_or(false);
92            map.insert(
93                func_name,
94                FuncDef {
95                    params,
96                    variadic_last,
97                    return_type: func.return_type.as_ref().map(|r| r.to_string()),
98                },
99            );
100        }
101        for class in &idx.classes {
102            for method in &class.methods {
103                let method_name = method.name.to_string();
104                let params: Vec<String> =
105                    method.params.iter().map(|p| p.name.to_string()).collect();
106                let variadic_last = method.params.last().map(|p| p.variadic).unwrap_or(false);
107                let func_def = FuncDef {
108                    params: params.clone(),
109                    variadic_last,
110                    return_type: method.return_type.as_ref().map(|r| r.to_string()),
111                };
112                // Register with qualified key "ClassName::methodName" for unambiguous lookup
113                let cn = class.name.as_ref();
114                let qualified = format!("{}::{}", cn, method_name);
115                map.insert(qualified, func_def.clone());
116                // Also register __construct under the class name so `new ClassName(...)` gets hints.
117                if method_name == "__construct" {
118                    map.entry(cn.to_string()).or_insert_with(|| FuncDef {
119                        params: params.clone(),
120                        variadic_last,
121                        return_type: None,
122                    });
123                }
124                // Register with short name as fallback for backwards compatibility
125                map.entry(method_name).or_insert(func_def);
126            }
127        }
128    }
129}
130
131/// Extract param names and whether the last param is variadic from a param list.
132fn params_from_list(params: &[Param<'_, '_>]) -> (Vec<String>, bool) {
133    let names = params.iter().map(|p| p.name.to_string()).collect();
134    let variadic_last = params.last().map(|p| p.variadic).unwrap_or(false);
135    (names, variadic_last)
136}
137
138fn collect_defs_stmts(stmts: &[Stmt<'_, '_>], map: &mut HashMap<String, FuncDef>) {
139    for stmt in stmts {
140        match &stmt.kind {
141            StmtKind::Function(f) => {
142                let (params, variadic_last) = params_from_list(&f.params);
143                let return_type = f.return_type.as_ref().map(|t| format_type_hint(t));
144                map.insert(
145                    f.name.to_string(),
146                    FuncDef {
147                        params,
148                        variadic_last,
149                        return_type,
150                    },
151                );
152            }
153            StmtKind::Class(c) => {
154                for member in c.body.members.iter() {
155                    if let ClassMemberKind::Method(m) = &member.kind {
156                        let (params, variadic_last) = params_from_list(&m.params);
157                        let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
158                        let func_def = FuncDef {
159                            params: params.clone(),
160                            variadic_last,
161                            return_type: return_type.clone(),
162                        };
163                        // Register with qualified key "ClassName::methodName" for unambiguous lookup
164                        if let Some(cn) = c.name {
165                            let qualified = format!("{}::{}", cn, m.name);
166                            map.insert(qualified, func_def.clone());
167                        }
168                        // Register __construct under the class name so `new ClassName(...)` gets hints.
169                        if m.name == "__construct"
170                            && let Some(class_name) = c.name
171                        {
172                            map.insert(
173                                class_name.to_string(),
174                                FuncDef {
175                                    params: params.clone(),
176                                    variadic_last,
177                                    return_type: None,
178                                },
179                            );
180                        }
181                        map.insert(m.name.to_string(), func_def);
182                    }
183                }
184            }
185            StmtKind::Trait(t) => {
186                for member in t.body.members.iter() {
187                    if let ClassMemberKind::Method(m) = &member.kind {
188                        let (params, variadic_last) = params_from_list(&m.params);
189                        let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
190                        let func_def = FuncDef {
191                            params,
192                            variadic_last,
193                            return_type,
194                        };
195                        // Register with qualified key for unambiguous lookup
196                        let qualified = format!("{}::{}", t.name, m.name);
197                        map.insert(qualified, func_def.clone());
198                        map.insert(m.name.to_string(), func_def);
199                    }
200                }
201            }
202            StmtKind::Enum(e) => {
203                for member in e.body.members.iter() {
204                    if let EnumMemberKind::Method(m) = &member.kind {
205                        let (params, variadic_last) = params_from_list(&m.params);
206                        let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
207                        let func_def = FuncDef {
208                            params,
209                            variadic_last,
210                            return_type,
211                        };
212                        // Register with qualified key for unambiguous lookup
213                        let qualified = format!("{}::{}", e.name, m.name);
214                        map.insert(qualified, func_def.clone());
215                        map.insert(m.name.to_string(), func_def);
216                    }
217                }
218            }
219            StmtKind::Namespace(ns) => {
220                if let NamespaceBody::Braced(inner) = &ns.body {
221                    collect_defs_stmts(&inner.stmts, map);
222                }
223            }
224            // Register closure/arrow-function variables so `$fn(...)` call sites get hints.
225            StmtKind::Expression(e) => {
226                if let ExprKind::Assign(assign) = &e.kind
227                    && let ExprKind::Variable(var_name) = &assign.target.kind
228                {
229                    let key = format!("${}", var_name.as_str());
230                    match &assign.value.kind {
231                        ExprKind::Closure(c) => {
232                            let (params, variadic_last) = params_from_list(&c.params);
233                            let return_type = c.return_type.as_ref().map(|t| format_type_hint(t));
234                            map.insert(
235                                key,
236                                FuncDef {
237                                    params,
238                                    variadic_last,
239                                    return_type,
240                                },
241                            );
242                        }
243                        ExprKind::ArrowFunction(a) => {
244                            let (params, variadic_last) = params_from_list(&a.params);
245                            let return_type = a.return_type.as_ref().map(|t| format_type_hint(t));
246                            map.insert(
247                                key,
248                                FuncDef {
249                                    params,
250                                    variadic_last,
251                                    return_type,
252                                },
253                            );
254                        }
255                        _ => {}
256                    }
257                }
258            }
259            _ => {}
260        }
261    }
262}
263
264// === AST walking ===
265
266fn hints_in_stmts(
267    sv: SourceView<'_>,
268    stmts: &[Stmt<'_, '_>],
269    defs: &HashMap<String, FuncDef>,
270    type_map: &TypeMap,
271    analysis: Option<&mir_analyzer::FileAnalysis>,
272    range: Range,
273    out: &mut Vec<InlayHint>,
274) {
275    for stmt in stmts {
276        hints_in_stmt(sv, stmt, defs, type_map, analysis, range, out);
277    }
278}
279
280fn hints_in_stmt(
281    sv: SourceView<'_>,
282    stmt: &Stmt<'_, '_>,
283    defs: &HashMap<String, FuncDef>,
284    type_map: &TypeMap,
285    analysis: Option<&mir_analyzer::FileAnalysis>,
286    range: Range,
287    out: &mut Vec<InlayHint>,
288) {
289    match &stmt.kind {
290        StmtKind::Expression(e) => hints_in_expr(sv, e, defs, type_map, analysis, range, out),
291        StmtKind::Return(Some(v)) => hints_in_expr(sv, v, defs, type_map, analysis, range, out),
292        StmtKind::Echo(exprs) => {
293            for expr in exprs.iter() {
294                hints_in_expr(sv, expr, defs, type_map, analysis, range, out);
295            }
296        }
297        StmtKind::Function(f) => {
298            hints_in_stmts(sv, &f.body.stmts, defs, type_map, analysis, range, out);
299        }
300        StmtKind::Class(c) => {
301            for member in c.body.members.iter() {
302                if let ClassMemberKind::Method(m) = &member.kind
303                    && let Some(body) = &m.body
304                {
305                    hints_in_stmts(sv, &body.stmts, defs, type_map, analysis, range, out);
306                }
307            }
308        }
309        StmtKind::Trait(t) => {
310            for member in t.body.members.iter() {
311                if let ClassMemberKind::Method(m) = &member.kind
312                    && let Some(body) = &m.body
313                {
314                    hints_in_stmts(sv, &body.stmts, defs, type_map, analysis, range, out);
315                }
316            }
317        }
318        StmtKind::Enum(e) => {
319            for member in e.body.members.iter() {
320                if let EnumMemberKind::Method(m) = &member.kind
321                    && let Some(body) = &m.body
322                {
323                    hints_in_stmts(sv, &body.stmts, defs, type_map, analysis, range, out);
324                }
325            }
326        }
327        StmtKind::Namespace(ns) => {
328            if let NamespaceBody::Braced(inner) = &ns.body {
329                hints_in_stmts(sv, &inner.stmts, defs, type_map, analysis, range, out);
330            }
331        }
332        StmtKind::If(i) => {
333            hints_in_expr(sv, &i.condition, defs, type_map, analysis, range, out);
334            hints_in_stmt(sv, i.then_branch, defs, type_map, analysis, range, out);
335            for ei in i.elseif_branches.iter() {
336                hints_in_expr(sv, &ei.condition, defs, type_map, analysis, range, out);
337                hints_in_stmt(sv, &ei.body, defs, type_map, analysis, range, out);
338            }
339            if let Some(e) = &i.else_branch {
340                hints_in_stmt(sv, e, defs, type_map, analysis, range, out);
341            }
342        }
343        StmtKind::While(w) => {
344            hints_in_expr(sv, &w.condition, defs, type_map, analysis, range, out);
345            hints_in_stmt(sv, w.body, defs, type_map, analysis, range, out);
346        }
347        StmtKind::For(f) => {
348            for e in f.init.iter() {
349                hints_in_expr(sv, e, defs, type_map, analysis, range, out);
350            }
351            for cond in f.condition.iter() {
352                hints_in_expr(sv, cond, defs, type_map, analysis, range, out);
353            }
354            for e in f.update.iter() {
355                hints_in_expr(sv, e, defs, type_map, analysis, range, out);
356            }
357            hints_in_stmt(sv, f.body, defs, type_map, analysis, range, out);
358        }
359        StmtKind::Foreach(f) => {
360            hints_in_expr(sv, &f.expr, defs, type_map, analysis, range, out);
361            // Emit type hint after the value variable, e.g. `foreach ($arr as $item /* : Foo */)`.
362            if let ExprKind::Variable(val_name) = &f.value.kind {
363                let key = format!("${}", val_name.as_str());
364                if let Some(ty) = foreach_var_class(type_map, analysis, &key, f.value.span.start) {
365                    let pos = sv.position_of(f.value.span.end);
366                    if pos_in_range(pos, range) {
367                        out.push(make_foreach_type_hint(pos, &ty));
368                    }
369                }
370            }
371            // Emit type hint after the key variable if present, e.g. `foreach ($map as $key => $value)`.
372            if let Some(key_expr) = &f.key
373                && let ExprKind::Variable(key_name) = &key_expr.kind
374            {
375                let key = format!("${}", key_name.as_str());
376                if let Some(ty) = foreach_var_class(type_map, analysis, &key, key_expr.span.start) {
377                    let pos = sv.position_of(key_expr.span.end);
378                    if pos_in_range(pos, range) {
379                        out.push(make_foreach_type_hint(pos, &ty));
380                    }
381                }
382            }
383            hints_in_stmt(sv, f.body, defs, type_map, analysis, range, out);
384        }
385        StmtKind::TryCatch(t) => {
386            hints_in_stmts(sv, &t.body.stmts, defs, type_map, analysis, range, out);
387            for catch in t.catches.iter() {
388                hints_in_stmts(sv, &catch.body.stmts, defs, type_map, analysis, range, out);
389            }
390            if let Some(finally) = &t.finally {
391                hints_in_stmts(sv, &finally.stmts, defs, type_map, analysis, range, out);
392            }
393        }
394        StmtKind::Block(stmts) => {
395            hints_in_stmts(sv, &stmts.stmts, defs, type_map, analysis, range, out)
396        }
397        _ => {}
398    }
399}
400
401fn hints_in_expr(
402    sv: SourceView<'_>,
403    expr: &Expr<'_, '_>,
404    defs: &HashMap<String, FuncDef>,
405    type_map: &TypeMap,
406    analysis: Option<&mir_analyzer::FileAnalysis>,
407    range: Range,
408    out: &mut Vec<InlayHint>,
409) {
410    match &expr.kind {
411        ExprKind::FunctionCall(f) => {
412            // Look up by identifier name or by variable name (for closure vars like `$fn(...)`).
413            let key: Option<String> = ident_name(f.name).map(|n| n.to_string()).or_else(|| {
414                if let ExprKind::Variable(n) = &f.name.kind {
415                    Some(format!("${}", n.as_str()))
416                } else {
417                    None
418                }
419            });
420            if let Some(k) = key
421                && let Some(def) = defs.get(&k)
422            {
423                emit_param_hints(sv, &f.args, def, &k, range, out);
424            }
425            hints_in_expr(sv, f.name, defs, type_map, analysis, range, out);
426            for arg in f.args.iter() {
427                hints_in_expr(sv, &arg.value, defs, type_map, analysis, range, out);
428            }
429        }
430        ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
431            if let Some(name) = ident_name(m.method)
432                && let Some(def) = defs.get(name)
433            {
434                emit_param_hints(sv, &m.args, def, name, range, out);
435            }
436            hints_in_expr(sv, m.object, defs, type_map, analysis, range, out);
437            for arg in m.args.iter() {
438                hints_in_expr(sv, &arg.value, defs, type_map, analysis, range, out);
439            }
440        }
441        ExprKind::StaticMethodCall(m) => {
442            if let Some(name) = ident_name(m.method)
443                && let Some(def) = defs.get(name)
444            {
445                emit_param_hints(sv, &m.args, def, name, range, out);
446            }
447            hints_in_expr(sv, m.class, defs, type_map, analysis, range, out);
448            for arg in m.args.iter() {
449                hints_in_expr(sv, &arg.value, defs, type_map, analysis, range, out);
450            }
451        }
452        ExprKind::New(n) => {
453            if let Some(class_name) = ident_name(n.class)
454                && let Some(def) = defs.get(class_name)
455            {
456                emit_param_hints(sv, &n.args, def, class_name, range, out);
457            }
458            for arg in n.args.iter() {
459                hints_in_expr(sv, &arg.value, defs, type_map, analysis, range, out);
460            }
461        }
462        ExprKind::Assign(a) => {
463            // Emit return-type hint after a function call on the RHS
464            emit_return_type_hint(sv, a.value, defs, range, out);
465            hints_in_expr(sv, a.target, defs, type_map, analysis, range, out);
466            hints_in_expr(sv, a.value, defs, type_map, analysis, range, out);
467        }
468        // Walk into closure bodies so nested function calls get hints.
469        ExprKind::Closure(c) => {
470            hints_in_stmts(sv, &c.body.stmts, defs, type_map, analysis, range, out);
471        }
472        // Walk into arrow function bodies so nested calls get hints.
473        // No return-type hint: the annotation is already visible in the source,
474        // and php-lsp has no type inference to supply hints for unannotated fns.
475        ExprKind::ArrowFunction(a) => {
476            hints_in_expr(sv, a.body, defs, type_map, analysis, range, out);
477        }
478        ExprKind::Parenthesized(e) => hints_in_expr(sv, e, defs, type_map, analysis, range, out),
479        ExprKind::Ternary(t) => {
480            hints_in_expr(sv, t.condition, defs, type_map, analysis, range, out);
481            if let Some(then_expr) = t.then_expr {
482                hints_in_expr(sv, then_expr, defs, type_map, analysis, range, out);
483            }
484            hints_in_expr(sv, t.else_expr, defs, type_map, analysis, range, out);
485        }
486        ExprKind::NullCoalesce(n) => {
487            hints_in_expr(sv, n.left, defs, type_map, analysis, range, out);
488            hints_in_expr(sv, n.right, defs, type_map, analysis, range, out);
489        }
490        ExprKind::Binary(b) => {
491            hints_in_expr(sv, b.left, defs, type_map, analysis, range, out);
492            hints_in_expr(sv, b.right, defs, type_map, analysis, range, out);
493        }
494        ExprKind::CloneWith(target, withs) => {
495            hints_in_expr(sv, target, defs, type_map, analysis, range, out);
496            hints_in_expr(sv, withs, defs, type_map, analysis, range, out);
497        }
498        _ => {}
499    }
500}
501
502fn emit_param_hints(
503    sv: SourceView<'_>,
504    args: &[php_ast::Arg<'_, '_>],
505    def: &FuncDef,
506    func_name: &str,
507    range: Range,
508    out: &mut Vec<InlayHint>,
509) {
510    for (i, arg) in args.iter().enumerate() {
511        // Skip named arguments (they already have the label in sv.source())
512        if arg.name.is_some() {
513            continue;
514        }
515        // For a variadic last param, repeat its name for every excess argument.
516        let param = if let Some(p) = def.params.get(i) {
517            p
518        } else if def.variadic_last {
519            match def.params.last() {
520                Some(p) => p,
521                None => continue,
522            }
523        } else {
524            continue;
525        };
526        let pos = sv.position_of(arg.span.start);
527        if pos_in_range(pos, range) {
528            out.push(make_param_hint(pos, param, func_name));
529        }
530    }
531}
532
533fn emit_return_type_hint(
534    sv: SourceView<'_>,
535    expr: &Expr<'_, '_>,
536    defs: &HashMap<String, FuncDef>,
537    range: Range,
538    out: &mut Vec<InlayHint>,
539) {
540    let name = match &expr.kind {
541        ExprKind::FunctionCall(f) => ident_name(f.name),
542        ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => ident_name(m.method),
543        ExprKind::StaticMethodCall(m) => ident_name(m.method),
544        _ => return,
545    };
546    if let Some(name) = name
547        && let Some(def) = defs.get(name)
548        && let Some(ret_type) = &def.return_type
549    {
550        if ret_type == "void" {
551            return;
552        }
553        let pos = sv.position_of(expr.span.end);
554        if pos_in_range(pos, range) {
555            out.push(make_return_hint(pos, ret_type, name));
556        }
557    }
558}
559
560fn ident_name<'a>(expr: &'a Expr<'_, '_>) -> Option<&'a str> {
561    if let ExprKind::Identifier(name) = &expr.kind {
562        Some(name)
563    } else {
564        None
565    }
566}
567
568fn make_param_hint(position: Position, param_name: &str, func_name: &str) -> InlayHint {
569    InlayHint {
570        position,
571        label: InlayHintLabel::String(format!("{}:", param_name)),
572        kind: Some(InlayHintKind::PARAMETER),
573        text_edits: None,
574        tooltip: None,
575        padding_left: None,
576        padding_right: Some(true),
577        data: Some(json!({"php_lsp_fn": func_name})),
578    }
579}
580
581fn make_return_hint(position: Position, ret_type: &str, func_name: &str) -> InlayHint {
582    InlayHint {
583        position,
584        label: InlayHintLabel::String(format!(": {ret_type}")),
585        kind: Some(InlayHintKind::TYPE),
586        text_edits: None,
587        tooltip: None,
588        padding_left: Some(true),
589        padding_right: None,
590        data: Some(json!({"php_lsp_fn": func_name})),
591    }
592}
593
594fn make_foreach_type_hint(position: Position, ty: &str) -> InlayHint {
595    InlayHint {
596        position,
597        label: InlayHintLabel::String(format!(": {ty}")),
598        kind: Some(InlayHintKind::TYPE),
599        text_edits: None,
600        tooltip: None,
601        padding_left: Some(true),
602        padding_right: None,
603        data: None,
604    }
605}
606
607fn pos_in_range(pos: Position, range: Range) -> bool {
608    if pos.line < range.start.line || pos.line > range.end.line {
609        return false;
610    }
611    if pos.line == range.start.line && pos.character < range.start.character {
612        return false;
613    }
614    if pos.line == range.end.line && pos.character >= range.end.character {
615        return false;
616    }
617    true
618}