1use rustc_hash::FxHashMap;
2use std::sync::{Arc, OnceLock};
3
4pub(crate) mod analyzer_db;
5pub(crate) mod attributes;
6pub mod batch;
7pub(crate) mod body_analysis;
8#[doc(hidden)]
9pub mod cache;
10pub(crate) mod call;
11pub(crate) mod class;
12pub(crate) mod collector;
13pub(crate) mod contradiction;
14#[doc(hidden)]
15pub mod db;
16pub(crate) mod dead_code;
17pub(crate) mod diagnostics;
18pub(crate) mod expr;
19pub mod file_analyzer;
20pub(crate) mod flow_state;
21pub(crate) mod generic;
22pub mod indexing;
23#[doc(hidden)]
24pub mod metrics;
25pub(crate) mod narrowing;
26#[doc(hidden)]
27pub mod parse_cache;
28#[doc(hidden)]
29pub mod parser;
30#[doc(hidden)]
31pub mod perf_fixture;
32pub mod php_version;
33pub mod prelude;
34pub(crate) mod reference_key;
35pub mod session;
36pub mod source_provider;
37pub(crate) mod stmt;
38#[doc(hidden)]
39pub mod stub_cache;
40#[doc(hidden)]
41pub mod stubs;
42pub(crate) mod subtype;
43pub mod suppression;
44pub(crate) mod taint;
45mod tmp_suffix;
46pub(crate) mod type_env;
47pub(crate) mod util;
48
49pub use batch::{
50 analyze_source, analyze_source_with_options, dead_code_issue_kinds, discover_files,
51 AnalysisResult, BatchOptions,
52};
53pub use file_analyzer::{FileAnalysis, FileAnalyzer};
54pub use indexing::{IndexBatchOutcome, IndexCancel, IndexParallelism};
55pub use parser::type_from_hint::type_from_hint;
56pub use parser::{DocblockParser, ParsedDocblock};
57pub use php_version::{ParsePhpVersionError, PhpVersion};
58pub use session::{AnalysisSession, ReferenceIncludes, SubtypeClassSite};
59pub use source_provider::{FsSourceProvider, SourceProvider};
60
61pub(crate) fn fqcn_case_mismatch(written: &str, canonical: &str) -> Option<(String, String)> {
65 let w = written.trim_start_matches('\\');
66 let c = canonical.trim_start_matches('\\');
67 if w == c || !w.eq_ignore_ascii_case(c) {
68 return None;
69 }
70 let w_last = w.rsplit('\\').next().unwrap_or(w);
71 let c_last = c.rsplit('\\').next().unwrap_or(c);
72 if w_last != c_last {
73 let w_prefix = w.rsplit_once('\\').map_or("", |(p, _)| p);
74 let c_prefix = c.rsplit_once('\\').map_or("", |(p, _)| p);
75 if w_prefix == c_prefix {
76 return Some((w_last.to_string(), c_last.to_string()));
77 }
78 }
79 Some((w.to_string(), c.to_string()))
80}
81pub use stubs::{
82 is_builtin_constant, is_builtin_function, stub_files, stub_path_for_class,
83 ChainedClassResolver, StubClassResolver, StubVfs,
84};
85
86#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord)]
108pub struct Position {
109 pub line: u32,
110 pub column: u32,
111}
112
113#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
115pub struct Range {
116 pub start: Position,
117 pub end: Position,
118}
119
120#[derive(Debug, Clone, PartialEq, Eq, Hash)]
127pub enum Name {
128 Class(std::sync::Arc<str>),
130 Function(std::sync::Arc<str>),
132 Method {
134 class: std::sync::Arc<str>,
135 name: std::sync::Arc<str>,
136 },
137 Property {
139 class: std::sync::Arc<str>,
140 name: std::sync::Arc<str>,
141 },
142 ClassConstant {
144 class: std::sync::Arc<str>,
145 name: std::sync::Arc<str>,
146 },
147 GlobalConstant(std::sync::Arc<str>),
149}
150
151impl Name {
152 pub fn method(class: impl Into<std::sync::Arc<str>>, name: &str) -> Self {
155 Name::Method {
156 class: class.into(),
157 name: std::sync::Arc::from(name.to_ascii_lowercase()),
158 }
159 }
160
161 pub fn class(fqcn: impl Into<std::sync::Arc<str>>) -> Self {
163 Name::Class(fqcn.into())
164 }
165
166 pub fn function(fqn: impl Into<std::sync::Arc<str>>) -> Self {
168 Name::Function(fqn.into())
169 }
170
171 pub fn property(
173 class: impl Into<std::sync::Arc<str>>,
174 name: impl Into<std::sync::Arc<str>>,
175 ) -> Self {
176 Name::Property {
177 class: class.into(),
178 name: name.into(),
179 }
180 }
181
182 pub fn class_constant(
184 class: impl Into<std::sync::Arc<str>>,
185 name: impl Into<std::sync::Arc<str>>,
186 ) -> Self {
187 Name::ClassConstant {
188 class: class.into(),
189 name: name.into(),
190 }
191 }
192
193 pub fn global_constant(fqn: impl Into<std::sync::Arc<str>>) -> Self {
195 Name::GlobalConstant(fqn.into())
196 }
197
198 pub fn codebase_key(&self) -> String {
208 match self {
209 Name::Class(fqcn) => format!("cls:{fqcn}"),
210 Name::Function(fqn) => format!("fn:{fqn}"),
211 Name::Method { class, name } => format!("meth:{class}::{name}"),
212 Name::Property { class, name } => format!("prop:{class}::{name}"),
213 Name::ClassConstant { class, name } => format!("cnst:{class}::{name}"),
214 Name::GlobalConstant(fqn) => format!("gcnst:{fqn}"),
215 }
216 }
217}
218
219pub(crate) fn defining_file_lookup_key(symbol_key: &str) -> &str {
227 let stripped = symbol_key
228 .strip_prefix("meth:")
229 .or_else(|| symbol_key.strip_prefix("prop:"))
230 .or_else(|| symbol_key.strip_prefix("cnst:"))
231 .or_else(|| symbol_key.strip_prefix("cls:"))
232 .or_else(|| symbol_key.strip_prefix("fn:"))
233 .or_else(|| symbol_key.strip_prefix("gcnst:"))
234 .unwrap_or(symbol_key);
235 match stripped.split_once("::") {
236 Some((class, _)) => class,
237 None => stripped,
238 }
239}
240
241#[derive(Debug, Clone, PartialEq, Eq)]
243pub enum SymbolLookupError {
244 NotFound,
246 NoSourceLocation,
249}
250
251#[derive(Debug, Clone, Copy, PartialEq, Eq)]
253pub enum LoadOutcome {
254 AlreadyLoaded,
256 Loaded,
259 NotResolvable,
263}
264
265impl LoadOutcome {
266 pub fn is_loaded(self) -> bool {
269 !matches!(self, LoadOutcome::NotResolvable)
270 }
271}
272
273pub trait ClassResolver: Send + Sync {
281 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf>;
284}
285
286impl ClassResolver for composer::Psr4Map {
287 fn resolve(&self, fqcn: &str) -> Option<std::path::PathBuf> {
288 composer::Psr4Map::resolve(self, fqcn)
289 }
290}
291
292impl std::fmt::Display for SymbolLookupError {
293 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
294 match self {
295 SymbolLookupError::NotFound => write!(f, "symbol not found"),
296 SymbolLookupError::NoSourceLocation => write!(f, "symbol has no source location"),
297 }
298 }
299}
300
301impl std::error::Error for SymbolLookupError {}
302
303#[derive(Debug, Clone)]
306pub struct HoverInfo {
307 pub ty: Type,
309 pub docstring: Option<String>,
311 pub definition: Option<mir_types::Location>,
313}
314
315#[derive(Debug)]
318pub struct DependencyGraph {
319 files: Vec<Arc<str>>,
320 file_ids: FxHashMap<Arc<str>, u32>,
321 dependencies: Vec<Vec<u32>>,
323 dependents: Vec<Vec<u32>>,
325 legacy_dependencies: OnceLock<FxHashMap<String, Vec<String>>>,
326 legacy_dependents: OnceLock<FxHashMap<String, Vec<String>>>,
327}
328
329impl Clone for DependencyGraph {
330 fn clone(&self) -> Self {
331 Self {
332 files: self.files.clone(),
333 file_ids: self.file_ids.clone(),
334 dependencies: self.dependencies.clone(),
335 dependents: self.dependents.clone(),
336 legacy_dependencies: OnceLock::new(),
337 legacy_dependents: OnceLock::new(),
338 }
339 }
340}
341
342impl DependencyGraph {
343 pub(crate) fn from_compact_parts(
344 files: Vec<Arc<str>>,
345 file_ids: FxHashMap<Arc<str>, u32>,
346 dependencies: Vec<Vec<u32>>,
347 dependents: Vec<Vec<u32>>,
348 ) -> Self {
349 Self {
350 files,
351 file_ids,
352 dependencies,
353 dependents,
354 legacy_dependencies: OnceLock::new(),
355 legacy_dependents: OnceLock::new(),
356 }
357 }
358
359 pub fn file_count(&self) -> usize {
361 self.files.len()
362 }
363
364 pub fn dependency_edge_count(&self) -> usize {
366 self.dependencies.iter().map(Vec::len).sum()
367 }
368
369 pub fn dependent_edge_count(&self) -> usize {
371 self.dependents.iter().map(Vec::len).sum()
372 }
373
374 fn file_path(&self, id: u32) -> &Arc<str> {
375 &self.files[id as usize]
376 }
377
378 fn stringify_adjacency(&self, adjacency: &[Vec<u32>]) -> FxHashMap<String, Vec<String>> {
379 adjacency
380 .iter()
381 .enumerate()
382 .filter(|(_, deps)| !deps.is_empty())
383 .map(|(file_id, deps)| {
384 (
385 self.files[file_id].to_string(),
386 deps.iter()
387 .map(|&dep_id| self.file_path(dep_id).to_string())
388 .collect(),
389 )
390 })
391 .collect()
392 }
393
394 fn direct_arcs<'a>(
395 &'a self,
396 file: &str,
397 adjacency: &'a [Vec<u32>],
398 ) -> impl Iterator<Item = &'a Arc<str>> {
399 self.file_ids
400 .get(file)
401 .and_then(|&id| adjacency.get(id as usize))
402 .into_iter()
403 .flatten()
404 .map(|&id| self.file_path(id))
405 }
406
407 pub fn dependency_paths_of(&self, file: &str) -> Vec<Arc<str>> {
409 self.direct_arcs(file, &self.dependencies)
410 .cloned()
411 .collect()
412 }
413
414 pub fn dependent_paths_of(&self, file: &str) -> Vec<Arc<str>> {
416 self.direct_arcs(file, &self.dependents).cloned().collect()
417 }
418
419 pub fn dependencies_of(&self, file: &str) -> &[String] {
421 self.legacy_dependencies
422 .get_or_init(|| self.stringify_adjacency(&self.dependencies))
423 .get(file)
424 .map(|v| v.as_slice())
425 .unwrap_or(&[])
426 }
427
428 pub fn dependents_of(&self, file: &str) -> &[String] {
430 self.legacy_dependents
431 .get_or_init(|| self.stringify_adjacency(&self.dependents))
432 .get(file)
433 .map(|v| v.as_slice())
434 .unwrap_or(&[])
435 }
436
437 pub fn transitive_dependencies(&self, file: &str) -> Vec<String> {
439 let mut visited = rustc_hash::FxHashSet::default();
440 let Some(&seed) = self.file_ids.get(file) else {
441 return Vec::new();
442 };
443 let mut queue = vec![seed];
444 visited.insert(seed);
445 let mut result = Vec::new();
446
447 while let Some(current) = queue.pop() {
448 for &dep in &self.dependencies[current as usize] {
449 if visited.insert(dep) {
450 queue.push(dep);
451 result.push(self.file_path(dep).to_string());
452 }
453 }
454 }
455 result
456 }
457
458 pub fn transitive_dependents(&self, file: &str) -> Vec<String> {
460 let mut visited = rustc_hash::FxHashSet::default();
461 let Some(&seed) = self.file_ids.get(file) else {
462 return Vec::new();
463 };
464 let mut queue = vec![seed];
465 visited.insert(seed);
466 let mut result = Vec::new();
467
468 while let Some(current) = queue.pop() {
469 for &dep in &self.dependents[current as usize] {
470 if visited.insert(dep) {
471 queue.push(dep);
472 result.push(self.file_path(dep).to_string());
473 }
474 }
475 }
476 result
477 }
478}
479
480pub mod symbol;
481pub use mir_codebase::definitions::{DeclaredParam, FunctionDef, TemplateParam, Visibility};
482pub use mir_issues::{Issue, IssueKind, Severity};
483pub use mir_types::Type;
484
485#[cfg(test)]
486mod dependency_graph_tests {
487 use super::*;
488
489 fn graph(edges: &[(&str, &[&str])]) -> DependencyGraph {
490 let mut files: Vec<Arc<str>> = edges
491 .iter()
492 .flat_map(|(from, deps)| {
493 std::iter::once(*from)
494 .chain(deps.iter().copied())
495 .map(Arc::<str>::from)
496 })
497 .collect();
498 files.sort();
499 files.dedup();
500
501 let file_ids: FxHashMap<Arc<str>, u32> = files
502 .iter()
503 .enumerate()
504 .map(|(id, file)| (file.clone(), id as u32))
505 .collect();
506 let mut dependencies = vec![Vec::new(); files.len()];
507 let mut dependents = vec![Vec::new(); files.len()];
508
509 for (from, deps) in edges {
510 let from_id = file_ids[*from];
511 for dep in *deps {
512 let dep_id = file_ids[*dep];
513 dependencies[from_id as usize].push(dep_id);
514 dependents[dep_id as usize].push(from_id);
515 }
516 }
517 for adjacency in [&mut dependencies, &mut dependents] {
518 for deps in adjacency {
519 deps.sort();
520 deps.dedup();
521 }
522 }
523
524 DependencyGraph::from_compact_parts(files, file_ids, dependencies, dependents)
525 }
526
527 #[test]
528 fn transitive_traversal_deduplicates_diamond_graphs() {
529 let graph = graph(&[
530 ("a.php", &["b.php", "c.php"]),
531 ("b.php", &["d.php"]),
532 ("c.php", &["d.php"]),
533 ]);
534
535 let deps = graph.transitive_dependencies("a.php");
536 assert_eq!(
537 deps.iter().filter(|path| path.as_str() == "d.php").count(),
538 1
539 );
540 assert_eq!(deps.len(), 3);
541
542 let dependents = graph.transitive_dependents("d.php");
543 assert_eq!(
544 dependents
545 .iter()
546 .filter(|path| path.as_str() == "a.php")
547 .count(),
548 1
549 );
550 assert_eq!(dependents.len(), 3);
551 }
552
553 #[test]
554 fn direct_arc_accessors_do_not_require_legacy_string_maps() {
555 let graph = graph(&[("consumer.php", &["service.php"])]);
556
557 let deps = graph.dependency_paths_of("consumer.php");
558 assert_eq!(deps, vec![Arc::<str>::from("service.php")]);
559 assert!(graph.legacy_dependencies.get().is_none());
560
561 let dependents = graph.dependent_paths_of("service.php");
562 assert_eq!(dependents, vec![Arc::<str>::from("consumer.php")]);
563 assert!(graph.legacy_dependents.get().is_none());
564
565 assert_eq!(
566 graph.dependencies_of("consumer.php"),
567 &["service.php".to_string()]
568 );
569 assert!(graph.legacy_dependencies.get().is_some());
570 }
571}
572
573pub fn location_from_span(
583 span: php_ast::Span,
584 file: std::sync::Arc<str>,
585 source: &str,
586 source_map: &php_rs_parser::source_map::SourceMap,
587) -> mir_types::Location {
588 let (line, col_start) = diagnostics::offset_to_line_col(source, span.start, source_map);
589 let (line_end, col_end) = if span.start < span.end {
590 diagnostics::offset_to_line_col(source, span.end, source_map)
591 } else {
592 (line, col_start)
593 };
594 mir_types::Location {
595 file,
596 line,
597 line_end,
598 col_start,
599 col_end: diagnostics::clamp_col_end(line, line_end, col_start, col_end),
600 }
601}
602pub use symbol::{DeclarationKind, DocumentSymbol, ReferenceKind, ResolvedSymbol};
603
604pub mod composer;
605pub use composer::{ComposerError, Psr4Map};
606pub use type_env::ScopeId;
607
608#[doc(hidden)]
609pub mod test_utils;