1use std::mem::ManuallyDrop;
3use std::path::{Path, PathBuf};
4use std::sync::Arc;
5
6use parking_lot::Mutex;
7
8use rayon::prelude::*;
9
10use std::collections::{HashMap, HashSet};
11
12use crate::cache::{hash_content, AnalysisCache};
13use crate::db::{
14 collect_file_definitions, collect_file_definitions_uncached, FileDefinitions, MirDatabase,
15 MirDb, SourceFile,
16};
17use crate::pass2::Pass2Driver;
18use crate::php_version::PhpVersion;
19use crate::shared_db::SharedDb;
20use mir_issues::Issue;
21use mir_types::Union;
22use salsa::Setter as _;
23
24pub(crate) use crate::pass2::merge_return_types;
25
26pub struct ProjectAnalyzer {
40 shared_db: Arc<SharedDb>,
43 cache: Option<AnalysisCache>,
45 pub on_file_done: Option<Arc<dyn Fn() + Send + Sync>>,
47 pub psr4: Option<Arc<crate::composer::Psr4Map>>,
49 pub find_dead_code: bool,
51 pub php_version: Option<PhpVersion>,
54 pub stub_files: Vec<PathBuf>,
56 pub stub_dirs: Vec<PathBuf>,
58}
59
60struct ParsedProjectFile {
61 file: Arc<str>,
62 source: Arc<str>,
63 parsed: ManuallyDrop<php_rs_parser::ParseResult<'static, 'static>>,
64 arena: ManuallyDrop<Box<bumpalo::Bump>>,
65}
66
67impl ParsedProjectFile {
68 fn new(file: Arc<str>, source: Arc<str>) -> Self {
69 let arena = Box::new(crate::arena::create_parse_arena(source.len()));
70 let parsed = php_rs_parser::parse(&arena, &source);
71 let parsed = unsafe {
75 std::mem::transmute::<
76 php_rs_parser::ParseResult<'_, '_>,
77 php_rs_parser::ParseResult<'static, 'static>,
78 >(parsed)
79 };
80 Self {
81 file,
82 source,
83 parsed: ManuallyDrop::new(parsed),
84 arena: ManuallyDrop::new(arena),
85 }
86 }
87
88 fn source(&self) -> &str {
89 self.source.as_ref()
90 }
91
92 fn parsed(&self) -> &php_rs_parser::ParseResult<'_, '_> {
93 &self.parsed
94 }
95}
96
97impl Drop for ParsedProjectFile {
98 fn drop(&mut self) {
99 unsafe {
100 ManuallyDrop::drop(&mut self.parsed);
101 ManuallyDrop::drop(&mut self.arena);
102 }
103 }
104}
105
106unsafe impl Send for ParsedProjectFile {}
110unsafe impl Sync for ParsedProjectFile {}
111
112impl ProjectAnalyzer {
113 pub fn new() -> Self {
114 Self {
115 shared_db: Arc::new(SharedDb::new()),
116 cache: None,
117 on_file_done: None,
118 psr4: None,
119 find_dead_code: false,
120 php_version: None,
121 stub_files: Vec::new(),
122 stub_dirs: Vec::new(),
123 }
124 }
125
126 pub fn with_cache(cache_dir: &Path) -> Self {
128 Self {
129 shared_db: Arc::new(SharedDb::new()),
130 cache: Some(AnalysisCache::open(cache_dir)),
131 on_file_done: None,
132 psr4: None,
133 find_dead_code: false,
134 php_version: None,
135 stub_files: Vec::new(),
136 stub_dirs: Vec::new(),
137 }
138 }
139
140 pub fn set_cache_dir(&mut self, cache_dir: &Path) {
142 self.cache = Some(AnalysisCache::open(cache_dir));
143 }
144
145 pub fn from_composer(
149 root: &Path,
150 ) -> Result<(Self, crate::composer::Psr4Map), crate::composer::ComposerError> {
151 let map = crate::composer::Psr4Map::from_composer(root)?;
152 let psr4 = Arc::new(map.clone());
153 let analyzer = Self {
154 shared_db: Arc::new(SharedDb::new()),
155 cache: None,
156 on_file_done: None,
157 psr4: Some(psr4),
158 find_dead_code: false,
159 php_version: None,
160 stub_files: Vec::new(),
161 stub_dirs: Vec::new(),
162 };
163 Ok((analyzer, map))
164 }
165
166 pub fn with_php_version(mut self, version: PhpVersion) -> Self {
168 self.php_version = Some(version);
169 self
170 }
171
172 pub fn with_dead_code(mut self, enabled: bool) -> Self {
174 self.find_dead_code = enabled;
175 self
176 }
177
178 pub fn with_progress_callback(mut self, callback: Arc<dyn Fn() + Send + Sync>) -> Self {
180 self.on_file_done = Some(callback);
181 self
182 }
183
184 pub fn with_stub_files(mut self, files: Vec<PathBuf>) -> Self {
186 self.stub_files = files;
187 self
188 }
189
190 pub fn with_stub_dirs(mut self, dirs: Vec<PathBuf>) -> Self {
192 self.stub_dirs = dirs;
193 self
194 }
195
196 pub fn with_cache_dir(mut self, cache_dir: &Path) -> Self {
198 self.cache = Some(AnalysisCache::open(cache_dir));
199 self
200 }
201
202 pub fn with_psr4(mut self, map: Arc<crate::composer::Psr4Map>) -> Self {
204 self.psr4 = Some(map);
205 self
206 }
207
208 fn resolved_php_version(&self) -> PhpVersion {
211 self.php_version.unwrap_or(PhpVersion::LATEST)
212 }
213
214 fn type_exists(&self, fqcn: &str) -> bool {
215 let db = self.snapshot_db();
216 crate::db::type_exists_via_db(&db, fqcn)
217 }
218
219 pub fn contains_function(&self, fqn: &str) -> bool {
221 let db = self.snapshot_db();
222 db.lookup_function_node(fqn).is_some_and(|n| n.active(&db))
223 }
224
225 pub fn contains_class(&self, fqcn: &str) -> bool {
227 let db = self.snapshot_db();
228 db.lookup_class_node(fqcn).is_some_and(|n| n.active(&db))
229 }
230
231 pub fn contains_method(&self, class: &str, name: &str) -> bool {
233 let db = self.snapshot_db();
234 let name_lower = name.to_ascii_lowercase();
235 db.lookup_method_node(class, &name_lower)
236 .is_some_and(|n| n.active(&db))
237 }
238
239 fn snapshot_db(&self) -> MirDb {
244 self.shared_db.snapshot_db()
245 }
246
247 #[doc(hidden)]
249 pub fn salsa_db_for_test(
250 &self,
251 ) -> &parking_lot::Mutex<(
252 MirDb,
253 std::collections::HashMap<Arc<str>, crate::db::SourceFile>,
254 )> {
255 &self.shared_db.salsa
256 }
257
258 #[doc(hidden)]
262 pub fn member_location(
263 &self,
264 fqcn: &str,
265 member_name: &str,
266 ) -> Option<mir_codebase::storage::Location> {
267 let db = self.snapshot_db();
268 crate::db::member_location_via_db(&db, fqcn, member_name)
269 }
270
271 #[doc(hidden)]
275 pub fn symbol_location(&self, symbol: &str) -> Option<mir_codebase::storage::Location> {
276 let db = self.snapshot_db();
277 db.lookup_class_node(symbol)
278 .filter(|n| n.active(&db))
279 .and_then(|n| n.location(&db))
280 .or_else(|| {
281 db.lookup_function_node(symbol)
282 .filter(|n| n.active(&db))
283 .and_then(|n| n.location(&db))
284 })
285 }
286
287 #[doc(hidden)]
292 pub fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
293 let db = self.snapshot_db();
294 db.reference_locations(symbol)
295 }
296
297 pub fn definition_of(
301 &self,
302 symbol: &crate::Symbol,
303 ) -> Result<mir_codebase::storage::Location, crate::SymbolLookupError> {
304 let db = self.snapshot_db();
305 match symbol {
306 crate::Symbol::Class(fqcn) => {
307 let node = db
308 .lookup_class_node(fqcn.as_ref())
309 .filter(|n| n.active(&db))
310 .ok_or(crate::SymbolLookupError::NotFound)?;
311 node.location(&db)
312 .ok_or(crate::SymbolLookupError::NoSourceLocation)
313 }
314 crate::Symbol::Function(fqn) => {
315 let node = db
316 .lookup_function_node(fqn.as_ref())
317 .filter(|n| n.active(&db))
318 .ok_or(crate::SymbolLookupError::NotFound)?;
319 node.location(&db)
320 .ok_or(crate::SymbolLookupError::NoSourceLocation)
321 }
322 crate::Symbol::Method { class, name }
323 | crate::Symbol::Property { class, name }
324 | crate::Symbol::ClassConstant { class, name } => {
325 crate::db::member_location_via_db(&db, class, name)
326 .ok_or(crate::SymbolLookupError::NotFound)
327 }
328 crate::Symbol::GlobalConstant(_) => Err(crate::SymbolLookupError::NoSourceLocation),
329 }
330 }
331
332 pub fn references_to(&self, symbol: &crate::Symbol) -> Vec<(Arc<str>, crate::Range)> {
336 let db = self.snapshot_db();
337 let key = symbol.codebase_key();
338 db.reference_locations(&key)
339 .into_iter()
340 .map(|(file, line, col_start, col_end)| {
341 let range = crate::Range {
342 start: crate::Position {
343 line,
344 column: col_start as u32,
345 },
346 end: crate::Position {
347 line,
348 column: col_end as u32,
349 },
350 };
351 (file, range)
352 })
353 .collect()
354 }
355
356 pub fn load_stubs(&self) {
360 let php_version = self.resolved_php_version();
361
362 let paths: Vec<&'static str> = crate::stubs::stub_files().iter().map(|&(p, _)| p).collect();
364 self.shared_db.ingest_stub_paths(&paths, php_version);
365
366 self.shared_db
368 .ingest_user_stubs(&self.stub_files, &self.stub_dirs);
369 }
370
371 fn collect_and_ingest_source(&self, file: Arc<str>, src: &str) -> FileDefinitions {
372 self.shared_db.collect_and_ingest_file(file, src)
373 }
374
375 pub fn analyze(&self, paths: &[PathBuf]) -> AnalysisResult {
377 let mut all_issues = Vec::new();
378
379 self.load_stubs();
381
382 let parsed_files: Vec<ParsedProjectFile> = paths
384 .par_iter()
385 .filter_map(|path| match std::fs::read_to_string(path) {
386 Ok(src) => {
387 let file = Arc::from(path.to_string_lossy().as_ref());
388 Some(ParsedProjectFile::new(file, Arc::from(src)))
389 }
390 Err(e) => {
391 eprintln!("Cannot read {}: {}", path.display(), e);
392 None
393 }
394 })
395 .collect();
396
397 let file_data: Vec<(Arc<str>, Arc<str>)> = parsed_files
398 .iter()
399 .map(|parsed| (parsed.file.clone(), parsed.source.clone()))
400 .collect();
401
402 if let Some(cache) = &self.cache {
404 let changed: Vec<String> = file_data
405 .par_iter()
406 .filter_map(|(f, src)| {
407 let h = hash_content(src.as_ref());
408 if cache.get(f, &h).is_none() {
409 Some(f.to_string())
410 } else {
411 None
412 }
413 })
414 .collect();
415 if !changed.is_empty() {
416 cache.evict_with_dependents(&changed);
417 }
418 }
419
420 {
422 let mut guard = self.shared_db.salsa.lock();
423 let (ref mut db, ref mut files) = *guard;
424 for parsed in &parsed_files {
425 match files.get(parsed.file.as_ref()) {
426 Some(&sf) => {
427 if sf.text(db).as_ref() != parsed.source() {
428 sf.set_text(db).to(parsed.source.clone());
429 }
430 }
431 None => {
432 let file = parsed.file.clone();
433 let sf = SourceFile::new(db, file.clone(), parsed.source.clone());
434 files.insert(file, sf);
435 }
436 }
437 }
438 }
439
440 let file_defs: Vec<FileDefinitions> = parsed_files
442 .par_iter()
443 .map(|parsed| {
444 let parse_result = parsed.parsed();
445 let mut all_issues: Vec<Issue> = parse_result
446 .errors
447 .iter()
448 .map(|err| {
449 Issue::new(
450 mir_issues::IssueKind::ParseError {
451 message: err.to_string(),
452 },
453 mir_issues::Location {
454 file: parsed.file.clone(),
455 line: 1,
456 line_end: 1,
457 col_start: 0,
458 col_end: 0,
459 },
460 )
461 })
462 .collect();
463 let collector = crate::collector::DefinitionCollector::new_for_slice(
464 parsed.file.clone(),
465 parsed.source(),
466 &parse_result.source_map,
467 );
468 let (slice, collector_issues) = collector.collect_slice(&parse_result.program);
469 all_issues.extend(collector_issues);
470 FileDefinitions {
471 slice: Arc::new(slice),
472 issues: Arc::new(all_issues),
473 }
474 })
475 .collect();
476
477 let mut files_with_parse_errors: std::collections::HashSet<Arc<str>> =
478 std::collections::HashSet::new();
479 let mut files_needing_inference: std::collections::HashSet<Arc<str>> =
480 std::collections::HashSet::new();
481 {
482 let mut guard = self.shared_db.salsa.lock();
483 let (ref mut db, _) = *guard;
484 for defs in file_defs {
485 for issue in defs.issues.iter() {
486 if matches!(issue.kind, mir_issues::IssueKind::ParseError { .. }) {
487 files_with_parse_errors.insert(issue.location.file.clone());
488 }
489 }
490 if stub_slice_needs_inference(&defs.slice) {
491 if let Some(file) = defs.slice.file.as_ref() {
492 files_needing_inference.insert(file.clone());
493 }
494 }
495 db.ingest_stub_slice(&defs.slice);
496 all_issues.extend(Arc::unwrap_or_clone(defs.issues));
497 }
498 }
499
500 if let Some(psr4) = &self.psr4 {
502 self.lazy_load_missing_classes(psr4.clone(), &mut all_issues);
503 }
504
505 if let Some(cache) = &self.cache {
509 let db_snapshot = {
510 let guard = self.shared_db.salsa.lock();
511 guard.0.clone()
512 };
513 let rev = build_reverse_deps(&db_snapshot);
514 cache.set_reverse_deps(rev);
515 }
516
517 let analyzed_file_set: std::collections::HashSet<std::sync::Arc<str>> =
523 file_data.iter().map(|(f, _)| f.clone()).collect();
524 {
525 let class_db = {
526 let guard = self.shared_db.salsa.lock();
527 guard.0.clone()
528 };
529 let class_issues =
530 crate::class::ClassAnalyzer::with_files(&class_db, analyzed_file_set, &file_data)
531 .analyze_all();
532 all_issues.extend(class_issues);
533 }
534
535 let db_priming = {
539 let guard = self.shared_db.salsa.lock();
540 guard.0.clone()
541 };
542
543 let filtered_parsed: Vec<_> = parsed_files
552 .par_iter()
553 .filter(|parsed| {
554 !files_with_parse_errors.contains(&parsed.file)
555 && files_needing_inference.contains(&parsed.file)
556 })
557 .collect();
558
559 let (functions, methods) =
560 run_inference_sweep(db_priming, filtered_parsed, self.resolved_php_version());
561
562 {
563 let mut guard = self.shared_db.salsa.lock();
564 guard.0.commit_inferred_return_types(functions, methods);
565 }
566
567 let db_main = {
568 let guard = self.shared_db.salsa.lock();
569 guard.0.clone()
570 };
571
572 let pass2_results: Vec<(Vec<Issue>, Vec<crate::symbol::ResolvedSymbol>)> = parsed_files
574 .par_iter()
575 .filter(|parsed| !files_with_parse_errors.contains(&parsed.file))
576 .map_with(db_main, |db, parsed| {
577 let driver =
578 Pass2Driver::new(&*db as &dyn MirDatabase, self.resolved_php_version());
579 let result = if let Some(cache) = &self.cache {
580 let h = hash_content(parsed.source());
581 if let Some((cached_issues, ref_locs)) = cache.get(&parsed.file, &h) {
582 db.replay_reference_locations(parsed.file.clone(), &ref_locs);
583 (cached_issues, Vec::new())
584 } else {
585 let parse_result = parsed.parsed();
586 let (issues, symbols) = driver.analyze_bodies(
587 &parse_result.program,
588 parsed.file.clone(),
589 parsed.source(),
590 &parse_result.source_map,
591 );
592 let ref_locs = extract_reference_locations(&*db, &parsed.file);
593 cache.put(&parsed.file, h, issues.clone(), ref_locs);
594 (issues, symbols)
595 }
596 } else {
597 let parse_result = parsed.parsed();
598 driver.analyze_bodies(
599 &parse_result.program,
600 parsed.file.clone(),
601 parsed.source(),
602 &parse_result.source_map,
603 )
604 };
605 if let Some(cb) = &self.on_file_done {
606 cb();
607 }
608 result
609 })
610 .collect();
611
612 let mut all_symbols = Vec::new();
613 for (issues, symbols) in pass2_results {
614 all_issues.extend(issues);
615 all_symbols.extend(symbols);
616 }
617
618 if let Some(psr4) = &self.psr4 {
624 self.lazy_load_from_body_issues(
625 psr4.clone(),
626 &file_data,
627 &files_with_parse_errors,
628 &mut all_issues,
629 &mut all_symbols,
630 );
631 }
632
633 if let Some(cache) = &self.cache {
635 cache.flush();
636 }
637
638 if self.find_dead_code {
641 let salsa = self.shared_db.salsa.lock();
642 let dead_code_issues = crate::dead_code::DeadCodeAnalyzer::new(&salsa.0).analyze();
643 drop(salsa);
644 all_issues.extend(dead_code_issues);
645 }
646
647 AnalysisResult::build(all_issues, std::collections::HashMap::new(), all_symbols)
648 }
649
650 fn lazy_load_missing_classes(
651 &self,
652 psr4: Arc<crate::composer::Psr4Map>,
653 all_issues: &mut Vec<Issue>,
654 ) {
655 use std::collections::HashSet;
656
657 let max_depth = 10;
658 let mut loaded: HashSet<String> = HashSet::new();
659
660 for _ in 0..max_depth {
661 let mut to_load: Vec<(String, PathBuf)> = Vec::new();
662
663 let mut try_queue = |fqcn: &str| {
664 if !self.type_exists(fqcn) && !loaded.contains(fqcn) {
665 if let Some(path) = psr4.resolve(fqcn) {
666 to_load.push((fqcn.to_string(), path));
667 }
668 }
669 };
670
671 let mut inheritance_candidates = Vec::new();
673 let import_candidates = {
674 let guard = self.shared_db.salsa.lock();
675 let db = &guard.0;
676 for fqcn in db.active_class_node_fqcns() {
677 let Some(node) = db.lookup_class_node(&fqcn) else {
678 continue;
679 };
680 if node.is_interface(db) {
681 for parent in node.extends(db).iter() {
682 inheritance_candidates.push(parent.to_string());
683 }
684 } else if node.is_enum(db) {
685 for iface in node.interfaces(db).iter() {
686 inheritance_candidates.push(iface.to_string());
687 }
688 } else if node.is_trait(db) {
689 for used in node.traits(db).iter() {
690 inheritance_candidates.push(used.to_string());
691 }
692 } else {
693 if let Some(parent) = node.parent(db) {
694 inheritance_candidates.push(parent.to_string());
695 }
696 for iface in node.interfaces(db).iter() {
697 inheritance_candidates.push(iface.to_string());
698 }
699 }
700 }
701 db.file_import_snapshots()
702 .into_iter()
703 .flat_map(|(_, imports)| imports.into_values())
704 .collect::<Vec<_>>()
705 };
706 for fqcn in inheritance_candidates {
707 try_queue(&fqcn);
708 }
709
710 for fqcn in import_candidates {
714 try_queue(&fqcn);
715 }
716
717 if to_load.is_empty() {
718 break;
719 }
720
721 for (fqcn, path) in to_load {
722 loaded.insert(fqcn);
723 if let Ok(src) = std::fs::read_to_string(&path) {
724 let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
725 let defs = self.collect_and_ingest_source(file, &src);
726 all_issues.extend(Arc::unwrap_or_clone(defs.issues));
727 }
728 }
729 }
730 }
731
732 fn lazy_load_from_body_issues(
733 &self,
734 psr4: Arc<crate::composer::Psr4Map>,
735 file_data: &[(Arc<str>, Arc<str>)],
736 files_with_parse_errors: &HashSet<Arc<str>>,
737 all_issues: &mut Vec<Issue>,
738 all_symbols: &mut Vec<crate::symbol::ResolvedSymbol>,
739 ) {
740 use mir_issues::IssueKind;
741
742 let max_depth = 5;
743 let mut loaded: HashSet<String> = HashSet::new();
744
745 for _ in 0..max_depth {
746 let mut to_load: HashMap<String, PathBuf> = HashMap::new();
749
750 for issue in all_issues.iter() {
751 if let IssueKind::UndefinedClass { name } = &issue.kind {
752 if !self.type_exists(name) && !loaded.contains(name) {
753 if let Some(path) = psr4.resolve(name) {
754 to_load.entry(name.clone()).or_insert(path);
755 }
756 }
757 }
758 }
759
760 if to_load.is_empty() {
761 break;
762 }
763
764 loaded.extend(to_load.keys().cloned());
765
766 for path in to_load.values() {
767 if let Ok(src) = std::fs::read_to_string(path) {
768 let file: Arc<str> = Arc::from(path.to_string_lossy().as_ref());
769 let _ = self.collect_and_ingest_source(file, &src);
770 }
771 }
772
773 self.lazy_load_missing_classes(psr4.clone(), all_issues);
777
778 let files_to_reanalyze: HashSet<Arc<str>> = all_issues
781 .iter()
782 .filter_map(|i| {
783 if let IssueKind::UndefinedClass { name } = &i.kind {
784 if self.type_exists(name) {
785 return Some(i.location.file.clone());
786 }
787 }
788 None
789 })
790 .collect();
791
792 if files_to_reanalyze.is_empty() {
793 break;
794 }
795
796 all_issues.retain(|i| !files_to_reanalyze.contains(&i.location.file));
797 all_symbols.retain(|s| !files_to_reanalyze.contains(&s.file));
798
799 let sweep: Vec<(Arc<str>, Arc<str>)> = file_data
813 .iter()
814 .filter(|(f, _)| {
815 !files_with_parse_errors.contains(f) && files_to_reanalyze.contains(f)
816 })
817 .cloned()
818 .collect();
819
820 let (inferred_fns, inferred_methods) = crate::session::gather_inferred_types(
821 {
822 let guard = self.shared_db.salsa.lock();
823 guard.0.clone()
824 },
825 &sweep,
826 self.resolved_php_version(),
827 );
828
829 {
830 let mut guard_db = self.shared_db.salsa.lock();
831 guard_db
832 .0
833 .commit_inferred_return_types(inferred_fns, inferred_methods);
834 }
835
836 let db_full = {
837 let guard = self.shared_db.salsa.lock();
838 guard.0.clone()
839 };
840
841 let reanalysis: Vec<(Vec<Issue>, Vec<crate::symbol::ResolvedSymbol>)> = file_data
842 .par_iter()
843 .filter(|(f, _)| {
844 !files_with_parse_errors.contains(f) && files_to_reanalyze.contains(f)
845 })
846 .map_with(db_full, |db, (file, src)| {
847 let driver =
848 Pass2Driver::new(&*db as &dyn MirDatabase, self.resolved_php_version());
849 let arena = crate::arena::create_parse_arena(src.len());
850 let parsed = php_rs_parser::parse(&arena, src);
851 driver.analyze_bodies(&parsed.program, file.clone(), src, &parsed.source_map)
852 })
853 .collect();
854
855 for (issues, symbols) in reanalysis {
856 all_issues.extend(issues);
857 all_symbols.extend(symbols);
858 }
859 }
860 }
861
862 pub fn re_analyze_file(&self, file_path: &str, new_content: &str) -> AnalysisResult {
871 if let Some(cache) = &self.cache {
873 let h = hash_content(new_content);
874 if let Some((issues, ref_locs)) = cache.get(file_path, &h) {
875 let file: Arc<str> = Arc::from(file_path);
876 let guard = self.shared_db.salsa.lock();
877 guard.0.replay_reference_locations(file, &ref_locs);
878 return AnalysisResult::build(issues, HashMap::new(), Vec::new());
879 }
880 }
881
882 let file: Arc<str> = Arc::from(file_path);
883
884 {
885 let mut guard = self.shared_db.salsa.lock();
886 let (ref mut db, _) = *guard;
887 db.remove_file_definitions(file_path);
888 }
889
890 let file_defs = {
892 let mut guard = self.shared_db.salsa.lock();
893 let (ref mut db, ref mut files) = *guard;
894 let salsa_file = match files.get(&file) {
895 Some(&sf) => {
896 sf.set_text(db).to(Arc::from(new_content));
897 sf
898 }
899 None => {
900 let sf = SourceFile::new(db, file.clone(), Arc::from(new_content));
901 files.insert(file.clone(), sf);
902 sf
903 }
904 };
905 collect_file_definitions(db, salsa_file)
906 };
907
908 let mut all_issues: Vec<Issue> = Arc::unwrap_or_clone(file_defs.issues.clone());
909
910 let symbols = {
913 let mut guard = self.shared_db.salsa.lock();
914 let (ref mut db, _) = *guard;
915
916 db.ingest_stub_slice(&file_defs.slice);
917
918 let arena = bumpalo::Bump::new();
922 let parsed = php_rs_parser::parse(&arena, new_content);
923
924 if parsed.errors.is_empty() {
925 let db_ref: &dyn MirDatabase = db;
926 let driver = Pass2Driver::new_inference_only(db_ref, self.resolved_php_version());
927 driver.analyze_bodies(
928 &parsed.program,
929 file.clone(),
930 new_content,
931 &parsed.source_map,
932 );
933 let inferred = driver.take_inferred_types();
934 db.commit_inferred_return_types(inferred.functions, inferred.methods);
935
936 let db_ref: &dyn MirDatabase = db;
937 let driver = Pass2Driver::new(db_ref, self.resolved_php_version());
938 let (body_issues, symbols) = driver.analyze_bodies(
939 &parsed.program,
940 file.clone(),
941 new_content,
942 &parsed.source_map,
943 );
944 all_issues.extend(body_issues);
945 symbols
946 } else {
947 Vec::new()
948 }
949 };
950
951 if let Some(cache) = &self.cache {
952 let h = hash_content(new_content);
953 cache.evict_with_dependents(&[file_path.to_string()]);
954 let guard = self.shared_db.salsa.lock();
955 let ref_locs = extract_reference_locations(&guard.0, &file);
956 cache.put(file_path, h, all_issues.clone(), ref_locs);
957 }
958
959 AnalysisResult::build(all_issues, HashMap::new(), symbols)
960 }
961
962 pub fn analyze_source(source: &str) -> AnalysisResult {
965 let analyzer = ProjectAnalyzer::new();
966 let file: Arc<str> = Arc::from("<source>");
967 let mut db = MirDb::default();
968 for slice in crate::stubs::builtin_stub_slices_for_version(analyzer.resolved_php_version())
969 {
970 db.ingest_stub_slice(&slice);
971 }
972 let salsa_file = SourceFile::new(&db, file.clone(), Arc::from(source));
973 let file_defs = collect_file_definitions(&db, salsa_file);
974 db.ingest_stub_slice(&file_defs.slice);
975 let mut all_issues = Arc::unwrap_or_clone(file_defs.issues);
976 if all_issues
977 .iter()
978 .any(|issue| matches!(issue.kind, mir_issues::IssueKind::ParseError { .. }))
979 {
980 return AnalysisResult::build(all_issues, std::collections::HashMap::new(), Vec::new());
981 }
982 let mut type_envs = std::collections::HashMap::new();
983 let mut all_symbols = Vec::new();
984 let arena = bumpalo::Bump::new();
985 let result = php_rs_parser::parse(&arena, source);
986
987 let driver = Pass2Driver::new_inference_only(&db, analyzer.resolved_php_version());
988 driver.analyze_bodies(&result.program, file.clone(), source, &result.source_map);
989 let inferred = driver.take_inferred_types();
990 db.commit_inferred_return_types(inferred.functions, inferred.methods);
991
992 let driver = Pass2Driver::new(&db, analyzer.resolved_php_version());
993 all_issues.extend(driver.analyze_bodies_typed(
994 &result.program,
995 file.clone(),
996 source,
997 &result.source_map,
998 &mut type_envs,
999 &mut all_symbols,
1000 ));
1001 AnalysisResult::build(all_issues, type_envs, all_symbols)
1002 }
1003
1004 pub fn discover_files(root: &Path) -> Vec<PathBuf> {
1006 if root.is_file() {
1007 return vec![root.to_path_buf()];
1008 }
1009 let mut files = Vec::new();
1010 collect_php_files(root, &mut files);
1011 files
1012 }
1013
1014 pub fn collect_types_only(&self, paths: &[PathBuf]) {
1017 let file_data: Vec<(Arc<str>, Arc<str>)> = paths
1018 .par_iter()
1019 .filter_map(|path| {
1020 let src = std::fs::read_to_string(path).ok()?;
1021 Some((
1022 Arc::from(path.to_string_lossy().as_ref()),
1023 Arc::<str>::from(src),
1024 ))
1025 })
1026 .collect();
1027
1028 let source_files: Vec<SourceFile> = {
1029 let mut guard = self.shared_db.salsa.lock();
1030 let (ref mut db, ref mut files) = *guard;
1031 file_data
1032 .iter()
1033 .map(|(file, src)| match files.get(file) {
1034 Some(&sf) => {
1035 if sf.text(db).as_ref() != src.as_ref() {
1036 sf.set_text(db).to(src.clone());
1037 }
1038 sf
1039 }
1040 None => {
1041 let file = file.clone();
1042 let sf = SourceFile::new(db, file.clone(), src.clone());
1043 files.insert(file, sf);
1044 sf
1045 }
1046 })
1047 .collect()
1048 };
1049
1050 let db_pass1 = {
1051 let guard = self.shared_db.salsa.lock();
1052 guard.0.clone()
1053 };
1054
1055 let file_defs: Vec<FileDefinitions> = source_files
1056 .par_iter()
1057 .map_with(db_pass1, |db, salsa_file| {
1058 collect_file_definitions_uncached(&*db, *salsa_file)
1059 })
1060 .collect();
1061
1062 let mut guard = self.shared_db.salsa.lock();
1063 let (ref mut db, _) = *guard;
1064 for defs in file_defs {
1065 db.ingest_stub_slice(&defs.slice);
1066 }
1067 drop(guard);
1068
1069 crate::collector::print_collector_stats();
1071 }
1072}
1073
1074impl Default for ProjectAnalyzer {
1075 fn default() -> Self {
1076 Self::new()
1077 }
1078}
1079
1080#[allow(clippy::type_complexity)]
1083fn run_inference_sweep(
1084 db_priming: MirDb,
1085 parsed_files: Vec<&ParsedProjectFile>,
1086 php_version: PhpVersion,
1087) -> (Vec<(Arc<str>, Union)>, Vec<(Arc<str>, Arc<str>, Union)>) {
1088 let functions = Arc::new(Mutex::new(Vec::new()));
1089 let methods = Arc::new(Mutex::new(Vec::new()));
1090
1091 rayon::in_place_scope(|s| {
1092 for parsed in parsed_files {
1093 let db = db_priming.clone();
1094 let functions = Arc::clone(&functions);
1095 let methods = Arc::clone(&methods);
1096
1097 s.spawn(move |_| {
1098 let driver = Pass2Driver::new_inference_only(&db as &dyn MirDatabase, php_version);
1099 let parse_result = parsed.parsed();
1100 driver.analyze_bodies(
1101 &parse_result.program,
1102 parsed.file.clone(),
1103 parsed.source(),
1104 &parse_result.source_map,
1105 );
1106
1107 let inferred = driver.take_inferred_types();
1108 {
1109 let mut funcs = functions.lock();
1110 funcs.extend(inferred.functions);
1111 }
1112 {
1113 let mut meths = methods.lock();
1114 meths.extend(inferred.methods);
1115 }
1116 });
1117 }
1118 });
1119
1120 let functions = Arc::try_unwrap(functions)
1121 .map(|mutex| mutex.into_inner())
1122 .unwrap_or_else(|arc| arc.lock().clone());
1123 let methods = Arc::try_unwrap(methods)
1124 .map(|mutex| mutex.into_inner())
1125 .unwrap_or_else(|arc| arc.lock().clone());
1126
1127 (functions, methods)
1128}
1129
1130fn stub_slice_needs_inference(slice: &mir_codebase::storage::StubSlice) -> bool {
1131 slice
1132 .functions
1133 .iter()
1134 .any(|func| func.return_type.is_none())
1135 || slice.classes.iter().any(|class| {
1136 class
1137 .own_methods
1138 .values()
1139 .any(|method| !method.is_abstract && method.return_type.is_none())
1140 })
1141 || slice.traits.iter().any(|tr| {
1142 tr.own_methods
1143 .values()
1144 .any(|method| !method.is_abstract && method.return_type.is_none())
1145 })
1146 || slice.enums.iter().any(|en| {
1147 en.own_methods
1148 .values()
1149 .any(|method| !method.is_abstract && method.return_type.is_none())
1150 })
1151}
1152
1153pub(crate) fn collect_php_files(dir: &Path, out: &mut Vec<PathBuf>) {
1154 if let Ok(entries) = std::fs::read_dir(dir) {
1155 for entry in entries.flatten() {
1156 if entry.file_type().map(|ft| ft.is_symlink()).unwrap_or(false) {
1157 continue;
1158 }
1159 let path = entry.path();
1160 if path.is_dir() {
1161 let name = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
1162 if matches!(
1163 name,
1164 "vendor" | ".git" | "node_modules" | ".cache" | ".pnpm-store"
1165 ) {
1166 continue;
1167 }
1168 collect_php_files(&path, out);
1169 } else if path.extension().and_then(|e| e.to_str()) == Some("php") {
1170 out.push(path);
1171 }
1172 }
1173 }
1174}
1175
1176fn build_reverse_deps(db: &dyn crate::db::MirDatabase) -> HashMap<String, HashSet<String>> {
1179 let mut reverse: HashMap<String, HashSet<String>> = HashMap::new();
1180
1181 let mut add_edge = |symbol: &str, dependent_file: &str| {
1182 if let Some(defining_file) = db.symbol_defining_file(symbol) {
1183 let def = defining_file.as_ref().to_string();
1184 if def != dependent_file {
1185 reverse
1186 .entry(def)
1187 .or_default()
1188 .insert(dependent_file.to_string());
1189 }
1190 }
1191 };
1192
1193 for (file, imports) in db.file_import_snapshots() {
1194 let file = file.as_ref().to_string();
1195 for fqcn in imports.values() {
1196 add_edge(fqcn, &file);
1197 }
1198 }
1199
1200 for fqcn in db.active_class_node_fqcns() {
1201 let kind = match crate::db::class_kind_via_db(db, fqcn.as_ref()) {
1205 Some(k) if !k.is_interface && !k.is_trait && !k.is_enum => k,
1206 _ => continue,
1207 };
1208 let _ = kind;
1209 let Some(file) = db
1210 .symbol_defining_file(fqcn.as_ref())
1211 .map(|f| f.as_ref().to_string())
1212 else {
1213 continue;
1214 };
1215
1216 let Some(node) = db.lookup_class_node(fqcn.as_ref()) else {
1217 continue;
1218 };
1219 if let Some(parent) = node.parent(db) {
1220 add_edge(parent.as_ref(), &file);
1221 }
1222 for iface in node.interfaces(db).iter() {
1223 add_edge(iface.as_ref(), &file);
1224 }
1225 for tr in node.traits(db).iter() {
1226 add_edge(tr.as_ref(), &file);
1227 }
1228 }
1229
1230 reverse
1231}
1232
1233fn extract_reference_locations(
1234 db: &dyn crate::db::MirDatabase,
1235 file: &Arc<str>,
1236) -> Vec<(String, u32, u16, u16)> {
1237 db.extract_file_reference_locations(file.as_ref())
1238 .into_iter()
1239 .map(|(sym, line, col_start, col_end)| (sym.to_string(), line, col_start, col_end))
1240 .collect()
1241}
1242
1243pub struct AnalysisResult {
1244 pub issues: Vec<Issue>,
1245 #[doc(hidden)]
1246 pub type_envs: std::collections::HashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
1247 pub symbols: Vec<crate::symbol::ResolvedSymbol>,
1249 symbols_by_file: HashMap<Arc<str>, std::ops::Range<usize>>,
1253}
1254
1255impl AnalysisResult {
1256 fn build(
1257 issues: Vec<Issue>,
1258 type_envs: std::collections::HashMap<crate::type_env::ScopeId, crate::type_env::TypeEnv>,
1259 mut symbols: Vec<crate::symbol::ResolvedSymbol>,
1260 ) -> Self {
1261 symbols.sort_unstable_by(|a, b| a.file.as_ref().cmp(b.file.as_ref()));
1262 let mut symbols_by_file: HashMap<Arc<str>, std::ops::Range<usize>> = HashMap::new();
1263 let mut i = 0;
1264 while i < symbols.len() {
1265 let file = Arc::clone(&symbols[i].file);
1266 let start = i;
1267 while i < symbols.len() && symbols[i].file == file {
1268 i += 1;
1269 }
1270 symbols_by_file.insert(file, start..i);
1271 }
1272 Self {
1273 issues,
1274 type_envs,
1275 symbols,
1276 symbols_by_file,
1277 }
1278 }
1279}
1280
1281impl AnalysisResult {
1282 pub fn error_count(&self) -> usize {
1283 self.issues
1284 .iter()
1285 .filter(|i| i.severity == mir_issues::Severity::Error)
1286 .count()
1287 }
1288
1289 pub fn warning_count(&self) -> usize {
1290 self.issues
1291 .iter()
1292 .filter(|i| i.severity == mir_issues::Severity::Warning)
1293 .count()
1294 }
1295
1296 pub fn issues_by_file(&self) -> HashMap<std::sync::Arc<str>, Vec<&Issue>> {
1298 let mut map: HashMap<std::sync::Arc<str>, Vec<&Issue>> = HashMap::new();
1299 for issue in &self.issues {
1300 map.entry(issue.location.file.clone())
1301 .or_default()
1302 .push(issue);
1303 }
1304 map
1305 }
1306
1307 pub fn count_by_severity(&self) -> Vec<(mir_issues::Severity, usize)> {
1310 let mut counts: std::collections::BTreeMap<mir_issues::Severity, usize> =
1311 std::collections::BTreeMap::new();
1312 for issue in &self.issues {
1313 *counts.entry(issue.severity).or_insert(0) += 1;
1314 }
1315 counts.into_iter().collect()
1316 }
1317
1318 pub fn total_issue_count(&self) -> usize {
1320 self.issues.len()
1321 }
1322
1323 pub fn filter_issues<'a, F>(&'a self, predicate: F) -> impl Iterator<Item = &'a Issue>
1326 where
1327 F: Fn(&Issue) -> bool + 'a,
1328 {
1329 self.issues.iter().filter(move |i| predicate(i))
1330 }
1331
1332 pub fn symbol_at(
1335 &self,
1336 file: &str,
1337 byte_offset: u32,
1338 ) -> Option<&crate::symbol::ResolvedSymbol> {
1339 let range = self.symbols_by_file.get(file)?;
1340 self.symbols[range.clone()]
1341 .iter()
1342 .filter(|s| s.span.start <= byte_offset && byte_offset < s.span.end)
1343 .min_by_key(|s| s.span.end - s.span.start)
1344 }
1345}