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;
13use crate::types::array_inference::collect_array_map_returns;
14
15/// Resolve a foreach value/key variable's class (short name) for its type hint.
16///
17/// mir-primary: queries the recorded `ResolvedSymbol` at the variable's byte
18/// offset — works whenever mir knows the array element type (e.g. typed arrays,
19/// `@var list<T>` annotations, `enum::cases()`).
20///
21/// Fallback: for cases where mir's `array_map` stub returns plain `array` and
22/// the element type is therefore `mixed`, consult the `array_map_returns` map
23/// (pre-computed from array_map/array_filter callback return-type hints in the
24/// source) keyed by the name of the iterated array variable.
25fn foreach_var_class(
26    analysis: Option<&mir_analyzer::FileAnalysis>,
27    array_map_returns: &HashMap<String, String>,
28    foreach_iterable: &Expr<'_, '_>,
29    var_offset: u32,
30) -> Option<String> {
31    analysis
32        .and_then(|a| crate::types::type_query::type_at_offset(a, var_offset))
33        .and_then(crate::types::type_query::primary_class_name)
34        .map(|fqcn| fqn_short_name(&fqcn).to_string())
35        .or_else(|| {
36            if let ExprKind::Variable(arr_name) = &foreach_iterable.kind {
37                array_map_returns.get(arr_name.as_str()).cloned()
38            } else {
39                None
40            }
41        })
42}
43
44#[derive(Clone)]
45struct FuncDef {
46    params: Vec<String>,
47    /// Whether the last parameter is variadic (`...$name`).
48    variadic_last: bool,
49    return_type: Option<String>,
50}
51
52/// Returns parameter-name inlay hints AND return-type hints for all
53/// function/method declarations and calls in `doc`.
54///
55/// `workspace_files` is the list of all indexed files; definitions not found
56/// in the current document fall back to this workspace index so that calls to
57/// cross-file functions/methods still get parameter-name hints.
58pub fn inlay_hints(
59    _source: &str,
60    doc: &ParsedDoc,
61    analysis: Option<&mir_analyzer::FileAnalysis>,
62    range: Range,
63    workspace_files: &[(Url, Arc<FileIndex>)],
64) -> Vec<InlayHint> {
65    let sv = doc.view();
66    let mut defs = collect_defs(&doc.program().stmts);
67    collect_defs_from_workspace(workspace_files, &mut defs);
68    // Pre-compute array_map/array_filter element types for the foreach fallback.
69    // mir's array_map stub returns plain `array`, so this map bridges the gap
70    // until the stub gains generic annotations.
71    let array_map_returns = collect_array_map_returns(&doc.program().stmts);
72    let mut hints = Vec::new();
73    hints_in_stmts(
74        sv,
75        &doc.program().stmts,
76        &defs,
77        &array_map_returns,
78        analysis,
79        range,
80        &mut hints,
81    );
82    hints
83}
84
85// === Definition collection ===
86
87fn collect_defs(stmts: &[Stmt<'_, '_>]) -> HashMap<String, FuncDef> {
88    let mut map = HashMap::new();
89    collect_defs_stmts(stmts, &mut map);
90    map
91}
92
93/// Populate `map` with function and method definitions from the workspace index.
94/// Entries already present (from the current file's AST) are not overwritten so
95/// that the in-file definition always wins over a potentially stale index entry.
96fn collect_defs_from_workspace(
97    workspace_files: &[(Url, Arc<FileIndex>)],
98    map: &mut HashMap<String, FuncDef>,
99) {
100    for (_, idx) in workspace_files {
101        for func in &idx.functions {
102            let func_name = func.name.to_string();
103            if map.contains_key(&func_name) {
104                continue;
105            }
106            let params: Vec<String> = func.params.iter().map(|p| p.name.to_string()).collect();
107            let variadic_last = func.params.last().map(|p| p.variadic).unwrap_or(false);
108            map.insert(
109                func_name,
110                FuncDef {
111                    params,
112                    variadic_last,
113                    return_type: func.return_type.as_ref().map(|r| r.to_string()),
114                },
115            );
116        }
117        for class in &idx.classes {
118            for method in &class.methods {
119                let method_name = method.name.to_string();
120                let params: Vec<String> =
121                    method.params.iter().map(|p| p.name.to_string()).collect();
122                let variadic_last = method.params.last().map(|p| p.variadic).unwrap_or(false);
123                let func_def = FuncDef {
124                    params: params.clone(),
125                    variadic_last,
126                    return_type: method.return_type.as_ref().map(|r| r.to_string()),
127                };
128                // Register with qualified key "ClassName::methodName" for unambiguous lookup
129                let cn = class.name.as_ref();
130                let qualified = format!("{}::{}", cn, method_name);
131                map.insert(qualified, func_def.clone());
132                // Also register __construct under the class name so `new ClassName(...)` gets hints.
133                if method_name == "__construct" {
134                    map.entry(cn.to_string()).or_insert_with(|| FuncDef {
135                        params: params.clone(),
136                        variadic_last,
137                        return_type: None,
138                    });
139                }
140                // Register with short name as fallback for backwards compatibility
141                map.entry(method_name).or_insert(func_def);
142            }
143        }
144    }
145}
146
147/// Extract param names and whether the last param is variadic from a param list.
148fn params_from_list(params: &[Param<'_, '_>]) -> (Vec<String>, bool) {
149    let names = params.iter().map(|p| p.name.to_string()).collect();
150    let variadic_last = params.last().map(|p| p.variadic).unwrap_or(false);
151    (names, variadic_last)
152}
153
154fn collect_defs_stmts(stmts: &[Stmt<'_, '_>], map: &mut HashMap<String, FuncDef>) {
155    for stmt in stmts {
156        match &stmt.kind {
157            StmtKind::Function(f) => {
158                let (params, variadic_last) = params_from_list(&f.params);
159                let return_type = f.return_type.as_ref().map(|t| format_type_hint(t));
160                map.insert(
161                    f.name.to_string(),
162                    FuncDef {
163                        params,
164                        variadic_last,
165                        return_type,
166                    },
167                );
168            }
169            StmtKind::Class(c) => {
170                for member in c.body.members.iter() {
171                    if let ClassMemberKind::Method(m) = &member.kind {
172                        let (params, variadic_last) = params_from_list(&m.params);
173                        let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
174                        let func_def = FuncDef {
175                            params: params.clone(),
176                            variadic_last,
177                            return_type: return_type.clone(),
178                        };
179                        // Register with qualified key "ClassName::methodName" for unambiguous lookup
180                        if let Some(cn) = c.name {
181                            let qualified = format!("{}::{}", cn, m.name);
182                            map.insert(qualified, func_def.clone());
183                        }
184                        // Register __construct under the class name so `new ClassName(...)` gets hints.
185                        if m.name == "__construct"
186                            && let Some(class_name) = c.name
187                        {
188                            map.insert(
189                                class_name.to_string(),
190                                FuncDef {
191                                    params: params.clone(),
192                                    variadic_last,
193                                    return_type: None,
194                                },
195                            );
196                        }
197                        map.insert(m.name.to_string(), func_def);
198                    }
199                }
200            }
201            StmtKind::Trait(t) => {
202                for member in t.body.members.iter() {
203                    if let ClassMemberKind::Method(m) = &member.kind {
204                        let (params, variadic_last) = params_from_list(&m.params);
205                        let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
206                        let func_def = FuncDef {
207                            params,
208                            variadic_last,
209                            return_type,
210                        };
211                        // Register with qualified key for unambiguous lookup
212                        let qualified = format!("{}::{}", t.name, m.name);
213                        map.insert(qualified, func_def.clone());
214                        map.insert(m.name.to_string(), func_def);
215                    }
216                }
217            }
218            StmtKind::Enum(e) => {
219                for member in e.body.members.iter() {
220                    if let EnumMemberKind::Method(m) = &member.kind {
221                        let (params, variadic_last) = params_from_list(&m.params);
222                        let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
223                        let func_def = FuncDef {
224                            params,
225                            variadic_last,
226                            return_type,
227                        };
228                        // Register with qualified key for unambiguous lookup
229                        let qualified = format!("{}::{}", e.name, m.name);
230                        map.insert(qualified, func_def.clone());
231                        map.insert(m.name.to_string(), func_def);
232                    }
233                }
234            }
235            StmtKind::Namespace(ns) => {
236                if let NamespaceBody::Braced(inner) = &ns.body {
237                    collect_defs_stmts(&inner.stmts, map);
238                }
239            }
240            // Register closure/arrow-function variables so `$fn(...)` call sites get hints.
241            StmtKind::Expression(e) => {
242                if let ExprKind::Assign(assign) = &e.kind
243                    && let ExprKind::Variable(var_name) = &assign.target.kind
244                {
245                    let key = format!("${}", var_name.as_str());
246                    match &assign.value.kind {
247                        ExprKind::Closure(c) => {
248                            let (params, variadic_last) = params_from_list(&c.params);
249                            let return_type = c.return_type.as_ref().map(|t| format_type_hint(t));
250                            map.insert(
251                                key,
252                                FuncDef {
253                                    params,
254                                    variadic_last,
255                                    return_type,
256                                },
257                            );
258                        }
259                        ExprKind::ArrowFunction(a) => {
260                            let (params, variadic_last) = params_from_list(&a.params);
261                            let return_type = a.return_type.as_ref().map(|t| format_type_hint(t));
262                            map.insert(
263                                key,
264                                FuncDef {
265                                    params,
266                                    variadic_last,
267                                    return_type,
268                                },
269                            );
270                        }
271                        _ => {}
272                    }
273                }
274            }
275            _ => {}
276        }
277    }
278}
279
280// === AST walking ===
281
282fn hints_in_stmts(
283    sv: SourceView<'_>,
284    stmts: &[Stmt<'_, '_>],
285    defs: &HashMap<String, FuncDef>,
286    array_map_returns: &HashMap<String, String>,
287    analysis: Option<&mir_analyzer::FileAnalysis>,
288    range: Range,
289    out: &mut Vec<InlayHint>,
290) {
291    for stmt in stmts {
292        hints_in_stmt(sv, stmt, defs, array_map_returns, analysis, range, out);
293    }
294}
295
296fn hints_in_stmt(
297    sv: SourceView<'_>,
298    stmt: &Stmt<'_, '_>,
299    defs: &HashMap<String, FuncDef>,
300    array_map_returns: &HashMap<String, String>,
301    analysis: Option<&mir_analyzer::FileAnalysis>,
302    range: Range,
303    out: &mut Vec<InlayHint>,
304) {
305    match &stmt.kind {
306        StmtKind::Expression(e) => {
307            hints_in_expr(sv, e, defs, array_map_returns, analysis, range, out)
308        }
309        StmtKind::Return(Some(v)) => {
310            hints_in_expr(sv, v, defs, array_map_returns, analysis, range, out)
311        }
312        StmtKind::Echo(exprs) => {
313            for expr in exprs.iter() {
314                hints_in_expr(sv, expr, defs, array_map_returns, analysis, range, out);
315            }
316        }
317        StmtKind::Function(f) => {
318            hints_in_stmts(
319                sv,
320                &f.body.stmts,
321                defs,
322                array_map_returns,
323                analysis,
324                range,
325                out,
326            );
327        }
328        StmtKind::Class(c) => {
329            for member in c.body.members.iter() {
330                if let ClassMemberKind::Method(m) = &member.kind
331                    && let Some(body) = &m.body
332                {
333                    hints_in_stmts(
334                        sv,
335                        &body.stmts,
336                        defs,
337                        array_map_returns,
338                        analysis,
339                        range,
340                        out,
341                    );
342                }
343            }
344        }
345        StmtKind::Trait(t) => {
346            for member in t.body.members.iter() {
347                if let ClassMemberKind::Method(m) = &member.kind
348                    && let Some(body) = &m.body
349                {
350                    hints_in_stmts(
351                        sv,
352                        &body.stmts,
353                        defs,
354                        array_map_returns,
355                        analysis,
356                        range,
357                        out,
358                    );
359                }
360            }
361        }
362        StmtKind::Enum(e) => {
363            for member in e.body.members.iter() {
364                if let EnumMemberKind::Method(m) = &member.kind
365                    && let Some(body) = &m.body
366                {
367                    hints_in_stmts(
368                        sv,
369                        &body.stmts,
370                        defs,
371                        array_map_returns,
372                        analysis,
373                        range,
374                        out,
375                    );
376                }
377            }
378        }
379        StmtKind::Namespace(ns) => {
380            if let NamespaceBody::Braced(inner) = &ns.body {
381                hints_in_stmts(
382                    sv,
383                    &inner.stmts,
384                    defs,
385                    array_map_returns,
386                    analysis,
387                    range,
388                    out,
389                );
390            }
391        }
392        StmtKind::If(i) => {
393            hints_in_expr(
394                sv,
395                &i.condition,
396                defs,
397                array_map_returns,
398                analysis,
399                range,
400                out,
401            );
402            hints_in_stmt(
403                sv,
404                i.then_branch,
405                defs,
406                array_map_returns,
407                analysis,
408                range,
409                out,
410            );
411            for ei in i.elseif_branches.iter() {
412                hints_in_expr(
413                    sv,
414                    &ei.condition,
415                    defs,
416                    array_map_returns,
417                    analysis,
418                    range,
419                    out,
420                );
421                hints_in_stmt(sv, &ei.body, defs, array_map_returns, analysis, range, out);
422            }
423            if let Some(e) = &i.else_branch {
424                hints_in_stmt(sv, e, defs, array_map_returns, analysis, range, out);
425            }
426        }
427        StmtKind::While(w) => {
428            hints_in_expr(
429                sv,
430                &w.condition,
431                defs,
432                array_map_returns,
433                analysis,
434                range,
435                out,
436            );
437            hints_in_stmt(sv, w.body, defs, array_map_returns, analysis, range, out);
438        }
439        StmtKind::For(f) => {
440            for e in f.init.iter() {
441                hints_in_expr(sv, e, defs, array_map_returns, analysis, range, out);
442            }
443            for cond in f.condition.iter() {
444                hints_in_expr(sv, cond, defs, array_map_returns, analysis, range, out);
445            }
446            for e in f.update.iter() {
447                hints_in_expr(sv, e, defs, array_map_returns, analysis, range, out);
448            }
449            hints_in_stmt(sv, f.body, defs, array_map_returns, analysis, range, out);
450        }
451        StmtKind::Foreach(f) => {
452            hints_in_expr(sv, &f.expr, defs, array_map_returns, analysis, range, out);
453            // Emit type hint after the value variable, e.g. `foreach ($arr as $item /* : Foo */)`.
454            if let ExprKind::Variable(_) = &f.value.kind
455                && let Some(ty) =
456                    foreach_var_class(analysis, array_map_returns, &f.expr, f.value.span.start)
457            {
458                let pos = sv.position_of(f.value.span.end);
459                if pos_in_range(pos, range) {
460                    out.push(make_foreach_type_hint(pos, &ty));
461                }
462            }
463            // Emit type hint after the key variable if present, e.g. `foreach ($map as $key => $value)`.
464            // The array_map_returns map holds the element (value) type; do not use it for the
465            // key variable — array_map returns integer-keyed arrays and showing ": User" on $key
466            // would be misleading.  Only emit when mir itself resolves a concrete key type.
467            if let Some(key_expr) = &f.key
468                && let ExprKind::Variable(_) = &key_expr.kind
469                && let Some(ty) =
470                    foreach_var_class(analysis, &HashMap::new(), &f.expr, key_expr.span.start)
471            {
472                let pos = sv.position_of(key_expr.span.end);
473                if pos_in_range(pos, range) {
474                    out.push(make_foreach_type_hint(pos, &ty));
475                }
476            }
477            hints_in_stmt(sv, f.body, defs, array_map_returns, analysis, range, out);
478        }
479        StmtKind::TryCatch(t) => {
480            hints_in_stmts(
481                sv,
482                &t.body.stmts,
483                defs,
484                array_map_returns,
485                analysis,
486                range,
487                out,
488            );
489            for catch in t.catches.iter() {
490                hints_in_stmts(
491                    sv,
492                    &catch.body.stmts,
493                    defs,
494                    array_map_returns,
495                    analysis,
496                    range,
497                    out,
498                );
499            }
500            if let Some(finally) = &t.finally {
501                hints_in_stmts(
502                    sv,
503                    &finally.stmts,
504                    defs,
505                    array_map_returns,
506                    analysis,
507                    range,
508                    out,
509                );
510            }
511        }
512        StmtKind::Block(stmts) => hints_in_stmts(
513            sv,
514            &stmts.stmts,
515            defs,
516            array_map_returns,
517            analysis,
518            range,
519            out,
520        ),
521        _ => {}
522    }
523}
524
525fn hints_in_expr(
526    sv: SourceView<'_>,
527    expr: &Expr<'_, '_>,
528    defs: &HashMap<String, FuncDef>,
529    array_map_returns: &HashMap<String, String>,
530    analysis: Option<&mir_analyzer::FileAnalysis>,
531    range: Range,
532    out: &mut Vec<InlayHint>,
533) {
534    match &expr.kind {
535        ExprKind::FunctionCall(f) => {
536            // Look up by identifier name or by variable name (for closure vars like `$fn(...)`).
537            let key: Option<String> = ident_name(f.name).map(|n| n.to_string()).or_else(|| {
538                if let ExprKind::Variable(n) = &f.name.kind {
539                    Some(format!("${}", n.as_str()))
540                } else {
541                    None
542                }
543            });
544            if let Some(k) = key
545                && let Some(def) = defs.get(&k)
546            {
547                emit_param_hints(sv, &f.args, def, &k, range, out);
548            }
549            hints_in_expr(sv, f.name, defs, array_map_returns, analysis, range, out);
550            for arg in f.args.iter() {
551                hints_in_expr(
552                    sv,
553                    &arg.value,
554                    defs,
555                    array_map_returns,
556                    analysis,
557                    range,
558                    out,
559                );
560            }
561        }
562        ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
563            if let Some(name) = ident_name(m.method)
564                && let Some(def) = defs.get(name)
565            {
566                emit_param_hints(sv, &m.args, def, name, range, out);
567            }
568            hints_in_expr(sv, m.object, defs, array_map_returns, analysis, range, out);
569            for arg in m.args.iter() {
570                hints_in_expr(
571                    sv,
572                    &arg.value,
573                    defs,
574                    array_map_returns,
575                    analysis,
576                    range,
577                    out,
578                );
579            }
580        }
581        ExprKind::StaticMethodCall(m) => {
582            if let Some(name) = ident_name(m.method)
583                && let Some(def) = defs.get(name)
584            {
585                emit_param_hints(sv, &m.args, def, name, range, out);
586            }
587            hints_in_expr(sv, m.class, defs, array_map_returns, analysis, range, out);
588            for arg in m.args.iter() {
589                hints_in_expr(
590                    sv,
591                    &arg.value,
592                    defs,
593                    array_map_returns,
594                    analysis,
595                    range,
596                    out,
597                );
598            }
599        }
600        ExprKind::New(n) => {
601            if let Some(class_name) = ident_name(n.class)
602                && let Some(def) = defs.get(class_name)
603            {
604                emit_param_hints(sv, &n.args, def, class_name, range, out);
605            }
606            for arg in n.args.iter() {
607                hints_in_expr(
608                    sv,
609                    &arg.value,
610                    defs,
611                    array_map_returns,
612                    analysis,
613                    range,
614                    out,
615                );
616            }
617        }
618        ExprKind::Assign(a) => {
619            // Emit return-type hint after a function call on the RHS
620            emit_return_type_hint(sv, a.value, defs, range, out);
621            hints_in_expr(sv, a.target, defs, array_map_returns, analysis, range, out);
622            hints_in_expr(sv, a.value, defs, array_map_returns, analysis, range, out);
623        }
624        // Walk into closure bodies so nested function calls get hints.
625        ExprKind::Closure(c) => {
626            hints_in_stmts(
627                sv,
628                &c.body.stmts,
629                defs,
630                array_map_returns,
631                analysis,
632                range,
633                out,
634            );
635        }
636        // Walk into arrow function bodies so nested calls get hints.
637        // No return-type hint: the annotation is already visible in the source,
638        // and php-lsp has no type inference to supply hints for unannotated fns.
639        ExprKind::ArrowFunction(a) => {
640            hints_in_expr(sv, a.body, defs, array_map_returns, analysis, range, out);
641        }
642        ExprKind::Parenthesized(e) => {
643            hints_in_expr(sv, e, defs, array_map_returns, analysis, range, out)
644        }
645        ExprKind::Ternary(t) => {
646            hints_in_expr(
647                sv,
648                t.condition,
649                defs,
650                array_map_returns,
651                analysis,
652                range,
653                out,
654            );
655            if let Some(then_expr) = t.then_expr {
656                hints_in_expr(sv, then_expr, defs, array_map_returns, analysis, range, out);
657            }
658            hints_in_expr(
659                sv,
660                t.else_expr,
661                defs,
662                array_map_returns,
663                analysis,
664                range,
665                out,
666            );
667        }
668        ExprKind::NullCoalesce(n) => {
669            hints_in_expr(sv, n.left, defs, array_map_returns, analysis, range, out);
670            hints_in_expr(sv, n.right, defs, array_map_returns, analysis, range, out);
671        }
672        ExprKind::Binary(b) => {
673            hints_in_expr(sv, b.left, defs, array_map_returns, analysis, range, out);
674            hints_in_expr(sv, b.right, defs, array_map_returns, analysis, range, out);
675        }
676        ExprKind::CloneWith(target, withs) => {
677            hints_in_expr(sv, target, defs, array_map_returns, analysis, range, out);
678            hints_in_expr(sv, withs, defs, array_map_returns, analysis, range, out);
679        }
680        _ => {}
681    }
682}
683
684fn emit_param_hints(
685    sv: SourceView<'_>,
686    args: &[php_ast::Arg<'_, '_>],
687    def: &FuncDef,
688    func_name: &str,
689    range: Range,
690    out: &mut Vec<InlayHint>,
691) {
692    for (i, arg) in args.iter().enumerate() {
693        // Skip named arguments (they already have the label in sv.source())
694        if arg.name.is_some() {
695            continue;
696        }
697        // For a variadic last param, repeat its name for every excess argument.
698        let param = if let Some(p) = def.params.get(i) {
699            p
700        } else if def.variadic_last {
701            match def.params.last() {
702                Some(p) => p,
703                None => continue,
704            }
705        } else {
706            continue;
707        };
708        let pos = sv.position_of(arg.span.start);
709        if pos_in_range(pos, range) {
710            out.push(make_param_hint(pos, param, func_name));
711        }
712    }
713}
714
715fn emit_return_type_hint(
716    sv: SourceView<'_>,
717    expr: &Expr<'_, '_>,
718    defs: &HashMap<String, FuncDef>,
719    range: Range,
720    out: &mut Vec<InlayHint>,
721) {
722    let name = match &expr.kind {
723        ExprKind::FunctionCall(f) => ident_name(f.name),
724        ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => ident_name(m.method),
725        ExprKind::StaticMethodCall(m) => ident_name(m.method),
726        _ => return,
727    };
728    if let Some(name) = name
729        && let Some(def) = defs.get(name)
730        && let Some(ret_type) = &def.return_type
731    {
732        if ret_type == "void" {
733            return;
734        }
735        let pos = sv.position_of(expr.span.end);
736        if pos_in_range(pos, range) {
737            out.push(make_return_hint(pos, ret_type, name));
738        }
739    }
740}
741
742fn ident_name<'a>(expr: &'a Expr<'_, '_>) -> Option<&'a str> {
743    if let ExprKind::Identifier(name) = &expr.kind {
744        Some(name)
745    } else {
746        None
747    }
748}
749
750fn make_param_hint(position: Position, param_name: &str, func_name: &str) -> InlayHint {
751    InlayHint {
752        position,
753        label: InlayHintLabel::String(format!("{}:", param_name)),
754        kind: Some(InlayHintKind::PARAMETER),
755        text_edits: None,
756        tooltip: None,
757        padding_left: None,
758        padding_right: Some(true),
759        data: Some(json!({"php_lsp_fn": func_name})),
760    }
761}
762
763fn make_return_hint(position: Position, ret_type: &str, func_name: &str) -> InlayHint {
764    InlayHint {
765        position,
766        label: InlayHintLabel::String(format!(": {ret_type}")),
767        kind: Some(InlayHintKind::TYPE),
768        text_edits: None,
769        tooltip: None,
770        padding_left: Some(true),
771        padding_right: None,
772        data: Some(json!({"php_lsp_fn": func_name})),
773    }
774}
775
776fn make_foreach_type_hint(position: Position, ty: &str) -> InlayHint {
777    InlayHint {
778        position,
779        label: InlayHintLabel::String(format!(": {ty}")),
780        kind: Some(InlayHintKind::TYPE),
781        text_edits: None,
782        tooltip: None,
783        padding_left: Some(true),
784        padding_right: None,
785        data: None,
786    }
787}
788
789fn pos_in_range(pos: Position, range: Range) -> bool {
790    if pos.line < range.start.line || pos.line > range.end.line {
791        return false;
792    }
793    if pos.line == range.start.line && pos.character < range.start.character {
794        return false;
795    }
796    if pos.line == range.end.line && pos.character >= range.end.character {
797        return false;
798    }
799    true
800}