1use super::*;
2
3impl AnalysisSession {
4 pub fn definition_of(
18 &self,
19 symbol: &crate::Name,
20 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
21 match symbol {
23 crate::Name::Class(fqcn) => {
24 let _ = self.load_class(fqcn.as_ref());
25 }
26 crate::Name::Function(fqn) => {
27 let _ = self.load_class(fqn.as_ref());
28 }
29 crate::Name::Method { class, .. }
30 | crate::Name::Property { class, .. }
31 | crate::Name::ClassConstant { class, .. } => {
32 let _ = self.load_class(class.as_ref());
33 }
34 _ => {}
35 }
36 self.definition_of_cached(symbol)
37 }
38
39 pub fn definition_of_cached(
45 &self,
46 symbol: &crate::Name,
47 ) -> Result<mir_types::Location, crate::SymbolLookupError> {
48 let db = self.snapshot_db();
49 match symbol {
50 crate::Name::Class(fqcn) => {
51 let here = crate::db::Fqcn::from_str(&db, fqcn.as_ref());
52 let class = crate::db::find_class_like(&db, here)
53 .ok_or(crate::SymbolLookupError::NotFound)?;
54 class
55 .location()
56 .cloned()
57 .ok_or(crate::SymbolLookupError::NoSourceLocation)
58 }
59 crate::Name::Function(fqn) => {
60 let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
61 let f = crate::db::find_function(&db, here)
62 .ok_or(crate::SymbolLookupError::NotFound)?;
63 f.location
64 .clone()
65 .ok_or(crate::SymbolLookupError::NoSourceLocation)
66 }
67 crate::Name::Method { class, name }
68 | crate::Name::Property { class, name }
69 | crate::Name::ClassConstant { class, name } => {
70 crate::db::member_location(&db, class, name)
71 .ok_or(crate::SymbolLookupError::NotFound)
72 }
73 crate::Name::GlobalConstant(_) => Err(crate::SymbolLookupError::NoSourceLocation),
74 }
75 }
76
77 pub fn hover(
91 &self,
92 symbol: &crate::Name,
93 ) -> Result<crate::HoverInfo, crate::SymbolLookupError> {
94 match symbol {
98 crate::Name::Class(fqcn) => {
99 self.load_class(fqcn.as_ref());
100 }
101 crate::Name::Method { class, .. }
102 | crate::Name::Property { class, .. }
103 | crate::Name::ClassConstant { class, .. } => {
104 self.load_class(class.as_ref());
108 }
109 _ => {}
110 }
111 self.hover_cached(symbol)
112 }
113
114 pub fn hover_cached(
117 &self,
118 symbol: &crate::Name,
119 ) -> Result<crate::HoverInfo, crate::SymbolLookupError> {
120 use mir_types::{Atomic, Type};
121 let db = self.snapshot_db();
122 match symbol {
123 crate::Name::Function(fqn) => {
124 let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
125 let f = crate::db::find_function(&db, here)
126 .ok_or(crate::SymbolLookupError::NotFound)?;
127 let ty = f
128 .return_type
129 .as_deref()
130 .cloned()
131 .unwrap_or_else(Type::mixed);
132 let docstring = f.docstring.as_ref().map(|s| s.to_string());
133 Ok(crate::HoverInfo {
134 ty,
135 docstring,
136 definition: f.location.clone(),
137 })
138 }
139 crate::Name::Method { class, name } => {
140 let here = crate::db::Fqcn::from_str(&db, class.as_ref());
141 let (_, m) = crate::db::find_method_in_chain(&db, here, name)
142 .ok_or(crate::SymbolLookupError::NotFound)?;
143 let ty = m
144 .return_type
145 .as_deref()
146 .cloned()
147 .unwrap_or_else(Type::mixed);
148 let docstring = m.docstring.as_ref().map(|s| s.to_string());
149 Ok(crate::HoverInfo {
150 ty,
151 docstring,
152 definition: m.location.clone(),
153 })
154 }
155 crate::Name::Class(fqcn) => {
156 let here = crate::db::Fqcn::from_str(&db, fqcn.as_ref());
157 let class = crate::db::find_class_like(&db, here)
158 .ok_or(crate::SymbolLookupError::NotFound)?;
159 let ty = Type::single(Atomic::TNamedObject {
160 fqcn: mir_types::Name::from(fqcn.as_ref()),
161 type_params: mir_types::union::empty_type_params(),
162 });
163 Ok(crate::HoverInfo {
164 ty,
165 docstring: None,
166 definition: class.location().cloned(),
167 })
168 }
169 crate::Name::Property { class, name } => {
170 let here = crate::db::Fqcn::from_str(&db, class.as_ref());
171 let (_, p) = crate::db::find_property_in_chain(&db, here, name)
172 .ok_or(crate::SymbolLookupError::NotFound)?;
173 let ty = p.ty.as_deref().cloned().unwrap_or_else(Type::mixed);
174 Ok(crate::HoverInfo {
175 ty,
176 docstring: None,
177 definition: p.location.clone(),
178 })
179 }
180 crate::Name::ClassConstant { class, name } => {
181 let here = crate::db::Fqcn::from_str(&db, class.as_ref());
182 let (_, c) = crate::db::find_class_constant_in_chain(&db, here, name)
183 .ok_or(crate::SymbolLookupError::NotFound)?;
184 Ok(crate::HoverInfo {
185 ty: c.ty.clone(),
186 docstring: None,
187 definition: c.location.clone(),
188 })
189 }
190 crate::Name::GlobalConstant(fqn) => {
191 let here = crate::db::Fqcn::from_str(&db, fqn.as_ref());
192 let ty = crate::db::find_global_constant(&db, here)
193 .ok_or(crate::SymbolLookupError::NotFound)?;
194 Ok(crate::HoverInfo {
195 ty: (*ty).clone(),
196 docstring: None,
197 definition: None,
198 })
199 }
200 }
201 }
202
203 #[doc(hidden)]
207 pub fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
208 use crate::db::MirDatabase;
209 let db = self.snapshot_db();
210 db.reference_locations(symbol)
211 }
212
213 pub fn subtype_files(&self, class_fqn: &str) -> Vec<Arc<str>> {
224 let files = self.snapshot_db().source_file_paths();
225 let mut out: Vec<Arc<str>> = self
226 .indexed_subtype_classes(class_fqn, &files, false)
227 .into_iter()
228 .map(|s| s.file)
229 .collect();
230 out.sort();
231 out.dedup();
232 out
233 }
234
235 pub fn indexed_use_import_locations(
247 &self,
248 symbol: &crate::Name,
249 files: &[Arc<str>],
250 ) -> Vec<(Arc<str>, crate::Range)> {
251 let key = format!("use:{}", symbol.codebase_key());
252 let scope: rustc_hash::FxHashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
253 let guard = self.db.salsa.read();
254 let mut out: Vec<(Arc<str>, crate::Range)> = guard
255 .reference_locations(&key)
256 .into_iter()
257 .filter(|(file, ..)| scope.contains(file.as_ref()))
258 .map(|(file, line, col_start, col_end)| {
259 (file, span_range(line, col_start as u32, col_end as u32))
260 })
261 .collect();
262 out.sort_by(|a, b| {
263 a.0.cmp(&b.0)
264 .then(a.1.start.line.cmp(&b.1.start.line))
265 .then(a.1.start.column.cmp(&b.1.start.column))
266 });
267 out.dedup();
268 out
269 }
270
271 pub fn indexed_references_to(
291 &self,
292 symbol: &crate::Name,
293 files: &[Arc<str>],
294 include_declaration: bool,
295 should_cancel: &(dyn Fn() -> bool + Sync),
296 ) -> Option<Vec<(Arc<str>, crate::Range)>> {
297 use std::panic::AssertUnwindSafe;
298
299 use rayon::prelude::*;
300
301 let key = symbol.codebase_key();
302
303 let stale: Vec<Arc<str>> = loop {
307 if should_cancel() {
308 return None;
309 }
310 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
311 let db = self.snapshot_db();
312 files
313 .iter()
314 .filter(|f| {
315 db.lookup_source_file(f.as_ref()).is_some_and(|sf| {
316 let text = sf.text(&db as &dyn MirDatabase);
317 !self.is_ref_committed(f.as_ref(), &text)
318 })
319 })
320 .cloned()
321 .collect::<Vec<_>>()
322 }));
323 match attempt {
324 Ok(v) => break v,
325 Err(_) if should_cancel() => return None,
326 Err(_) => {}
327 }
328 };
329
330 if !stale.is_empty() {
331 for path in &stale {
335 if should_cancel() {
336 return None;
337 }
338 self.prepare_file_for_analysis(path);
339 }
340
341 let analyzed = loop {
344 if should_cancel() {
345 return None;
346 }
347 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
348 let db_main = self.snapshot_db();
349 stale
350 .par_iter()
351 .map_with(db_main, |db, path| {
352 let sf = db.lookup_source_file(path.as_ref())?;
353 let text = sf.text(&*db as &dyn MirDatabase);
354 let out = crate::db::analyze_file(&*db as &dyn MirDatabase, sf);
355 let defs =
356 crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
357 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
358 Some((path.clone(), text, out, entries))
359 })
360 .flatten()
361 .collect::<Vec<_>>()
362 }));
363 match attempt {
364 Ok(v) => break v,
365 Err(_) if should_cancel() => return None,
366 Err(_) => {}
367 }
368 };
369 let guard = self.db.salsa.read();
370 for (file, text, out, entries) in &analyzed {
371 guard.set_file_reference_locations(file.as_ref(), out.ref_locs.to_vec());
372 self.mark_ref_committed(file, text, Some(out));
373 if !self.is_defs_committed(file.as_ref(), text) {
374 guard.set_file_class_edges(file, entries.clone());
375 self.mark_defs_committed(file, text);
376 }
377 }
378 }
379
380 let hierarchy: Vec<String> = match symbol {
393 crate::Name::Method { class, name } => {
394 if name.as_ref() == "__construct" || class.is_empty() {
395 if class.is_empty() {
396 Vec::new()
397 } else {
398 vec![class.trim_start_matches('\\').to_string()]
399 }
400 } else {
401 self.member_hierarchy_classes(class.as_ref())
402 }
403 }
404 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
405 if class.is_empty() {
406 Vec::new()
407 } else {
408 self.member_hierarchy_classes(class.as_ref())
409 }
410 }
411 _ => Vec::new(),
412 };
413 let primary_keys: Vec<String> = match symbol {
414 crate::Name::Method { name, .. } => hierarchy
415 .iter()
416 .map(|c| format!("meth:{c}::{name}"))
417 .collect(),
418 crate::Name::Property { name, .. } => hierarchy
419 .iter()
420 .map(|c| format!("prop:{c}::{name}"))
421 .collect(),
422 crate::Name::ClassConstant { name, .. } => hierarchy
423 .iter()
424 .map(|c| format!("cnst:{c}::{name}"))
425 .collect(),
426 _ => vec![key.clone()],
427 };
428 let fallback_key: Option<String> = match symbol {
429 crate::Name::Method { name, .. } => Some(format!("methname:{name}")),
430 crate::Name::Property { name, .. } => Some(format!("propname:{name}")),
431 _ => None,
432 };
433 let scope: rustc_hash::FxHashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
434 let read_keys = |keys: &[String]| -> Vec<(Arc<str>, crate::Range)> {
435 let guard = self.db.salsa.read();
436 let mut merged: Vec<(Arc<str>, u32, u16, u16)> = Vec::new();
437 for k in keys {
438 merged.extend(guard.reference_locations(k));
439 }
440 merged
441 .into_iter()
442 .filter(|(file, ..)| scope.contains(file.as_ref()))
443 .map(|(file, line, col_start, col_end)| {
444 (file, span_range(line, col_start as u32, col_end as u32))
445 })
446 .collect()
447 };
448 let mut out = read_keys(&primary_keys);
449 if out.is_empty() {
450 if let Some(fk) = fallback_key {
451 out = read_keys(std::slice::from_ref(&fk));
452 }
453 }
454 out.sort_by(|a, b| {
455 a.0.cmp(&b.0)
456 .then(a.1.start.line.cmp(&b.1.start.line))
457 .then(a.1.start.column.cmp(&b.1.start.column))
458 });
459 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
460
461 if include_declaration {
462 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
466 crate::Name::Method { class, .. }
467 | crate::Name::Property { class, .. }
468 | crate::Name::ClassConstant { class, .. } => {
469 if class.is_empty() {
470 match symbol {
474 crate::Name::Method { name, .. } => {
475 read_keys(&[format!("methdecl:{name}")])
476 }
477 crate::Name::Property { name, .. } => {
478 read_keys(&[format!("propdecl:{name}")])
479 }
480 crate::Name::ClassConstant { name, .. } => {
481 read_keys(&[format!("cnstdecl:{name}")])
482 }
483 _ => Vec::new(),
484 }
485 } else {
486 salsa::Cancelled::catch(AssertUnwindSafe(|| {
487 self.member_decl_sites(&hierarchy, symbol)
488 }))
489 .unwrap_or_default()
490 }
491 }
492 _ => salsa::Cancelled::catch(AssertUnwindSafe(|| {
493 self.declaration_name_range(symbol).into_iter().collect()
494 }))
495 .unwrap_or_default(),
496 };
497 for (file, range) in decls {
498 if scope.contains(file.as_ref())
499 && !out.iter().any(|(f, r)| *f == file && *r == range)
500 {
501 out.push((file, range));
502 }
503 }
504 }
505 Some(out)
506 }
507
508 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
514 use std::panic::AssertUnwindSafe;
515 let target = class_fqn.trim_start_matches('\\').to_string();
516 let mut out: Vec<String> = vec![target.clone()];
517 let ancestors = salsa::Cancelled::catch(AssertUnwindSafe(|| {
518 let db = self.snapshot_db();
519 let here = crate::db::Fqcn::from_str(&db, &target);
520 crate::db::class_ancestors_by_fqcn(&db, here)
521 .iter()
522 .skip(1)
523 .map(|a| a.trim_start_matches('\\').to_string())
524 .collect::<Vec<_>>()
525 }))
526 .unwrap_or_default();
527 out.extend(ancestors);
528 let subs = {
529 let guard = self.db.salsa.read();
530 guard.subtype_sites_of(&target, true)
531 };
532 out.extend(
533 subs.into_iter()
534 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
535 );
536 let mut seen: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
537 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
538 out
539 }
540
541 fn member_decl_sites(
547 &self,
548 classes: &[String],
549 symbol: &crate::Name,
550 ) -> Vec<(Arc<str>, crate::Range)> {
551 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
552 let db = self.snapshot_db();
553 for class in classes {
554 let here = crate::db::Fqcn::from_str(&db, class);
555 let (loc, needle) = match symbol {
556 crate::Name::Method { name, .. } => {
557 let Some(m) = crate::db::find_method_in_class(&db, here, name) else {
558 continue;
559 };
560 (m.location.clone(), name.to_string())
561 }
562 crate::Name::Property { name, .. } => {
563 let Some(p) = crate::db::find_property_in_class(&db, here, name) else {
564 continue;
565 };
566 (p.location.clone(), name.to_string())
567 }
568 crate::Name::ClassConstant { name, .. } => {
569 let Some(c) = crate::db::find_class_constant_in_class(&db, here, name) else {
570 continue;
571 };
572 (c.location.clone(), name.to_string())
573 }
574 _ => continue,
575 };
576 let Some(loc) = loc else { continue };
577 let range = self.refine_location_to_name(&loc, &needle);
578 out.push((loc.file.clone(), range));
579 }
580 out
581 }
582
583 pub fn declaration_name_range(&self, symbol: &crate::Name) -> Option<(Arc<str>, crate::Range)> {
587 if let crate::Name::GlobalConstant(fqn) = symbol {
588 return self.global_constant_decl_range(fqn);
589 }
590 let loc = self.definition_of(symbol).ok()?;
591 let short = match symbol {
592 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
593 crate::db::subtype_index::short_name_of(f)
594 }
595 crate::Name::Method { name, .. }
596 | crate::Name::Property { name, .. }
597 | crate::Name::ClassConstant { name, .. } => name.as_ref(),
598 };
599 let file = loc.file.clone();
603 let range = self.refine_location_to_name(&loc, short);
604 Some((file, range))
605 }
606
607 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
612 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
613 let text = {
614 let db = self.snapshot_db();
615 db.lookup_source_file(loc.file.as_ref())
616 .map(|sf| sf.text(&db as &dyn MirDatabase))
617 };
618 let Some(text) = text else {
619 return fallback;
620 };
621 let needle_chars = needle.chars().count() as u32;
622 let first_line = loc.line.saturating_sub(1) as usize;
623 for case_insensitive in [false, true] {
628 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
629 let line_no = idx as u32 + 1;
630 if line_no > loc.line_end {
631 break;
632 }
633 let min_col = if line_no == loc.line {
634 loc.col_start as usize
635 } else {
636 0
637 };
638 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
639 {
640 return span_range(line_no, col, col + needle_chars);
641 }
642 }
643 }
644 fallback
645 }
646
647 pub fn indexed_subtype_classes(
661 &self,
662 class_fqn: &str,
663 files: &[Arc<str>],
664 include_trait_users: bool,
665 ) -> Vec<SubtypeClassSite> {
666 let mut scanned: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
667 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
668 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
669 while !pending.is_empty() {
670 let needles: Vec<String> = pending
671 .drain(..)
672 .filter(|f| scanned.insert(f.clone()))
673 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
674 .collect();
675 if !needles.is_empty() {
676 self.commit_defs_for_matching(files, &needles);
677 }
678 sites = {
679 let guard = self.db.salsa.read();
680 guard.subtype_sites_of_lenient(class_fqn, include_trait_users)
681 };
682 pending = sites
683 .iter()
684 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
685 .filter(|f| !scanned.contains(f))
686 .collect();
687 }
688 let mut out: Vec<SubtypeClassSite> = sites
689 .into_iter()
690 .filter_map(|s| {
691 let loc = s.location.as_ref()?;
692 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
693 let range = self.refine_location_to_name(loc, &short);
694 Some(SubtypeClassSite {
695 fqcn: s.fqcn,
696 kind: s.kind,
697 is_abstract: s.is_abstract,
698 file: s.file,
699 range,
700 })
701 })
702 .collect();
703 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
708 let short_lc = crate::db::subtype_index::short_name_of(&root_lc).to_string();
709 let scope: rustc_hash::FxHashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
710 let anon: Vec<(Arc<str>, u32, u16, u16)> = {
711 let guard = self.db.salsa.read();
712 let mut v = guard.reference_locations(&format!("impl:{root_lc}"));
713 v.extend(guard.reference_locations(&format!("implshort:{short_lc}")));
714 v.sort();
715 v.dedup();
716 v
717 };
718 for (file, line, cs, ce) in anon {
719 if !scope.contains(file.as_ref()) {
720 continue;
721 }
722 let range = span_range(line, cs as u32, ce as u32);
723 if out.iter().any(|s| s.file == file && s.range == range) {
724 continue;
725 }
726 out.push(SubtypeClassSite {
727 fqcn: Arc::from("class@anonymous"),
728 kind: crate::db::ClassLikeKind::Class,
729 is_abstract: false,
730 file,
731 range,
732 });
733 }
734 out
735 }
736
737 pub fn indexed_method_implementations(
741 &self,
742 class_fqn: &str,
743 method: &str,
744 files: &[Arc<str>],
745 ) -> Vec<(Arc<str>, Arc<str>, crate::Range)> {
746 use std::panic::AssertUnwindSafe;
747 let subs = self.indexed_subtype_classes(class_fqn, files, false);
748 if subs.is_empty() {
749 return Vec::new();
750 }
751 loop {
752 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
753 let db = self.snapshot_db();
754 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
755 for sub in &subs {
756 let here = crate::db::Fqcn::from_str(&db, sub.fqcn.as_ref());
757 let Some(m) = crate::db::find_method_in_class(&db, here, method) else {
758 continue;
759 };
760 if m.is_abstract {
761 continue;
762 }
763 let Some(loc) = m.location.as_ref() else {
764 continue;
765 };
766 let range = self.refine_location_to_name(loc, method);
767 out.push((sub.fqcn.clone(), loc.file.clone(), range));
768 }
769 out
770 }));
771 if let Ok(mut out) = attempt {
772 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
773 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
774 return out;
775 }
776 }
777 }
778
779 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
783 use std::panic::AssertUnwindSafe;
784
785 use rayon::prelude::*;
786
787 let committed_any: rustc_hash::FxHashSet<Arc<str>> = {
788 let guard = self.defs_committed_keys();
789 guard.into_iter().collect()
790 };
791 let work = loop {
792 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
793 let db_main = self.snapshot_db();
794 files
795 .par_iter()
796 .map_with(db_main, |db, path| {
797 let sf = db.lookup_source_file(path.as_ref())?;
798 let text = sf.text(&*db as &dyn MirDatabase);
799 if self.is_defs_committed(path.as_ref(), &text) {
800 return None;
801 }
802 if !committed_any.contains(path.as_ref())
806 && !shorts.iter().any(|s| mentions_identifier(&text, s))
807 {
808 return None;
809 }
810 let defs =
811 crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
812 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
813 Some((path.clone(), text, entries))
814 })
815 .flatten()
816 .collect::<Vec<_>>()
817 }));
818 if let Ok(v) = attempt {
819 break v;
820 }
821 };
822 if work.is_empty() {
823 return;
824 }
825 let guard = self.db.salsa.read();
826 for (file, text, entries) in &work {
827 guard.set_file_class_edges(file, entries.clone());
828 self.mark_defs_committed(file, text);
829 }
830 }
831
832 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
837 use std::panic::AssertUnwindSafe;
838 let short = crate::db::subtype_index::short_name_of(fqn).to_string();
839 salsa::Cancelled::catch(AssertUnwindSafe(|| {
840 let db = self.snapshot_db();
841 let index = crate::db::workspace_index(&db);
842 let loc = index
843 .constants
844 .get(&mir_types::Name::from(fqn.trim_start_matches('\\')))?;
845 let file = loc.file().path(&db);
846 let sf = db.lookup_source_file(file.as_ref())?;
847 let text = sf.text(&db as &dyn MirDatabase);
848 for (idx, line) in text.lines().enumerate() {
849 let trimmed = line.trim_start();
850 let is_decl_line = trimmed.starts_with("const ")
851 || trimmed.contains("define(")
852 || trimmed.contains("define (");
853 if !is_decl_line {
854 continue;
855 }
856 if let Some(col) = identifier_char_col(line, &short, 0, false) {
857 let n = short.chars().count() as u32;
858 return Some((file, span_range(idx as u32 + 1, col, col + n)));
859 }
860 }
861 None
862 }))
863 .ok()
864 .flatten()
865 }
866
867 pub fn class_issues(&self, files: &[Arc<str>]) -> Vec<crate::Issue> {
877 let db = self.snapshot_db();
878 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
879 let file_data: Vec<(Arc<str>, Arc<str>)> = files
885 .iter()
886 .filter_map(|f| {
887 let sf = db.lookup_source_file(f)?;
888 Some((f.clone(), sf.text(&db as &dyn crate::db::MirDatabase)))
889 })
890 .collect();
891 crate::class::ClassAnalyzer::with_files(&db, file_set, &file_data).analyze_all()
892 }
893
894 pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
900 use crate::symbol::{DeclarationKind, DocumentSymbol};
901
902 let db = self.snapshot_db();
903 let Some(sf) = db.lookup_source_file(file) else {
904 return Vec::new();
905 };
906 let defs = crate::db::collect_file_definitions(&db, sf);
907 let mut out: Vec<DocumentSymbol> = Vec::new();
908
909 let class_children = |methods: &mir_codebase::definitions::MemberMap<
910 Arc<mir_codebase::definitions::MethodDef>,
911 >,
912 props: Option<
913 &mir_codebase::definitions::MemberMap<mir_codebase::definitions::PropertyDef>,
914 >,
915 consts: &mir_codebase::definitions::MemberMap<
916 mir_codebase::definitions::ConstantDef,
917 >,
918 is_enum: bool|
919 -> Vec<DocumentSymbol> {
920 let mut out: Vec<DocumentSymbol> = Vec::new();
921 for (_, m) in methods.iter() {
922 out.push(DocumentSymbol {
923 name: m.name.clone(),
924 kind: DeclarationKind::Method,
925 location: m.location.clone(),
926 children: Vec::new(),
927 });
928 }
929 if let Some(props) = props {
930 for (_, p) in props.iter() {
931 out.push(DocumentSymbol {
932 name: p.name.clone(),
933 kind: DeclarationKind::Property,
934 location: p.location.clone(),
935 children: Vec::new(),
936 });
937 }
938 }
939 let const_kind = if is_enum {
940 DeclarationKind::EnumCase
941 } else {
942 DeclarationKind::Constant
943 };
944 for (_, c) in consts.iter() {
945 out.push(DocumentSymbol {
946 name: c.name.clone(),
947 kind: const_kind,
948 location: c.location.clone(),
949 children: Vec::new(),
950 });
951 }
952 out
953 };
954
955 for c in defs.slice.classes.iter() {
956 out.push(DocumentSymbol {
957 name: c.fqcn.clone(),
958 kind: DeclarationKind::Class,
959 location: c.location.clone(),
960 children: class_children(
961 &c.own_methods,
962 Some(&c.own_properties),
963 &c.own_constants,
964 false,
965 ),
966 });
967 }
968 for i in defs.slice.interfaces.iter() {
969 out.push(DocumentSymbol {
970 name: i.fqcn.clone(),
971 kind: DeclarationKind::Interface,
972 location: i.location.clone(),
973 children: class_children(&i.own_methods, None, &i.own_constants, false),
974 });
975 }
976 for t in defs.slice.traits.iter() {
977 out.push(DocumentSymbol {
978 name: t.fqcn.clone(),
979 kind: DeclarationKind::Trait,
980 location: t.location.clone(),
981 children: class_children(
982 &t.own_methods,
983 Some(&t.own_properties),
984 &t.own_constants,
985 false,
986 ),
987 });
988 }
989 for e in defs.slice.enums.iter() {
990 let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
991 for (_, case) in e.cases.iter() {
992 children.push(DocumentSymbol {
993 name: case.name.clone(),
994 kind: DeclarationKind::EnumCase,
995 location: case.location.clone(),
996 children: Vec::new(),
997 });
998 }
999 out.push(DocumentSymbol {
1000 name: e.fqcn.clone(),
1001 kind: DeclarationKind::Enum,
1002 location: e.location.clone(),
1003 children,
1004 });
1005 }
1006 for f in defs.slice.functions.iter() {
1007 out.push(DocumentSymbol {
1008 name: f.fqn.clone(),
1009 kind: DeclarationKind::Function,
1010 location: f.location.clone(),
1011 children: Vec::new(),
1012 });
1013 }
1014 for (name, _) in defs.slice.constants.iter() {
1015 out.push(DocumentSymbol {
1016 name: name.clone(),
1017 kind: DeclarationKind::Constant,
1018 location: None,
1019 children: Vec::new(),
1020 });
1021 }
1022 out
1023 }
1024}
1025
1026#[derive(Debug, Clone)]
1029pub struct SubtypeClassSite {
1030 pub fqcn: Arc<str>,
1032 pub kind: crate::db::ClassLikeKind,
1033 pub is_abstract: bool,
1034 pub file: Arc<str>,
1035 pub range: crate::Range,
1037}
1038
1039fn span_range(line: u32, col_start: u32, col_end: u32) -> crate::Range {
1042 crate::Range {
1043 start: crate::Position {
1044 line,
1045 column: col_start,
1046 },
1047 end: crate::Position {
1048 line,
1049 column: col_end,
1050 },
1051 }
1052}
1053
1054fn identifier_char_col(
1058 line: &str,
1059 needle: &str,
1060 min_col: usize,
1061 case_insensitive: bool,
1062) -> Option<u32> {
1063 if needle.is_empty() {
1064 return None;
1065 }
1066 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
1067 let chars: Vec<char> = line.chars().collect();
1068 let needle_chars: Vec<char> = needle.chars().collect();
1069 let n = needle_chars.len();
1070 if chars.len() < n {
1071 return None;
1072 }
1073 for start in min_col..=chars.len().saturating_sub(n) {
1074 let matches = chars[start..start + n]
1075 .iter()
1076 .zip(needle_chars.iter())
1077 .all(|(a, b)| {
1078 if case_insensitive {
1079 a.eq_ignore_ascii_case(b)
1080 } else {
1081 a == b
1082 }
1083 });
1084 if !matches {
1085 continue;
1086 }
1087 let before_ok = start == 0 || !is_ident(chars[start - 1]);
1088 let after = start + n;
1089 let after_ok = after >= chars.len() || !is_ident(chars[after]);
1090 if before_ok && after_ok {
1091 return Some(start as u32);
1092 }
1093 }
1094 None
1095}
1096
1097fn mentions_identifier(hay: &str, needle: &str) -> bool {
1102 if needle.is_empty() {
1103 return false;
1104 }
1105 let hay_b = hay.as_bytes();
1106 let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
1107 let mut from = 0;
1108 while let Some(rel) = hay[from..].find(needle) {
1109 let idx = from + rel;
1110 let before_ok = idx == 0 || !is_ident(hay_b[idx - 1]);
1111 let end = idx + needle.len();
1112 let after_ok = end >= hay_b.len() || !is_ident(hay_b[end]);
1113 if before_ok && after_ok {
1114 return true;
1115 }
1116 from = idx + 1;
1117 }
1118 false
1119}