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