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 current_gen = self.index_generation();
312 let db = self.snapshot_db();
313 files
314 .iter()
315 .filter(|f| {
316 db.lookup_source_file(f.as_ref()).is_some_and(|sf| {
317 let text = sf.text(&db as &dyn MirDatabase);
318 !self.is_ref_committed(f.as_ref(), &text, current_gen)
319 })
320 })
321 .cloned()
322 .collect::<Vec<_>>()
323 }));
324 match attempt {
325 Ok(v) => break v,
326 Err(_) if should_cancel() => return None,
327 Err(_) => {}
328 }
329 };
330
331 if !stale.is_empty() {
332 for path in &stale {
336 if should_cancel() {
337 return None;
338 }
339 self.prepare_file_for_analysis(path);
340 }
341
342 let (commit_gen, analyzed) = loop {
345 if should_cancel() {
346 return None;
347 }
348 let gen = self.index_generation();
352 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
353 let db_main = self.snapshot_db();
354 stale
355 .par_iter()
356 .map_with(db_main, |db, path| {
357 let sf = db.lookup_source_file(path.as_ref())?;
358 let text = sf.text(&*db as &dyn MirDatabase);
359 let out = crate::db::analyze_file(&*db as &dyn MirDatabase, sf);
360 let defs =
361 crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
362 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
363 Some((path.clone(), text, out, entries))
364 })
365 .flatten()
366 .collect::<Vec<_>>()
367 }));
368 match attempt {
369 Ok(v) => break (gen, v),
370 Err(_) if should_cancel() => return None,
371 Err(_) => {}
372 }
373 };
374 let guard = self.db.salsa.read();
375 for (file, text, out, entries) in &analyzed {
376 if !self.ref_commit_is_current(file.as_ref(), text, out) {
379 guard.set_file_reference_locations(file.as_ref(), out.ref_locs.to_vec());
380 }
381 self.mark_ref_committed(
382 file,
383 text,
384 Some(out),
385 commit_gen,
386 !out.has_unresolved_names(),
387 );
388 if !self.is_defs_committed(file.as_ref(), text) {
389 guard.set_file_class_edges(file, entries.clone());
390 self.mark_defs_committed(file, text);
391 }
392 }
393 }
394
395 let hierarchy: Vec<String> = match symbol {
408 crate::Name::Method { class, name } => {
409 if name.as_ref() == "__construct" || class.is_empty() {
410 if class.is_empty() {
411 Vec::new()
412 } else {
413 vec![class.trim_start_matches('\\').to_string()]
414 }
415 } else {
416 self.member_hierarchy_classes(class.as_ref())
417 }
418 }
419 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
420 if class.is_empty() {
421 Vec::new()
422 } else {
423 self.member_hierarchy_classes(class.as_ref())
424 }
425 }
426 _ => Vec::new(),
427 };
428 let primary_keys: Vec<String> = match symbol {
429 crate::Name::Method { name, .. } => hierarchy
430 .iter()
431 .map(|c| format!("meth:{c}::{name}"))
432 .collect(),
433 crate::Name::Property { name, .. } => hierarchy
434 .iter()
435 .map(|c| format!("prop:{c}::{name}"))
436 .collect(),
437 crate::Name::ClassConstant { name, .. } => hierarchy
438 .iter()
439 .map(|c| format!("cnst:{c}::{name}"))
440 .collect(),
441 _ => vec![key.clone()],
442 };
443 let fallback_key: Option<String> = match symbol {
444 crate::Name::Method { name, .. } => Some(format!("methname:{name}")),
445 crate::Name::Property { name, .. } => Some(format!("propname:{name}")),
446 _ => None,
447 };
448 let scope: rustc_hash::FxHashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
449 let read_keys = |keys: &[String]| -> Vec<(Arc<str>, crate::Range)> {
450 let guard = self.db.salsa.read();
451 let mut merged: Vec<(Arc<str>, u32, u16, u16)> = Vec::new();
452 for k in keys {
453 merged.extend(guard.reference_locations(k));
454 }
455 merged
456 .into_iter()
457 .filter(|(file, ..)| scope.contains(file.as_ref()))
458 .map(|(file, line, col_start, col_end)| {
459 (file, span_range(line, col_start as u32, col_end as u32))
460 })
461 .collect()
462 };
463 let mut out = read_keys(&primary_keys);
464 if out.is_empty() {
465 if let Some(fk) = fallback_key {
466 out = read_keys(std::slice::from_ref(&fk));
467 }
468 }
469 out.sort_by(|a, b| {
470 a.0.cmp(&b.0)
471 .then(a.1.start.line.cmp(&b.1.start.line))
472 .then(a.1.start.column.cmp(&b.1.start.column))
473 });
474 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
475
476 if include_declaration {
477 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
481 crate::Name::Method { class, .. }
482 | crate::Name::Property { class, .. }
483 | crate::Name::ClassConstant { class, .. } => {
484 if class.is_empty() {
485 match symbol {
489 crate::Name::Method { name, .. } => {
490 read_keys(&[format!("methdecl:{name}")])
491 }
492 crate::Name::Property { name, .. } => {
493 read_keys(&[format!("propdecl:{name}")])
494 }
495 crate::Name::ClassConstant { name, .. } => {
496 read_keys(&[format!("cnstdecl:{name}")])
497 }
498 _ => Vec::new(),
499 }
500 } else {
501 salsa::Cancelled::catch(AssertUnwindSafe(|| {
502 self.member_decl_sites(&hierarchy, symbol)
503 }))
504 .unwrap_or_default()
505 }
506 }
507 _ => salsa::Cancelled::catch(AssertUnwindSafe(|| {
508 self.declaration_name_range(symbol).into_iter().collect()
509 }))
510 .unwrap_or_default(),
511 };
512 for (file, range) in decls {
513 if scope.contains(file.as_ref())
514 && !out.iter().any(|(f, r)| *f == file && *r == range)
515 {
516 out.push((file, range));
517 }
518 }
519 }
520 Some(out)
521 }
522
523 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
529 use std::panic::AssertUnwindSafe;
530 let target = class_fqn.trim_start_matches('\\').to_string();
531 let mut out: Vec<String> = vec![target.clone()];
532 let ancestors = salsa::Cancelled::catch(AssertUnwindSafe(|| {
533 let db = self.snapshot_db();
534 let here = crate::db::Fqcn::from_str(&db, &target);
535 crate::db::class_ancestors_by_fqcn(&db, here)
536 .iter()
537 .skip(1)
538 .map(|a| a.trim_start_matches('\\').to_string())
539 .collect::<Vec<_>>()
540 }))
541 .unwrap_or_default();
542 out.extend(ancestors);
543 let subs = {
544 let guard = self.db.salsa.read();
545 guard.subtype_sites_of(&target, true)
546 };
547 out.extend(
548 subs.into_iter()
549 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
550 );
551 let mut seen: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
552 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
553 out
554 }
555
556 fn member_decl_sites(
562 &self,
563 classes: &[String],
564 symbol: &crate::Name,
565 ) -> Vec<(Arc<str>, crate::Range)> {
566 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
567 let db = self.snapshot_db();
568 for class in classes {
569 let here = crate::db::Fqcn::from_str(&db, class);
570 let (loc, needle) = match symbol {
571 crate::Name::Method { name, .. } => {
572 let Some(m) = crate::db::find_method_in_class(&db, here, name) else {
573 continue;
574 };
575 (m.location.clone(), name.to_string())
576 }
577 crate::Name::Property { name, .. } => {
578 let Some(p) = crate::db::find_property_in_class(&db, here, name) else {
579 continue;
580 };
581 (p.location.clone(), name.to_string())
582 }
583 crate::Name::ClassConstant { name, .. } => {
584 let Some(c) = crate::db::find_class_constant_in_class(&db, here, name) else {
585 continue;
586 };
587 (c.location.clone(), name.to_string())
588 }
589 _ => continue,
590 };
591 let Some(loc) = loc else { continue };
592 let range = self.refine_location_to_name(&loc, &needle);
593 out.push((loc.file.clone(), range));
594 }
595 out
596 }
597
598 pub fn declaration_name_range(&self, symbol: &crate::Name) -> Option<(Arc<str>, crate::Range)> {
602 if let crate::Name::GlobalConstant(fqn) = symbol {
603 return self.global_constant_decl_range(fqn);
604 }
605 let loc = self.definition_of(symbol).ok()?;
606 let short = match symbol {
607 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
608 crate::db::subtype_index::short_name_of(f)
609 }
610 crate::Name::Method { name, .. }
611 | crate::Name::Property { name, .. }
612 | crate::Name::ClassConstant { name, .. } => name.as_ref(),
613 };
614 let file = loc.file.clone();
618 let range = self.refine_location_to_name(&loc, short);
619 Some((file, range))
620 }
621
622 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
627 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
628 let text = {
629 let db = self.snapshot_db();
630 db.lookup_source_file(loc.file.as_ref())
631 .map(|sf| sf.text(&db as &dyn MirDatabase))
632 };
633 let Some(text) = text else {
634 return fallback;
635 };
636 let needle_chars = needle.chars().count() as u32;
637 let first_line = loc.line.saturating_sub(1) as usize;
638 for case_insensitive in [false, true] {
643 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
644 let line_no = idx as u32 + 1;
645 if line_no > loc.line_end {
646 break;
647 }
648 let min_col = if line_no == loc.line {
649 loc.col_start as usize
650 } else {
651 0
652 };
653 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
654 {
655 return span_range(line_no, col, col + needle_chars);
656 }
657 }
658 }
659 fallback
660 }
661
662 pub fn indexed_subtype_classes(
676 &self,
677 class_fqn: &str,
678 files: &[Arc<str>],
679 include_trait_users: bool,
680 ) -> Vec<SubtypeClassSite> {
681 let mut scanned: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
682 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
683 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
684 while !pending.is_empty() {
685 let needles: Vec<String> = pending
686 .drain(..)
687 .filter(|f| scanned.insert(f.clone()))
688 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
689 .collect();
690 if !needles.is_empty() {
691 self.commit_defs_for_matching(files, &needles);
692 }
693 sites = {
694 let guard = self.db.salsa.read();
695 guard.subtype_sites_of_lenient(class_fqn, include_trait_users)
696 };
697 pending = sites
698 .iter()
699 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
700 .filter(|f| !scanned.contains(f))
701 .collect();
702 }
703 let mut out: Vec<SubtypeClassSite> = sites
704 .into_iter()
705 .filter_map(|s| {
706 let loc = s.location.as_ref()?;
707 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
708 let range = self.refine_location_to_name(loc, &short);
709 Some(SubtypeClassSite {
710 fqcn: s.fqcn,
711 kind: s.kind,
712 is_abstract: s.is_abstract,
713 file: s.file,
714 range,
715 })
716 })
717 .collect();
718 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
723 let short_lc = crate::db::subtype_index::short_name_of(&root_lc).to_string();
724 let scope: rustc_hash::FxHashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
725 let anon: Vec<(Arc<str>, u32, u16, u16)> = {
726 let guard = self.db.salsa.read();
727 let mut v = guard.reference_locations(&format!("impl:{root_lc}"));
728 v.extend(guard.reference_locations(&format!("implshort:{short_lc}")));
729 v.sort();
730 v.dedup();
731 v
732 };
733 for (file, line, cs, ce) in anon {
734 if !scope.contains(file.as_ref()) {
735 continue;
736 }
737 let range = span_range(line, cs as u32, ce as u32);
738 if out.iter().any(|s| s.file == file && s.range == range) {
739 continue;
740 }
741 out.push(SubtypeClassSite {
742 fqcn: Arc::from("class@anonymous"),
743 kind: crate::db::ClassLikeKind::Class,
744 is_abstract: false,
745 file,
746 range,
747 });
748 }
749 out
750 }
751
752 pub fn indexed_method_implementations(
756 &self,
757 class_fqn: &str,
758 method: &str,
759 files: &[Arc<str>],
760 ) -> Vec<(Arc<str>, Arc<str>, crate::Range)> {
761 use std::panic::AssertUnwindSafe;
762 let subs = self.indexed_subtype_classes(class_fqn, files, false);
763 if subs.is_empty() {
764 return Vec::new();
765 }
766 loop {
767 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
768 let db = self.snapshot_db();
769 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
770 for sub in &subs {
771 let here = crate::db::Fqcn::from_str(&db, sub.fqcn.as_ref());
772 let Some(m) = crate::db::find_method_in_class(&db, here, method) else {
773 continue;
774 };
775 if m.is_abstract {
776 continue;
777 }
778 let Some(loc) = m.location.as_ref() else {
779 continue;
780 };
781 let range = self.refine_location_to_name(loc, method);
782 out.push((sub.fqcn.clone(), loc.file.clone(), range));
783 }
784 out
785 }));
786 if let Ok(mut out) = attempt {
787 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
788 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
789 return out;
790 }
791 }
792 }
793
794 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
798 use std::panic::AssertUnwindSafe;
799
800 use rayon::prelude::*;
801
802 let committed_any: rustc_hash::FxHashSet<Arc<str>> = {
803 let guard = self.defs_committed_keys();
804 guard.into_iter().collect()
805 };
806 let work = loop {
807 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
808 let db_main = self.snapshot_db();
809 files
810 .par_iter()
811 .map_with(db_main, |db, path| {
812 let sf = db.lookup_source_file(path.as_ref())?;
813 let text = sf.text(&*db as &dyn MirDatabase);
814 if self.is_defs_committed(path.as_ref(), &text) {
815 return None;
816 }
817 if !committed_any.contains(path.as_ref())
821 && !shorts.iter().any(|s| mentions_identifier(&text, s))
822 {
823 return None;
824 }
825 let defs =
826 crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
827 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
828 Some((path.clone(), text, entries))
829 })
830 .flatten()
831 .collect::<Vec<_>>()
832 }));
833 if let Ok(v) = attempt {
834 break v;
835 }
836 };
837 if work.is_empty() {
838 return;
839 }
840 let guard = self.db.salsa.read();
841 for (file, text, entries) in &work {
842 guard.set_file_class_edges(file, entries.clone());
843 self.mark_defs_committed(file, text);
844 }
845 }
846
847 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
852 use std::panic::AssertUnwindSafe;
853 let short = crate::db::subtype_index::short_name_of(fqn).to_string();
854 salsa::Cancelled::catch(AssertUnwindSafe(|| {
855 let db = self.snapshot_db();
856 let index = crate::db::workspace_index(&db);
857 let loc = index
858 .constants
859 .get(&mir_types::Name::from(fqn.trim_start_matches('\\')))?;
860 let file = loc.file().path(&db);
861 let sf = db.lookup_source_file(file.as_ref())?;
862 let text = sf.text(&db as &dyn MirDatabase);
863 for (idx, line) in text.lines().enumerate() {
864 let trimmed = line.trim_start();
865 let is_decl_line = trimmed.starts_with("const ")
866 || trimmed.contains("define(")
867 || trimmed.contains("define (");
868 if !is_decl_line {
869 continue;
870 }
871 if let Some(col) = identifier_char_col(line, &short, 0, false) {
872 let n = short.chars().count() as u32;
873 return Some((file, span_range(idx as u32 + 1, col, col + n)));
874 }
875 }
876 None
877 }))
878 .ok()
879 .flatten()
880 }
881
882 pub fn class_issues(&self, files: &[Arc<str>]) -> Vec<crate::Issue> {
892 let db = self.snapshot_db();
893 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
894 let file_data: Vec<(Arc<str>, Arc<str>)> = files
900 .iter()
901 .filter_map(|f| {
902 let sf = db.lookup_source_file(f)?;
903 Some((f.clone(), sf.text(&db as &dyn crate::db::MirDatabase)))
904 })
905 .collect();
906 crate::class::ClassAnalyzer::with_files(&db, file_set, &file_data).analyze_all()
907 }
908
909 pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
915 use crate::symbol::{DeclarationKind, DocumentSymbol};
916
917 let db = self.snapshot_db();
918 let Some(sf) = db.lookup_source_file(file) else {
919 return Vec::new();
920 };
921 let defs = crate::db::collect_file_definitions(&db, sf);
922 let mut out: Vec<DocumentSymbol> = Vec::new();
923
924 let class_children = |methods: &mir_codebase::definitions::MemberMap<
925 Arc<mir_codebase::definitions::MethodDef>,
926 >,
927 props: Option<
928 &mir_codebase::definitions::MemberMap<mir_codebase::definitions::PropertyDef>,
929 >,
930 consts: &mir_codebase::definitions::MemberMap<
931 mir_codebase::definitions::ConstantDef,
932 >,
933 is_enum: bool|
934 -> Vec<DocumentSymbol> {
935 let mut out: Vec<DocumentSymbol> = Vec::new();
936 for (_, m) in methods.iter() {
937 out.push(DocumentSymbol {
938 name: m.name.clone(),
939 kind: DeclarationKind::Method,
940 location: m.location.clone(),
941 children: Vec::new(),
942 });
943 }
944 if let Some(props) = props {
945 for (_, p) in props.iter() {
946 out.push(DocumentSymbol {
947 name: p.name.clone(),
948 kind: DeclarationKind::Property,
949 location: p.location.clone(),
950 children: Vec::new(),
951 });
952 }
953 }
954 let const_kind = if is_enum {
955 DeclarationKind::EnumCase
956 } else {
957 DeclarationKind::Constant
958 };
959 for (_, c) in consts.iter() {
960 out.push(DocumentSymbol {
961 name: c.name.clone(),
962 kind: const_kind,
963 location: c.location.clone(),
964 children: Vec::new(),
965 });
966 }
967 out
968 };
969
970 for c in defs.slice.classes.iter() {
971 out.push(DocumentSymbol {
972 name: c.fqcn.clone(),
973 kind: DeclarationKind::Class,
974 location: c.location.clone(),
975 children: class_children(
976 &c.own_methods,
977 Some(&c.own_properties),
978 &c.own_constants,
979 false,
980 ),
981 });
982 }
983 for i in defs.slice.interfaces.iter() {
984 out.push(DocumentSymbol {
985 name: i.fqcn.clone(),
986 kind: DeclarationKind::Interface,
987 location: i.location.clone(),
988 children: class_children(&i.own_methods, None, &i.own_constants, false),
989 });
990 }
991 for t in defs.slice.traits.iter() {
992 out.push(DocumentSymbol {
993 name: t.fqcn.clone(),
994 kind: DeclarationKind::Trait,
995 location: t.location.clone(),
996 children: class_children(
997 &t.own_methods,
998 Some(&t.own_properties),
999 &t.own_constants,
1000 false,
1001 ),
1002 });
1003 }
1004 for e in defs.slice.enums.iter() {
1005 let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
1006 for (_, case) in e.cases.iter() {
1007 children.push(DocumentSymbol {
1008 name: case.name.clone(),
1009 kind: DeclarationKind::EnumCase,
1010 location: case.location.clone(),
1011 children: Vec::new(),
1012 });
1013 }
1014 out.push(DocumentSymbol {
1015 name: e.fqcn.clone(),
1016 kind: DeclarationKind::Enum,
1017 location: e.location.clone(),
1018 children,
1019 });
1020 }
1021 for f in defs.slice.functions.iter() {
1022 out.push(DocumentSymbol {
1023 name: f.fqn.clone(),
1024 kind: DeclarationKind::Function,
1025 location: f.location.clone(),
1026 children: Vec::new(),
1027 });
1028 }
1029 for (name, _) in defs.slice.constants.iter() {
1030 out.push(DocumentSymbol {
1031 name: name.clone(),
1032 kind: DeclarationKind::Constant,
1033 location: None,
1034 children: Vec::new(),
1035 });
1036 }
1037 out
1038 }
1039}
1040
1041#[derive(Debug, Clone)]
1044pub struct SubtypeClassSite {
1045 pub fqcn: Arc<str>,
1047 pub kind: crate::db::ClassLikeKind,
1048 pub is_abstract: bool,
1049 pub file: Arc<str>,
1050 pub range: crate::Range,
1052}
1053
1054fn span_range(line: u32, col_start: u32, col_end: u32) -> crate::Range {
1057 crate::Range {
1058 start: crate::Position {
1059 line,
1060 column: col_start,
1061 },
1062 end: crate::Position {
1063 line,
1064 column: col_end,
1065 },
1066 }
1067}
1068
1069fn identifier_char_col(
1073 line: &str,
1074 needle: &str,
1075 min_col: usize,
1076 case_insensitive: bool,
1077) -> Option<u32> {
1078 if needle.is_empty() {
1079 return None;
1080 }
1081 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
1082 let chars: Vec<char> = line.chars().collect();
1083 let needle_chars: Vec<char> = needle.chars().collect();
1084 let n = needle_chars.len();
1085 if chars.len() < n {
1086 return None;
1087 }
1088 for start in min_col..=chars.len().saturating_sub(n) {
1089 let matches = chars[start..start + n]
1090 .iter()
1091 .zip(needle_chars.iter())
1092 .all(|(a, b)| {
1093 if case_insensitive {
1094 a.eq_ignore_ascii_case(b)
1095 } else {
1096 a == b
1097 }
1098 });
1099 if !matches {
1100 continue;
1101 }
1102 let before_ok = start == 0 || !is_ident(chars[start - 1]);
1103 let after = start + n;
1104 let after_ok = after >= chars.len() || !is_ident(chars[after]);
1105 if before_ok && after_ok {
1106 return Some(start as u32);
1107 }
1108 }
1109 None
1110}
1111
1112fn mentions_identifier(hay: &str, needle: &str) -> bool {
1117 if needle.is_empty() {
1118 return false;
1119 }
1120 let hay_b = hay.as_bytes();
1121 let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
1122 let mut from = 0;
1123 while let Some(rel) = hay[from..].find(needle) {
1124 let idx = from + rel;
1125 let before_ok = idx == 0 || !is_ident(hay_b[idx - 1]);
1126 let end = idx + needle.len();
1127 let after_ok = end >= hay_b.len() || !is_ident(hay_b[end]);
1128 if before_ok && after_ok {
1129 return true;
1130 }
1131 from = idx + 1;
1132 }
1133 false
1134}