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