1use std::collections::HashMap;
6
7use php_ast::{
8 BinaryOp, ClassMemberKind, EnumMemberKind, ExprKind, NamespaceBody, Stmt, StmtKind, TypeHint,
9 TypeHintKind,
10};
11use tower_lsp::lsp_types::Position;
12
13use crate::ast::{MethodReturnsMap, ParsedDoc, SourceView};
14use crate::docblock::{docblock_before, parse_docblock};
15use crate::phpstorm_meta::PhpStormMeta;
16
17pub type FunctionReturnsMap = HashMap<String, String>;
19
20pub type StaticMethodReturnsMap = HashMap<String, HashMap<String, String>>;
23
24#[derive(Debug, Default, Clone)]
26pub struct TypeMap(HashMap<String, String>);
27
28impl TypeMap {
29 #[cfg(test)]
33 pub fn from_doc(doc: &ParsedDoc) -> Self {
34 Self::from_doc_with_meta(doc, None, None)
35 }
36
37 pub fn from_doc_with_meta(
42 doc: &ParsedDoc,
43 meta: Option<&PhpStormMeta>,
44 doc_returns: Option<&MethodReturnsMap>,
45 ) -> Self {
46 let owned_returns;
47 let returns: &MethodReturnsMap = match doc_returns {
48 Some(r) => r,
49 None => {
50 owned_returns = build_method_returns(doc);
51 &owned_returns
52 }
53 };
54 let fn_returns = build_function_returns(doc);
55 let mut map = HashMap::new();
56 collect_types_stmts(
57 doc.source(),
58 &doc.program().stmts,
59 &mut map,
60 meta,
61 std::slice::from_ref(&returns),
62 &fn_returns,
63 None,
64 doc,
65 );
66 TypeMap(map)
67 }
68
69 pub fn from_docs_with_meta<'a>(
73 doc: &ParsedDoc,
74 doc_returns: &MethodReturnsMap,
75 other_docs: impl IntoIterator<Item = (&'a ParsedDoc, &'a MethodReturnsMap)>,
76 meta: Option<&'a PhpStormMeta>,
77 ) -> Self {
78 let mut all_returns: Vec<&MethodReturnsMap> = vec![doc_returns];
79 all_returns.extend(other_docs.into_iter().map(|(_, r)| r));
80 let fn_returns = build_function_returns(doc);
81 let mut map = HashMap::new();
82 collect_types_stmts(
83 doc.source(),
84 &doc.program().stmts,
85 &mut map,
86 meta,
87 &all_returns,
88 &fn_returns,
89 None,
90 doc,
91 );
92 TypeMap(map)
93 }
94
95 pub fn from_docs_at_position<'a>(
101 doc: &ParsedDoc,
102 doc_returns: &MethodReturnsMap,
103 other_docs: impl IntoIterator<Item = (&'a ParsedDoc, &'a MethodReturnsMap)>,
104 meta: Option<&'a PhpStormMeta>,
105 position: Position,
106 ) -> Self {
107 let cursor_byte = {
108 let line_starts = doc.line_starts();
109 let line = position.line as usize;
110 if line < line_starts.len() {
111 let line_start = line_starts[line] as usize;
112 let col_byte = crate::util::utf16_offset_to_byte(
113 &doc.source()[line_start..],
114 position.character as usize,
115 );
116 Some((line_start + col_byte) as u32)
117 } else {
118 None
119 }
120 };
121 let mut all_returns: Vec<&MethodReturnsMap> = vec![doc_returns];
122 all_returns.extend(other_docs.into_iter().map(|(_, r)| r));
123 let fn_returns = build_function_returns(doc);
124 let mut map = HashMap::new();
125 collect_types_stmts(
126 doc.source(),
127 &doc.program().stmts,
128 &mut map,
129 meta,
130 &all_returns,
131 &fn_returns,
132 cursor_byte,
133 doc,
134 );
135 TypeMap(map)
136 }
137
138 pub fn get<'a>(&'a self, var: &str) -> Option<&'a str> {
140 self.0.get(var).map(|s| s.as_str())
141 }
142
143 pub(crate) fn chain_type_at_cursor(
147 &self,
148 stmts: &[php_ast::Stmt<'_, '_>],
149 cursor_byte: u32,
150 method_returns: &[&MethodReturnsMap],
151 ) -> Option<String> {
152 find_call_type_in_stmts(stmts, cursor_byte, &self.0, method_returns)
153 }
154}
155
156pub fn build_method_returns(doc: &ParsedDoc) -> MethodReturnsMap {
158 let mut out = HashMap::new();
159 collect_method_returns_stmts(doc.source(), &doc.program().stmts, &mut out, doc);
160 out
161}
162
163pub fn build_function_returns(doc: &ParsedDoc) -> FunctionReturnsMap {
165 let mut out = HashMap::new();
166 collect_function_returns_stmts(doc.source(), &doc.program().stmts, &mut out, doc);
167 out
168}
169
170pub fn build_static_method_returns(doc: &ParsedDoc) -> StaticMethodReturnsMap {
172 let mut out = HashMap::new();
173 collect_static_method_returns_stmts(doc.source(), &doc.program().stmts, &mut out, doc);
174 out
175}
176
177pub(crate) fn resolve_expr_type(
181 expr: &php_ast::Expr<'_, '_>,
182 map: &HashMap<String, String>,
183 method_returns: &[&MethodReturnsMap],
184) -> Option<String> {
185 match &expr.kind {
186 ExprKind::Variable(v) => map.get(&format!("${}", v.as_str())).cloned(),
187 ExprKind::MethodCall(mc) => {
188 let obj_type = resolve_expr_type(mc.object, map, method_returns)?;
189 let method_name = match &mc.method.kind {
190 ExprKind::Identifier(n) => n.as_str(),
191 _ => return None,
192 };
193 lookup_method_return(method_returns, &obj_type, method_name).map(|s| s.to_string())
194 }
195 ExprKind::StaticMethodCall(smc) => {
196 let class_name = match &smc.class.kind {
197 ExprKind::Identifier(n) => n.as_str(),
198 _ => return None,
199 };
200 let method_name = smc.method.name_str()?;
201 lookup_static_method_return(method_returns, class_name, method_name)
202 .map(|s| s.to_string())
203 }
204 ExprKind::CloneWith(obj, _) => resolve_expr_type(obj, map, method_returns),
206 _ => None,
207 }
208}
209
210fn find_call_type_in_stmts(
214 stmts: &[Stmt<'_, '_>],
215 cursor: u32,
216 vars: &HashMap<String, String>,
217 method_returns: &[&MethodReturnsMap],
218) -> Option<String> {
219 for stmt in stmts {
220 if !span_contains_cursor(stmt.span, cursor) {
221 continue;
222 }
223 let result = match &stmt.kind {
224 StmtKind::Expression(e) => find_call_type_in_expr(e, cursor, vars, method_returns),
225 StmtKind::Return(Some(e)) => find_call_type_in_expr(e, cursor, vars, method_returns),
226 StmtKind::Echo(exprs) => exprs
227 .iter()
228 .find_map(|e| find_call_type_in_expr(e, cursor, vars, method_returns)),
229 StmtKind::Function(f) => {
230 find_call_type_in_stmts(&f.body.stmts, cursor, vars, method_returns)
231 }
232 StmtKind::Class(c) => c.body.members.iter().find_map(|m| {
233 if let ClassMemberKind::Method(method) = &m.kind
234 && let Some(body) = &method.body
235 {
236 find_call_type_in_stmts(&body.stmts, cursor, vars, method_returns)
237 } else {
238 None
239 }
240 }),
241 StmtKind::Namespace(ns) => {
242 if let NamespaceBody::Braced(inner) = &ns.body {
243 find_call_type_in_stmts(&inner.stmts, cursor, vars, method_returns)
244 } else {
245 None
246 }
247 }
248 _ => None,
249 };
250 if result.is_some() {
251 return result;
252 }
253 }
254 None
255}
256
257fn find_call_type_in_expr(
258 expr: &php_ast::Expr<'_, '_>,
259 cursor: u32,
260 vars: &HashMap<String, String>,
261 method_returns: &[&MethodReturnsMap],
262) -> Option<String> {
263 if !span_contains_cursor(expr.span, cursor) {
264 return None;
265 }
266 match &expr.kind {
267 ExprKind::MethodCall(mc) | ExprKind::NullsafeMethodCall(mc) => {
268 find_call_type_in_expr(mc.object, cursor, vars, method_returns)
269 .or_else(|| resolve_expr_type(expr, vars, method_returns))
272 }
273 ExprKind::Assign(a) => find_call_type_in_expr(a.value, cursor, vars, method_returns),
274 _ => None,
275 }
276}
277
278#[inline]
279fn span_contains_cursor(span: php_ast::Span, cursor: u32) -> bool {
280 cursor >= span.start && cursor <= span.end
283}
284
285fn lookup_method_return<'a>(
289 maps: &'a [&'a MethodReturnsMap],
290 class_name: &str,
291 method_name: &str,
292) -> Option<&'a str> {
293 for m in maps.iter().rev() {
294 if let Some(class_rets) = m.get(class_name)
295 && let Some(ret) = class_rets.get(method_name)
296 {
297 return Some(ret.as_str());
298 }
299 }
300 None
301}
302
303fn lookup_static_method_return<'a>(
308 maps: &'a [&'a MethodReturnsMap],
309 class_name: &str,
310 method_name: &str,
311) -> Option<&'a str> {
312 lookup_method_return(maps, class_name, method_name)
313}
314
315fn collect_method_returns_stmts(
316 source: &str,
317 stmts: &[Stmt<'_, '_>],
318 out: &mut HashMap<String, HashMap<String, String>>,
319 doc: &ParsedDoc,
320) {
321 for stmt in stmts {
322 match &stmt.kind {
323 StmtKind::Class(c) => {
324 let class_name = match c.name {
325 Some(n) => n.to_string(),
326 None => continue,
327 };
328 for member in c.body.members.iter() {
329 if let ClassMemberKind::Method(m) = &member.kind
330 && let Some(ret) = extract_method_return_class(
331 source,
332 member.span.start,
333 m,
334 &class_name,
335 doc,
336 )
337 {
338 out.entry(class_name.clone())
339 .or_default()
340 .insert(m.name.to_string(), ret);
341 }
342 }
343 }
344 StmtKind::Trait(t) => {
345 let trait_name = t.name.to_string();
346 for member in t.body.members.iter() {
347 if let ClassMemberKind::Method(m) = &member.kind
348 && let Some(ret) = extract_method_return_class(
349 source,
350 member.span.start,
351 m,
352 &trait_name,
353 doc,
354 )
355 {
356 out.entry(trait_name.clone())
357 .or_default()
358 .insert(m.name.to_string(), ret);
359 }
360 }
361 }
362 StmtKind::Enum(e) => {
363 let enum_name = e.name.to_string();
364 for member in e.body.members.iter() {
365 if let EnumMemberKind::Method(m) = &member.kind
366 && let Some(ret) = extract_method_return_class(
367 source,
368 member.span.start,
369 m,
370 &enum_name,
371 doc,
372 )
373 {
374 out.entry(enum_name.clone())
375 .or_default()
376 .insert(m.name.to_string(), ret);
377 }
378 }
379 }
380 StmtKind::Namespace(ns) => {
381 if let NamespaceBody::Braced(inner) = &ns.body {
382 collect_method_returns_stmts(source, &inner.stmts, out, doc);
383 }
384 }
385 _ => {}
386 }
387 }
388}
389
390fn collect_function_returns_stmts(
391 source: &str,
392 stmts: &[Stmt<'_, '_>],
393 out: &mut FunctionReturnsMap,
394 doc: &ParsedDoc,
395) {
396 for stmt in stmts {
397 match &stmt.kind {
398 StmtKind::Function(f) => {
399 if let Some(ret) = extract_function_return_class(source, stmt.span.start, f, doc) {
400 out.insert(f.name.to_string(), ret);
401 }
402 }
403 StmtKind::Namespace(ns) => {
404 if let NamespaceBody::Braced(inner) = &ns.body {
405 collect_function_returns_stmts(source, &inner.stmts, out, doc);
406 }
407 }
408 _ => {}
409 }
410 }
411}
412
413fn collect_static_method_returns_stmts(
414 source: &str,
415 stmts: &[Stmt<'_, '_>],
416 out: &mut StaticMethodReturnsMap,
417 doc: &ParsedDoc,
418) {
419 for stmt in stmts {
420 match &stmt.kind {
421 StmtKind::Class(c) => {
422 let class_name = match c.name {
423 Some(n) => n.to_string(),
424 None => continue,
425 };
426 for member in c.body.members.iter() {
427 if let ClassMemberKind::Method(m) = &member.kind
428 && m.is_static
429 && let Some(ret) = extract_method_return_class(
430 source,
431 member.span.start,
432 m,
433 &class_name,
434 doc,
435 )
436 {
437 out.entry(class_name.clone())
438 .or_default()
439 .insert(m.name.to_string(), ret);
440 }
441 }
442 }
443 StmtKind::Namespace(ns) => {
444 if let NamespaceBody::Braced(inner) = &ns.body {
445 collect_static_method_returns_stmts(source, &inner.stmts, out, doc);
446 }
447 }
448 _ => {}
449 }
450 }
451}
452
453fn extract_method_return_class(
454 source: &str,
455 member_start: u32,
456 m: &php_ast::MethodDecl<'_, '_>,
457 enclosing_class: &str,
458 doc: &ParsedDoc,
459) -> Option<String> {
460 if let Some(hint) = &m.return_type
462 && let Some(s) = type_hint_to_class_string(hint, Some(enclosing_class), Some(doc))
463 {
464 return Some(s);
465 }
466 if let Some(raw) = docblock_before(source, member_start) {
468 let db = parse_docblock(&raw);
469 if let Some(ret) = db.return_type {
470 for part in ret.type_hint.split('|') {
471 let part = part.trim().trim_start_matches('\\').trim_start_matches('?');
472 let short = part.rsplit('\\').next().unwrap_or(part);
473 if short == "self" || short == "static" {
474 return Some(enclosing_class.to_string());
475 }
476 let first = short.chars().next().unwrap_or('_');
477 if first.is_uppercase() && !matches!(short, "void" | "never" | "null") {
478 return Some(short.to_string());
479 }
480 }
481 }
482 }
483 None
484}
485
486fn extract_function_return_class(
487 source: &str,
488 function_start: u32,
489 f: &php_ast::FunctionDecl<'_, '_>,
490 doc: &ParsedDoc,
491) -> Option<String> {
492 if let Some(hint) = &f.return_type
494 && let Some(s) = type_hint_to_class_string(hint, None, Some(doc))
495 {
496 return Some(s);
497 }
498 if let Some(raw) = docblock_before(source, function_start) {
500 let db = parse_docblock(&raw);
501 if let Some(ret) = db.return_type {
502 for part in ret.type_hint.split('|') {
503 let part = part.trim().trim_start_matches('\\').trim_start_matches('?');
504 let short = part.rsplit('\\').next().unwrap_or(part);
505 let first = short.chars().next().unwrap_or('_');
506 if first.is_uppercase() && !matches!(short, "void" | "never" | "null") {
507 return Some(short.to_string());
508 }
509 }
510 }
511 }
512 None
513}
514
515fn type_hint_to_class_string(
523 hint: &TypeHint<'_, '_>,
524 enclosing_class: Option<&str>,
525 doc: Option<&ParsedDoc>,
526) -> Option<String> {
527 use mir_types::Atomic;
528 let union = mir_analyzer::parser::type_from_hint(hint, enclosing_class);
529 let classes: Vec<String> = union
530 .types
531 .iter()
532 .filter_map(|a| match a {
533 Atomic::TNamedObject { fqcn, .. }
534 | Atomic::TSelf { fqcn }
535 | Atomic::TStaticObject { fqcn } => {
536 let short = fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
537 Some(short.to_string())
538 }
539 Atomic::TParent { fqcn } => {
540 if let (Some(doc), Some(enc_class)) = (doc, enclosing_class) {
542 if let Some(parent) = parent_class_name(doc, enc_class) {
543 let short = parent.rsplit('\\').next().unwrap_or(&parent);
544 Some(short.to_string())
545 } else {
546 let short = fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
548 Some(short.to_string())
549 }
550 } else {
551 let short = fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
553 Some(short.to_string())
554 }
555 }
556 Atomic::TIntersection { parts } => {
557 let intersection_classes: Vec<String> = parts
558 .iter()
559 .flat_map(|part| {
560 part.types.iter().filter_map(|a| match a {
561 Atomic::TNamedObject { fqcn, .. }
562 | Atomic::TSelf { fqcn }
563 | Atomic::TStaticObject { fqcn } => {
564 let short = fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
565 Some(short.to_string())
566 }
567 Atomic::TParent { fqcn } => {
568 if let (Some(doc), Some(enc_class)) = (doc, enclosing_class) {
570 if let Some(parent) = parent_class_name(doc, enc_class) {
571 let short = parent.rsplit('\\').next().unwrap_or(&parent);
572 Some(short.to_string())
573 } else {
574 let short =
575 fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
576 Some(short.to_string())
577 }
578 } else {
579 let short = fqcn.rsplit('\\').next().unwrap_or(fqcn.as_ref());
580 Some(short.to_string())
581 }
582 }
583 _ => None,
584 })
585 })
586 .collect();
587 if intersection_classes.is_empty() {
588 None
589 } else {
590 Some(intersection_classes.join("|"))
591 }
592 }
593 _ => None,
594 })
595 .collect();
596 if classes.is_empty() {
597 None
598 } else {
599 Some(classes.join("|"))
600 }
601}
602
603#[allow(clippy::too_many_arguments)]
604fn collect_types_stmts(
605 source: &str,
606 stmts: &[Stmt<'_, '_>],
607 map: &mut HashMap<String, String>,
608 meta: Option<&PhpStormMeta>,
609 method_returns: &[&MethodReturnsMap],
610 function_returns: &FunctionReturnsMap,
611 cursor_byte: Option<u32>,
612 doc: &ParsedDoc,
613) {
614 for stmt in stmts {
615 if let Some(raw) = docblock_before(source, stmt.span.start) {
617 let db = parse_docblock(&raw);
618 if let Some(type_str) = db.var_type {
619 let class_name = type_str
622 .split('|')
623 .map(|p| p.trim().trim_start_matches('\\').trim_start_matches('?'))
624 .find(|p| p.chars().next().map(|c| c.is_uppercase()).unwrap_or(false))
625 .and_then(|p| p.rsplit('\\').next())
626 .map(|p| p.to_string());
627 if let Some(class_name) = class_name {
628 if let Some(vname) = db.var_name {
629 map.insert(format!("${}", vname.as_str()), class_name);
631 } else if let StmtKind::Expression(e) = &stmt.kind {
632 if let ExprKind::Assign(a) = &e.kind
634 && let ExprKind::Variable(vn) = &a.target.kind
635 {
636 map.insert(format!("${}", vn.as_str()), class_name);
637 }
638 }
639 }
640 }
641 }
642
643 match &stmt.kind {
644 StmtKind::Expression(e) => collect_types_expr(
645 source,
646 e,
647 map,
648 meta,
649 method_returns,
650 function_returns,
651 cursor_byte,
652 doc,
653 ),
654 StmtKind::Function(f) => {
655 let in_scope =
657 cursor_byte.is_none_or(|c| stmt.span.start <= c && c <= stmt.span.end);
658 if !in_scope {
659 continue;
660 }
661 if let Some(raw) = docblock_before(source, stmt.span.start) {
663 let db = parse_docblock(&raw);
664 for param in &db.params {
665 let classes: Vec<&str> = param
667 .type_hint
668 .split('|')
669 .map(|p| p.trim().trim_start_matches('\\').trim_start_matches('?'))
670 .filter(|p| p.chars().next().map(|c| c.is_uppercase()).unwrap_or(false))
671 .filter_map(|p| p.rsplit('\\').next())
672 .collect();
673 if !classes.is_empty() {
674 let key = if param.name.starts_with('$') {
675 param.name.clone()
676 } else {
677 format!("${}", param.name)
678 };
679 map.entry(key).or_insert_with(|| classes.join("|"));
680 }
681 }
682 }
683 for p in f.params.iter() {
684 if let Some(hint) = &p.type_hint
685 && let Some(class_str) = type_hint_to_class_string(hint, None, Some(doc))
686 {
687 map.insert(format!("${}", p.name), class_str);
688 }
689 }
690 collect_types_stmts(
691 source,
692 &f.body.stmts,
693 map,
694 meta,
695 method_returns,
696 function_returns,
697 cursor_byte,
698 doc,
699 );
700 }
701 StmtKind::Class(c) => {
702 let class_name = c.name.map(|n| n.to_string());
703 for member in c.body.members.iter() {
704 if let ClassMemberKind::Method(m) = &member.kind {
705 let in_scope = cursor_byte
707 .is_none_or(|cb| member.span.start <= cb && cb <= member.span.end);
708 if !in_scope {
709 continue;
710 }
711 if let Some(raw) = docblock_before(source, member.span.start) {
713 let db = parse_docblock(&raw);
714 for param in &db.params {
715 let classes: Vec<&str> = param
717 .type_hint
718 .split('|')
719 .map(|p| {
720 p.trim().trim_start_matches('\\').trim_start_matches('?')
721 })
722 .filter(|p| {
723 p.chars().next().map(|c| c.is_uppercase()).unwrap_or(false)
724 })
725 .filter_map(|p| p.rsplit('\\').next())
726 .collect();
727 if !classes.is_empty() {
728 let key = if param.name.starts_with('$') {
729 param.name.clone()
730 } else {
731 format!("${}", param.name)
732 };
733 map.entry(key).or_insert_with(|| classes.join("|"));
734 }
735 }
736 }
737 for p in m.params.iter() {
738 if let Some(hint) = &p.type_hint
739 && let Some(class_str) = type_hint_to_class_string(
740 hint,
741 class_name.as_deref(),
742 Some(doc),
743 )
744 {
745 map.insert(format!("${}", p.name), class_str);
746 }
747 }
748 if !m.is_static
750 && let Some(ref cname) = class_name
751 {
752 map.insert("$this".to_string(), cname.clone());
753 }
754 if let Some(body) = &m.body {
755 collect_types_stmts(
756 source,
757 &body.stmts,
758 map,
759 meta,
760 method_returns,
761 function_returns,
762 cursor_byte,
763 doc,
764 );
765 }
766 }
767 }
768 }
769 StmtKind::Trait(t) => {
770 for member in t.body.members.iter() {
771 if let ClassMemberKind::Method(m) = &member.kind {
772 let in_scope = cursor_byte
773 .is_none_or(|cb| member.span.start <= cb && cb <= member.span.end);
774 if !in_scope {
775 continue;
776 }
777 for p in m.params.iter() {
778 if let Some(hint) = &p.type_hint
779 && let Some(class_str) =
780 type_hint_to_class_string(hint, None, Some(doc))
781 {
782 map.insert(format!("${}", p.name), class_str);
783 }
784 }
785 if let Some(body) = &m.body {
786 collect_types_stmts(
787 source,
788 &body.stmts,
789 map,
790 meta,
791 method_returns,
792 function_returns,
793 cursor_byte,
794 doc,
795 );
796 }
797 }
798 }
799 }
800 StmtKind::Enum(e) => {
801 for member in e.body.members.iter() {
802 if let EnumMemberKind::Method(m) = &member.kind {
803 let in_scope = cursor_byte
804 .is_none_or(|cb| member.span.start <= cb && cb <= member.span.end);
805 if !in_scope {
806 continue;
807 }
808 for p in m.params.iter() {
809 if let Some(hint) = &p.type_hint
810 && let Some(class_str) =
811 type_hint_to_class_string(hint, None, Some(doc))
812 {
813 map.insert(format!("${}", p.name), class_str);
814 }
815 }
816 if let Some(body) = &m.body {
817 collect_types_stmts(
818 source,
819 &body.stmts,
820 map,
821 meta,
822 method_returns,
823 function_returns,
824 cursor_byte,
825 doc,
826 );
827 }
828 }
829 }
830 }
831 StmtKind::Namespace(ns) => {
832 if let NamespaceBody::Braced(inner) = &ns.body {
833 collect_types_stmts(
834 source,
835 &inner.stmts,
836 map,
837 meta,
838 method_returns,
839 function_returns,
840 cursor_byte,
841 doc,
842 );
843 }
844 }
845 StmtKind::If(if_stmt) => {
847 if let ExprKind::Binary(b) = &if_stmt.condition.kind
849 && b.op == BinaryOp::Instanceof
850 && let (ExprKind::Variable(var_name), ExprKind::Identifier(class)) =
851 (&b.left.kind, &b.right.kind)
852 {
853 let var_key = format!("${}", var_name.as_str());
854 let narrowed = class
855 .as_str()
856 .trim_start_matches('\\')
857 .rsplit('\\')
858 .next()
859 .unwrap_or(class)
860 .to_string();
861 map.insert(var_key, narrowed);
866 }
867 collect_types_stmts(
868 source,
869 std::slice::from_ref(if_stmt.then_branch),
870 map,
871 meta,
872 method_returns,
873 function_returns,
874 cursor_byte,
875 doc,
876 );
877 for elseif in if_stmt.elseif_branches.iter() {
878 collect_types_stmts(
879 source,
880 std::slice::from_ref(&elseif.body),
881 map,
882 meta,
883 method_returns,
884 function_returns,
885 cursor_byte,
886 doc,
887 );
888 }
889 if let Some(else_branch) = if_stmt.else_branch {
890 collect_types_stmts(
891 source,
892 std::slice::from_ref(else_branch),
893 map,
894 meta,
895 method_returns,
896 function_returns,
897 cursor_byte,
898 doc,
899 );
900 }
901 }
902
903 StmtKind::Foreach(f) => {
905 if let ExprKind::Variable(arr_name) = &f.expr.kind {
906 let elem_key = format!("${}[]", arr_name.as_str());
907 if let Some(elem_type) = map.get(&elem_key).cloned()
908 && let ExprKind::Variable(val_name) = &f.value.kind
909 {
910 map.insert(format!("${}", val_name.as_str()), elem_type);
911 }
912 }
913 collect_types_stmts(
914 source,
915 std::slice::from_ref(f.body),
916 map,
917 meta,
918 method_returns,
919 function_returns,
920 cursor_byte,
921 doc,
922 );
923 }
924 StmtKind::TryCatch(t) => {
927 collect_types_stmts(
928 source,
929 &t.body.stmts,
930 map,
931 meta,
932 method_returns,
933 function_returns,
934 cursor_byte,
935 doc,
936 );
937 for catch in t.catches.iter() {
938 if let Some(var_name) = &catch.var
939 && let Some(first_type) = catch.types.first()
940 {
941 let class_name = first_type
942 .to_string_repr()
943 .trim_start_matches('\\')
944 .rsplit('\\')
945 .next()
946 .unwrap_or("")
947 .to_string();
948 if !class_name.is_empty() {
949 map.insert(format!("${}", var_name), class_name);
950 }
951 }
952 collect_types_stmts(
953 source,
954 &catch.body.stmts,
955 map,
956 meta,
957 method_returns,
958 function_returns,
959 cursor_byte,
960 doc,
961 );
962 }
963 if let Some(finally) = &t.finally {
964 collect_types_stmts(
965 source,
966 &finally.stmts,
967 map,
968 meta,
969 method_returns,
970 function_returns,
971 cursor_byte,
972 doc,
973 );
974 }
975 }
976
977 StmtKind::StaticVar(vars) => {
979 for var in vars.iter() {
980 let var_key = format!("${}", &var.name.to_string());
981 if let Some(default) = &var.default {
982 if let ExprKind::New(new_expr) = &default.kind
983 && let Some(class_name) = extract_class_name(new_expr.class)
984 {
985 map.insert(var_key.clone(), class_name);
986 }
987 if let ExprKind::Array(_) = &default.kind {
988 map.insert(var_key, "array".to_string());
989 }
990 }
991 }
992 }
993
994 _ => {}
995 }
996 }
997}
998
999#[allow(clippy::too_many_arguments)]
1000fn collect_types_expr(
1001 source: &str,
1002 expr: &php_ast::Expr<'_, '_>,
1003 map: &mut HashMap<String, String>,
1004 meta: Option<&PhpStormMeta>,
1005 method_returns: &[&MethodReturnsMap],
1006 function_returns: &FunctionReturnsMap,
1007 cursor_byte: Option<u32>,
1008 doc: &ParsedDoc,
1009) {
1010 match &expr.kind {
1011 ExprKind::Assign(assign) => {
1012 if let ExprKind::Variable(var_name) = &assign.target.kind {
1013 if assign.op == php_ast::AssignOp::Coalesce {
1016 if let ExprKind::New(new_expr) = &assign.value.kind
1017 && let Some(class_name) = extract_class_name(new_expr.class)
1018 {
1019 map.entry(format!("${}", var_name.as_str()))
1020 .or_insert(class_name);
1021 }
1022 collect_types_expr(
1023 source,
1024 assign.value,
1025 map,
1026 meta,
1027 method_returns,
1028 function_returns,
1029 cursor_byte,
1030 doc,
1031 );
1032 return;
1033 }
1034 if let ExprKind::New(new_expr) = &assign.value.kind
1035 && let Some(class_name) = extract_class_name(new_expr.class)
1036 {
1037 map.insert(format!("${}", var_name.as_str()), class_name);
1038 }
1039 if let ExprKind::Variable(src_var) = &assign.value.kind
1041 && let Some(src_type) = map.get(&format!("${}", src_var.as_str())).cloned()
1042 {
1043 map.insert(format!("${}", var_name.as_str()), src_type);
1044 }
1045 if let ExprKind::CloneWith(obj, _overrides) = &assign.value.kind
1047 && let Some(src_type) = resolve_var_type_str(obj, map)
1048 {
1049 map.insert(format!("${}", var_name.as_str()), src_type);
1050 }
1051 if let ExprKind::MethodCall(mc) = &assign.value.kind
1054 && let ExprKind::Identifier(method_name) = &mc.method.kind
1055 && let Some(obj_type) = resolve_expr_type(mc.object, map, method_returns)
1056 && let Some(ret_type) =
1057 lookup_method_return(method_returns, &obj_type, method_name.as_str())
1058 {
1059 map.insert(format!("${}", var_name.as_str()), ret_type.to_string());
1060 }
1061 if let ExprKind::StaticMethodCall(smc) = &assign.value.kind
1064 && let ExprKind::Identifier(class_name) = &smc.class.kind
1065 && let Some(method_name) = smc.method.name_str()
1066 && let Some(ret_type) = lookup_static_method_return(
1067 method_returns,
1068 class_name.as_str(),
1069 method_name,
1070 )
1071 {
1072 map.insert(format!("${}", var_name.as_str()), ret_type.to_string());
1073 }
1074 if let ExprKind::FunctionCall(fc) = &assign.value.kind
1076 && let ExprKind::Identifier(fn_name) = &fc.name.kind
1077 && let Some(ret_type) = function_returns.get(fn_name.as_str())
1078 {
1079 map.insert(format!("${}", var_name.as_str()), ret_type.clone());
1080 }
1081 if let Some(meta) = meta
1083 && let Some(inferred) = infer_from_meta_method_call(assign.value, map, meta)
1084 {
1085 map.insert(format!("${}", var_name.as_str()), inferred);
1086 }
1087 if let Some(elem_type) = extract_array_callback_return_type(assign.value) {
1089 map.insert(format!("${}[]", var_name.as_str()), elem_type);
1090 }
1091 if let ExprKind::StaticPropertyAccess(s) = &assign.value.kind
1094 && let ExprKind::Identifier(class_name) = &s.class.kind
1095 {
1096 map.insert(format!("${}", var_name.as_str()), class_name.to_string());
1097 }
1098 if let ExprKind::ClassConstAccess(c) = &assign.value.kind
1100 && let ExprKind::Identifier(class_name) = &c.class.kind
1101 {
1102 map.insert(format!("${}", var_name.as_str()), class_name.to_string());
1103 }
1104 }
1105 else if let ExprKind::Array(elements) = &assign.target.kind {
1107 for elem in elements.iter() {
1108 if let ExprKind::Variable(var_name) = &elem.value.kind {
1111 map.entry(format!("${}", var_name.as_str())).or_default();
1112 } else if let Some(key) = &elem.key
1113 && let ExprKind::Variable(var_name) = &key.kind
1114 {
1115 map.entry(format!("${}", var_name.as_str())).or_default();
1116 }
1117 }
1118 }
1119 collect_types_expr(
1120 source,
1121 assign.value,
1122 map,
1123 meta,
1124 method_returns,
1125 function_returns,
1126 cursor_byte,
1127 doc,
1128 );
1129 }
1130
1131 ExprKind::StaticMethodCall(s) => {
1133 if let ExprKind::Identifier(class) = &s.class.kind
1134 && class.as_str() == "Closure"
1135 && s.method.name_str() == Some("bind")
1136 && let Some(obj_arg) = s.args.get(1)
1137 && let Some(cls) = resolve_var_type_str(&obj_arg.value, map)
1138 {
1139 map.insert("$this".to_string(), cls);
1140 }
1141 }
1142
1143 ExprKind::MethodCall(m) => {
1145 if let ExprKind::Identifier(method) = &m.method.kind {
1146 let mname = method.as_str();
1147 if (mname == "bindTo" || mname == "call")
1148 && let Some(obj_arg) = m.args.first()
1149 && let Some(cls) = resolve_var_type_str(&obj_arg.value, map)
1150 {
1151 map.insert("$this".to_string(), cls);
1152 }
1153 }
1154 }
1155
1156 ExprKind::Closure(c) => {
1158 for p in c.params.iter() {
1159 if let Some(hint) = &p.type_hint
1160 && let TypeHintKind::Named(name) = &hint.kind
1161 {
1162 map.insert(format!("${}", p.name), name.to_string_repr().to_string());
1163 }
1164 }
1165 let use_var_snapshot: Vec<(String, String)> = c
1169 .use_vars
1170 .iter()
1171 .filter_map(|uv| {
1172 let key = format!("${}", &uv.name.to_string());
1173 map.get(&key).map(|ty| (key, ty.clone()))
1174 })
1175 .collect();
1176 collect_types_stmts(
1177 source,
1178 &c.body.stmts,
1179 map,
1180 meta,
1181 method_returns,
1182 function_returns,
1183 cursor_byte,
1184 doc,
1185 );
1186 for (key, ty) in use_var_snapshot {
1189 map.insert(key, ty);
1190 }
1191 }
1192
1193 ExprKind::ArrowFunction(af) => {
1194 for p in af.params.iter() {
1195 if let Some(hint) = &p.type_hint
1196 && let TypeHintKind::Named(name) = &hint.kind
1197 {
1198 map.insert(format!("${}", p.name), name.to_string_repr().to_string());
1199 }
1200 }
1201 collect_types_expr(
1202 source,
1203 af.body,
1204 map,
1205 meta,
1206 method_returns,
1207 function_returns,
1208 cursor_byte,
1209 doc,
1210 );
1211 }
1212
1213 _ => {}
1214 }
1215}
1216
1217fn extract_array_callback_return_type(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
1221 let ExprKind::FunctionCall(call) = &expr.kind else {
1222 return None;
1223 };
1224 let fn_name = match &call.name.kind {
1225 ExprKind::Identifier(n) => n.as_str(),
1226 _ => return None,
1227 };
1228 if fn_name != "array_map" && fn_name != "array_filter" {
1229 return None;
1230 }
1231 let callback_arg = call.args.first()?;
1232 extract_callback_return_type(&callback_arg.value)
1233}
1234
1235fn extract_callback_return_type(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
1237 let hint = match &expr.kind {
1238 ExprKind::Closure(c) => c.return_type.as_ref()?,
1239 ExprKind::ArrowFunction(af) => af.return_type.as_ref()?,
1240 _ => return None,
1241 };
1242 if let TypeHintKind::Named(name) = &hint.kind {
1243 let s = name.to_string_repr();
1244 let base = s.trim_start_matches('\\');
1245 let short = base.rsplit('\\').next().unwrap_or(base);
1246 if short
1247 .chars()
1248 .next()
1249 .map(|c| c.is_uppercase())
1250 .unwrap_or(false)
1251 {
1252 return Some(short.to_string());
1253 }
1254 }
1255 None
1256}
1257
1258fn resolve_var_type_str(
1260 expr: &php_ast::Expr<'_, '_>,
1261 map: &HashMap<String, String>,
1262) -> Option<String> {
1263 if let ExprKind::Variable(v) = &expr.kind {
1264 map.get(&format!("${}", v.as_str())).cloned()
1265 } else {
1266 None
1267 }
1268}
1269
1270fn extract_class_name(expr: &php_ast::Expr<'_, '_>) -> Option<String> {
1271 match &expr.kind {
1272 ExprKind::Identifier(name) => Some(name.as_str().to_string()),
1273 _ => None,
1274 }
1275}
1276
1277fn infer_from_meta_method_call(
1280 expr: &php_ast::Expr<'_, '_>,
1281 var_map: &HashMap<String, String>,
1282 meta: &PhpStormMeta,
1283) -> Option<String> {
1284 let ExprKind::MethodCall(m) = &expr.kind else {
1285 return None;
1286 };
1287 let receiver_class = match &m.object.kind {
1289 ExprKind::Variable(v) => {
1290 let key = format!("${}", v.as_str());
1291 var_map.get(&key)?.clone()
1292 }
1293 _ => return None,
1294 };
1295 let method_name = match &m.method.kind {
1297 ExprKind::Identifier(n) => n.to_string(),
1298 _ => return None,
1299 };
1300 let arg = m.args.first()?;
1302 let arg_str = match &arg.value.kind {
1303 ExprKind::String(s) => s.trim_start_matches('\\').to_string(),
1304 ExprKind::ClassConstAccess(c) if c.member.name_str() == Some("class") => {
1305 match &c.class.kind {
1306 ExprKind::Identifier(n) => n
1307 .trim_start_matches('\\')
1308 .rsplit('\\')
1309 .next()
1310 .unwrap_or(n)
1311 .to_string(),
1312 _ => return None,
1313 }
1314 }
1315 _ => return None,
1316 };
1317 meta.resolve_return_type(&receiver_class, &method_name, &arg_str)
1318 .map(|s| s.to_string())
1319}
1320
1321pub fn parent_class_name(doc: &ParsedDoc, class_name: &str) -> Option<String> {
1323 parent_in_stmts(&doc.program().stmts, class_name)
1324}
1325
1326fn parent_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str) -> Option<String> {
1327 for stmt in stmts {
1328 match &stmt.kind {
1329 StmtKind::Class(c)
1330 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
1331 {
1332 return c.extends.as_ref().map(|n| n.to_string_repr().to_string());
1333 }
1334 StmtKind::Namespace(ns) => {
1335 if let NamespaceBody::Braced(inner) = &ns.body
1336 && let found @ Some(_) = parent_in_stmts(&inner.stmts, class_name)
1337 {
1338 return found;
1339 }
1340 }
1341 _ => {}
1342 }
1343 }
1344 None
1345}
1346
1347#[derive(Debug, Default)]
1349pub struct ClassMembers {
1350 pub methods: Vec<(String, bool)>,
1352 pub properties: Vec<(String, bool)>,
1354 pub readonly_properties: Vec<String>,
1356 pub constants: Vec<String>,
1357 pub parent: Option<String>,
1359 pub trait_uses: Vec<String>,
1361 pub found: bool,
1365}
1366
1367pub fn members_of_class(doc: &ParsedDoc, class_name: &str) -> ClassMembers {
1370 let mut out = ClassMembers::default();
1371 out.parent = collect_members_stmts(doc.source(), &doc.program().stmts, class_name, &mut out);
1372 out
1373}
1374
1375fn collect_members_stmts(
1376 source: &str,
1377 stmts: &[Stmt<'_, '_>],
1378 class_name: &str,
1379 out: &mut ClassMembers,
1380) -> Option<String> {
1381 for stmt in stmts {
1382 match &stmt.kind {
1383 StmtKind::Class(c)
1384 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
1385 {
1386 out.found = true;
1387 if let Some(raw) = docblock_before(source, stmt.span.start) {
1389 let db = parse_docblock(&raw);
1390 for prop in &db.properties {
1391 out.properties.push((prop.name.clone(), false));
1392 }
1393 for method in &db.methods {
1394 out.methods.push((method.name.clone(), method.is_static));
1395 }
1396 }
1397 for member in c.body.members.iter() {
1398 match &member.kind {
1399 ClassMemberKind::Method(m) => {
1400 out.methods.push((m.name.to_string(), m.is_static));
1401 if m.name == "__construct" {
1403 for p in m.params.iter() {
1404 if p.visibility.is_some() {
1405 out.properties.push((p.name.to_string(), false));
1406 let param_src =
1410 &source[p.span.start as usize..p.span.end as usize];
1411 if param_src.contains("readonly") {
1412 out.readonly_properties.push(p.name.to_string());
1413 }
1414 }
1415 }
1416 }
1417 }
1418 ClassMemberKind::Property(p) => {
1419 out.properties.push((p.name.to_string(), p.is_static));
1420 if p.is_readonly {
1421 out.readonly_properties.push(p.name.to_string());
1422 }
1423 }
1424 ClassMemberKind::ClassConst(c) => {
1425 out.constants.push(c.name.to_string());
1426 }
1427 ClassMemberKind::TraitUse(t) => {
1428 for name in t.traits.iter() {
1429 out.trait_uses.push(name.to_string_repr().to_string());
1430 }
1431 }
1432 }
1433 }
1434 return c.extends.as_ref().map(|n| n.to_string_repr().to_string());
1435 }
1436 StmtKind::Enum(e) if e.name == class_name => {
1437 out.found = true;
1438 let is_backed = e.scalar_type.is_some();
1439 out.properties.push(("name".to_string(), false));
1441 if is_backed {
1442 out.properties.push(("value".to_string(), false));
1443 }
1444 out.methods.push(("cases".to_string(), true));
1446 if is_backed {
1447 out.methods.push(("from".to_string(), true));
1448 out.methods.push(("tryFrom".to_string(), true));
1449 }
1450 for member in e.body.members.iter() {
1452 match &member.kind {
1453 EnumMemberKind::Case(c) => {
1454 out.constants.push(c.name.to_string());
1455 }
1456 EnumMemberKind::Method(m) => {
1457 out.methods.push((m.name.to_string(), m.is_static));
1458 }
1459 EnumMemberKind::ClassConst(c) => {
1460 out.constants.push(c.name.to_string());
1461 }
1462 _ => {}
1463 }
1464 }
1465 return None; }
1467 StmtKind::Trait(t) if t.name == class_name => {
1468 out.found = true;
1469 for member in t.body.members.iter() {
1470 match &member.kind {
1471 ClassMemberKind::Method(m) => {
1472 out.methods.push((m.name.to_string(), m.is_static));
1473 }
1474 ClassMemberKind::Property(p) => {
1475 out.properties.push((p.name.to_string(), p.is_static));
1476 }
1477 ClassMemberKind::ClassConst(c) => {
1478 out.constants.push(c.name.to_string());
1479 }
1480 ClassMemberKind::TraitUse(t) => {
1481 for name in t.traits.iter() {
1482 out.trait_uses.push(name.to_string_repr().to_string());
1483 }
1484 }
1485 }
1486 }
1487 return None; }
1489 StmtKind::Namespace(ns) => {
1490 if let NamespaceBody::Braced(inner) = &ns.body {
1491 let result = collect_members_stmts(source, &inner.stmts, class_name, out);
1492 if result.is_some() {
1493 return result;
1494 }
1495 }
1496 }
1497 _ => {}
1498 }
1499 }
1500 None
1501}
1502
1503pub fn mixin_classes_of(doc: &ParsedDoc, class_name: &str) -> Vec<String> {
1505 let source = doc.source();
1506 mixin_classes_in_stmts(source, &doc.program().stmts, class_name)
1507}
1508
1509fn mixin_classes_in_stmts(source: &str, stmts: &[Stmt<'_, '_>], class_name: &str) -> Vec<String> {
1510 for stmt in stmts {
1511 match &stmt.kind {
1512 StmtKind::Class(c)
1513 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
1514 {
1515 if let Some(raw) = docblock_before(source, stmt.span.start) {
1516 return parse_docblock(&raw).mixins;
1517 }
1518 return vec![];
1519 }
1520 StmtKind::Namespace(ns) => {
1521 if let NamespaceBody::Braced(inner) = &ns.body {
1522 let found = mixin_classes_in_stmts(source, &inner.stmts, class_name);
1523 if !found.is_empty() {
1524 return found;
1525 }
1526 }
1527 }
1528 _ => {}
1529 }
1530 }
1531 vec![]
1532}
1533
1534pub fn enclosing_class_at(_source: &str, doc: &ParsedDoc, position: Position) -> Option<String> {
1536 let sv = doc.view();
1537 enclosing_class_in_stmts(sv, &doc.program().stmts, position)
1538}
1539
1540pub fn enclosing_class_range_at(
1545 doc: &ParsedDoc,
1546 position: Position,
1547) -> Option<tower_lsp::lsp_types::Range> {
1548 let sv = doc.view();
1549 enclosing_class_range_in_stmts(sv, &doc.program().stmts, position)
1550}
1551
1552pub fn collect_all_class_ranges(doc: &ParsedDoc) -> Vec<tower_lsp::lsp_types::Range> {
1556 let sv = doc.view();
1557 let mut out = Vec::new();
1558 collect_class_ranges_in_stmts(sv, &doc.program().stmts, &mut out);
1559 out
1560}
1561
1562fn collect_class_ranges_in_stmts(
1563 sv: SourceView<'_>,
1564 stmts: &[Stmt<'_, '_>],
1565 out: &mut Vec<tower_lsp::lsp_types::Range>,
1566) {
1567 for stmt in stmts {
1568 match &stmt.kind {
1569 StmtKind::Class(_)
1570 | StmtKind::Interface(_)
1571 | StmtKind::Trait(_)
1572 | StmtKind::Enum(_) => {
1573 out.push(sv.range_of(stmt.span));
1574 }
1575 StmtKind::Namespace(ns) => {
1576 if let NamespaceBody::Braced(inner) = &ns.body {
1577 collect_class_ranges_in_stmts(sv, &inner.stmts, out);
1578 }
1579 }
1580 _ => {}
1581 }
1582 }
1583}
1584
1585fn enclosing_class_range_in_stmts(
1586 sv: SourceView<'_>,
1587 stmts: &[Stmt<'_, '_>],
1588 pos: Position,
1589) -> Option<tower_lsp::lsp_types::Range> {
1590 for stmt in stmts {
1591 match &stmt.kind {
1592 StmtKind::Class(_)
1593 | StmtKind::Interface(_)
1594 | StmtKind::Trait(_)
1595 | StmtKind::Enum(_) => {
1596 let r = sv.range_of(stmt.span);
1597 if pos.line >= r.start.line && pos.line <= r.end.line {
1598 return Some(r);
1599 }
1600 }
1601 StmtKind::Namespace(ns) => {
1602 if let NamespaceBody::Braced(inner) = &ns.body
1603 && let Some(r) = enclosing_class_range_in_stmts(sv, &inner.stmts, pos)
1604 {
1605 return Some(r);
1606 }
1607 }
1608 _ => {}
1609 }
1610 }
1611 None
1612}
1613
1614fn enclosing_class_in_stmts(
1615 sv: SourceView<'_>,
1616 stmts: &[Stmt<'_, '_>],
1617 pos: Position,
1618) -> Option<String> {
1619 for stmt in stmts {
1620 match &stmt.kind {
1621 StmtKind::Class(c) => {
1622 let start = sv.position_of(stmt.span.start).line;
1623 let end = sv.position_of(stmt.span.end).line;
1624 if pos.line >= start && pos.line <= end {
1625 return c.name.map(|n| n.to_string());
1626 }
1627 }
1628 StmtKind::Interface(i) => {
1629 let start = sv.position_of(stmt.span.start).line;
1630 let end = sv.position_of(stmt.span.end).line;
1631 if pos.line >= start && pos.line <= end {
1632 return Some(i.name.to_string());
1633 }
1634 }
1635 StmtKind::Trait(t) => {
1636 let start = sv.position_of(stmt.span.start).line;
1637 let end = sv.position_of(stmt.span.end).line;
1638 if pos.line >= start && pos.line <= end {
1639 return Some(t.name.to_string());
1640 }
1641 }
1642 StmtKind::Enum(e) => {
1643 let start = sv.position_of(stmt.span.start).line;
1644 let end = sv.position_of(stmt.span.end).line;
1645 if pos.line >= start && pos.line <= end {
1646 return Some(e.name.to_string());
1647 }
1648 }
1649 StmtKind::Namespace(ns) => {
1650 if let NamespaceBody::Braced(inner) = &ns.body
1651 && let Some(found) = enclosing_class_in_stmts(sv, &inner.stmts, pos)
1652 {
1653 return Some(found);
1654 }
1655 }
1656 _ => {}
1657 }
1658 }
1659 None
1660}
1661
1662pub fn params_of_function(doc: &ParsedDoc, func_name: &str) -> Vec<String> {
1664 let mut out = Vec::new();
1665 collect_params_stmts(&doc.program().stmts, func_name, &mut out);
1666 out
1667}
1668
1669pub fn params_of_method(doc: &ParsedDoc, class_name: &str, method_name: &str) -> Vec<String> {
1672 let mut out = Vec::new();
1673 collect_method_params_stmts(&doc.program().stmts, class_name, method_name, &mut out);
1674 out
1675}
1676
1677fn collect_method_params_stmts(
1678 stmts: &[php_ast::Stmt<'_, '_>],
1679 class_name: &str,
1680 method_name: &str,
1681 out: &mut Vec<String>,
1682) {
1683 for stmt in stmts {
1684 match &stmt.kind {
1685 StmtKind::Class(c)
1686 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
1687 {
1688 for member in c.body.members.iter() {
1689 if let ClassMemberKind::Method(m) = &member.kind
1690 && m.name == method_name
1691 {
1692 for p in m.params.iter() {
1693 out.push(p.name.to_string());
1694 }
1695 return;
1696 }
1697 }
1698 }
1699 StmtKind::Namespace(ns) => {
1700 if let NamespaceBody::Braced(inner) = &ns.body {
1701 collect_method_params_stmts(&inner.stmts, class_name, method_name, out);
1702 }
1703 }
1704 _ => {}
1705 }
1706 }
1707}
1708
1709pub fn is_enum(doc: &ParsedDoc, class_name: &str) -> bool {
1711 is_enum_in_stmts(&doc.program().stmts, class_name)
1712}
1713
1714fn is_enum_in_stmts(stmts: &[Stmt<'_, '_>], name: &str) -> bool {
1715 for stmt in stmts {
1716 match &stmt.kind {
1717 StmtKind::Enum(e) if e.name == name => return true,
1718 StmtKind::Namespace(ns) => {
1719 if let NamespaceBody::Braced(inner) = &ns.body
1720 && is_enum_in_stmts(&inner.stmts, name)
1721 {
1722 return true;
1723 }
1724 }
1725 _ => {}
1726 }
1727 }
1728 false
1729}
1730
1731pub fn is_backed_enum(doc: &ParsedDoc, class_name: &str) -> bool {
1734 is_backed_enum_in_stmts(&doc.program().stmts, class_name)
1735}
1736
1737fn is_backed_enum_in_stmts(stmts: &[Stmt<'_, '_>], name: &str) -> bool {
1738 for stmt in stmts {
1739 match &stmt.kind {
1740 StmtKind::Enum(e) if e.name == name => return e.scalar_type.is_some(),
1741 StmtKind::Namespace(ns) => {
1742 if let NamespaceBody::Braced(inner) = &ns.body
1743 && is_backed_enum_in_stmts(&inner.stmts, name)
1744 {
1745 return true;
1746 }
1747 }
1748 _ => {}
1749 }
1750 }
1751 false
1752}
1753
1754fn collect_params_stmts(stmts: &[Stmt<'_, '_>], func_name: &str, out: &mut Vec<String>) {
1755 for stmt in stmts {
1756 match &stmt.kind {
1757 StmtKind::Function(f) if f.name == func_name => {
1758 for p in f.params.iter() {
1759 out.push(p.name.to_string());
1760 }
1761 return;
1762 }
1763 StmtKind::Class(c) => {
1764 for member in c.body.members.iter() {
1765 if let ClassMemberKind::Method(m) = &member.kind
1766 && m.name == func_name
1767 {
1768 for p in m.params.iter() {
1769 out.push(p.name.to_string());
1770 }
1771 return;
1772 }
1773 }
1774 }
1775 StmtKind::Namespace(ns) => {
1776 if let NamespaceBody::Braced(inner) = &ns.body {
1777 collect_params_stmts(&inner.stmts, func_name, out);
1778 }
1779 }
1780 _ => {}
1781 }
1782 }
1783}
1784
1785#[cfg(test)]
1786mod tests {
1787 use super::*;
1788
1789 #[test]
1790 fn infers_type_from_new_expression() {
1791 let src = "<?php\n$obj = new Foo();";
1792 let doc = ParsedDoc::parse(src.to_string());
1793 let tm = TypeMap::from_doc(&doc);
1794 assert_eq!(tm.get("$obj"), Some("Foo"));
1795 }
1796
1797 #[test]
1798 fn unknown_variable_returns_none() {
1799 let src = "<?php\n$obj = new Foo();";
1800 let doc = ParsedDoc::parse(src.to_string());
1801 let tm = TypeMap::from_doc(&doc);
1802 assert!(tm.get("$other").is_none());
1803 }
1804
1805 #[test]
1806 fn multiple_assignments() {
1807 let src = "<?php\n$a = new Foo();\n$b = new Bar();";
1808 let doc = ParsedDoc::parse(src.to_string());
1809 let tm = TypeMap::from_doc(&doc);
1810 assert_eq!(tm.get("$a"), Some("Foo"));
1811 assert_eq!(tm.get("$b"), Some("Bar"));
1812 }
1813
1814 #[test]
1815 fn later_assignment_overwrites() {
1816 let src = "<?php\n$a = new Foo();\n$a = new Bar();";
1817 let doc = ParsedDoc::parse(src.to_string());
1818 let tm = TypeMap::from_doc(&doc);
1819 assert_eq!(tm.get("$a"), Some("Bar"));
1820 }
1821
1822 #[test]
1823 fn infers_type_from_typed_param() {
1824 let src = "<?php\nfunction process(Mailer $mailer): void { $mailer-> }";
1825 let doc = ParsedDoc::parse(src.to_string());
1826 let tm = TypeMap::from_doc(&doc);
1827 assert_eq!(tm.get("$mailer"), Some("Mailer"));
1828 }
1829
1830 #[test]
1831 fn parent_class_name_finds_parent() {
1832 let src = "<?php\nclass Base {}\nclass Child extends Base {}";
1833 let doc = ParsedDoc::parse(src.to_string());
1834 assert_eq!(parent_class_name(&doc, "Child"), Some("Base".to_string()));
1835 }
1836
1837 #[test]
1838 fn parent_class_name_returns_none_for_top_level() {
1839 let src = "<?php\nclass Base {}";
1840 let doc = ParsedDoc::parse(src.to_string());
1841 assert!(parent_class_name(&doc, "Base").is_none());
1842 }
1843
1844 #[test]
1845 fn members_of_class_includes_parent_field() {
1846 let src = "<?php\nclass Base {}\nclass Child extends Base {}";
1847 let doc = ParsedDoc::parse(src.to_string());
1848 let m = members_of_class(&doc, "Child");
1849 assert_eq!(m.parent.as_deref(), Some("Base"));
1850 }
1851
1852 #[test]
1853 fn members_of_class_finds_methods() {
1854 let src = "<?php\nclass Calc { public function add() {} public function sub() {} }";
1855 let doc = ParsedDoc::parse(src.to_string());
1856 let members = members_of_class(&doc, "Calc");
1857 let names: Vec<&str> = members.methods.iter().map(|(n, _)| n.as_str()).collect();
1858 assert!(names.contains(&"add"), "missing 'add'");
1859 assert!(names.contains(&"sub"), "missing 'sub'");
1860 }
1861
1862 #[test]
1863 fn members_of_unknown_class_is_empty() {
1864 let src = "<?php\nclass Calc { public function add() {} }";
1865 let doc = ParsedDoc::parse(src.to_string());
1866 let members = members_of_class(&doc, "Unknown");
1867 assert!(members.methods.is_empty());
1868 }
1869
1870 #[test]
1871 fn constructor_promoted_params_appear_as_properties() {
1872 let src = "<?php\nclass Point {\n public function __construct(\n public float $x,\n public float $y,\n ) {}\n}";
1873 let doc = ParsedDoc::parse(src.to_string());
1874 let members = members_of_class(&doc, "Point");
1875 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
1876 assert!(
1877 prop_names.contains(&"x"),
1878 "promoted param x should be a property"
1879 );
1880 assert!(
1881 prop_names.contains(&"y"),
1882 "promoted param y should be a property"
1883 );
1884 }
1885
1886 #[test]
1887 fn promoted_readonly_params_appear_in_readonly_properties() {
1888 let src = "<?php\nclass User {\n public function __construct(\n public readonly string $name,\n public int $age,\n ) {}\n}";
1889 let doc = ParsedDoc::parse(src.to_string());
1890 let members = members_of_class(&doc, "User");
1891 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
1892 assert!(
1893 prop_names.contains(&"name"),
1894 "promoted param name should be a property"
1895 );
1896 assert!(
1897 prop_names.contains(&"age"),
1898 "promoted param age should be a property"
1899 );
1900 assert!(
1901 members.readonly_properties.contains(&"name".to_string()),
1902 "readonly promoted param name should be in readonly_properties"
1903 );
1904 assert!(
1905 !members.readonly_properties.contains(&"age".to_string()),
1906 "non-readonly promoted param age should not be in readonly_properties"
1907 );
1908 }
1909
1910 #[test]
1911 fn enum_instance_members_include_name() {
1912 let src = "<?php\nenum Status { case Active; case Inactive; }";
1913 let doc = ParsedDoc::parse(src.to_string());
1914 let members = members_of_class(&doc, "Status");
1915 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
1916 assert!(
1917 prop_names.contains(&"name"),
1918 "pure enum should expose ->name"
1919 );
1920 assert!(
1921 !prop_names.contains(&"value"),
1922 "pure enum should not expose ->value"
1923 );
1924 }
1925
1926 #[test]
1927 fn backed_enum_exposes_value_and_factory_methods() {
1928 let src = "<?php\nenum Color: string { case Red = 'red'; }";
1929 let doc = ParsedDoc::parse(src.to_string());
1930 let members = members_of_class(&doc, "Color");
1931 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
1932 let method_names: Vec<&str> = members.methods.iter().map(|(n, _)| n.as_str()).collect();
1933 assert!(
1934 prop_names.contains(&"value"),
1935 "backed enum should expose ->value"
1936 );
1937 assert!(
1938 method_names.contains(&"from"),
1939 "backed enum should have ::from()"
1940 );
1941 assert!(
1942 method_names.contains(&"tryFrom"),
1943 "backed enum should have ::tryFrom()"
1944 );
1945 assert!(
1946 method_names.contains(&"cases"),
1947 "enum should have ::cases()"
1948 );
1949 }
1950
1951 #[test]
1952 fn enum_cases_appear_as_constants() {
1953 let src = "<?php\nenum Status { case Active; case Inactive; }";
1954 let doc = ParsedDoc::parse(src.to_string());
1955 let members = members_of_class(&doc, "Status");
1956 assert!(members.constants.contains(&"Active".to_string()));
1957 assert!(members.constants.contains(&"Inactive".to_string()));
1958 }
1959
1960 #[test]
1961 fn trait_members_are_collected() {
1962 let src = "<?php\ntrait Logging { public function log() {} public string $logFile; }";
1963 let doc = ParsedDoc::parse(src.to_string());
1964 let members = members_of_class(&doc, "Logging");
1965 let method_names: Vec<&str> = members.methods.iter().map(|(n, _)| n.as_str()).collect();
1966 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
1967 assert!(
1968 method_names.contains(&"log"),
1969 "trait method log should be collected"
1970 );
1971 assert!(
1972 prop_names.contains(&"logFile"),
1973 "trait property logFile should be collected"
1974 );
1975 }
1976
1977 #[test]
1978 fn class_with_trait_use_lists_trait() {
1979 let src = "<?php\ntrait Logging { public function log() {} }\nclass App { use Logging; }";
1980 let doc = ParsedDoc::parse(src.to_string());
1981 let members = members_of_class(&doc, "App");
1982 assert!(
1983 members.trait_uses.contains(&"Logging".to_string()),
1984 "should list used trait"
1985 );
1986 }
1987
1988 #[test]
1989 fn var_docblock_with_explicit_varname_infers_type() {
1990 let src = "<?php\n/** @var Mailer $mailer */\n$mailer = $container->get('mailer');";
1991 let doc = ParsedDoc::parse(src.to_string());
1992 let tm = TypeMap::from_doc(&doc);
1993 assert_eq!(
1994 tm.get("$mailer"),
1995 Some("Mailer"),
1996 "@var with explicit name should map the variable"
1997 );
1998 }
1999
2000 #[test]
2001 fn var_docblock_without_varname_infers_from_assignment() {
2002 let src = "<?php\n/** @var Repository */\n$repo = $this->getRepository();";
2003 let doc = ParsedDoc::parse(src.to_string());
2004 let tm = TypeMap::from_doc(&doc);
2005 assert_eq!(
2006 tm.get("$repo"),
2007 Some("Repository"),
2008 "@var without name should use assignment LHS"
2009 );
2010 }
2011
2012 #[test]
2013 fn var_docblock_does_not_map_primitive_types() {
2014 let src = "<?php\n/** @var string */\n$name = 'hello';";
2015 let doc = ParsedDoc::parse(src.to_string());
2016 let tm = TypeMap::from_doc(&doc);
2017 assert!(
2019 tm.get("$name").is_none(),
2020 "primitive @var should not produce a class mapping"
2021 );
2022 }
2023
2024 #[test]
2025 fn var_nullable_docblock_maps_to_class() {
2026 let src = "<?php\n/** @var ?Mailer $mailer */\n$mailer = null;";
2029 let doc = ParsedDoc::parse(src.to_string());
2030 let tm = TypeMap::from_doc(&doc);
2031 assert_eq!(
2032 tm.get("$mailer"),
2033 Some("Mailer"),
2034 "@var ?Foo should map to 'Foo', not 'Foo|null'"
2035 );
2036 }
2037
2038 #[test]
2039 fn var_union_docblock_maps_first_class() {
2040 let src = "<?php\n/** @var Repository|null $repo */\n$repo = null;";
2042 let doc = ParsedDoc::parse(src.to_string());
2043 let tm = TypeMap::from_doc(&doc);
2044 assert_eq!(
2045 tm.get("$repo"),
2046 Some("Repository"),
2047 "@var Foo|null should map to 'Foo', not 'Foo|null'"
2048 );
2049 }
2050
2051 #[test]
2052 fn is_enum_pure() {
2053 let src = "<?php\nenum Suit { case Hearts; case Clubs; }";
2054 let doc = ParsedDoc::parse(src.to_string());
2055 assert!(is_enum(&doc, "Suit"));
2056 assert!(!is_backed_enum(&doc, "Suit"));
2057 }
2058
2059 #[test]
2060 fn is_backed_enum_string() {
2061 let src = "<?php\nenum Status: string { case Active = 'active'; }";
2062 let doc = ParsedDoc::parse(src.to_string());
2063 assert!(is_enum(&doc, "Status"));
2064 assert!(is_backed_enum(&doc, "Status"));
2065 }
2066
2067 #[test]
2068 fn is_enum_false_for_class() {
2069 let src = "<?php\nclass Foo {}";
2070 let doc = ParsedDoc::parse(src.to_string());
2071 assert!(!is_enum(&doc, "Foo"));
2072 assert!(!is_backed_enum(&doc, "Foo"));
2073 }
2074
2075 #[test]
2076 fn array_map_with_typed_closure_populates_element_type() {
2077 let src = "<?php\n$objs = new Foo();\n$result = array_map(fn($x): Bar => $x->transform(), $objs);";
2078 let doc = ParsedDoc::parse(src.to_string());
2079 let tm = TypeMap::from_doc(&doc);
2080 assert_eq!(
2081 tm.get("$result[]"),
2082 Some("Bar"),
2083 "array_map with typed fn callback should store element type as $result[]"
2084 );
2085 }
2086
2087 #[test]
2088 fn foreach_propagates_array_map_element_type() {
2089 let src = "<?php\n$items = array_map(fn($x): Widget => $x, []);\nforeach ($items as $item) { $item-> }";
2090 let doc = ParsedDoc::parse(src.to_string());
2091 let tm = TypeMap::from_doc(&doc);
2092 assert_eq!(
2093 tm.get("$item"),
2094 Some("Widget"),
2095 "foreach over array_map result should propagate element type to loop variable"
2096 );
2097 }
2098
2099 #[test]
2100 fn closure_use_var_type_is_available_inside_body() {
2101 let src = "<?php\n$svc = new PaymentService();\n$fn = function() use ($svc) { $svc->process(); };";
2102 let doc = ParsedDoc::parse(src.to_string());
2103 let tm = TypeMap::from_doc(&doc);
2104 assert_eq!(
2105 tm.get("$svc"),
2106 Some("PaymentService"),
2107 "captured use variable should retain its outer type inside closure body"
2108 );
2109 }
2110
2111 #[test]
2112 fn closure_use_var_inner_assignment_does_not_override_outer_type() {
2113 let src = "<?php\n$svc = new PaymentService();\n$fn = function() use ($svc) { $svc = new OtherService(); };";
2116 let doc = ParsedDoc::parse(src.to_string());
2117 let tm = TypeMap::from_doc(&doc);
2118 assert_eq!(
2120 tm.get("$svc"),
2121 Some("PaymentService"),
2122 "outer type should not be overwritten by inner assignment in closure"
2123 );
2124 }
2125
2126 #[test]
2127 fn closure_bind_maps_this_to_obj_class() {
2128 let src = "<?php\n$service = new Mailer();\n$fn = Closure::bind(function() {}, $service);";
2129 let doc = ParsedDoc::parse(src.to_string());
2130 let tm = TypeMap::from_doc(&doc);
2131 assert_eq!(
2132 tm.get("$this"),
2133 Some("Mailer"),
2134 "Closure::bind with typed object should map $this to that class"
2135 );
2136 }
2137
2138 #[test]
2139 fn instanceof_narrows_variable_type() {
2140 let src = "<?php\nif ($x instanceof Foo) { $x->foo(); }";
2141 let doc = ParsedDoc::parse(src.to_string());
2142 let tm = TypeMap::from_doc(&doc);
2143 assert_eq!(
2144 tm.get("$x"),
2145 Some("Foo"),
2146 "instanceof should narrow $x to Foo inside the if body"
2147 );
2148 }
2149
2150 #[test]
2151 fn instanceof_narrows_fqn_to_short_name() {
2152 let src = "<?php\nif ($x instanceof App\\Services\\Mailer) { $x->send(); }";
2153 let doc = ParsedDoc::parse(src.to_string());
2154 let tm = TypeMap::from_doc(&doc);
2155 assert_eq!(
2156 tm.get("$x"),
2157 Some("Mailer"),
2158 "instanceof with FQN should narrow to short name"
2159 );
2160 }
2161
2162 #[test]
2163 fn closure_bind_to_maps_this_to_obj_class() {
2164 let src = "<?php\n$svc = new Logger();\n$fn = function() {};\n$bound = $fn->bindTo($svc);";
2165 let doc = ParsedDoc::parse(src.to_string());
2166 let tm = TypeMap::from_doc(&doc);
2167 assert_eq!(
2168 tm.get("$this"),
2169 Some("Logger"),
2170 "bindTo() should map $this to the bound object's class"
2171 );
2172 }
2173
2174 #[test]
2175 fn param_docblock_type_inferred() {
2176 let src = "<?php\n/**\n * @param Mailer $mailer\n */\nfunction send($mailer) { $mailer-> }";
2177 let doc = ParsedDoc::parse(src.to_string());
2178 let tm = TypeMap::from_doc(&doc);
2179 assert_eq!(tm.get("$mailer"), Some("Mailer"));
2180 }
2181
2182 #[test]
2183 fn param_docblock_does_not_override_ast_hint() {
2184 let src = "<?php\n/**\n * @param OtherClass $x\n */\nfunction foo(Foo $x) {}";
2185 let doc = ParsedDoc::parse(src.to_string());
2186 let tm = TypeMap::from_doc(&doc);
2187 assert_eq!(tm.get("$x"), Some("Foo"));
2189 }
2190
2191 #[test]
2192 fn method_chain_return_type_from_ast_hint() {
2193 let src = "<?php\nclass Repo {\n public function findFirst(): User { }\n}\nclass User { public function getName(): string {} }\n$repo = new Repo();\n$user = $repo->findFirst();";
2194 let doc = ParsedDoc::parse(src.to_string());
2195 let tm = TypeMap::from_doc(&doc);
2196 assert_eq!(tm.get("$user"), Some("User"));
2197 }
2198
2199 #[test]
2200 fn method_chain_return_type_from_docblock() {
2201 let src = "<?php\nclass Repo {\n /** @return Product */\n public function latest() {}\n}\n$repo = new Repo();\n$product = $repo->latest();";
2202 let doc = ParsedDoc::parse(src.to_string());
2203 let tm = TypeMap::from_doc(&doc);
2204 assert_eq!(tm.get("$product"), Some("Product"));
2205 }
2206
2207 #[test]
2208 fn not_null_check_preserves_existing_type() {
2209 let src = "<?php\n$x = new Foo();\nif ($x !== null) { $x-> }";
2210 let doc = ParsedDoc::parse(src.to_string());
2211 let tm = TypeMap::from_doc(&doc);
2212 assert_eq!(tm.get("$x"), Some("Foo"));
2213 }
2214
2215 #[test]
2216 fn self_return_type_resolves_to_class() {
2217 let src = "<?php\nclass Builder {\n public function setName(string $n): self { return $this; }\n}\n$b = new Builder();\n$b2 = $b->setName('x');";
2218 let doc = ParsedDoc::parse(src.to_string());
2219 let tm = TypeMap::from_doc(&doc);
2220 assert_eq!(tm.get("$b2"), Some("Builder"));
2221 }
2222
2223 #[test]
2224 fn null_coalesce_assign_infers_type() {
2225 let src = "<?php\n$obj ??= new Foo();";
2226 let doc = ParsedDoc::parse(src.to_string());
2227 let tm = TypeMap::from_doc(&doc);
2228 assert_eq!(tm.get("$obj"), Some("Foo"));
2229 }
2230
2231 #[test]
2232 fn docblock_property_appears_in_members() {
2233 let src =
2234 "<?php\n/**\n * @property string $email\n * @property-read int $id\n */\nclass User {}";
2235 let doc = ParsedDoc::parse(src.to_string());
2236 let members = members_of_class(&doc, "User");
2237 let props: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
2238 assert!(props.contains(&"email"));
2239 assert!(props.contains(&"id"));
2240 }
2241
2242 #[test]
2243 fn docblock_method_appears_in_members() {
2244 let src = "<?php\n/**\n * @method User find(int $id)\n * @method static Builder where(string $col, mixed $val)\n */\nclass Model {}";
2245 let doc = ParsedDoc::parse(src.to_string());
2246 let members = members_of_class(&doc, "Model");
2247 let method_names: Vec<&str> = members.methods.iter().map(|(n, _)| n.as_str()).collect();
2248 assert!(method_names.contains(&"find"));
2249 assert!(method_names.contains(&"where"));
2250 let where_static = members
2251 .methods
2252 .iter()
2253 .find(|(n, _)| n == "where")
2254 .map(|(_, s)| *s);
2255 assert_eq!(where_static, Some(true));
2256 }
2257
2258 #[test]
2259 fn union_type_param_maps_both_classes() {
2260 let src = "<?php\nfunction f(Foo|Bar $x) {}";
2262 let doc = ParsedDoc::parse(src.to_string());
2263 let tm = TypeMap::from_doc(&doc);
2264 let val = tm.get("$x").expect("$x should be in the type map");
2265 assert!(
2266 val.contains("Foo"),
2267 "union type should contain 'Foo', got: {}",
2268 val
2269 );
2270 assert!(
2271 val.contains("Bar"),
2272 "union type should contain 'Bar', got: {}",
2273 val
2274 );
2275 }
2276
2277 #[test]
2278 fn nullable_param_resolves_to_class() {
2279 let src = "<?php\nfunction f(?Foo $x) {}";
2281 let doc = ParsedDoc::parse(src.to_string());
2282 let tm = TypeMap::from_doc(&doc);
2283 assert_eq!(
2284 tm.get("$x"),
2285 Some("Foo"),
2286 "nullable type hint ?Foo should map $x to Foo"
2287 );
2288 }
2289
2290 #[test]
2291 fn static_return_type_resolves_to_class() {
2292 let src = concat!(
2294 "<?php\n",
2295 "class Builder {\n",
2296 " public function build(): static { return $this; }\n",
2297 "}\n",
2298 "$b = new Builder();\n",
2299 "$b2 = $b->build();\n",
2300 );
2301 let doc = ParsedDoc::parse(src.to_string());
2302 let tm = TypeMap::from_doc(&doc);
2303 assert_eq!(
2304 tm.get("$b2"),
2305 Some("Builder"),
2306 "method returning :static should resolve to the enclosing class 'Builder'"
2307 );
2308 }
2309
2310 #[test]
2311 fn null_assignment_does_not_overwrite_class() {
2312 let src = "<?php\n$x = new Foo();\n$x = null;\n";
2315 let doc = ParsedDoc::parse(src.to_string());
2316 let tm = TypeMap::from_doc(&doc);
2317 assert_eq!(
2320 tm.get("$x"),
2321 Some("Foo"),
2322 "$x should retain its Foo type after being assigned null"
2323 );
2324 }
2325
2326 #[test]
2327 fn infers_type_from_assignment_inside_trait_method() {
2328 let src = "<?php\ntrait Builder { public function make(): void { $obj = new Widget(); } }";
2329 let doc = ParsedDoc::parse(src.to_string());
2330 let tm = TypeMap::from_doc(&doc);
2331 assert_eq!(
2332 tm.get("$obj"),
2333 Some("Widget"),
2334 "type map should walk into trait method bodies"
2335 );
2336 }
2337
2338 #[test]
2339 fn infers_type_from_assignment_inside_enum_method() {
2340 let src = "<?php\nenum Color { case Red; public function make(): void { $obj = new Palette(); } }";
2341 let doc = ParsedDoc::parse(src.to_string());
2342 let tm = TypeMap::from_doc(&doc);
2343 assert_eq!(
2344 tm.get("$obj"),
2345 Some("Palette"),
2346 "type map should walk into enum method bodies"
2347 );
2348 }
2349}