1use std::sync::Arc;
12
13use php_ast::{ClassMemberKind, EnumMemberKind, NamespaceBody, Stmt, StmtKind};
14
15use crate::ast::{ParsedDoc, format_type_hint};
16use crate::docblock::docblock_before;
17
18#[derive(Debug, Clone, Default)]
21pub struct FileIndex {
22 pub namespace: Option<Box<str>>,
23 pub functions: Vec<FunctionDef>,
24 pub classes: Vec<ClassDef>,
25 pub constants: Vec<Box<str>>,
26}
27
28#[derive(Debug, Clone)]
29pub struct FunctionDef {
30 pub name: Box<str>,
31 pub fqn: Box<str>,
33 pub params: Vec<ParamDef>,
34 pub return_type: Option<Box<str>>,
35 pub doc: Option<Box<str>>,
37 pub start_line: u32,
38 pub name_char: u32,
40}
41
42#[derive(Debug, Clone)]
43pub struct ParamDef {
44 pub name: Box<str>,
45 pub type_hint: Option<Box<str>>,
46 pub has_default: bool,
47 pub variadic: bool,
48}
49
50#[derive(Debug, Clone)]
51pub struct ClassDef {
52 pub name: Box<str>,
53 pub fqn: Box<str>,
55 pub kind: ClassKind,
56 pub is_abstract: bool,
57 pub parent: Option<Arc<str>>,
59 pub implements: Vec<Arc<str>>,
60 pub traits: Vec<Arc<str>>,
61 pub methods: Vec<MethodDef>,
62 pub properties: Vec<PropertyDef>,
63 pub constants: Vec<Box<str>>,
64 pub cases: Vec<Box<str>>,
66 pub start_line: u32,
67 pub name_char: u32,
69}
70
71#[derive(Debug, Clone, Copy, PartialEq, Eq)]
72pub enum ClassKind {
73 Class,
74 Interface,
75 Trait,
76 Enum,
77}
78
79#[derive(Debug, Clone)]
80pub struct MethodDef {
81 pub name: Box<str>,
82 pub is_static: bool,
83 pub is_abstract: bool,
84 pub visibility: Visibility,
85 pub params: Vec<ParamDef>,
86 pub return_type: Option<Box<str>>,
87 pub doc: Option<Box<str>>,
88 pub start_line: u32,
89 pub name_char: u32,
91}
92
93#[derive(Debug, Clone, Copy, PartialEq, Eq)]
94pub enum Visibility {
95 Public,
96 Protected,
97 Private,
98}
99
100#[derive(Debug, Clone)]
101pub struct PropertyDef {
102 pub name: Box<str>,
103 pub is_static: bool,
104 pub type_hint: Option<Box<str>>,
105 pub visibility: Visibility,
106 pub start_line: u32,
107 pub name_char: u32,
109}
110
111impl FileIndex {
114 pub fn extract(doc: &ParsedDoc) -> Self {
116 let source = doc.source();
117 let view = doc.view();
118 let mut index = FileIndex::default();
119 collect_stmts(source, &view, &doc.program().stmts, None, &mut index);
120 index
121 }
122}
123
124fn fqn(namespace: Option<&str>, name: &str) -> Box<str> {
127 match namespace {
128 Some(ns) if !ns.is_empty() => format!("{}\\{}", ns, name).into(),
129 _ => name.into(),
130 }
131}
132
133fn collect_stmts(
134 source: &str,
135 view: &crate::ast::SourceView<'_>,
136 stmts: &[Stmt<'_, '_>],
137 namespace: Option<&str>,
138 index: &mut FileIndex,
139) {
140 use crate::ast::str_offset;
141
142 let name_char = |name: &str| -> u32 {
143 str_offset(source, name)
144 .map(|off| view.position_of(off).character)
145 .unwrap_or(0)
146 };
147
148 let mut cur_ns: Option<Box<str>> = namespace.map(|s| s.into());
150
151 for stmt in stmts {
152 match &stmt.kind {
153 StmtKind::Namespace(ns) => {
155 let ns_name = ns.name.as_ref().map(|n| n.to_string_repr().into());
156
157 match &ns.body {
158 NamespaceBody::Braced(inner) => {
159 let ns_str = ns_name.as_deref();
161 if index.namespace.is_none() {
163 index.namespace = ns_name.clone();
164 }
165 collect_stmts(source, view, inner, ns_str, index);
166 }
167 NamespaceBody::Simple => {
168 if index.namespace.is_none() {
170 index.namespace = ns_name.clone();
171 }
172 cur_ns = ns_name;
173 }
174 }
175 }
176
177 StmtKind::Function(f) => {
179 let doc_text = docblock_before(source, stmt.span.start).map(|s| s.into());
180 let start_line = view.position_of(stmt.span.start).line;
181 let ns = cur_ns.as_deref();
182 let f_name = f.name.to_string();
183 index.functions.push(FunctionDef {
184 name: f_name.clone().into(),
185 fqn: fqn(ns, &f_name),
186 params: extract_params(&f.params),
187 return_type: f.return_type.as_ref().map(|t| format_type_hint(t).into()),
188 doc: doc_text,
189 start_line,
190 name_char: name_char(&f_name),
191 });
192 }
193
194 StmtKind::Class(c) => {
196 let Some(class_name) = c.name else { continue };
197 let class_name_str = class_name.to_string();
198 let start_line = view.position_of(stmt.span.start).line;
199 let ns = cur_ns.as_deref();
200
201 let mut class_def = ClassDef {
202 name: class_name_str.clone().into(),
203 fqn: fqn(ns, &class_name_str),
204 kind: ClassKind::Class,
205 is_abstract: c.modifiers.is_abstract,
206 parent: c
207 .extends
208 .as_ref()
209 .map(|e| Arc::from(e.to_string_repr().as_ref())),
210 implements: c
211 .implements
212 .iter()
213 .map(|i| Arc::from(i.to_string_repr().as_ref()))
214 .collect(),
215 traits: Vec::new(),
216 methods: Vec::new(),
217 properties: Vec::new(),
218 constants: Vec::new(),
219 cases: Vec::new(),
220 start_line,
221 name_char: name_char(&class_name_str),
222 };
223
224 for member in c.members.iter() {
225 match &member.kind {
226 ClassMemberKind::Method(m) => {
227 let mdoc = docblock_before(source, member.span.start).map(|s| s.into());
228 let mstart = view.position_of(member.span.start).line;
229 let vis = method_visibility(m.visibility);
230 let method_params = extract_params(&m.params);
231 for ast_param in m.params.iter() {
233 if ast_param.visibility.is_some() {
234 let pvis = method_visibility(ast_param.visibility);
235 let pstart = view.position_of(ast_param.span.start).line;
236 class_def.properties.push(PropertyDef {
237 name: ast_param.name.to_string().into(),
238 is_static: false,
239 type_hint: ast_param
240 .type_hint
241 .as_ref()
242 .map(|t| format_type_hint(t).into()),
243 visibility: pvis,
244 start_line: pstart,
245 name_char: name_char(&ast_param.name.to_string()),
246 });
247 }
248 }
249 class_def.methods.push(MethodDef {
250 name: m.name.to_string().into(),
251 is_static: m.is_static,
252 is_abstract: m.is_abstract,
253 visibility: vis,
254 params: method_params,
255 return_type: m
256 .return_type
257 .as_ref()
258 .map(|t| format_type_hint(t).into()),
259 doc: mdoc,
260 start_line: mstart,
261 name_char: name_char(&m.name.to_string()),
262 });
263 }
264 ClassMemberKind::Property(p) => {
265 let vis = method_visibility(p.visibility);
266 let pstart = view.position_of(member.span.start).line;
267 class_def.properties.push(PropertyDef {
268 name: p.name.to_string().into(),
269 is_static: p.is_static,
270 type_hint: p.type_hint.as_ref().map(|t| format_type_hint(t).into()),
271 visibility: vis,
272 start_line: pstart,
273 name_char: name_char(&p.name.to_string()),
274 });
275 }
276 ClassMemberKind::ClassConst(cc) => {
277 class_def.constants.push(cc.name.to_string().into());
278 }
279 ClassMemberKind::TraitUse(tu) => {
280 for t in tu.traits.iter() {
281 class_def
282 .traits
283 .push(Arc::from(t.to_string_repr().as_ref()));
284 }
285 }
286 }
287 }
288 index.classes.push(class_def);
289 }
290
291 StmtKind::Interface(i) => {
293 let start_line = view.position_of(stmt.span.start).line;
294 let ns = cur_ns.as_deref();
295
296 let mut iface_def = ClassDef {
297 name: i.name.to_string().into(),
298 fqn: fqn(ns, &i.name.to_string()),
299 kind: ClassKind::Interface,
300 is_abstract: true,
301 parent: None,
302 implements: i
303 .extends
304 .iter()
305 .map(|e| Arc::from(e.to_string_repr().as_ref()))
306 .collect(),
307 traits: Vec::new(),
308 methods: Vec::new(),
309 properties: Vec::new(),
310 constants: Vec::new(),
311 cases: Vec::new(),
312 start_line,
313 name_char: name_char(&i.name.to_string()),
314 };
315
316 for member in i.members.iter() {
317 match &member.kind {
318 ClassMemberKind::Method(m) => {
319 let mdoc = docblock_before(source, member.span.start).map(|s| s.into());
320 let mstart = view.position_of(member.span.start).line;
321 iface_def.methods.push(MethodDef {
322 name: m.name.to_string().into(),
323 is_static: m.is_static,
324 is_abstract: true,
325 visibility: Visibility::Public,
326 params: extract_params(&m.params),
327 return_type: m
328 .return_type
329 .as_ref()
330 .map(|t| format_type_hint(t).into()),
331 doc: mdoc,
332 start_line: mstart,
333 name_char: name_char(&m.name.to_string()),
334 });
335 }
336 ClassMemberKind::ClassConst(cc) => {
337 iface_def.constants.push(cc.name.to_string().into());
338 }
339 _ => {}
340 }
341 }
342 index.classes.push(iface_def);
343 }
344
345 StmtKind::Trait(t) => {
347 let start_line = view.position_of(stmt.span.start).line;
348 let ns = cur_ns.as_deref();
349
350 let mut trait_def = ClassDef {
351 name: t.name.to_string().into(),
352 fqn: fqn(ns, &t.name.to_string()),
353 kind: ClassKind::Trait,
354 is_abstract: false,
355 parent: None,
356 implements: Vec::new(),
357 traits: Vec::new(),
358 methods: Vec::new(),
359 properties: Vec::new(),
360 constants: Vec::new(),
361 cases: Vec::new(),
362 start_line,
363 name_char: name_char(&t.name.to_string()),
364 };
365
366 for member in t.members.iter() {
367 match &member.kind {
368 ClassMemberKind::Method(m) => {
369 let mdoc = docblock_before(source, member.span.start).map(|s| s.into());
370 let mstart = view.position_of(member.span.start).line;
371 let vis = method_visibility(m.visibility);
372 trait_def.methods.push(MethodDef {
373 name: m.name.to_string().into(),
374 is_static: m.is_static,
375 is_abstract: m.is_abstract,
376 visibility: vis,
377 params: extract_params(&m.params),
378 return_type: m
379 .return_type
380 .as_ref()
381 .map(|t| format_type_hint(t).into()),
382 doc: mdoc,
383 start_line: mstart,
384 name_char: name_char(&m.name.to_string()),
385 });
386 }
387 ClassMemberKind::Property(p) => {
388 let vis = method_visibility(p.visibility);
389 let pstart = view.position_of(member.span.start).line;
390 trait_def.properties.push(PropertyDef {
391 name: p.name.to_string().into(),
392 is_static: p.is_static,
393 type_hint: p.type_hint.as_ref().map(|t| format_type_hint(t).into()),
394 visibility: vis,
395 start_line: pstart,
396 name_char: name_char(&p.name.to_string()),
397 });
398 }
399 ClassMemberKind::ClassConst(cc) => {
400 trait_def.constants.push(cc.name.to_string().into());
401 }
402 ClassMemberKind::TraitUse(tu) => {
403 for tr in tu.traits.iter() {
404 trait_def
405 .traits
406 .push(Arc::from(tr.to_string_repr().as_ref()));
407 }
408 }
409 }
410 }
411 index.classes.push(trait_def);
412 }
413
414 StmtKind::Enum(e) => {
416 let start_line = view.position_of(stmt.span.start).line;
417 let ns = cur_ns.as_deref();
418
419 let mut enum_def = ClassDef {
420 name: e.name.to_string().into(),
421 fqn: fqn(ns, &e.name.to_string()),
422 kind: ClassKind::Enum,
423 is_abstract: false,
424 parent: None,
425 implements: e
426 .implements
427 .iter()
428 .map(|i| Arc::from(i.to_string_repr().as_ref()))
429 .collect(),
430 traits: Vec::new(),
431 methods: Vec::new(),
432 properties: Vec::new(),
433 constants: Vec::new(),
434 cases: Vec::new(),
435 start_line,
436 name_char: name_char(&e.name.to_string()),
437 };
438
439 for member in e.members.iter() {
440 match &member.kind {
441 EnumMemberKind::Case(c) => {
442 enum_def.cases.push(c.name.to_string().into());
443 }
444 EnumMemberKind::Method(m) => {
445 let mdoc = docblock_before(source, member.span.start).map(|s| s.into());
446 let mstart = view.position_of(member.span.start).line;
447 let vis = method_visibility(m.visibility);
448 enum_def.methods.push(MethodDef {
449 name: m.name.to_string().into(),
450 is_static: m.is_static,
451 is_abstract: m.is_abstract,
452 visibility: vis,
453 params: extract_params(&m.params),
454 return_type: m
455 .return_type
456 .as_ref()
457 .map(|t| format_type_hint(t).into()),
458 doc: mdoc,
459 start_line: mstart,
460 name_char: name_char(&m.name.to_string()),
461 });
462 }
463 EnumMemberKind::ClassConst(cc) => {
464 enum_def.constants.push(cc.name.to_string().into());
465 }
466 _ => {}
467 }
468 }
469 index.classes.push(enum_def);
470 }
471
472 StmtKind::Const(consts) => {
474 for c in consts.iter() {
475 index.constants.push(c.name.to_string().into());
476 }
477 }
478
479 _ => {}
480 }
481 }
482}
483
484fn extract_params<'a, 'b>(params: &[php_ast::Param<'a, 'b>]) -> Vec<ParamDef> {
485 params
486 .iter()
487 .map(|p| ParamDef {
488 name: p.name.to_string().into(),
489 type_hint: p.type_hint.as_ref().map(|t| format_type_hint(t).into()),
490 has_default: p.default.is_some(),
491 variadic: p.variadic,
492 })
493 .collect()
494}
495
496fn method_visibility(vis: Option<php_ast::Visibility>) -> Visibility {
497 match vis {
498 Some(php_ast::Visibility::Protected) => Visibility::Protected,
499 Some(php_ast::Visibility::Private) => Visibility::Private,
500 _ => Visibility::Public,
501 }
502}
503
504#[cfg(test)]
507mod tests {
508 use super::*;
509
510 #[test]
511 fn extracts_class_and_method() {
512 let src = "<?php\nclass Greeter {\n public function greet(string $name): string {}\n}";
513 let doc = ParsedDoc::parse(src.to_string());
514 let idx = FileIndex::extract(&doc);
515 assert_eq!(idx.classes.len(), 1);
516 let cls = &idx.classes[0];
517 assert_eq!(cls.name, "Greeter".into());
518 assert_eq!(cls.kind, ClassKind::Class);
519 assert_eq!(cls.start_line, 1);
520 assert_eq!(cls.methods.len(), 1);
521 let method = &cls.methods[0];
522 assert_eq!(method.name, "greet".into());
523 assert_eq!(method.return_type.as_deref(), Some("string"));
524 assert_eq!(method.params.len(), 1);
525 assert_eq!(method.params[0].name, "name".into());
526 assert_eq!(method.params[0].type_hint.as_deref(), Some("string"));
527 }
528
529 #[test]
530 fn extracts_function() {
531 let src = "<?php\nfunction add(int $a, int $b): int {}";
532 let doc = ParsedDoc::parse(src.to_string());
533 let idx = FileIndex::extract(&doc);
534 assert_eq!(idx.functions.len(), 1);
535 let f = &idx.functions[0];
536 assert_eq!(f.name, "add".into());
537 assert_eq!(f.return_type.as_deref(), Some("int"));
538 assert_eq!(f.params.len(), 2);
539 }
540
541 #[test]
542 fn extracts_namespace() {
543 let src = "<?php\nnamespace App\\Services;\nclass Mailer {}";
544 let doc = ParsedDoc::parse(src.to_string());
545 let idx = FileIndex::extract(&doc);
546 assert_eq!(idx.namespace.as_deref(), Some("App\\Services"));
547 assert_eq!(idx.classes[0].fqn, "App\\Services\\Mailer".into());
548 }
549
550 #[test]
551 fn extracts_braced_namespace() {
552 let src = "<?php\nnamespace App\\Models {\n class User {}\n}";
553 let doc = ParsedDoc::parse(src.to_string());
554 let idx = FileIndex::extract(&doc);
555 assert_eq!(idx.namespace.as_deref(), Some("App\\Models"));
556 assert_eq!(idx.classes[0].fqn, "App\\Models\\User".into());
557 }
558
559 #[test]
560 fn extracts_interface() {
561 let src = "<?php\ninterface Countable {\n public function count(): int;\n}";
562 let doc = ParsedDoc::parse(src.to_string());
563 let idx = FileIndex::extract(&doc);
564 assert_eq!(idx.classes.len(), 1);
565 assert_eq!(idx.classes[0].kind, ClassKind::Interface);
566 assert_eq!(idx.classes[0].methods[0].name, "count".into());
567 assert!(idx.classes[0].methods[0].is_abstract);
568 }
569
570 #[test]
571 fn extracts_trait() {
572 let src = "<?php\ntrait Loggable {\n public function log(): void {}\n}";
573 let doc = ParsedDoc::parse(src.to_string());
574 let idx = FileIndex::extract(&doc);
575 assert_eq!(idx.classes[0].kind, ClassKind::Trait);
576 assert_eq!(idx.classes[0].methods[0].name, "log".into());
577 }
578
579 #[test]
580 fn extracts_enum_cases() {
581 let src = "<?php\nenum Status { case Active; case Inactive; }";
582 let doc = ParsedDoc::parse(src.to_string());
583 let idx = FileIndex::extract(&doc);
584 assert_eq!(idx.classes[0].kind, ClassKind::Enum);
585 assert!(idx.classes[0].cases.iter().any(|c| c.as_ref() == "Active"));
586 assert!(
587 idx.classes[0]
588 .cases
589 .iter()
590 .any(|c| c.as_ref() == "Inactive")
591 );
592 }
593
594 #[test]
595 fn extracts_class_properties_and_constants() {
596 let src = "<?php\nclass Config {\n public string $host;\n const VERSION = '1.0';\n}";
597 let doc = ParsedDoc::parse(src.to_string());
598 let idx = FileIndex::extract(&doc);
599 let cls = &idx.classes[0];
600 assert_eq!(cls.properties.len(), 1);
601 assert_eq!(cls.properties[0].name, "host".into());
602 assert!(cls.constants.iter().any(|c| c.as_ref() == "VERSION"));
603 }
604
605 #[test]
606 fn extracts_trait_use() {
607 let src = "<?php\ntrait T {}\nclass MyClass { use T; }";
608 let doc = ParsedDoc::parse(src.to_string());
609 let idx = FileIndex::extract(&doc);
610 let cls = idx
611 .classes
612 .iter()
613 .find(|c| c.name.as_ref() == "MyClass")
614 .unwrap();
615 assert!(cls.traits.iter().any(|t| t.as_ref() == "T"));
616 }
617
618 #[test]
619 fn extracts_class_implements_and_extends() {
620 let src = "<?php\nclass Dog extends Animal implements Pet, Movable {}";
621 let doc = ParsedDoc::parse(src.to_string());
622 let idx = FileIndex::extract(&doc);
623 let cls = &idx.classes[0];
624 assert_eq!(cls.parent.as_deref(), Some("Animal"));
625 assert!(cls.implements.iter().any(|i| i.as_ref() == "Pet"));
626 assert!(cls.implements.iter().any(|i| i.as_ref() == "Movable"));
627 }
628
629 #[test]
630 fn constructor_promoted_params_become_properties() {
631 let src = "<?php\nclass User {\n public function __construct(public string $name) {}\n}";
632 let doc = ParsedDoc::parse(src.to_string());
633 let idx = FileIndex::extract(&doc);
634 let cls = &idx.classes[0];
635 assert!(
637 cls.properties.iter().any(|p| p.name.as_ref() == "name"),
638 "expected promoted property 'name', got: {:?}",
639 cls.properties.iter().map(|p| &p.name).collect::<Vec<_>>()
640 );
641 }
642}