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