1#![allow(deprecated)]
2
3use std::sync::Arc;
4
5use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
6use tower_lsp::lsp_types::{
7 DocumentSymbol, Location, OneOf, Position, Range, SymbolInformation, SymbolKind, Url,
8 WorkspaceSymbol,
9};
10
11use crate::ast::{ParsedDoc, SourceView, name_range};
12use crate::docblock::{docblock_before, parse_docblock};
13
14pub fn document_symbols(_source: &str, doc: &ParsedDoc) -> Vec<DocumentSymbol> {
15 let sv = doc.view();
16 symbols_from_statements(sv, &doc.program().stmts)
17}
18
19pub fn resolve_workspace_symbol(
23 mut symbol: WorkspaceSymbol,
24 docs: &[(Url, Arc<ParsedDoc>)],
25) -> WorkspaceSymbol {
26 let uri = match &symbol.location {
27 OneOf::Left(_) => return symbol,
29 OneOf::Right(wl) => wl.uri.clone(),
30 };
31 for (doc_uri, doc) in docs {
32 if doc_uri == &uri {
33 let range = name_range(doc.source(), doc.line_starts(), &symbol.name);
34 symbol.location = OneOf::Left(Location { uri, range });
35 break;
36 }
37 }
38 symbol
39}
40
41fn parse_kind_filter(query: &str) -> (Option<SymbolKind>, &str) {
54 let Some(rest) = query.strip_prefix('#') else {
55 return (None, query);
56 };
57 let (prefix, term) = match rest.split_once(':') {
58 Some((p, t)) => (p, t),
59 None => return (None, query),
60 };
61 let kind = match prefix.to_lowercase().as_str() {
62 "class" | "c" => SymbolKind::CLASS,
63 "fn" | "function" | "f" => SymbolKind::FUNCTION,
64 "method" | "m" => SymbolKind::METHOD,
65 "interface" | "i" => SymbolKind::INTERFACE,
66 "enum" | "e" => SymbolKind::ENUM,
67 "const" | "constant" => SymbolKind::CONSTANT,
68 "prop" | "property" | "p" => SymbolKind::PROPERTY,
69 _ => return (None, query),
70 };
71 (Some(kind), term)
72}
73
74fn symbols_from_statements(sv: SourceView<'_>, stmts: &[Stmt<'_, '_>]) -> Vec<DocumentSymbol> {
75 let mut symbols = Vec::new();
76 for stmt in stmts {
77 match &stmt.kind {
78 StmtKind::Namespace(ns) => {
79 if let NamespaceBody::Braced(inner) = &ns.body {
80 symbols.extend(symbols_from_statements(sv, inner));
81 }
82 }
83 _ => {
84 if let Some(sym) = statement_to_symbol(sv, stmt) {
85 symbols.push(sym);
86 }
87 }
88 }
89 }
90 symbols
91}
92
93fn stmt_range(sv: SourceView<'_>, stmt: &Stmt<'_, '_>) -> Range {
94 let start = sv.position_of(stmt.span.start);
95 let end = sv.position_of(stmt.span.end);
96 Range { start, end }
97}
98
99fn member_range(sv: SourceView<'_>, member: &php_ast::ClassMember<'_, '_>) -> Range {
100 let start = sv.position_of(member.span.start);
101 let end = sv.position_of(member.span.end);
102 Range { start, end }
103}
104
105fn param_range(sv: SourceView<'_>, param: &php_ast::Param<'_, '_>) -> Range {
106 let start = sv.position_of(param.span.start);
107 let end = sv.position_of(param.span.end);
108 Range { start, end }
109}
110
111fn statement_to_symbol(sv: SourceView<'_>, stmt: &Stmt<'_, '_>) -> Option<DocumentSymbol> {
112 match &stmt.kind {
113 StmtKind::Function(f) => {
114 let range = stmt_range(sv, stmt);
115 let selection_range = sv.name_range(f.name);
116 let detail = Some(format_fn_signature(&f.params, f.return_type.as_ref()));
117 let is_deprecated = docblock_before(sv.source(), stmt.span.start)
118 .filter(|raw| parse_docblock(raw).deprecated.is_some())
119 .map(|_| true);
120
121 let param_children: Vec<DocumentSymbol> = f
122 .params
123 .iter()
124 .map(|p| {
125 let prange = param_range(sv, p);
126 let psel = sv.name_range(p.name);
127 DocumentSymbol {
128 name: format!("${}", p.name),
129 detail: None,
130 kind: SymbolKind::VARIABLE,
131 tags: None,
132 deprecated: None,
133 range: prange,
134 selection_range: psel,
135 children: None,
136 }
137 })
138 .collect();
139
140 Some(DocumentSymbol {
141 name: f.name.to_string(),
142 detail,
143 kind: SymbolKind::FUNCTION,
144 tags: None,
145 deprecated: is_deprecated,
146 range,
147 selection_range,
148 children: if param_children.is_empty() {
149 None
150 } else {
151 Some(param_children)
152 },
153 })
154 }
155
156 StmtKind::Class(c) => {
157 let name = c.name?;
158 let range = stmt_range(sv, stmt);
159 let selection_range = sv.name_range(name);
160 let class_deprecated = docblock_before(sv.source(), stmt.span.start)
161 .filter(|raw| parse_docblock(raw).deprecated.is_some())
162 .map(|_| true);
163
164 let children: Vec<DocumentSymbol> = c
165 .members
166 .iter()
167 .flat_map(|member| -> Vec<DocumentSymbol> {
168 match &member.kind {
169 ClassMemberKind::Method(m) => {
170 let mrange = member_range(sv, member);
171 let msel = sv.name_range(m.name);
172 let detail =
173 Some(format_fn_signature(&m.params, m.return_type.as_ref()));
174 let method_deprecated = docblock_before(sv.source(), member.span.start)
175 .filter(|raw| parse_docblock(raw).deprecated.is_some())
176 .map(|_| true);
177 vec![DocumentSymbol {
178 name: m.name.to_string(),
179 detail,
180 kind: SymbolKind::METHOD,
181 tags: None,
182 deprecated: method_deprecated,
183 range: mrange,
184 selection_range: msel,
185 children: None,
186 }]
187 }
188 ClassMemberKind::Property(p) => {
189 let prange = member_range(sv, member);
190 let psel = sv.name_range(p.name);
191 let prop_deprecated = docblock_before(sv.source(), member.span.start)
192 .filter(|raw| parse_docblock(raw).deprecated.is_some())
193 .map(|_| true);
194 vec![DocumentSymbol {
195 name: format!("${}", p.name),
196 detail: None,
197 kind: SymbolKind::PROPERTY,
198 tags: None,
199 deprecated: prop_deprecated,
200 range: prange,
201 selection_range: psel,
202 children: None,
203 }]
204 }
205 ClassMemberKind::ClassConst(cc) => {
206 let crange = member_range(sv, member);
207 let csel = sv.name_range(cc.name);
208 let const_deprecated = docblock_before(sv.source(), member.span.start)
209 .filter(|raw| parse_docblock(raw).deprecated.is_some())
210 .map(|_| true);
211 vec![DocumentSymbol {
212 name: cc.name.to_string(),
213 detail: None,
214 kind: SymbolKind::CONSTANT,
215 tags: None,
216 deprecated: const_deprecated,
217 range: crange,
218 selection_range: csel,
219 children: None,
220 }]
221 }
222 _ => vec![],
223 }
224 })
225 .collect();
226
227 Some(DocumentSymbol {
228 name: name.to_string(),
229 detail: None,
230 kind: SymbolKind::CLASS,
231 tags: None,
232 deprecated: class_deprecated,
233 range,
234 selection_range,
235 children: if children.is_empty() {
236 None
237 } else {
238 Some(children)
239 },
240 })
241 }
242
243 StmtKind::Interface(i) => {
244 let range = stmt_range(sv, stmt);
245 let selection_range = sv.name_range(i.name);
246 let iface_deprecated = docblock_before(sv.source(), stmt.span.start)
247 .filter(|raw| parse_docblock(raw).deprecated.is_some())
248 .map(|_| true);
249 let children: Vec<DocumentSymbol> = i
250 .members
251 .iter()
252 .filter_map(|member| match &member.kind {
253 ClassMemberKind::Method(m) => {
254 let mrange = member_range(sv, member);
255 let msel = sv.name_range(m.name);
256 let method_deprecated = docblock_before(sv.source(), member.span.start)
257 .filter(|raw| parse_docblock(raw).deprecated.is_some())
258 .map(|_| true);
259 Some(DocumentSymbol {
260 name: m.name.to_string(),
261 detail: Some(format_fn_signature(&m.params, m.return_type.as_ref())),
262 kind: SymbolKind::METHOD,
263 tags: None,
264 deprecated: method_deprecated,
265 range: mrange,
266 selection_range: msel,
267 children: None,
268 })
269 }
270 ClassMemberKind::ClassConst(cc) => {
271 let crange = member_range(sv, member);
272 let csel = sv.name_range(cc.name);
273 let const_deprecated = docblock_before(sv.source(), member.span.start)
274 .filter(|raw| parse_docblock(raw).deprecated.is_some())
275 .map(|_| true);
276 Some(DocumentSymbol {
277 name: cc.name.to_string(),
278 detail: None,
279 kind: SymbolKind::CONSTANT,
280 tags: None,
281 deprecated: const_deprecated,
282 range: crange,
283 selection_range: csel,
284 children: None,
285 })
286 }
287 _ => None,
288 })
289 .collect();
290 Some(DocumentSymbol {
291 name: i.name.to_string(),
292 detail: None,
293 kind: SymbolKind::INTERFACE,
294 tags: None,
295 deprecated: iface_deprecated,
296 range,
297 selection_range,
298 children: if children.is_empty() {
299 None
300 } else {
301 Some(children)
302 },
303 })
304 }
305
306 StmtKind::Trait(t) => {
307 let range = stmt_range(sv, stmt);
308 let selection_range = sv.name_range(t.name);
309 let trait_deprecated = docblock_before(sv.source(), stmt.span.start)
310 .filter(|raw| parse_docblock(raw).deprecated.is_some())
311 .map(|_| true);
312 let children: Vec<DocumentSymbol> = t
313 .members
314 .iter()
315 .filter_map(|member| {
316 if let ClassMemberKind::Method(m) = &member.kind {
317 let mrange = member_range(sv, member);
318 let msel = sv.name_range(m.name);
319 let method_deprecated = docblock_before(sv.source(), member.span.start)
320 .filter(|raw| parse_docblock(raw).deprecated.is_some())
321 .map(|_| true);
322 Some(DocumentSymbol {
323 name: m.name.to_string(),
324 detail: Some(format_fn_signature(&m.params, m.return_type.as_ref())),
325 kind: SymbolKind::METHOD,
326 tags: None,
327 deprecated: method_deprecated,
328 range: mrange,
329 selection_range: msel,
330 children: None,
331 })
332 } else {
333 None
334 }
335 })
336 .collect();
337
338 Some(DocumentSymbol {
339 name: t.name.to_string(),
340 detail: None,
341 kind: SymbolKind::CLASS,
342 tags: None,
343 deprecated: trait_deprecated,
344 range,
345 selection_range,
346 children: if children.is_empty() {
347 None
348 } else {
349 Some(children)
350 },
351 })
352 }
353
354 StmtKind::Enum(e) => {
355 let range = stmt_range(sv, stmt);
356 let selection_range = sv.name_range(e.name);
357 let enum_deprecated = docblock_before(sv.source(), stmt.span.start)
358 .filter(|raw| parse_docblock(raw).deprecated.is_some())
359 .map(|_| true);
360 let children: Vec<DocumentSymbol> = e
361 .members
362 .iter()
363 .filter_map(|member| match &member.kind {
364 EnumMemberKind::Case(c) => {
365 let crange = Range {
366 start: sv.position_of(member.span.start),
367 end: sv.position_of(member.span.end),
368 };
369 let csel = sv.name_range(c.name);
370 Some(DocumentSymbol {
371 name: c.name.to_string(),
372 detail: None,
373 kind: SymbolKind::ENUM_MEMBER,
374 tags: None,
375 deprecated: None,
376 range: crange,
377 selection_range: csel,
378 children: None,
379 })
380 }
381 EnumMemberKind::Method(m) => {
382 let mrange = Range {
383 start: sv.position_of(member.span.start),
384 end: sv.position_of(member.span.end),
385 };
386 let msel = sv.name_range(m.name);
387 let method_deprecated = docblock_before(sv.source(), member.span.start)
388 .filter(|raw| parse_docblock(raw).deprecated.is_some())
389 .map(|_| true);
390 Some(DocumentSymbol {
391 name: m.name.to_string(),
392 detail: Some(format_fn_signature(&m.params, m.return_type.as_ref())),
393 kind: SymbolKind::METHOD,
394 tags: None,
395 deprecated: method_deprecated,
396 range: mrange,
397 selection_range: msel,
398 children: None,
399 })
400 }
401 _ => None,
402 })
403 .collect();
404
405 Some(DocumentSymbol {
406 name: e.name.to_string(),
407 detail: None,
408 kind: SymbolKind::ENUM,
409 tags: None,
410 deprecated: enum_deprecated,
411 range,
412 selection_range,
413 children: if children.is_empty() {
414 None
415 } else {
416 Some(children)
417 },
418 })
419 }
420
421 _ => None,
422 }
423}
424
425fn format_fn_signature(
426 params: &[php_ast::Param<'_, '_>],
427 ret: Option<&php_ast::TypeHint<'_, '_>>,
428) -> String {
429 use crate::ast::format_type_hint;
430 let params_str = params
431 .iter()
432 .map(|p| {
433 let mut s = String::new();
434 if p.by_ref {
435 s.push('&');
436 }
437 if p.variadic {
438 s.push_str("...");
439 }
440 if let Some(t) = &p.type_hint {
441 s.push_str(&format!("{} ", format_type_hint(t)));
442 }
443 s.push_str(&format!("${}", p.name));
444 s
445 })
446 .collect::<Vec<_>>()
447 .join(", ");
448 let ret_str = ret
449 .map(|r| format!(": {}", format_type_hint(r)))
450 .unwrap_or_default();
451 format!("({}){}", params_str, ret_str)
452}
453
454fn _pos_from_offset(sv: SourceView<'_>, offset: u32) -> Position {
455 sv.position_of(offset)
456}
457
458#[allow(deprecated)]
464pub fn workspace_symbols_from_index(
465 query: &str,
466 indexes: &[(Url, Arc<crate::file_index::FileIndex>)],
467) -> Vec<SymbolInformation> {
468 use crate::file_index::ClassKind;
469 use crate::util::fuzzy_camel_match;
470
471 let (kind_filter, term) = parse_kind_filter(query);
472 let matches_kind = |k: SymbolKind| kind_filter.is_none_or(|f| f == k);
473
474 let line_range = |line: u32| -> Range {
475 let pos = Position { line, character: 0 };
476 Range {
477 start: pos,
478 end: pos,
479 }
480 };
481
482 let mut results = Vec::new();
483 for (uri, idx) in indexes {
484 if matches_kind(SymbolKind::FUNCTION) {
485 for f in &idx.functions {
486 if fuzzy_camel_match(term, &f.name) {
487 results.push(SymbolInformation {
488 name: f.name.clone(),
489 kind: SymbolKind::FUNCTION,
490 location: Location {
491 uri: uri.clone(),
492 range: line_range(f.start_line),
493 },
494 tags: None,
495 deprecated: None,
496 container_name: None,
497 });
498 }
499 }
500 }
501 for cls in &idx.classes {
502 let class_kind = match cls.kind {
503 ClassKind::Class | ClassKind::Trait => SymbolKind::CLASS,
504 ClassKind::Interface => SymbolKind::INTERFACE,
505 ClassKind::Enum => SymbolKind::ENUM,
506 };
507 if matches_kind(class_kind) && fuzzy_camel_match(term, &cls.name) {
508 results.push(SymbolInformation {
509 name: cls.name.clone(),
510 kind: class_kind,
511 location: Location {
512 uri: uri.clone(),
513 range: line_range(cls.start_line),
514 },
515 tags: None,
516 deprecated: None,
517 container_name: None,
518 });
519 }
520 if matches_kind(SymbolKind::METHOD) {
521 for m in &cls.methods {
522 if fuzzy_camel_match(term, &m.name) {
523 results.push(SymbolInformation {
524 name: m.name.clone(),
525 kind: SymbolKind::METHOD,
526 location: Location {
527 uri: uri.clone(),
528 range: line_range(m.start_line),
529 },
530 tags: None,
531 deprecated: None,
532 container_name: Some(cls.name.clone()),
533 });
534 }
535 }
536 }
537 if matches_kind(SymbolKind::ENUM_MEMBER) && cls.kind == ClassKind::Enum {
538 for case in &cls.cases {
539 if fuzzy_camel_match(term, case) {
540 results.push(SymbolInformation {
541 name: case.clone(),
542 kind: SymbolKind::ENUM_MEMBER,
543 location: Location {
544 uri: uri.clone(),
545 range: line_range(cls.start_line),
546 },
547 tags: None,
548 deprecated: None,
549 container_name: Some(cls.name.clone()),
550 });
551 }
552 }
553 }
554 }
555 }
556 results
557}
558
559pub fn workspace_symbols_from_workspace(
566 query: &str,
567 wi: &crate::db::workspace_index::WorkspaceIndexData,
568) -> Vec<SymbolInformation> {
569 workspace_symbols_from_index(query, &wi.files)
570}
571
572#[cfg(test)]
573mod tests {
574 use super::*;
575
576 #[test]
577 fn function_has_function_kind_and_signature_detail() {
578 let src = "<?php\nfunction greet(string $name): string {}";
579 let doc = ParsedDoc::parse(src.to_string());
580 let syms = document_symbols(src, &doc);
581 let f = syms
582 .iter()
583 .find(|s| s.name == "greet")
584 .expect("greet not found");
585 assert_eq!(f.kind, SymbolKind::FUNCTION);
586 let detail = f.detail.as_deref().unwrap_or("");
587 assert!(
588 detail.contains("$name"),
589 "detail should contain '$name', got: {detail}"
590 );
591 assert!(
592 detail.contains(": string"),
593 "detail should contain return type, got: {detail}"
594 );
595 }
596
597 #[test]
598 fn function_parameters_are_variable_children() {
599 let src = "<?php\nfunction process($input, $count) {}";
600 let doc = ParsedDoc::parse(src.to_string());
601 let syms = document_symbols(src, &doc);
602 let f = syms
603 .iter()
604 .find(|s| s.name == "process")
605 .expect("process not found");
606 let children = f.children.as_ref().expect("should have children");
607 assert!(
608 children
609 .iter()
610 .any(|c| c.name == "$input" && c.kind == SymbolKind::VARIABLE),
611 "missing $input child"
612 );
613 assert!(
614 children
615 .iter()
616 .any(|c| c.name == "$count" && c.kind == SymbolKind::VARIABLE),
617 "missing $count child"
618 );
619 }
620
621 #[test]
622 fn class_has_class_kind_with_method_children() {
623 let src = "<?php\nclass Calc { public function add() {} public function sub() {} }";
624 let doc = ParsedDoc::parse(src.to_string());
625 let syms = document_symbols(src, &doc);
626 let c = syms
627 .iter()
628 .find(|s| s.name == "Calc")
629 .expect("Calc not found");
630 assert_eq!(c.kind, SymbolKind::CLASS);
631 let children = c.children.as_ref().expect("should have method children");
632 assert!(
633 children
634 .iter()
635 .any(|m| m.name == "add" && m.kind == SymbolKind::METHOD)
636 );
637 assert!(
638 children
639 .iter()
640 .any(|m| m.name == "sub" && m.kind == SymbolKind::METHOD)
641 );
642 }
643
644 #[test]
645 fn interface_has_interface_kind() {
646 let src = "<?php\ninterface Serializable {}";
647 let doc = ParsedDoc::parse(src.to_string());
648 let syms = document_symbols(src, &doc);
649 let i = syms
650 .iter()
651 .find(|s| s.name == "Serializable")
652 .expect("Serializable not found");
653 assert_eq!(i.kind, SymbolKind::INTERFACE);
654 }
655
656 #[test]
657 fn trait_has_class_kind() {
658 let src = "<?php\ntrait Loggable {}";
659 let doc = ParsedDoc::parse(src.to_string());
660 let syms = document_symbols(src, &doc);
661 let t = syms
662 .iter()
663 .find(|s| s.name == "Loggable")
664 .expect("Loggable not found");
665 assert_eq!(t.kind, SymbolKind::CLASS);
666 }
667
668 #[test]
669 fn symbols_inside_namespace_are_returned() {
670 let src = "<?php\nnamespace App {\nfunction render() {}\nclass View {}\n}";
671 let doc = ParsedDoc::parse(src.to_string());
672 let syms = document_symbols(src, &doc);
673 assert!(syms.iter().any(|s| s.name == "render"), "missing 'render'");
674 assert!(syms.iter().any(|s| s.name == "View"), "missing 'View'");
675 }
676
677 #[test]
678 fn range_start_lte_selection_range_start() {
679 let src = "<?php\nfunction hello(string $x): int {}";
680 let doc = ParsedDoc::parse(src.to_string());
681 let syms = document_symbols(src, &doc);
682 let f = syms
683 .iter()
684 .find(|s| s.name == "hello")
685 .expect("hello not found");
686 assert!(f.range.start.line <= f.selection_range.start.line);
687 if f.range.start.line == f.selection_range.start.line {
688 assert!(f.range.start.character <= f.selection_range.start.character);
689 }
690 }
691
692 #[test]
693 fn class_properties_are_property_children() {
694 let src = "<?php\nclass User { public string $name; private int $age; }";
695 let doc = ParsedDoc::parse(src.to_string());
696 let syms = document_symbols(src, &doc);
697 let c = syms
698 .iter()
699 .find(|s| s.name == "User")
700 .expect("User not found");
701 let children = c.children.as_ref().expect("should have children");
702 assert!(
703 children
704 .iter()
705 .any(|ch| ch.name == "$name" && ch.kind == SymbolKind::PROPERTY)
706 );
707 assert!(
708 children
709 .iter()
710 .any(|ch| ch.name == "$age" && ch.kind == SymbolKind::PROPERTY)
711 );
712 }
713
714 #[test]
715 fn class_constants_are_constant_children() {
716 let src = "<?php\nclass Status { const ACTIVE = 1; const INACTIVE = 0; }";
717 let doc = ParsedDoc::parse(src.to_string());
718 let syms = document_symbols(src, &doc);
719 let c = syms
720 .iter()
721 .find(|s| s.name == "Status")
722 .expect("Status not found");
723 let children = c.children.as_ref().expect("should have children");
724 assert!(
725 children
726 .iter()
727 .any(|ch| ch.name == "ACTIVE" && ch.kind == SymbolKind::CONSTANT)
728 );
729 assert!(
730 children
731 .iter()
732 .any(|ch| ch.name == "INACTIVE" && ch.kind == SymbolKind::CONSTANT)
733 );
734 }
735
736 #[test]
737 fn trait_methods_are_method_children() {
738 let src = "<?php\ntrait Loggable { public function log() {} public function warn() {} }";
739 let doc = ParsedDoc::parse(src.to_string());
740 let syms = document_symbols(src, &doc);
741 let t = syms
742 .iter()
743 .find(|s| s.name == "Loggable")
744 .expect("Loggable not found");
745 let children = t
746 .children
747 .as_ref()
748 .expect("trait should have method children");
749 assert!(
750 children
751 .iter()
752 .any(|m| m.name == "log" && m.kind == SymbolKind::METHOD)
753 );
754 assert!(
755 children
756 .iter()
757 .any(|m| m.name == "warn" && m.kind == SymbolKind::METHOD)
758 );
759 }
760
761 #[test]
762 fn partial_ast_on_parse_error_returns_valid_symbols() {
763 let src = "<?php\nfunction valid() {}\nclass {";
764 let doc = ParsedDoc::parse(src.to_string());
765 let syms = document_symbols(src, &doc);
766 assert!(
767 syms.iter().any(|s| s.name == "valid"),
768 "should still return 'valid' despite parse error"
769 );
770 }
771
772 #[test]
773 fn function_symbol_has_correct_range() {
774 let src = "<?php\nfunction myFunc() {}";
777 let doc = ParsedDoc::parse(src.to_string());
778 let syms = document_symbols(src, &doc);
779 let f = syms
780 .iter()
781 .find(|s| s.name == "myFunc")
782 .expect("myFunc not found");
783 assert_eq!(
784 f.kind,
785 SymbolKind::FUNCTION,
786 "symbol should have FUNCTION kind"
787 );
788 assert_eq!(
789 f.range.start.line, 1,
790 "function range should start at line 1 (where 'function' keyword is)"
791 );
792 assert_eq!(
794 f.selection_range.start.line, 1,
795 "selection_range should start at line 1"
796 );
797 }
798
799 #[test]
800 fn enum_symbol_has_correct_kind() {
801 let src = "<?php\nenum Color { case Red; case Green; case Blue; }";
803 let doc = ParsedDoc::parse(src.to_string());
804 let syms = document_symbols(src, &doc);
805 let e = syms
806 .iter()
807 .find(|s| s.name == "Color")
808 .expect("Color enum not found");
809 assert_eq!(
810 e.kind,
811 SymbolKind::ENUM,
812 "enum should produce a symbol with SymbolKind::ENUM"
813 );
814 assert_eq!(e.range.start.line, 1, "enum range should start at line 1");
815 }
816
817 #[test]
818 fn interface_constants_are_constant_children() {
819 let src =
821 "<?php\ninterface Config {\n const VERSION = '1.0';\n const DEBUG = false;\n}";
822 let doc = ParsedDoc::parse(src.to_string());
823 let syms = document_symbols(src, &doc);
824 let i = syms
825 .iter()
826 .find(|s| s.name == "Config")
827 .expect("Config interface not found");
828 let children = i
829 .children
830 .as_ref()
831 .expect("interface should have constant children");
832 assert!(
833 children
834 .iter()
835 .any(|c| c.name == "VERSION" && c.kind == SymbolKind::CONSTANT),
836 "missing VERSION constant child, got: {:?}",
837 children.iter().map(|c| &c.name).collect::<Vec<_>>()
838 );
839 assert!(
840 children
841 .iter()
842 .any(|c| c.name == "DEBUG" && c.kind == SymbolKind::CONSTANT),
843 "missing DEBUG constant child"
844 );
845 assert_eq!(
846 children.len(),
847 2,
848 "expected exactly 2 constant children, got: {:?}",
849 children.iter().map(|c| &c.name).collect::<Vec<_>>()
850 );
851 }
852
853 #[test]
854 fn interface_with_only_methods_has_method_children() {
855 let src = "<?php\ninterface Runnable {\n public function run(): void;\n}";
856 let doc = ParsedDoc::parse(src.to_string());
857 let syms = document_symbols(src, &doc);
858 let i = syms
859 .iter()
860 .find(|s| s.name == "Runnable")
861 .expect("Runnable not found");
862 let children = i
863 .children
864 .as_ref()
865 .expect("interface with methods should have children");
866 assert!(
867 children
868 .iter()
869 .any(|c| c.name == "run" && c.kind == SymbolKind::METHOD),
870 "missing run method child, got: {:?}",
871 children.iter().map(|c| &c.name).collect::<Vec<_>>()
872 );
873 }
874
875 #[test]
876 fn parse_kind_filter_extracts_class_prefix() {
877 let (kind, term) = parse_kind_filter("#class:MyClass");
878 assert_eq!(kind, Some(SymbolKind::CLASS));
879 assert_eq!(term, "MyClass");
880 }
881
882 #[test]
883 fn parse_kind_filter_no_prefix_returns_none() {
884 let (kind, term) = parse_kind_filter("MyClass");
885 assert_eq!(kind, None);
886 assert_eq!(term, "MyClass");
887 }
888
889 #[test]
890 fn deprecated_function_sets_deprecated_field() {
891 let src = "<?php\n/** @deprecated Use newGreet() instead */\nfunction greet() {}";
892 let doc = ParsedDoc::parse(src.to_string());
893 let syms = document_symbols(src, &doc);
894 let f = syms
895 .iter()
896 .find(|s| s.name == "greet")
897 .expect("greet not found");
898 assert_eq!(
899 f.deprecated,
900 Some(true),
901 "deprecated function should have deprecated: Some(true)"
902 );
903 }
904
905 #[test]
906 fn non_deprecated_function_has_no_deprecated_field() {
907 let src = "<?php\n/** Does stuff. */\nfunction greet() {}";
908 let doc = ParsedDoc::parse(src.to_string());
909 let syms = document_symbols(src, &doc);
910 let f = syms
911 .iter()
912 .find(|s| s.name == "greet")
913 .expect("greet not found");
914 assert_eq!(
915 f.deprecated, None,
916 "non-deprecated function should have deprecated: None"
917 );
918 }
919
920 #[test]
921 fn deprecated_class_sets_deprecated_field() {
922 let src = "<?php\n/** @deprecated */\nclass OldService {}";
923 let doc = ParsedDoc::parse(src.to_string());
924 let syms = document_symbols(src, &doc);
925 let c = syms
926 .iter()
927 .find(|s| s.name == "OldService")
928 .expect("OldService not found");
929 assert_eq!(
930 c.deprecated,
931 Some(true),
932 "deprecated class should have deprecated: Some(true)"
933 );
934 }
935
936 #[test]
937 fn deprecated_method_sets_deprecated_field() {
938 let src =
939 "<?php\nclass Svc {\n /** @deprecated */\n public function oldMethod() {}\n}";
940 let doc = ParsedDoc::parse(src.to_string());
941 let syms = document_symbols(src, &doc);
942 let c = syms
943 .iter()
944 .find(|s| s.name == "Svc")
945 .expect("Svc not found");
946 let children = c.children.as_ref().expect("Svc should have children");
947 let m = children
948 .iter()
949 .find(|ch| ch.name == "oldMethod")
950 .expect("oldMethod not found");
951 assert_eq!(
952 m.deprecated,
953 Some(true),
954 "deprecated method should have deprecated: Some(true)"
955 );
956 }
957}