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