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).clone();
359 let out = crate::db::analyze_file(&*db as &dyn MirDatabase, sf).clone();
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).clone())
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).clone();
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).clone();
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((
922 f.clone(),
923 sf.text(&db as &dyn crate::db::MirDatabase).clone(),
924 ))
925 })
926 .collect();
927 crate::class::ClassAnalyzer::with_files(&db, file_set, &file_data).analyze_all()
928 }
929
930 pub fn document_symbols(&self, file: &str) -> Vec<crate::symbol::DocumentSymbol> {
936 use crate::symbol::{DeclarationKind, DocumentSymbol};
937
938 let db = self.snapshot_db();
939 let Some(sf) = db.lookup_source_file(file) else {
940 return Vec::new();
941 };
942 let defs = crate::db::collect_file_definitions(&db, sf);
943 let mut out: Vec<DocumentSymbol> = Vec::new();
944
945 let class_children = |methods: &mir_codebase::definitions::MemberMap<
946 Arc<mir_codebase::definitions::MethodDef>,
947 >,
948 props: Option<
949 &mir_codebase::definitions::MemberMap<mir_codebase::definitions::PropertyDef>,
950 >,
951 consts: &mir_codebase::definitions::MemberMap<
952 mir_codebase::definitions::ConstantDef,
953 >,
954 is_enum: bool|
955 -> Vec<DocumentSymbol> {
956 let mut out: Vec<DocumentSymbol> = Vec::new();
957 for (_, m) in methods.iter() {
958 out.push(DocumentSymbol {
959 name: m.name.clone(),
960 kind: DeclarationKind::Method,
961 location: m.location.clone(),
962 children: Vec::new(),
963 });
964 }
965 if let Some(props) = props {
966 for (_, p) in props.iter() {
967 out.push(DocumentSymbol {
968 name: p.name.clone(),
969 kind: DeclarationKind::Property,
970 location: p.location.clone(),
971 children: Vec::new(),
972 });
973 }
974 }
975 let const_kind = if is_enum {
976 DeclarationKind::EnumCase
977 } else {
978 DeclarationKind::Constant
979 };
980 for (_, c) in consts.iter() {
981 out.push(DocumentSymbol {
982 name: c.name.clone(),
983 kind: const_kind,
984 location: c.location.clone(),
985 children: Vec::new(),
986 });
987 }
988 out
989 };
990
991 for c in defs.slice.classes.iter() {
992 out.push(DocumentSymbol {
993 name: c.fqcn.clone(),
994 kind: DeclarationKind::Class,
995 location: c.location.clone(),
996 children: class_children(
997 &c.own_methods,
998 Some(&c.own_properties),
999 &c.own_constants,
1000 false,
1001 ),
1002 });
1003 }
1004 for i in defs.slice.interfaces.iter() {
1005 out.push(DocumentSymbol {
1006 name: i.fqcn.clone(),
1007 kind: DeclarationKind::Interface,
1008 location: i.location.clone(),
1009 children: class_children(&i.own_methods, None, &i.own_constants, false),
1010 });
1011 }
1012 for t in defs.slice.traits.iter() {
1013 out.push(DocumentSymbol {
1014 name: t.fqcn.clone(),
1015 kind: DeclarationKind::Trait,
1016 location: t.location.clone(),
1017 children: class_children(
1018 &t.own_methods,
1019 Some(&t.own_properties),
1020 &t.own_constants,
1021 false,
1022 ),
1023 });
1024 }
1025 for e in defs.slice.enums.iter() {
1026 let mut children = class_children(&e.own_methods, None, &e.own_constants, true);
1027 for (_, case) in e.cases.iter() {
1028 children.push(DocumentSymbol {
1029 name: case.name.clone(),
1030 kind: DeclarationKind::EnumCase,
1031 location: case.location.clone(),
1032 children: Vec::new(),
1033 });
1034 }
1035 out.push(DocumentSymbol {
1036 name: e.fqcn.clone(),
1037 kind: DeclarationKind::Enum,
1038 location: e.location.clone(),
1039 children,
1040 });
1041 }
1042 for f in defs.slice.functions.iter() {
1043 out.push(DocumentSymbol {
1044 name: f.fqn.clone(),
1045 kind: DeclarationKind::Function,
1046 location: f.location.clone(),
1047 children: Vec::new(),
1048 });
1049 }
1050 for (name, _) in defs.slice.constants.iter() {
1051 out.push(DocumentSymbol {
1052 name: name.clone(),
1053 kind: DeclarationKind::Constant,
1054 location: None,
1055 children: Vec::new(),
1056 });
1057 }
1058 out
1059 }
1060}
1061
1062#[derive(Debug, Clone)]
1065pub struct SubtypeClassSite {
1066 pub fqcn: Arc<str>,
1068 pub kind: crate::db::ClassLikeKind,
1069 pub is_abstract: bool,
1070 pub file: Arc<str>,
1071 pub range: crate::Range,
1073}
1074
1075fn span_range(line: u32, col_start: u32, col_end: u32) -> crate::Range {
1078 crate::Range {
1079 start: crate::Position {
1080 line,
1081 column: col_start,
1082 },
1083 end: crate::Position {
1084 line,
1085 column: col_end,
1086 },
1087 }
1088}
1089
1090fn identifier_char_col(
1094 line: &str,
1095 needle: &str,
1096 min_col: usize,
1097 case_insensitive: bool,
1098) -> Option<u32> {
1099 if needle.is_empty() {
1100 return None;
1101 }
1102 let is_ident = |c: char| c.is_ascii_alphanumeric() || c == '_';
1103 let chars: Vec<char> = line.chars().collect();
1104 let needle_chars: Vec<char> = needle.chars().collect();
1105 let n = needle_chars.len();
1106 if chars.len() < n {
1107 return None;
1108 }
1109 for start in min_col..=chars.len().saturating_sub(n) {
1110 let matches = chars[start..start + n]
1111 .iter()
1112 .zip(needle_chars.iter())
1113 .all(|(a, b)| {
1114 if case_insensitive {
1115 a.eq_ignore_ascii_case(b)
1116 } else {
1117 a == b
1118 }
1119 });
1120 if !matches {
1121 continue;
1122 }
1123 let before_ok = start == 0 || !is_ident(chars[start - 1]);
1124 let after = start + n;
1125 let after_ok = after >= chars.len() || !is_ident(chars[after]);
1126 if before_ok && after_ok {
1127 return Some(start as u32);
1128 }
1129 }
1130 None
1131}
1132
1133fn mentions_identifier(hay: &str, needle: &str) -> bool {
1138 if needle.is_empty() {
1139 return false;
1140 }
1141 let hay_b = hay.as_bytes();
1142 let is_ident = |b: u8| b.is_ascii_alphanumeric() || b == b'_';
1143 let mut from = 0;
1144 while let Some(rel) = hay[from..].find(needle) {
1145 let idx = from + rel;
1146 let before_ok = idx == 0 || !is_ident(hay_b[idx - 1]);
1147 let end = idx + needle.len();
1148 let after_ok = end >= hay_b.len() || !is_ident(hay_b[end]);
1149 if before_ok && after_ok {
1150 return true;
1151 }
1152 from = idx + 1;
1156 while from < hay_b.len() && (hay_b[from] & 0xC0) == 0x80 {
1157 from += 1;
1158 }
1159 }
1160 false
1161}