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