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