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 let put = if self.ref_commit_is_current(path.as_ref(), &text, &out) {
367 None
368 } else {
369 self.stage_ref_cache_put(
370 &*db as &dyn MirDatabase,
371 sf,
372 path.as_ref(),
373 &text,
374 &out,
375 )
376 };
377 Some((path.clone(), text, out, entries, put))
378 })
379 .flatten()
380 .collect::<Vec<_>>()
381 }));
382 match attempt {
383 Ok(v) => break (gen, v),
384 Err(_) if should_cancel() => return None,
385 Err(_) => {}
386 }
387 };
388 let mut analyzed = analyzed;
389 let guard = self.db.salsa.read();
390 for (file, text, out, entries, put) in analyzed.iter_mut() {
391 if !self.ref_commit_is_current(file.as_ref(), text, out) {
394 guard.set_file_reference_locations(file.as_ref(), out.ref_locs.to_vec());
395 }
396 if let Some(put) = put.take() {
397 self.apply_ref_cache_put(file.as_ref(), out, put);
398 }
399 self.mark_ref_committed(
400 file,
401 text,
402 Some(out),
403 commit_gen,
404 !out.has_unresolved_names(),
405 );
406 if !self.is_defs_committed(file.as_ref(), text) {
407 guard.set_file_class_edges(file, entries.clone());
408 self.mark_defs_committed(file, text);
409 }
410 }
411 }
412
413 let hierarchy: Vec<String> = match symbol {
426 crate::Name::Method { class, name } => {
427 if name.as_ref() == "__construct" || class.is_empty() {
428 if class.is_empty() {
429 Vec::new()
430 } else {
431 vec![class.trim_start_matches('\\').to_string()]
432 }
433 } else {
434 self.member_hierarchy_classes(class.as_ref())
435 }
436 }
437 crate::Name::Property { class, .. } | crate::Name::ClassConstant { class, .. } => {
438 if class.is_empty() {
439 Vec::new()
440 } else {
441 self.member_hierarchy_classes(class.as_ref())
442 }
443 }
444 _ => Vec::new(),
445 };
446 let primary_keys: Vec<String> = match symbol {
447 crate::Name::Method { name, .. } => hierarchy
448 .iter()
449 .map(|c| format!("meth:{c}::{name}"))
450 .collect(),
451 crate::Name::Property { name, .. } => hierarchy
452 .iter()
453 .map(|c| format!("prop:{c}::{name}"))
454 .collect(),
455 crate::Name::ClassConstant { name, .. } => hierarchy
456 .iter()
457 .map(|c| format!("cnst:{c}::{name}"))
458 .collect(),
459 _ => vec![key.clone()],
460 };
461 let fallback_key: Option<String> = match symbol {
462 crate::Name::Method { name, .. } => Some(format!("methname:{name}")),
463 crate::Name::Property { name, .. } => Some(format!("propname:{name}")),
464 _ => None,
465 };
466 let scope: rustc_hash::FxHashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
467 let read_keys = |keys: &[String]| -> Vec<(Arc<str>, crate::Range)> {
468 let guard = self.db.salsa.read();
469 let mut merged: Vec<(Arc<str>, u32, u16, u16)> = Vec::new();
470 for k in keys {
471 merged.extend(guard.reference_locations(k));
472 }
473 merged
474 .into_iter()
475 .filter(|(file, ..)| scope.contains(file.as_ref()))
476 .map(|(file, line, col_start, col_end)| {
477 (file, span_range(line, col_start as u32, col_end as u32))
478 })
479 .collect()
480 };
481 let mut out = read_keys(&primary_keys);
482 if out.is_empty() {
483 if let Some(fk) = fallback_key {
484 out = read_keys(std::slice::from_ref(&fk));
485 }
486 }
487 out.sort_by(|a, b| {
488 a.0.cmp(&b.0)
489 .then(a.1.start.line.cmp(&b.1.start.line))
490 .then(a.1.start.column.cmp(&b.1.start.column))
491 });
492 out.dedup_by(|a, b| a.0 == b.0 && a.1 == b.1);
493
494 if include_declaration {
495 let decls: Vec<(Arc<str>, crate::Range)> = match symbol {
499 crate::Name::Method { class, .. }
500 | crate::Name::Property { class, .. }
501 | crate::Name::ClassConstant { class, .. } => {
502 if class.is_empty() {
503 match symbol {
507 crate::Name::Method { name, .. } => {
508 read_keys(&[format!("methdecl:{name}")])
509 }
510 crate::Name::Property { name, .. } => {
511 read_keys(&[format!("propdecl:{name}")])
512 }
513 crate::Name::ClassConstant { name, .. } => {
514 read_keys(&[format!("cnstdecl:{name}")])
515 }
516 _ => Vec::new(),
517 }
518 } else {
519 salsa::Cancelled::catch(AssertUnwindSafe(|| {
520 self.member_decl_sites(&hierarchy, symbol)
521 }))
522 .unwrap_or_default()
523 }
524 }
525 _ => salsa::Cancelled::catch(AssertUnwindSafe(|| {
526 self.declaration_name_range(symbol).into_iter().collect()
527 }))
528 .unwrap_or_default(),
529 };
530 for (file, range) in decls {
531 if scope.contains(file.as_ref())
532 && !out.iter().any(|(f, r)| *f == file && *r == range)
533 {
534 out.push((file, range));
535 }
536 }
537 }
538 Some(out)
539 }
540
541 fn member_hierarchy_classes(&self, class_fqn: &str) -> Vec<String> {
547 use std::panic::AssertUnwindSafe;
548 let target = class_fqn.trim_start_matches('\\').to_string();
549 let mut out: Vec<String> = vec![target.clone()];
550 let ancestors = salsa::Cancelled::catch(AssertUnwindSafe(|| {
551 let db = self.snapshot_db();
552 let here = crate::db::Fqcn::from_str(&db, &target);
553 crate::db::class_ancestors_by_fqcn(&db, here)
554 .iter()
555 .skip(1)
556 .map(|a| a.trim_start_matches('\\').to_string())
557 .collect::<Vec<_>>()
558 }))
559 .unwrap_or_default();
560 out.extend(ancestors);
561 let subs = {
562 let guard = self.db.salsa.read();
563 guard.subtype_sites_of(&target, true)
564 };
565 out.extend(
566 subs.into_iter()
567 .map(|s| s.fqcn.trim_start_matches('\\').to_string()),
568 );
569 let mut seen: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
570 out.retain(|c| seen.insert(c.to_ascii_lowercase()));
571 out
572 }
573
574 fn member_decl_sites(
580 &self,
581 classes: &[String],
582 symbol: &crate::Name,
583 ) -> Vec<(Arc<str>, crate::Range)> {
584 let mut out: Vec<(Arc<str>, crate::Range)> = Vec::new();
585 let db = self.snapshot_db();
586 for class in classes {
587 let here = crate::db::Fqcn::from_str(&db, class);
588 let (loc, needle) = match symbol {
589 crate::Name::Method { name, .. } => {
590 let Some(m) = crate::db::find_method_in_class(&db, here, name) else {
591 continue;
592 };
593 (m.location.clone(), name.to_string())
594 }
595 crate::Name::Property { name, .. } => {
596 let Some(p) = crate::db::find_property_in_class(&db, here, name) else {
597 continue;
598 };
599 (p.location.clone(), name.to_string())
600 }
601 crate::Name::ClassConstant { name, .. } => {
602 let Some(c) = crate::db::find_class_constant_in_class(&db, here, name) else {
603 continue;
604 };
605 (c.location.clone(), name.to_string())
606 }
607 _ => continue,
608 };
609 let Some(loc) = loc else { continue };
610 let range = self.refine_location_to_name(&loc, &needle);
611 out.push((loc.file.clone(), range));
612 }
613 out
614 }
615
616 pub fn declaration_name_range(&self, symbol: &crate::Name) -> Option<(Arc<str>, crate::Range)> {
620 if let crate::Name::GlobalConstant(fqn) = symbol {
621 return self.global_constant_decl_range(fqn);
622 }
623 let loc = self.definition_of(symbol).ok()?;
624 let short = match symbol {
625 crate::Name::Class(f) | crate::Name::Function(f) | crate::Name::GlobalConstant(f) => {
626 crate::db::subtype_index::short_name_of(f)
627 }
628 crate::Name::Method { name, .. }
629 | crate::Name::Property { name, .. }
630 | crate::Name::ClassConstant { name, .. } => name.as_ref(),
631 };
632 let file = loc.file.clone();
636 let range = self.refine_location_to_name(&loc, short);
637 Some((file, range))
638 }
639
640 fn refine_location_to_name(&self, loc: &mir_types::Location, needle: &str) -> crate::Range {
645 let fallback = span_range(loc.line, loc.col_start as u32, loc.col_end as u32);
646 let text = {
647 let db = self.snapshot_db();
648 db.lookup_source_file(loc.file.as_ref())
649 .map(|sf| sf.text(&db as &dyn MirDatabase))
650 };
651 let Some(text) = text else {
652 return fallback;
653 };
654 let needle_chars = needle.chars().count() as u32;
655 let first_line = loc.line.saturating_sub(1) as usize;
656 for case_insensitive in [false, true] {
661 for (idx, line_text) in text.lines().enumerate().skip(first_line) {
662 let line_no = idx as u32 + 1;
663 if line_no > loc.line_end {
664 break;
665 }
666 let min_col = if line_no == loc.line {
667 loc.col_start as usize
668 } else {
669 0
670 };
671 if let Some(col) = identifier_char_col(line_text, needle, min_col, case_insensitive)
672 {
673 return span_range(line_no, col, col + needle_chars);
674 }
675 }
676 }
677 fallback
678 }
679
680 pub fn indexed_subtype_classes(
694 &self,
695 class_fqn: &str,
696 files: &[Arc<str>],
697 include_trait_users: bool,
698 ) -> Vec<SubtypeClassSite> {
699 let mut scanned: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
700 let mut pending: Vec<String> = vec![class_fqn.trim_start_matches('\\').to_string()];
701 let mut sites: Vec<crate::db::SubtypeSite> = Vec::new();
702 while !pending.is_empty() {
703 let needles: Vec<String> = pending
704 .drain(..)
705 .filter(|f| scanned.insert(f.clone()))
706 .map(|f| crate::db::subtype_index::short_name_of(&f).to_string())
707 .collect();
708 if !needles.is_empty() {
709 self.commit_defs_for_matching(files, &needles);
710 }
711 sites = {
712 let guard = self.db.salsa.read();
713 guard.subtype_sites_of_lenient(class_fqn, include_trait_users)
714 };
715 pending = sites
716 .iter()
717 .map(|s| s.fqcn.trim_start_matches('\\').to_string())
718 .filter(|f| !scanned.contains(f))
719 .collect();
720 }
721 let mut out: Vec<SubtypeClassSite> = sites
722 .into_iter()
723 .filter_map(|s| {
724 let loc = s.location.as_ref()?;
725 let short = crate::db::subtype_index::short_name_of(&s.fqcn).to_string();
726 let range = self.refine_location_to_name(loc, &short);
727 Some(SubtypeClassSite {
728 fqcn: s.fqcn,
729 kind: s.kind,
730 is_abstract: s.is_abstract,
731 file: s.file,
732 range,
733 })
734 })
735 .collect();
736 let root_lc = class_fqn.trim_start_matches('\\').to_ascii_lowercase();
741 let short_lc = crate::db::subtype_index::short_name_of(&root_lc).to_string();
742 let scope: rustc_hash::FxHashSet<&str> = files.iter().map(|f| f.as_ref()).collect();
743 let anon: Vec<(Arc<str>, u32, u16, u16)> = {
744 let guard = self.db.salsa.read();
745 let mut v = guard.reference_locations(&format!("impl:{root_lc}"));
746 v.extend(guard.reference_locations(&format!("implshort:{short_lc}")));
747 v.sort();
748 v.dedup();
749 v
750 };
751 for (file, line, cs, ce) in anon {
752 if !scope.contains(file.as_ref()) {
753 continue;
754 }
755 let range = span_range(line, cs as u32, ce as u32);
756 if out.iter().any(|s| s.file == file && s.range == range) {
757 continue;
758 }
759 out.push(SubtypeClassSite {
760 fqcn: Arc::from("class@anonymous"),
761 kind: crate::db::ClassLikeKind::Class,
762 is_abstract: false,
763 file,
764 range,
765 });
766 }
767 out
768 }
769
770 pub fn indexed_method_implementations(
774 &self,
775 class_fqn: &str,
776 method: &str,
777 files: &[Arc<str>],
778 ) -> Vec<(Arc<str>, Arc<str>, crate::Range)> {
779 use std::panic::AssertUnwindSafe;
780 let subs = self.indexed_subtype_classes(class_fqn, files, false);
781 if subs.is_empty() {
782 return Vec::new();
783 }
784 loop {
785 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
786 let db = self.snapshot_db();
787 let mut out: Vec<(Arc<str>, Arc<str>, crate::Range)> = Vec::new();
788 for sub in &subs {
789 let here = crate::db::Fqcn::from_str(&db, sub.fqcn.as_ref());
790 let Some(m) = crate::db::find_method_in_class(&db, here, method) else {
791 continue;
792 };
793 if m.is_abstract {
794 continue;
795 }
796 let Some(loc) = m.location.as_ref() else {
797 continue;
798 };
799 let range = self.refine_location_to_name(loc, method);
800 out.push((sub.fqcn.clone(), loc.file.clone(), range));
801 }
802 out
803 }));
804 if let Ok(mut out) = attempt {
805 out.sort_by(|a, b| a.1.cmp(&b.1).then(a.2.start.line.cmp(&b.2.start.line)));
806 out.dedup_by(|a, b| a.1 == b.1 && a.2 == b.2);
807 return out;
808 }
809 }
810 }
811
812 fn commit_defs_for_matching(&self, files: &[Arc<str>], shorts: &[String]) {
816 use std::panic::AssertUnwindSafe;
817
818 use rayon::prelude::*;
819
820 let committed_any: rustc_hash::FxHashSet<Arc<str>> = {
821 let guard = self.defs_committed_keys();
822 guard.into_iter().collect()
823 };
824 let work = loop {
825 let attempt = salsa::Cancelled::catch(AssertUnwindSafe(|| {
826 let db_main = self.snapshot_db();
827 files
828 .par_iter()
829 .map_with(db_main, |db, path| {
830 let sf = db.lookup_source_file(path.as_ref())?;
831 let text = sf.text(&*db as &dyn MirDatabase);
832 if self.is_defs_committed(path.as_ref(), &text) {
833 return None;
834 }
835 if !committed_any.contains(path.as_ref())
839 && !shorts.iter().any(|s| mentions_identifier(&text, s))
840 {
841 return None;
842 }
843 let defs =
844 crate::db::collect_file_definitions(&*db as &dyn MirDatabase, sf);
845 let entries = crate::db::subtype_index::entries_from_slice(&defs.slice);
846 Some((path.clone(), text, entries))
847 })
848 .flatten()
849 .collect::<Vec<_>>()
850 }));
851 if let Ok(v) = attempt {
852 break v;
853 }
854 };
855 if work.is_empty() {
856 return;
857 }
858 let guard = self.db.salsa.read();
859 for (file, text, entries) in &work {
860 guard.set_file_class_edges(file, entries.clone());
861 self.mark_defs_committed(file, text);
862 }
863 }
864
865 fn global_constant_decl_range(&self, fqn: &str) -> Option<(Arc<str>, crate::Range)> {
870 use std::panic::AssertUnwindSafe;
871 let short = crate::db::subtype_index::short_name_of(fqn).to_string();
872 salsa::Cancelled::catch(AssertUnwindSafe(|| {
873 let db = self.snapshot_db();
874 let index = crate::db::workspace_index(&db);
875 let loc = index
876 .constants
877 .get(&mir_types::Name::from(fqn.trim_start_matches('\\')))?;
878 let file = loc.file().path(&db);
879 let sf = db.lookup_source_file(file.as_ref())?;
880 let text = sf.text(&db as &dyn MirDatabase);
881 for (idx, line) in text.lines().enumerate() {
882 let trimmed = line.trim_start();
883 let is_decl_line = trimmed.starts_with("const ")
884 || trimmed.contains("define(")
885 || trimmed.contains("define (");
886 if !is_decl_line {
887 continue;
888 }
889 if let Some(col) = identifier_char_col(line, &short, 0, false) {
890 let n = short.chars().count() as u32;
891 return Some((file, span_range(idx as u32 + 1, col, col + n)));
892 }
893 }
894 None
895 }))
896 .ok()
897 .flatten()
898 }
899
900 pub fn class_issues(&self, files: &[Arc<str>]) -> Vec<crate::Issue> {
910 let db = self.snapshot_db();
911 let file_set: HashSet<Arc<str>> = files.iter().cloned().collect();
912 let file_data: Vec<(Arc<str>, Arc<str>)> = files
918 .iter()
919 .filter_map(|f| {
920 let sf = db.lookup_source_file(f)?;
921 Some((f.clone(), sf.text(&db as &dyn crate::db::MirDatabase)))
922 })
923 .collect();
924 crate::class::ClassAnalyzer::with_files(&db, file_set, &file_data).analyze_all()
925 }
926
927 pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
933 use crate::symbol::{DeclarationKind, DocumentSymbol};
934
935 let db = self.snapshot_db();
936 let Some(sf) = db.lookup_source_file(file) else {
937 return Vec::new();
938 };
939 let defs = crate::db::collect_file_definitions(&db, sf);
940 let mut out: Vec<DocumentSymbol> = Vec::new();
941
942 let class_children = |methods: &mir_codebase::definitions::MemberMap<
943 Arc<mir_codebase::definitions::MethodDef>,
944 >,
945 props: Option<
946 &mir_codebase::definitions::MemberMap<mir_codebase::definitions::PropertyDef>,
947 >,
948 consts: &mir_codebase::definitions::MemberMap<
949 mir_codebase::definitions::ConstantDef,
950 >,
951 is_enum: bool|
952 -> Vec<DocumentSymbol> {
953 let mut out: Vec<DocumentSymbol> = Vec::new();
954 for (_, m) in methods.iter() {
955 out.push(DocumentSymbol {
956 name: m.name.clone(),
957 kind: DeclarationKind::Method,
958 location: m.location.clone(),
959 children: Vec::new(),
960 });
961 }
962 if let Some(props) = props {
963 for (_, p) in props.iter() {
964 out.push(DocumentSymbol {
965 name: p.name.clone(),
966 kind: DeclarationKind::Property,
967 location: p.location.clone(),
968 children: Vec::new(),
969 });
970 }
971 }
972 let const_kind = if is_enum {
973 DeclarationKind::EnumCase
974 } else {
975 DeclarationKind::Constant
976 };
977 for (_, c) in consts.iter() {
978 out.push(DocumentSymbol {
979 name: c.name.clone(),
980 kind: const_kind,
981 location: c.location.clone(),
982 children: Vec::new(),
983 });
984 }
985 out
986 };
987
988 for c in defs.slice.classes.iter() {
989 out.push(DocumentSymbol {
990 name: c.fqcn.clone(),
991 kind: DeclarationKind::Class,
992 location: c.location.clone(),
993 children: class_children(
994 &c.own_methods,
995 Some(&c.own_properties),
996 &c.own_constants,
997 false,
998 ),
999 });
1000 }
1001 for i in defs.slice.interfaces.iter() {
1002 out.push(DocumentSymbol {
1003 name: i.fqcn.clone(),
1004 kind: DeclarationKind::Interface,
1005 location: i.location.clone(),
1006 children: class_children(&i.own_methods, None, &i.own_constants, false),
1007 });
1008 }
1009 for t in defs.slice.traits.iter() {
1010 out.push(DocumentSymbol {
1011 name: t.fqcn.clone(),
1012 kind: DeclarationKind::Trait,
1013 location: t.location.clone(),
1014 children: class_children(
1015 &t.own_methods,
1016 Some(&t.own_properties),
1017 &t.own_constants,
1018 false,
1019 ),
1020 });
1021 }
1022 for e in defs.slice.enums.iter() {
1023 let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
1024 for (_, case) in e.cases.iter() {
1025 children.push(DocumentSymbol {
1026 name: case.name.clone(),
1027 kind: DeclarationKind::EnumCase,
1028 location: case.location.clone(),
1029 children: Vec::new(),
1030 });
1031 }
1032 out.push(DocumentSymbol {
1033 name: e.fqcn.clone(),
1034 kind: DeclarationKind::Enum,
1035 location: e.location.clone(),
1036 children,
1037 });
1038 }
1039 for f in defs.slice.functions.iter() {
1040 out.push(DocumentSymbol {
1041 name: f.fqn.clone(),
1042 kind: DeclarationKind::Function,
1043 location: f.location.clone(),
1044 children: Vec::new(),
1045 });
1046 }
1047 for (name, _) in defs.slice.constants.iter() {
1048 out.push(DocumentSymbol {
1049 name: name.clone(),
1050 kind: DeclarationKind::Constant,
1051 location: None,
1052 children: Vec::new(),
1053 });
1054 }
1055 out
1056 }
1057}
1058
1059#[derive(Debug, Clone)]
1062pub struct SubtypeClassSite {
1063 pub fqcn: Arc<str>,
1065 pub kind: crate::db::ClassLikeKind,
1066 pub is_abstract: bool,
1067 pub file: Arc<str>,
1068 pub range: crate::Range,
1070}
1071
1072fn span_range(line: u32, col_start: u32, col_end: u32) -> crate::Range {
1075 crate::Range {
1076 start: crate::Position {
1077 line,
1078 column: col_start,
1079 },
1080 end: crate::Position {
1081 line,
1082 column: col_end,
1083 },
1084 }
1085}
1086
1087fn identifier_char_col(
1091 line: &str,
1092 needle: &str,
1093 min_col: usize,
1094 case_insensitive: bool,
1095) -> Option<u32> {
1096 if needle.is_empty() {
1097 return None;
1098 }
1099 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
1100 let chars: Vec<char> = line.chars().collect();
1101 let needle_chars: Vec<char> = needle.chars().collect();
1102 let n = needle_chars.len();
1103 if chars.len() < n {
1104 return None;
1105 }
1106 for start in min_col..=chars.len().saturating_sub(n) {
1107 let matches = chars[start..start + n]
1108 .iter()
1109 .zip(needle_chars.iter())
1110 .all(|(a, b)| {
1111 if case_insensitive {
1112 a.eq_ignore_ascii_case(b)
1113 } else {
1114 a == b
1115 }
1116 });
1117 if !matches {
1118 continue;
1119 }
1120 let before_ok = start == 0 || !is_ident(chars[start - 1]);
1121 let after = start + n;
1122 let after_ok = after >= chars.len() || !is_ident(chars[after]);
1123 if before_ok && after_ok {
1124 return Some(start as u32);
1125 }
1126 }
1127 None
1128}
1129
1130fn mentions_identifier(hay: &str, needle: &str) -> bool {
1135 if needle.is_empty() {
1136 return false;
1137 }
1138 let hay_b = hay.as_bytes();
1139 let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
1140 let mut from = 0;
1141 while let Some(rel) = hay[from..].find(needle) {
1142 let idx = from + rel;
1143 let before_ok = idx == 0 || !is_ident(hay_b[idx - 1]);
1144 let end = idx + needle.len();
1145 let after_ok = end >= hay_b.len() || !is_ident(hay_b[end]);
1146 if before_ok && after_ok {
1147 return true;
1148 }
1149 from = idx + 1;
1150 }
1151 false
1152}