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
14fn 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 variadic_last: bool,
35 return_type: Option<String>,
36}
37
38pub 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
59fn collect_defs(stmts: &[Stmt<'_, '_>]) -> HashMap<String, FuncDef> {
62 let mut map = HashMap::new();
63 collect_defs_stmts(stmts, &mut map);
64 map
65}
66
67fn 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 let cn = class.name.as_ref();
104 let qualified = format!("{}::{}", cn, method_name);
105 map.insert(qualified, func_def.clone());
106 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 map.entry(method_name).or_insert(func_def);
116 }
117 }
118 }
119}
120
121fn 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 if let Some(cn) = c.name {
155 let qualified = format!("{}::{}", cn, m.name);
156 map.insert(qualified, func_def.clone());
157 }
158 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 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 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 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
254fn 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 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 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 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(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 ExprKind::Closure(c) => {
452 hints_in_stmts(sv, &c.body.stmts, defs, analysis, range, out);
453 }
454 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 if arg.name.is_some() {
495 continue;
496 }
497 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}