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
15fn 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 variadic_last: bool,
49 return_type: Option<String>,
50}
51
52pub 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 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
85fn collect_defs(stmts: &[Stmt<'_, '_>]) -> HashMap<String, FuncDef> {
88 let mut map = HashMap::new();
89 collect_defs_stmts(stmts, &mut map);
90 map
91}
92
93fn 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 let cn = class.name.as_ref();
130 let qualified = format!("{}::{}", cn, method_name);
131 map.insert(qualified, func_def.clone());
132 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 map.entry(method_name).or_insert(func_def);
142 }
143 }
144 }
145}
146
147fn 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 if let Some(cn) = c.name {
181 let qualified = format!("{}::{}", cn, m.name);
182 map.insert(qualified, func_def.clone());
183 }
184 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 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 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 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
280fn 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 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 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 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(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 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 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 if arg.name.is_some() {
695 continue;
696 }
697 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}