1use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
6use tower_lsp::lsp_types::Position;
7
8use crate::document::ast::{ParsedDoc, SourceView};
9use crate::lang::docblock::{docblock_before, parse_docblock};
10
11pub fn parent_class_name(doc: &ParsedDoc, class_name: &str) -> Option<String> {
12 parent_in_stmts(&doc.program().stmts, class_name)
13}
14
15fn parent_in_stmts(stmts: &[Stmt<'_, '_>], class_name: &str) -> Option<String> {
16 for stmt in stmts {
17 match &stmt.kind {
18 StmtKind::Class(c)
19 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
20 {
21 return c.extends.as_ref().map(|n| n.to_string_repr().to_string());
22 }
23 StmtKind::Namespace(ns) => {
24 if let NamespaceBody::Braced(inner) = &ns.body
25 && let found @ Some(_) = parent_in_stmts(&inner.stmts, class_name)
26 {
27 return found;
28 }
29 }
30 _ => {}
31 }
32 }
33 None
34}
35
36#[derive(Debug, Default)]
38pub struct ClassMembers {
39 pub methods: Vec<(String, bool, bool)>,
41 pub properties: Vec<(String, bool)>,
43 pub readonly_properties: Vec<String>,
45 pub constants: Vec<String>,
46 pub parent: Option<String>,
48 pub trait_uses: Vec<String>,
50 pub found: bool,
54}
55
56pub fn members_of_class(doc: &ParsedDoc, class_name: &str) -> ClassMembers {
59 let mut out = ClassMembers::default();
60 out.parent = collect_members_stmts(doc.source(), &doc.program().stmts, class_name, &mut out);
61 out
62}
63
64fn collect_members_stmts(
65 source: &str,
66 stmts: &[Stmt<'_, '_>],
67 class_name: &str,
68 out: &mut ClassMembers,
69) -> Option<String> {
70 for stmt in stmts {
71 match &stmt.kind {
72 StmtKind::Class(c)
73 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
74 {
75 out.found = true;
76 if let Some(raw) = docblock_before(source, stmt.span.start) {
78 let db = parse_docblock(&raw);
79 for prop in &db.properties {
80 out.properties.push((prop.name.clone(), false));
81 }
82 for method in &db.methods {
83 out.methods.push((
84 method.name.clone(),
85 method.is_static,
86 !method.params.is_empty(),
87 ));
88 }
89 }
90 for member in c.body.members.iter() {
91 match &member.kind {
92 ClassMemberKind::Method(m) => {
93 out.methods.push((
94 m.name.to_string(),
95 m.is_static,
96 !m.params.is_empty(),
97 ));
98 if m.name == "__construct" {
99 for p in m.params.iter() {
100 if p.visibility.is_some() {
101 out.properties.push((p.name.to_string(), false));
102 let param_src =
104 &source[p.span.start as usize..p.span.end as usize];
105 if param_src.contains("readonly") {
106 out.readonly_properties.push(p.name.to_string());
107 }
108 }
109 }
110 }
111 }
112 ClassMemberKind::Property(p) => {
113 out.properties.push((p.name.to_string(), p.is_static));
114 if p.is_readonly {
115 out.readonly_properties.push(p.name.to_string());
116 }
117 }
118 ClassMemberKind::ClassConst(c) => {
119 out.constants.push(c.name.to_string());
120 }
121 ClassMemberKind::TraitUse(t) => {
122 for name in t.traits.iter() {
123 out.trait_uses.push(name.to_string_repr().to_string());
124 }
125 }
126 }
127 }
128 return c.extends.as_ref().map(|n| n.to_string_repr().to_string());
129 }
130 StmtKind::Enum(e) if e.name == class_name => {
131 out.found = true;
132 let is_backed = e.scalar_type.is_some();
133 out.properties.push(("name".to_string(), false));
134 if is_backed {
135 out.properties.push(("value".to_string(), false));
136 }
137 out.methods.push(("cases".to_string(), true, false));
138 if is_backed {
139 out.methods.push(("from".to_string(), true, true));
140 out.methods.push(("tryFrom".to_string(), true, true));
141 }
142 for member in e.body.members.iter() {
143 match &member.kind {
144 EnumMemberKind::Case(c) => {
145 out.constants.push(c.name.to_string());
146 }
147 EnumMemberKind::Method(m) => {
148 out.methods.push((
149 m.name.to_string(),
150 m.is_static,
151 !m.params.is_empty(),
152 ));
153 }
154 EnumMemberKind::ClassConst(c) => {
155 out.constants.push(c.name.to_string());
156 }
157 _ => {}
158 }
159 }
160 return None; }
162 StmtKind::Trait(t) if t.name == class_name => {
163 out.found = true;
164 for member in t.body.members.iter() {
165 match &member.kind {
166 ClassMemberKind::Method(m) => {
167 out.methods.push((
168 m.name.to_string(),
169 m.is_static,
170 !m.params.is_empty(),
171 ));
172 }
173 ClassMemberKind::Property(p) => {
174 out.properties.push((p.name.to_string(), p.is_static));
175 }
176 ClassMemberKind::ClassConst(c) => {
177 out.constants.push(c.name.to_string());
178 }
179 ClassMemberKind::TraitUse(t) => {
180 for name in t.traits.iter() {
181 out.trait_uses.push(name.to_string_repr().to_string());
182 }
183 }
184 }
185 }
186 return None; }
188 StmtKind::Namespace(ns) => {
189 if let NamespaceBody::Braced(inner) = &ns.body {
190 let result = collect_members_stmts(source, &inner.stmts, class_name, out);
191 if result.is_some() {
192 return result;
193 }
194 }
195 }
196 _ => {}
197 }
198 }
199 None
200}
201
202pub fn mixin_classes_of(doc: &ParsedDoc, class_name: &str) -> Vec<String> {
204 let source = doc.source();
205 mixin_classes_in_stmts(source, &doc.program().stmts, class_name)
206}
207
208fn mixin_classes_in_stmts(source: &str, stmts: &[Stmt<'_, '_>], class_name: &str) -> Vec<String> {
209 for stmt in stmts {
210 match &stmt.kind {
211 StmtKind::Class(c)
212 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
213 {
214 if let Some(raw) = docblock_before(source, stmt.span.start) {
215 return parse_docblock(&raw).mixins;
216 }
217 return vec![];
218 }
219 StmtKind::Namespace(ns) => {
220 if let NamespaceBody::Braced(inner) = &ns.body {
221 let found = mixin_classes_in_stmts(source, &inner.stmts, class_name);
222 if !found.is_empty() {
223 return found;
224 }
225 }
226 }
227 _ => {}
228 }
229 }
230 vec![]
231}
232
233pub fn enclosing_class_at(_source: &str, doc: &ParsedDoc, position: Position) -> Option<String> {
235 let sv = doc.view();
236 enclosing_class_in_stmts(sv, &doc.program().stmts, position)
237}
238
239pub fn enclosing_class_fqn_at(
245 _source: &str,
246 doc: &ParsedDoc,
247 position: Position,
248) -> Option<String> {
249 let sv = doc.view();
250 enclosing_class_fqn_in_stmts(sv, &doc.program().stmts, position, "")
251}
252
253fn enclosing_class_fqn_in_stmts(
254 sv: SourceView<'_>,
255 stmts: &[Stmt<'_, '_>],
256 pos: Position,
257 ns_prefix: &str,
258) -> Option<String> {
259 let make_fqn = |ns: &str, short: &str| -> String {
260 if ns.is_empty() {
261 short.to_owned()
262 } else {
263 format!("{}\\{}", ns, short)
264 }
265 };
266 let mut current_ns = ns_prefix.to_owned();
267 for stmt in stmts {
268 match &stmt.kind {
269 StmtKind::Class(c) => {
270 let start = sv.position_of(stmt.span.start).line;
271 let end = sv.position_of(stmt.span.end).line;
272 if pos.line >= start && pos.line <= end {
273 return c.name.map(|n| make_fqn(¤t_ns, &n.to_string()));
274 }
275 }
276 StmtKind::Interface(i) => {
277 let start = sv.position_of(stmt.span.start).line;
278 let end = sv.position_of(stmt.span.end).line;
279 if pos.line >= start && pos.line <= end {
280 return Some(make_fqn(¤t_ns, &i.name.to_string()));
281 }
282 }
283 StmtKind::Trait(t) => {
284 let start = sv.position_of(stmt.span.start).line;
285 let end = sv.position_of(stmt.span.end).line;
286 if pos.line >= start && pos.line <= end {
287 return Some(make_fqn(¤t_ns, &t.name.to_string()));
288 }
289 }
290 StmtKind::Enum(e) => {
291 let start = sv.position_of(stmt.span.start).line;
292 let end = sv.position_of(stmt.span.end).line;
293 if pos.line >= start && pos.line <= end {
294 return Some(make_fqn(¤t_ns, &e.name.to_string()));
295 }
296 }
297 StmtKind::Namespace(ns) => {
298 let ns_name = ns
299 .name
300 .as_ref()
301 .map(|n| n.to_string_repr().to_string())
302 .unwrap_or_default();
303 match &ns.body {
304 NamespaceBody::Braced(inner) => {
305 if let Some(found) =
306 enclosing_class_fqn_in_stmts(sv, &inner.stmts, pos, &ns_name)
307 {
308 return Some(found);
309 }
310 }
311 NamespaceBody::Simple => {
312 current_ns = ns_name;
313 }
314 }
315 }
316 _ => {}
317 }
318 }
319 None
320}
321
322pub fn enclosing_class_range_at(
327 doc: &ParsedDoc,
328 position: Position,
329) -> Option<tower_lsp::lsp_types::Range> {
330 let sv = doc.view();
331 enclosing_class_range_in_stmts(sv, &doc.program().stmts, position)
332}
333
334pub fn collect_all_class_ranges(doc: &ParsedDoc) -> Vec<tower_lsp::lsp_types::Range> {
338 let sv = doc.view();
339 let mut out = Vec::new();
340 collect_class_ranges_in_stmts(sv, &doc.program().stmts, &mut out);
341 out
342}
343
344fn collect_class_ranges_in_stmts(
345 sv: SourceView<'_>,
346 stmts: &[Stmt<'_, '_>],
347 out: &mut Vec<tower_lsp::lsp_types::Range>,
348) {
349 for stmt in stmts {
350 match &stmt.kind {
351 StmtKind::Class(_)
352 | StmtKind::Interface(_)
353 | StmtKind::Trait(_)
354 | StmtKind::Enum(_) => {
355 out.push(sv.range_of(stmt.span));
356 }
357 StmtKind::Namespace(ns) => {
358 if let NamespaceBody::Braced(inner) = &ns.body {
359 collect_class_ranges_in_stmts(sv, &inner.stmts, out);
360 }
361 }
362 _ => {}
363 }
364 }
365}
366
367fn enclosing_class_range_in_stmts(
368 sv: SourceView<'_>,
369 stmts: &[Stmt<'_, '_>],
370 pos: Position,
371) -> Option<tower_lsp::lsp_types::Range> {
372 for stmt in stmts {
373 match &stmt.kind {
374 StmtKind::Class(_)
375 | StmtKind::Interface(_)
376 | StmtKind::Trait(_)
377 | StmtKind::Enum(_) => {
378 let r = sv.range_of(stmt.span);
379 if pos.line >= r.start.line && pos.line <= r.end.line {
380 return Some(r);
381 }
382 }
383 StmtKind::Namespace(ns) => {
384 if let NamespaceBody::Braced(inner) = &ns.body
385 && let Some(r) = enclosing_class_range_in_stmts(sv, &inner.stmts, pos)
386 {
387 return Some(r);
388 }
389 }
390 _ => {}
391 }
392 }
393 None
394}
395
396fn enclosing_class_in_stmts(
397 sv: SourceView<'_>,
398 stmts: &[Stmt<'_, '_>],
399 pos: Position,
400) -> Option<String> {
401 for stmt in stmts {
402 match &stmt.kind {
403 StmtKind::Class(c) => {
404 let start = sv.position_of(stmt.span.start).line;
405 let end = sv.position_of(stmt.span.end).line;
406 if pos.line >= start && pos.line <= end {
407 return c.name.map(|n| n.to_string());
408 }
409 }
410 StmtKind::Interface(i) => {
411 let start = sv.position_of(stmt.span.start).line;
412 let end = sv.position_of(stmt.span.end).line;
413 if pos.line >= start && pos.line <= end {
414 return Some(i.name.to_string());
415 }
416 }
417 StmtKind::Trait(t) => {
418 let start = sv.position_of(stmt.span.start).line;
419 let end = sv.position_of(stmt.span.end).line;
420 if pos.line >= start && pos.line <= end {
421 return Some(t.name.to_string());
422 }
423 }
424 StmtKind::Enum(e) => {
425 let start = sv.position_of(stmt.span.start).line;
426 let end = sv.position_of(stmt.span.end).line;
427 if pos.line >= start && pos.line <= end {
428 return Some(e.name.to_string());
429 }
430 }
431 StmtKind::Namespace(ns) => {
432 if let NamespaceBody::Braced(inner) = &ns.body
433 && let Some(found) = enclosing_class_in_stmts(sv, &inner.stmts, pos)
434 {
435 return Some(found);
436 }
437 }
438 _ => {}
439 }
440 }
441 None
442}
443
444pub fn params_of_function(doc: &ParsedDoc, func_name: &str) -> Vec<String> {
446 let mut out = Vec::new();
447 collect_params_stmts(&doc.program().stmts, func_name, &mut out);
448 out
449}
450
451pub fn params_of_method(doc: &ParsedDoc, class_name: &str, method_name: &str) -> Vec<String> {
454 let mut out = Vec::new();
455 collect_method_params_stmts(&doc.program().stmts, class_name, method_name, &mut out);
456 out
457}
458
459fn collect_method_params_stmts(
460 stmts: &[php_ast::Stmt<'_, '_>],
461 class_name: &str,
462 method_name: &str,
463 out: &mut Vec<String>,
464) {
465 for stmt in stmts {
466 match &stmt.kind {
467 StmtKind::Class(c)
468 if c.name.as_ref().map(|n| n.to_string()) == Some(class_name.to_string()) =>
469 {
470 for member in c.body.members.iter() {
471 if let ClassMemberKind::Method(m) = &member.kind
472 && m.name == method_name
473 {
474 for p in m.params.iter() {
475 out.push(p.name.to_string());
476 }
477 return;
478 }
479 }
480 }
481 StmtKind::Namespace(ns) => {
482 if let NamespaceBody::Braced(inner) = &ns.body {
483 collect_method_params_stmts(&inner.stmts, class_name, method_name, out);
484 }
485 }
486 _ => {}
487 }
488 }
489}
490
491pub fn is_enum(doc: &ParsedDoc, class_name: &str) -> bool {
493 is_enum_in_stmts(&doc.program().stmts, class_name)
494}
495
496fn is_enum_in_stmts(stmts: &[Stmt<'_, '_>], name: &str) -> bool {
497 for stmt in stmts {
498 match &stmt.kind {
499 StmtKind::Enum(e) if e.name == name => return true,
500 StmtKind::Namespace(ns) => {
501 if let NamespaceBody::Braced(inner) = &ns.body
502 && is_enum_in_stmts(&inner.stmts, name)
503 {
504 return true;
505 }
506 }
507 _ => {}
508 }
509 }
510 false
511}
512
513pub fn is_backed_enum(doc: &ParsedDoc, class_name: &str) -> bool {
516 enum_backing_type(doc, class_name).is_some()
517}
518
519pub fn enum_backing_type(doc: &ParsedDoc, class_name: &str) -> Option<String> {
523 enum_backing_type_in_stmts(&doc.program().stmts, class_name)
524}
525
526fn enum_backing_type_in_stmts(stmts: &[Stmt<'_, '_>], name: &str) -> Option<String> {
527 for stmt in stmts {
528 match &stmt.kind {
529 StmtKind::Enum(e) if e.name == name => {
530 return e
531 .scalar_type
532 .as_ref()
533 .map(|t| t.to_string_repr().to_string());
534 }
535 StmtKind::Namespace(ns) => {
536 if let NamespaceBody::Braced(inner) = &ns.body
537 && let Some(ty) = enum_backing_type_in_stmts(&inner.stmts, name)
538 {
539 return Some(ty);
540 }
541 }
542 _ => {}
543 }
544 }
545 None
546}
547
548fn collect_params_stmts(stmts: &[Stmt<'_, '_>], func_name: &str, out: &mut Vec<String>) {
549 for stmt in stmts {
550 match &stmt.kind {
551 StmtKind::Function(f) if f.name == func_name => {
552 for p in f.params.iter() {
553 out.push(p.name.to_string());
554 }
555 return;
556 }
557 StmtKind::Class(c) => {
558 for member in c.body.members.iter() {
559 if let ClassMemberKind::Method(m) = &member.kind
560 && m.name == func_name
561 {
562 for p in m.params.iter() {
563 out.push(p.name.to_string());
564 }
565 return;
566 }
567 }
568 }
569 StmtKind::Namespace(ns) => {
570 if let NamespaceBody::Braced(inner) = &ns.body {
571 collect_params_stmts(&inner.stmts, func_name, out);
572 }
573 }
574 _ => {}
575 }
576 }
577}
578
579#[cfg(test)]
580mod tests {
581 use super::*;
582
583 #[test]
584 fn parent_class_name_finds_parent() {
585 let src = "<?php\nclass Base {}\nclass Child extends Base {}";
586 let doc = ParsedDoc::parse(src.to_string());
587 assert_eq!(parent_class_name(&doc, "Child"), Some("Base".to_string()));
588 }
589
590 #[test]
591 fn parent_class_name_returns_none_for_top_level() {
592 let src = "<?php\nclass Base {}";
593 let doc = ParsedDoc::parse(src.to_string());
594 assert!(parent_class_name(&doc, "Base").is_none());
595 }
596
597 #[test]
598 fn members_of_class_includes_parent_field() {
599 let src = "<?php\nclass Base {}\nclass Child extends Base {}";
600 let doc = ParsedDoc::parse(src.to_string());
601 let m = members_of_class(&doc, "Child");
602 assert_eq!(m.parent.as_deref(), Some("Base"));
603 }
604
605 #[test]
606 fn members_of_class_finds_methods() {
607 let src = "<?php\nclass Calc { public function add() {} public function sub() {} }";
608 let doc = ParsedDoc::parse(src.to_string());
609 let members = members_of_class(&doc, "Calc");
610 let names: Vec<&str> = members.methods.iter().map(|(n, _, _)| n.as_str()).collect();
611 assert!(names.contains(&"add"), "missing 'add'");
612 assert!(names.contains(&"sub"), "missing 'sub'");
613 }
614
615 #[test]
616 fn members_of_class_tracks_has_params_per_method() {
617 let src = "<?php\nclass Calc { public function reset() {} public function add(int $x) {} }";
618 let doc = ParsedDoc::parse(src.to_string());
619 let members = members_of_class(&doc, "Calc");
620 let has_params = |name: &str| {
621 members
622 .methods
623 .iter()
624 .find(|(n, _, _)| n == name)
625 .map(|(_, _, p)| *p)
626 };
627 assert_eq!(has_params("reset"), Some(false));
628 assert_eq!(has_params("add"), Some(true));
629 }
630
631 #[test]
632 fn members_of_unknown_class_is_empty() {
633 let src = "<?php\nclass Calc { public function add() {} }";
634 let doc = ParsedDoc::parse(src.to_string());
635 let members = members_of_class(&doc, "Unknown");
636 assert!(members.methods.is_empty());
637 }
638
639 #[test]
640 fn constructor_promoted_params_appear_as_properties() {
641 let src = "<?php\nclass Point {\n public function __construct(\n public float $x,\n public float $y,\n ) {}\n}";
642 let doc = ParsedDoc::parse(src.to_string());
643 let members = members_of_class(&doc, "Point");
644 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
645 assert!(
646 prop_names.contains(&"x"),
647 "promoted param x should be a property"
648 );
649 assert!(
650 prop_names.contains(&"y"),
651 "promoted param y should be a property"
652 );
653 }
654
655 #[test]
656 fn promoted_readonly_params_appear_in_readonly_properties() {
657 let src = "<?php\nclass User {\n public function __construct(\n public readonly string $name,\n public int $age,\n ) {}\n}";
658 let doc = ParsedDoc::parse(src.to_string());
659 let members = members_of_class(&doc, "User");
660 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
661 assert!(
662 prop_names.contains(&"name"),
663 "promoted param name should be a property"
664 );
665 assert!(
666 prop_names.contains(&"age"),
667 "promoted param age should be a property"
668 );
669 assert!(
670 members.readonly_properties.contains(&"name".to_string()),
671 "readonly promoted param name should be in readonly_properties"
672 );
673 assert!(
674 !members.readonly_properties.contains(&"age".to_string()),
675 "non-readonly promoted param age should not be in readonly_properties"
676 );
677 }
678
679 #[test]
680 fn enum_instance_members_include_name() {
681 let src = "<?php\nenum Status { case Active; case Inactive; }";
682 let doc = ParsedDoc::parse(src.to_string());
683 let members = members_of_class(&doc, "Status");
684 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
685 assert!(
686 prop_names.contains(&"name"),
687 "pure enum should expose ->name"
688 );
689 assert!(
690 !prop_names.contains(&"value"),
691 "pure enum should not expose ->value"
692 );
693 }
694
695 #[test]
696 fn backed_enum_exposes_value_and_factory_methods() {
697 let src = "<?php\nenum Color: string { case Red = 'red'; }";
698 let doc = ParsedDoc::parse(src.to_string());
699 let members = members_of_class(&doc, "Color");
700 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
701 let method_names: Vec<&str> = members.methods.iter().map(|(n, _, _)| n.as_str()).collect();
702 assert!(
703 prop_names.contains(&"value"),
704 "backed enum should expose ->value"
705 );
706 assert!(
707 method_names.contains(&"from"),
708 "backed enum should have ::from()"
709 );
710 assert!(
711 method_names.contains(&"tryFrom"),
712 "backed enum should have ::tryFrom()"
713 );
714 assert!(
715 method_names.contains(&"cases"),
716 "enum should have ::cases()"
717 );
718 }
719
720 #[test]
721 fn enum_cases_appear_as_constants() {
722 let src = "<?php\nenum Status { case Active; case Inactive; }";
723 let doc = ParsedDoc::parse(src.to_string());
724 let members = members_of_class(&doc, "Status");
725 assert!(members.constants.contains(&"Active".to_string()));
726 assert!(members.constants.contains(&"Inactive".to_string()));
727 }
728
729 #[test]
730 fn trait_members_are_collected() {
731 let src = "<?php\ntrait Logging { public function log() {} public string $logFile; }";
732 let doc = ParsedDoc::parse(src.to_string());
733 let members = members_of_class(&doc, "Logging");
734 let method_names: Vec<&str> = members.methods.iter().map(|(n, _, _)| n.as_str()).collect();
735 let prop_names: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
736 assert!(
737 method_names.contains(&"log"),
738 "trait method log should be collected"
739 );
740 assert!(
741 prop_names.contains(&"logFile"),
742 "trait property logFile should be collected"
743 );
744 }
745
746 #[test]
747 fn class_with_trait_use_lists_trait() {
748 let src = "<?php\ntrait Logging { public function log() {} }\nclass App { use Logging; }";
749 let doc = ParsedDoc::parse(src.to_string());
750 let members = members_of_class(&doc, "App");
751 assert!(
752 members.trait_uses.contains(&"Logging".to_string()),
753 "should list used trait"
754 );
755 }
756
757 #[test]
758 fn is_enum_pure() {
759 let src = "<?php\nenum Suit { case Hearts; case Clubs; }";
760 let doc = ParsedDoc::parse(src.to_string());
761 assert!(is_enum(&doc, "Suit"));
762 assert!(!is_backed_enum(&doc, "Suit"));
763 }
764
765 #[test]
766 fn is_backed_enum_string() {
767 let src = "<?php\nenum Status: string { case Active = 'active'; }";
768 let doc = ParsedDoc::parse(src.to_string());
769 assert!(is_enum(&doc, "Status"));
770 assert!(is_backed_enum(&doc, "Status"));
771 }
772
773 #[test]
774 fn is_enum_false_for_class() {
775 let src = "<?php\nclass Foo {}";
776 let doc = ParsedDoc::parse(src.to_string());
777 assert!(!is_enum(&doc, "Foo"));
778 assert!(!is_backed_enum(&doc, "Foo"));
779 }
780
781 #[test]
782 fn docblock_property_appears_in_members() {
783 let src =
784 "<?php\n/**\n * @property string $email\n * @property-read int $id\n */\nclass User {}";
785 let doc = ParsedDoc::parse(src.to_string());
786 let members = members_of_class(&doc, "User");
787 let props: Vec<&str> = members.properties.iter().map(|(n, _)| n.as_str()).collect();
788 assert!(props.contains(&"email"));
789 assert!(props.contains(&"id"));
790 }
791
792 #[test]
793 fn docblock_method_appears_in_members() {
794 let src = "<?php\n/**\n * @method User find(int $id)\n * @method static Builder where(string $col, mixed $val)\n */\nclass Model {}";
795 let doc = ParsedDoc::parse(src.to_string());
796 let members = members_of_class(&doc, "Model");
797 let method_names: Vec<&str> = members.methods.iter().map(|(n, _, _)| n.as_str()).collect();
798 assert!(method_names.contains(&"find"));
799 assert!(method_names.contains(&"where"));
800 let where_static = members
801 .methods
802 .iter()
803 .find(|(n, _, _)| n == "where")
804 .map(|(_, s, _)| *s);
805 assert_eq!(where_static, Some(true));
806 let find_has_params = members
807 .methods
808 .iter()
809 .find(|(n, _, _)| n == "find")
810 .map(|(_, _, p)| *p);
811 assert_eq!(find_has_params, Some(true));
812 }
813}