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