1use std::collections::HashMap;
2
3use php_ast::{
4 ClassMemberKind, EnumMemberKind, Expr, ExprKind, NamespaceBody, Param, Stmt, StmtKind,
5};
6use serde_json::json;
7use tower_lsp_server::ls_types::{InlayHint, InlayHintKind, InlayHintLabel, Position, Range};
8
9use crate::document::ast::{ParsedDoc, SourceView, format_type_hint};
10use crate::text::fqn_short_name;
11
12fn foreach_var_class(
19 analysis: Option<&mir_analyzer::FileAnalysis>,
20 var_offset: u32,
21) -> Option<String> {
22 analysis
23 .and_then(|a| crate::types::type_query::type_at_offset(a, var_offset))
24 .and_then(crate::types::type_query::primary_class_name)
25 .map(|fqcn| fqn_short_name(&fqcn).to_string())
26}
27
28pub fn inlay_hints(
32 _source: &str,
33 doc: &ParsedDoc,
34 analysis: Option<&mir_analyzer::FileAnalysis>,
35 db: Option<&mir_analyzer::db::MirDbStorage>,
36 range: Range,
37) -> Vec<InlayHint> {
38 let sv = doc.view();
39 let defs = collect_defs(&doc.program().stmts);
40 let mut hints = Vec::new();
41 let ctx = HintCtx {
42 sv,
43 defs: &defs,
44 analysis,
45 db,
46 range,
47 };
48 hints_in_stmts(&ctx, &doc.program().stmts, &mut hints);
49 hints
50}
51
52#[derive(Clone)]
53struct CallableSignature {
54 params: Vec<String>,
55 variadic_last: bool,
56 return_type: Option<String>,
57 tooltip: Option<TooltipSymbol>,
58}
59
60#[derive(Clone)]
61struct TooltipSymbol {
62 kind: &'static str,
63 name: String,
64 class: Option<String>,
65}
66
67struct HintCtx<'a> {
68 sv: SourceView<'a>,
69 defs: &'a HashMap<String, CallableSignature>,
70 analysis: Option<&'a mir_analyzer::FileAnalysis>,
71 db: Option<&'a mir_analyzer::db::MirDbStorage>,
72 range: Range,
73}
74
75fn collect_defs(stmts: &[Stmt<'_, '_>]) -> HashMap<String, CallableSignature> {
78 let mut map = HashMap::new();
79 collect_defs_stmts(stmts, &mut map);
80 map
81}
82
83fn params_from_list(params: &[Param<'_, '_>]) -> (Vec<String>, bool) {
85 let names = params
86 .iter()
87 .map(|p| p.name.to_string().trim_start_matches('$').to_string())
88 .collect();
89 let variadic_last = params.last().map(|p| p.variadic).unwrap_or(false);
90 (names, variadic_last)
91}
92
93fn collect_defs_stmts(stmts: &[Stmt<'_, '_>], map: &mut HashMap<String, CallableSignature>) {
94 for stmt in stmts {
95 match &stmt.kind {
96 StmtKind::Function(f) => {
97 let (params, variadic_last) = params_from_list(&f.params);
98 let return_type = f.return_type.as_ref().map(|t| format_type_hint(t));
99 map.insert(
100 f.name.to_string(),
101 CallableSignature {
102 params,
103 variadic_last,
104 return_type,
105 tooltip: Some(TooltipSymbol {
106 kind: "function",
107 name: f.name.to_string(),
108 class: None,
109 }),
110 },
111 );
112 }
113 StmtKind::Class(c) => {
114 for member in c.body.members.iter() {
115 if let ClassMemberKind::Method(m) = &member.kind {
116 let (params, variadic_last) = params_from_list(&m.params);
117 let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
118 let func_def = CallableSignature {
119 params: params.clone(),
120 variadic_last,
121 return_type: return_type.clone(),
122 tooltip: c.name.map(|cn| TooltipSymbol {
123 kind: "method",
124 name: m.name.to_string(),
125 class: Some(cn.to_string()),
126 }),
127 };
128 if let Some(cn) = c.name {
130 let qualified = format!("{}::{}", cn, m.name);
131 map.insert(qualified, func_def.clone());
132 }
133 if m.name == "__construct"
135 && let Some(class_name) = c.name
136 {
137 map.insert(
138 class_name.to_string(),
139 CallableSignature {
140 params: params.clone(),
141 variadic_last,
142 return_type: None,
143 tooltip: Some(TooltipSymbol {
144 kind: "class",
145 name: class_name.to_string(),
146 class: None,
147 }),
148 },
149 );
150 }
151 map.insert(m.name.to_string(), func_def);
152 }
153 }
154 }
155 StmtKind::Trait(t) => {
156 for member in t.body.members.iter() {
157 if let ClassMemberKind::Method(m) = &member.kind {
158 let (params, variadic_last) = params_from_list(&m.params);
159 let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
160 let func_def = CallableSignature {
161 params,
162 variadic_last,
163 return_type,
164 tooltip: Some(TooltipSymbol {
165 kind: "method",
166 name: m.name.to_string(),
167 class: Some(t.name.to_string()),
168 }),
169 };
170 let qualified = format!("{}::{}", t.name, m.name);
172 map.insert(qualified, func_def.clone());
173 map.insert(m.name.to_string(), func_def);
174 }
175 }
176 }
177 StmtKind::Enum(e) => {
178 for member in e.body.members.iter() {
179 if let EnumMemberKind::Method(m) = &member.kind {
180 let (params, variadic_last) = params_from_list(&m.params);
181 let return_type = m.return_type.as_ref().map(|t| format_type_hint(t));
182 let func_def = CallableSignature {
183 params,
184 variadic_last,
185 return_type,
186 tooltip: Some(TooltipSymbol {
187 kind: "method",
188 name: m.name.to_string(),
189 class: Some(e.name.to_string()),
190 }),
191 };
192 let qualified = format!("{}::{}", e.name, m.name);
194 map.insert(qualified, func_def.clone());
195 map.insert(m.name.to_string(), func_def);
196 }
197 }
198 }
199 StmtKind::Namespace(ns) => {
200 if let NamespaceBody::Braced(inner) = &ns.body {
201 collect_defs_stmts(&inner.stmts, map);
202 }
203 }
204 StmtKind::Expression(e) => {
206 if let ExprKind::Assign(assign) = &e.kind
207 && let ExprKind::Variable(var_name) = &assign.target.kind
208 {
209 let key = format!("${}", var_name.as_str());
210 match &assign.value.kind {
211 ExprKind::Closure(c) => {
212 let (params, variadic_last) = params_from_list(&c.params);
213 let return_type = c.return_type.as_ref().map(|t| format_type_hint(t));
214 map.insert(
215 key,
216 CallableSignature {
217 params,
218 variadic_last,
219 return_type,
220 tooltip: None,
221 },
222 );
223 }
224 ExprKind::ArrowFunction(a) => {
225 let (params, variadic_last) = params_from_list(&a.params);
226 let return_type = a.return_type.as_ref().map(|t| format_type_hint(t));
227 map.insert(
228 key,
229 CallableSignature {
230 params,
231 variadic_last,
232 return_type,
233 tooltip: None,
234 },
235 );
236 }
237 _ => {}
238 }
239 }
240 }
241 _ => {}
242 }
243 }
244}
245
246fn hints_in_stmts(ctx: &HintCtx<'_>, stmts: &[Stmt<'_, '_>], out: &mut Vec<InlayHint>) {
249 for stmt in stmts {
250 hints_in_stmt(ctx, stmt, out);
251 }
252}
253
254fn hints_in_stmt(ctx: &HintCtx<'_>, stmt: &Stmt<'_, '_>, out: &mut Vec<InlayHint>) {
255 match &stmt.kind {
256 StmtKind::Expression(e) => hints_in_expr(ctx, e, out),
257 StmtKind::Return(Some(v)) => hints_in_expr(ctx, v, out),
258 StmtKind::Echo(exprs) => {
259 for expr in exprs.iter() {
260 hints_in_expr(ctx, expr, out);
261 }
262 }
263 StmtKind::Function(f) => hints_in_stmts(ctx, &f.body.stmts, out),
264 StmtKind::Class(c) => {
265 for member in c.body.members.iter() {
266 if let ClassMemberKind::Method(m) = &member.kind
267 && let Some(body) = &m.body
268 {
269 hints_in_stmts(ctx, &body.stmts, out);
270 }
271 }
272 }
273 StmtKind::Trait(t) => {
274 for member in t.body.members.iter() {
275 if let ClassMemberKind::Method(m) = &member.kind
276 && let Some(body) = &m.body
277 {
278 hints_in_stmts(ctx, &body.stmts, out);
279 }
280 }
281 }
282 StmtKind::Enum(e) => {
283 for member in e.body.members.iter() {
284 if let EnumMemberKind::Method(m) = &member.kind
285 && let Some(body) = &m.body
286 {
287 hints_in_stmts(ctx, &body.stmts, out);
288 }
289 }
290 }
291 StmtKind::Namespace(ns) => {
292 if let NamespaceBody::Braced(inner) = &ns.body {
293 hints_in_stmts(ctx, &inner.stmts, out);
294 }
295 }
296 StmtKind::If(i) => {
297 hints_in_expr(ctx, &i.condition, out);
298 hints_in_stmt(ctx, i.then_branch, out);
299 for ei in i.elseif_branches.iter() {
300 hints_in_expr(ctx, &ei.condition, out);
301 hints_in_stmt(ctx, &ei.body, out);
302 }
303 if let Some(e) = &i.else_branch {
304 hints_in_stmt(ctx, e, out);
305 }
306 }
307 StmtKind::While(w) => {
308 hints_in_expr(ctx, &w.condition, out);
309 hints_in_stmt(ctx, w.body, out);
310 }
311 StmtKind::For(f) => {
312 for e in f.init.iter() {
313 hints_in_expr(ctx, e, out);
314 }
315 for cond in f.condition.iter() {
316 hints_in_expr(ctx, cond, out);
317 }
318 for e in f.update.iter() {
319 hints_in_expr(ctx, e, out);
320 }
321 hints_in_stmt(ctx, f.body, out);
322 }
323 StmtKind::Foreach(f) => {
324 hints_in_expr(ctx, &f.expr, out);
325 if let ExprKind::Variable(_) = &f.value.kind
327 && let Some(ty) = foreach_var_class(ctx.analysis, f.value.span.start)
328 {
329 let pos = ctx.sv.position_of(f.value.span.end);
330 if pos_in_range(pos, ctx.range) {
331 out.push(make_foreach_type_hint(pos, &ty));
332 }
333 }
334 if let Some(key_expr) = &f.key
336 && let ExprKind::Variable(_) = &key_expr.kind
337 && let Some(ty) = foreach_var_class(ctx.analysis, key_expr.span.start)
338 {
339 let pos = ctx.sv.position_of(key_expr.span.end);
340 if pos_in_range(pos, ctx.range) {
341 out.push(make_foreach_type_hint(pos, &ty));
342 }
343 }
344 hints_in_stmt(ctx, f.body, out);
345 }
346 StmtKind::TryCatch(t) => {
347 hints_in_stmts(ctx, &t.body.stmts, out);
348 for catch in t.catches.iter() {
349 hints_in_stmts(ctx, &catch.body.stmts, out);
350 }
351 if let Some(finally) = &t.finally {
352 hints_in_stmts(ctx, &finally.stmts, out);
353 }
354 }
355 StmtKind::Block(stmts) => hints_in_stmts(ctx, &stmts.stmts, out),
356 StmtKind::DoWhile(d) => {
357 hints_in_stmt(ctx, d.body, out);
358 hints_in_expr(ctx, &d.condition, out);
359 }
360 StmtKind::Switch(s) => {
361 hints_in_expr(ctx, &s.expr, out);
362 for case in s.body.cases.iter() {
363 if let Some(v) = &case.value {
364 hints_in_expr(ctx, v, out);
365 }
366 hints_in_stmts(ctx, &case.body, out);
367 }
368 }
369 _ => {}
370 }
371}
372
373fn hints_in_expr(ctx: &HintCtx<'_>, expr: &Expr<'_, '_>, out: &mut Vec<InlayHint>) {
374 match &expr.kind {
375 ExprKind::FunctionCall(f) => {
376 let def = ident_name(f.name)
377 .and_then(|_| callable_from_symbol(ctx, f.name.span.start))
378 .or_else(|| callable_from_local_function(ctx, f.name));
379 if let Some(def) = def {
380 emit_param_hints(ctx, &f.args, &def, out);
381 }
382 hints_in_expr(ctx, f.name, out);
383 for arg in f.args.iter() {
384 if let Some(value) = &arg.value {
385 hints_in_expr(ctx, value, out);
386 }
387 }
388 }
389 ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
390 if let Some(def) = callable_from_symbol(ctx, m.method.span.start) {
391 emit_param_hints(ctx, &m.args, &def, out);
392 }
393 hints_in_expr(ctx, m.object, out);
394 for arg in m.args.iter() {
395 if let Some(value) = &arg.value {
396 hints_in_expr(ctx, value, out);
397 }
398 }
399 }
400 ExprKind::StaticMethodCall(m) => {
401 if let Some(def) = callable_from_symbol(ctx, m.method.span.start) {
402 emit_param_hints(ctx, &m.args, &def, out);
403 }
404 hints_in_expr(ctx, m.class, out);
405 for arg in m.args.iter() {
406 if let Some(value) = &arg.value {
407 hints_in_expr(ctx, value, out);
408 }
409 }
410 }
411 ExprKind::New(n) => {
412 let def = callable_from_symbol(ctx, n.class.span.start).or_else(|| {
413 ident_name(n.class).and_then(|class_name| ctx.defs.get(class_name).cloned())
414 });
415 if let Some(def) = def {
416 emit_param_hints(ctx, &n.args, &def, out);
417 }
418 for arg in n.args.iter() {
419 if let Some(value) = &arg.value {
420 hints_in_expr(ctx, value, out);
421 }
422 }
423 }
424 ExprKind::Assign(a) => {
425 emit_return_type_hint(ctx, a.value, out);
427 hints_in_expr(ctx, a.target, out);
428 hints_in_expr(ctx, a.value, out);
429 }
430 ExprKind::Closure(c) => hints_in_stmts(ctx, &c.body.stmts, out),
432 ExprKind::ArrowFunction(a) => hints_in_expr(ctx, a.body, out),
436 ExprKind::Parenthesized(e) => hints_in_expr(ctx, e, out),
437 ExprKind::Ternary(t) => {
438 hints_in_expr(ctx, t.condition, out);
439 if let Some(then_expr) = t.then_expr {
440 hints_in_expr(ctx, then_expr, out);
441 }
442 hints_in_expr(ctx, t.else_expr, out);
443 }
444 ExprKind::NullCoalesce(n) => {
445 hints_in_expr(ctx, n.left, out);
446 hints_in_expr(ctx, n.right, out);
447 }
448 ExprKind::Binary(b) => {
449 hints_in_expr(ctx, b.left, out);
450 hints_in_expr(ctx, b.right, out);
451 }
452 ExprKind::CloneWith(target, withs) => {
453 hints_in_expr(ctx, target, out);
454 hints_in_expr(ctx, withs, out);
455 }
456 ExprKind::Match(m) => {
457 hints_in_expr(ctx, m.subject, out);
458 for arm in m.arms.iter() {
459 if let Some(conds) = &arm.conditions {
460 for c in conds.iter() {
461 hints_in_expr(ctx, c, out);
462 }
463 }
464 hints_in_expr(ctx, &arm.body, out);
465 }
466 }
467 _ => {}
468 }
469}
470
471fn emit_param_hints(
472 ctx: &HintCtx<'_>,
473 args: &[php_ast::Arg<'_, '_>],
474 def: &CallableSignature,
475 out: &mut Vec<InlayHint>,
476) {
477 for (i, arg) in args.iter().enumerate() {
478 if arg.unpack {
483 break;
484 }
485 if arg.name.is_some() {
487 continue;
488 }
489 let param = if let Some(p) = def.params.get(i) {
491 p
492 } else if def.variadic_last {
493 match def.params.last() {
494 Some(p) => p,
495 None => continue,
496 }
497 } else {
498 continue;
499 };
500 let pos = ctx.sv.position_of(arg.span.start);
501 if pos_in_range(pos, ctx.range) {
502 out.push(make_param_hint(pos, param, def.tooltip.as_ref()));
503 }
504 }
505}
506
507fn emit_return_type_hint(ctx: &HintCtx<'_>, expr: &Expr<'_, '_>, out: &mut Vec<InlayHint>) {
508 let def = match &expr.kind {
509 ExprKind::FunctionCall(f) => {
510 ident_name(f.name).and_then(|_| callable_from_symbol(ctx, f.name.span.start))
511 }
512 ExprKind::MethodCall(m) | ExprKind::NullsafeMethodCall(m) => {
513 callable_from_symbol(ctx, m.method.span.start)
514 }
515 ExprKind::StaticMethodCall(m) => callable_from_symbol(ctx, m.method.span.start),
516 _ => None,
517 };
518 if let Some(def) = def
519 && let Some(ret_type) = &def.return_type
520 {
521 if ret_type == "void" {
522 return;
523 }
524 let pos = ctx.sv.position_of(expr.span.end);
525 if pos_in_range(pos, ctx.range) {
526 out.push(make_return_hint(pos, ret_type, def.tooltip.as_ref()));
527 }
528 }
529}
530
531fn callable_from_local_function(
532 ctx: &HintCtx<'_>,
533 expr: &Expr<'_, '_>,
534) -> Option<CallableSignature> {
535 ident_name(expr)
536 .map(str::to_string)
537 .or_else(|| {
538 if let ExprKind::Variable(n) = &expr.kind {
539 Some(format!("${}", n.as_str()))
540 } else {
541 None
542 }
543 })
544 .and_then(|key| ctx.defs.get(&key).cloned())
545}
546
547fn callable_from_symbol(ctx: &HintCtx<'_>, offset: u32) -> Option<CallableSignature> {
548 let symbol = ctx.analysis?.symbol_at(offset)?.to_symbol()?;
549 callable_signature_from_name(ctx.db?, &symbol)
550}
551
552fn callable_signature_from_name(
553 db: &mir_analyzer::db::MirDbStorage,
554 symbol: &mir_analyzer::Name,
555) -> Option<CallableSignature> {
556 match symbol {
557 mir_analyzer::Name::Function(fqn) => {
558 let f = mir_analyzer::db::find_function(
559 db,
560 mir_analyzer::db::Fqcn::from_str(db, fqn.as_ref()),
561 )?;
562 if mir_analyzer::is_builtin_function(&f.short_name) {
563 return None;
564 }
565 Some(CallableSignature {
566 params: declared_param_names(&f.params),
567 variadic_last: f.params.last().is_some_and(|p| p.is_variadic),
568 return_type: f.effective_return_type().map(ToString::to_string),
569 tooltip: Some(TooltipSymbol {
570 kind: "function",
571 name: f.short_name.to_string(),
572 class: None,
573 }),
574 })
575 }
576 mir_analyzer::Name::Method { class, name } => {
577 let (_, m) = mir_analyzer::db::find_method_in_chain(
578 db,
579 mir_analyzer::db::Fqcn::from_str(db, class.as_ref()),
580 name.as_ref(),
581 )?;
582 Some(CallableSignature {
583 params: declared_param_names(&m.params),
584 variadic_last: m.params.last().is_some_and(|p| p.is_variadic),
585 return_type: m
586 .return_type
587 .as_deref()
588 .or(m.inferred_return_type.as_deref())
589 .map(ToString::to_string),
590 tooltip: Some(TooltipSymbol {
591 kind: "method",
592 name: m.name.to_string(),
593 class: Some(class.to_string()),
594 }),
595 })
596 }
597 mir_analyzer::Name::Class(fqcn) => {
598 let (_, m) = mir_analyzer::db::find_method_in_chain(
599 db,
600 mir_analyzer::db::Fqcn::from_str(db, fqcn.as_ref()),
601 "__construct",
602 )?;
603 Some(CallableSignature {
604 params: declared_param_names(&m.params),
605 variadic_last: m.params.last().is_some_and(|p| p.is_variadic),
606 return_type: None,
607 tooltip: Some(TooltipSymbol {
608 kind: "class",
609 name: fqcn.to_string(),
610 class: None,
611 }),
612 })
613 }
614 _ => None,
615 }
616}
617
618fn declared_param_names(params: &[mir_analyzer::DeclaredParam]) -> Vec<String> {
619 params
620 .iter()
621 .map(|p| p.name.as_str().trim_start_matches('$').to_string())
622 .collect()
623}
624
625fn ident_name<'a>(expr: &'a Expr<'_, '_>) -> Option<&'a str> {
626 if let ExprKind::Identifier(name) = &expr.kind {
627 Some(name)
628 } else {
629 None
630 }
631}
632
633fn make_param_hint(
634 position: Position,
635 param_name: &str,
636 symbol: Option<&TooltipSymbol>,
637) -> InlayHint {
638 InlayHint {
639 position,
640 label: InlayHintLabel::String(format!("{}:", param_name)),
641 kind: Some(InlayHintKind::PARAMETER),
642 text_edits: None,
643 tooltip: None,
644 padding_left: None,
645 padding_right: Some(true),
646 data: hint_data(symbol),
647 }
648}
649
650fn make_return_hint(
651 position: Position,
652 ret_type: &str,
653 symbol: Option<&TooltipSymbol>,
654) -> InlayHint {
655 InlayHint {
656 position,
657 label: InlayHintLabel::String(format!(": {ret_type}")),
658 kind: Some(InlayHintKind::TYPE),
659 text_edits: None,
660 tooltip: None,
661 padding_left: Some(true),
662 padding_right: None,
663 data: hint_data(symbol),
664 }
665}
666
667fn hint_data(symbol: Option<&TooltipSymbol>) -> Option<serde_json::Value> {
668 let symbol = symbol?;
669 let mut value = json!({
670 "php_lsp_fn": symbol.name,
671 "php_lsp_symbol_kind": symbol.kind,
672 });
673 if let Some(class) = &symbol.class {
674 value["php_lsp_class"] = json!(class);
675 }
676 Some(value)
677}
678
679fn make_foreach_type_hint(position: Position, ty: &str) -> InlayHint {
680 InlayHint {
681 position,
682 label: InlayHintLabel::String(format!(": {ty}")),
683 kind: Some(InlayHintKind::TYPE),
684 text_edits: None,
685 tooltip: None,
686 padding_left: Some(true),
687 padding_right: None,
688 data: None,
689 }
690}
691
692fn pos_in_range(pos: Position, range: Range) -> bool {
693 if pos.line < range.start.line || pos.line > range.end.line {
694 return false;
695 }
696 if pos.line == range.start.line && pos.character < range.start.character {
697 return false;
698 }
699 if pos.line == range.end.line && pos.character >= range.end.character {
700 return false;
701 }
702 true
703}