use std::sync::Arc;
use mir_types::Name;
use rustc_hash::FxHashMap;
use crate::db::{collect_file_definitions, MirDatabase, SourceFile};
#[salsa::input]
pub struct WorkspaceRevision {
pub revision: u64,
}
#[salsa::tracked]
pub fn workspace_classes(db: &dyn MirDatabase) -> Arc<[Arc<str>]> {
let rev = db
.workspace_revision()
.expect("WorkspaceRevision not initialized");
let _ = rev.revision(db);
let files = db.all_source_files();
let mut out: Vec<Arc<str>> = Vec::new();
for file in files.iter() {
let defs = collect_file_definitions(db, *file);
for c in defs.slice.classes.iter() {
out.push(c.fqcn.clone());
}
for i in defs.slice.interfaces.iter() {
out.push(i.fqcn.clone());
}
for t in defs.slice.traits.iter() {
out.push(t.fqcn.clone());
}
for e in defs.slice.enums.iter() {
out.push(e.fqcn.clone());
}
}
Arc::from(out)
}
#[salsa::tracked]
pub fn workspace_functions(db: &dyn MirDatabase) -> Arc<[Arc<str>]> {
let rev = db
.workspace_revision()
.expect("WorkspaceRevision not initialized");
let _ = rev.revision(db);
let files = db.all_source_files();
let mut out: Vec<Arc<str>> = Vec::new();
for file in files.iter() {
let defs = collect_file_definitions(db, *file);
for f in defs.slice.functions.iter() {
out.push(f.fqn.clone());
}
}
Arc::from(out)
}
#[derive(Clone)]
pub struct FileDeclarations {
pub class_like: Vec<(Name, SymbolLoc)>,
pub functions: Vec<(Name, SymbolLoc)>,
pub constants: Vec<(Name, SymbolLoc)>,
}
impl PartialEq for FileDeclarations {
fn eq(&self, other: &Self) -> bool {
self.class_like.len() == other.class_like.len()
&& self
.class_like
.iter()
.zip(&other.class_like)
.all(|(a, b)| a.0 == b.0)
&& self.functions.len() == other.functions.len()
&& self
.functions
.iter()
.zip(&other.functions)
.all(|(a, b)| a.0 == b.0)
&& self.constants.len() == other.constants.len()
&& self
.constants
.iter()
.zip(&other.constants)
.all(|(a, b)| a.0 == b.0)
}
}
#[salsa::tracked]
pub fn collect_file_declarations(db: &dyn MirDatabase, file: SourceFile) -> FileDeclarations {
let defs = collect_file_definitions(db, file);
decls_from_slice(&defs.slice, file)
}
pub fn decls_from_slice(
slice: &mir_codebase::definitions::StubSlice,
file: SourceFile,
) -> FileDeclarations {
let mut class_like = Vec::new();
let mut functions = Vec::new();
let mut constants = Vec::new();
for (idx, c) in slice.classes.iter().enumerate() {
class_like.push((
Name::new(c.fqcn.as_ref()).ascii_lowercase(),
SymbolLoc::Class { file, idx },
));
}
for (idx, i) in slice.interfaces.iter().enumerate() {
class_like.push((
Name::new(i.fqcn.as_ref()).ascii_lowercase(),
SymbolLoc::Interface { file, idx },
));
}
for (idx, t) in slice.traits.iter().enumerate() {
class_like.push((
Name::new(t.fqcn.as_ref()).ascii_lowercase(),
SymbolLoc::Trait { file, idx },
));
}
for (idx, e) in slice.enums.iter().enumerate() {
class_like.push((
Name::new(e.fqcn.as_ref()).ascii_lowercase(),
SymbolLoc::Enum { file, idx },
));
}
for (idx, f) in slice.functions.iter().enumerate() {
functions.push((
Name::new(f.fqn.as_ref()).ascii_lowercase(),
SymbolLoc::Function { file, idx },
));
}
for (idx, (name, _)) in slice.constants.iter().enumerate() {
constants.push((Name::new(name.as_ref()), SymbolLoc::Constant { file, idx }));
}
FileDeclarations {
class_like,
functions,
constants,
}
}
#[derive(Clone, Copy, PartialEq, Eq)]
pub enum SymbolLoc {
Class { file: SourceFile, idx: usize },
Interface { file: SourceFile, idx: usize },
Trait { file: SourceFile, idx: usize },
Enum { file: SourceFile, idx: usize },
Function { file: SourceFile, idx: usize },
Constant { file: SourceFile, idx: usize },
}
impl SymbolLoc {
pub fn file(&self) -> SourceFile {
match self {
SymbolLoc::Class { file, .. }
| SymbolLoc::Interface { file, .. }
| SymbolLoc::Trait { file, .. }
| SymbolLoc::Enum { file, .. }
| SymbolLoc::Function { file, .. }
| SymbolLoc::Constant { file, .. } => *file,
}
}
}
#[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum SymbolTier {
NativeStub = 0,
UserFile = 1,
UserStub = 2,
}
#[derive(Default, Clone)]
pub struct IndexDeclCounts {
pub class_like: FxHashMap<Name, u32>,
pub functions: FxHashMap<Name, u32>,
pub constants: FxHashMap<Name, u32>,
}
#[salsa::input]
pub struct WorkspaceSymbolIndexSingleton {
pub index: WorkspaceSymbolIndex,
pub revision: u64,
}
#[derive(Clone, Default)]
pub struct WorkspaceSymbolIndex {
pub class_like: Arc<FxHashMap<Name, SymbolLoc>>,
pub functions: Arc<FxHashMap<Name, SymbolLoc>>,
pub constants: Arc<FxHashMap<Name, SymbolLoc>>,
pub class_like_by_short_name: Arc<FxHashMap<Name, Vec<Name>>>,
}
impl PartialEq for WorkspaceSymbolIndex {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.class_like, &other.class_like)
&& Arc::ptr_eq(&self.functions, &other.functions)
&& Arc::ptr_eq(&self.constants, &other.constants)
&& Arc::ptr_eq(
&self.class_like_by_short_name,
&other.class_like_by_short_name,
)
}
}
pub fn short_name_key(fqcn_lower: Name) -> Name {
let s = fqcn_lower.as_str().trim_start_matches('\\');
match s.rsplit_once('\\') {
Some((_, short)) => Name::new(short),
None => Name::new(s),
}
}
pub fn workspace_index(db: &dyn MirDatabase) -> &WorkspaceSymbolIndex {
if let Some(s) = db.workspace_symbol_index_singleton() {
s.index(db)
} else {
workspace_symbol_index(db)
}
}
#[salsa::tracked]
pub fn workspace_symbol_index(db: &dyn MirDatabase) -> WorkspaceSymbolIndex {
let rev = db
.workspace_revision()
.expect("WorkspaceRevision not initialized");
let _ = rev.revision(db);
db.note_workspace_index_walk();
let files = db.all_source_files();
let mut class_like: FxHashMap<Name, SymbolLoc> = FxHashMap::default();
let mut functions: FxHashMap<Name, SymbolLoc> = FxHashMap::default();
let mut constants: FxHashMap<Name, SymbolLoc> = FxHashMap::default();
let mut class_like_by_short_name: FxHashMap<Name, Vec<Name>> = FxHashMap::default();
let user_stub_set: std::collections::HashSet<_> =
db.user_stub_source_files().into_iter().collect();
let (native_stubs, user_files): (Vec<SourceFile>, Vec<SourceFile>) = files
.into_iter()
.partition(|f| f.path(db).starts_with("stubs/"));
for file in &native_stubs {
let decls = collect_file_declarations(db, *file);
for (key, loc) in &decls.class_like {
class_like.entry(*key).or_insert(*loc);
}
for (key, loc) in &decls.functions {
functions.entry(*key).or_insert(*loc);
}
for (key, loc) in &decls.constants {
constants.entry(*key).or_insert(*loc);
}
}
for file in &user_files {
if user_stub_set.contains(file) {
continue; }
let decls = collect_file_declarations(db, *file);
for &(key, loc) in &decls.class_like {
class_like.insert(key, loc);
}
for &(key, loc) in &decls.functions {
functions.insert(key, loc);
}
for &(key, loc) in &decls.constants {
constants.insert(key, loc);
}
}
for file in &user_stub_set {
let decls = collect_file_declarations(db, *file);
for &(key, loc) in &decls.class_like {
class_like.insert(key, loc);
}
for &(key, loc) in &decls.functions {
functions.insert(key, loc);
}
for &(key, loc) in &decls.constants {
constants.insert(key, loc);
}
}
for &key in class_like.keys() {
class_like_by_short_name
.entry(short_name_key(key))
.or_default()
.push(key);
}
WorkspaceSymbolIndex {
class_like: Arc::new(class_like),
functions: Arc::new(functions),
constants: Arc::new(constants),
class_like_by_short_name: Arc::new(class_like_by_short_name),
}
}
#[derive(Clone, Default, Debug)]
pub struct GlobalVarMap(pub Arc<FxHashMap<Arc<str>, mir_types::Type>>);
impl PartialEq for GlobalVarMap {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.0, &other.0)
}
}
#[salsa::tracked]
pub fn workspace_global_vars(db: &dyn MirDatabase) -> GlobalVarMap {
let rev = db
.workspace_revision()
.expect("WorkspaceRevision not initialized");
let _ = rev.revision(db);
let files = db.all_source_files();
let mut out: FxHashMap<Arc<str>, mir_types::Type> = FxHashMap::default();
for file in files.iter() {
let defs = collect_file_definitions(db, *file);
for (name, ty) in &defs.slice.global_vars {
let gname: Arc<str> = Arc::from(name.strip_prefix('$').unwrap_or(name.as_ref()));
out.entry(gname).or_insert_with(|| ty.clone());
}
}
GlobalVarMap(Arc::new(out))
}
#[cfg(test)]
mod builder_equivalence_tests {
use super::*;
use std::sync::Arc;
#[test]
fn tracked_walk_matches_imperative_rebuild() {
let mut db = crate::db::MirDbStorage::default();
let native =
"<?php\nclass Dup {}\nclass StubOnly {}\nfunction dup_fn() {}\nconst DUP = 1;\n";
let native2 = "<?php\nclass Dup {}\n"; let user1 =
"<?php\nclass Dup {}\nclass UserOnly {}\nfunction dup_fn() {}\nconst DUP = 2;\n";
let user2 = "<?php\nnamespace App;\nclass Dup {}\n"; let ustub = "<?php\nclass Dup {}\nfunction dup_fn() {}\n";
db.upsert_source_file_with_durability(
Arc::from("stubs/std.php"),
Arc::from(native),
salsa::Durability::HIGH,
);
db.upsert_source_file_with_durability(
Arc::from("stubs/extra.php"),
Arc::from(native2),
salsa::Durability::HIGH,
);
db.upsert_source_file_with_durability(
Arc::from("user1.php"),
Arc::from(user1),
salsa::Durability::LOW,
);
db.upsert_source_file_with_durability(
Arc::from("app.php"),
Arc::from(user2),
salsa::Durability::LOW,
);
db.upsert_source_file_with_durability(
Arc::from("mystub.php"),
Arc::from(ustub),
salsa::Durability::LOW,
);
db.register_user_stub_path(Arc::from("mystub.php"));
let tracked = workspace_symbol_index(&db).clone();
db.rebuild_workspace_symbol_index();
let rebuilt = workspace_index(&db).clone();
fn maps_equal(a: &FxHashMap<Name, SymbolLoc>, b: &FxHashMap<Name, SymbolLoc>) -> bool {
a.len() == b.len() && a.iter().all(|(k, v)| b.get(k) == Some(v))
}
assert!(
maps_equal(&tracked.class_like, &rebuilt.class_like),
"class_like maps differ"
);
assert!(
maps_equal(&tracked.functions, &rebuilt.functions),
"functions maps differ"
);
assert!(
maps_equal(&tracked.constants, &rebuilt.constants),
"constants maps differ"
);
assert_eq!(
tracked.class_like_by_short_name.len(),
rebuilt.class_like_by_short_name.len()
);
for (short, fqcns) in tracked.class_like_by_short_name.iter() {
let a: std::collections::HashSet<_> = fqcns.iter().collect();
let b: std::collections::HashSet<_> = rebuilt
.class_like_by_short_name
.get(short)
.expect("bucket missing from imperative build")
.iter()
.collect();
assert_eq!(a, b, "short-name bucket {short:?} differs");
}
let dup = tracked.class_like.get(&Name::new("dup")).copied().unwrap();
assert_eq!(dup.file().path(&db).as_ref(), "mystub.php");
let dup_fn = tracked
.functions
.get(&Name::new("dup_fn"))
.copied()
.unwrap();
assert_eq!(dup_fn.file().path(&db).as_ref(), "mystub.php");
let dup_const = tracked.constants.get(&Name::new("DUP")).copied().unwrap();
assert_eq!(dup_const.file().path(&db).as_ref(), "user1.php");
}
}
#[cfg(test)]
mod decl_projection_tests {
use super::*;
use std::sync::Arc;
#[test]
fn decls_from_slice_matches_tracked_projection() {
let mut db = crate::db::MirDbStorage::default();
let text = "<?php\nnamespace App;\n\
const TOP = 1;\n\
interface I {}\n\
trait T {}\n\
enum E { case A; }\n\
abstract class C implements I { use T; }\n\
class D extends C {}\n\
function f(): void {}\n\
function g(int $x): int { return $x; }\n";
let sf = db.upsert_source_file_with_durability(
Arc::from("proj.php"),
Arc::from(text),
salsa::Durability::LOW,
);
let tracked = collect_file_declarations(&db, sf);
let slice = collect_file_definitions(&db, sf).slice.clone();
let projected = decls_from_slice(&slice, sf);
let pairs = [
(&tracked.class_like, &projected.class_like),
(&tracked.functions, &projected.functions),
(&tracked.constants, &projected.constants),
];
for (t, p) in pairs {
assert_eq!(t.len(), p.len());
for ((tn, tl), (pn, pl)) in t.iter().zip(p.iter()) {
assert_eq!(tn, pn);
assert!(tl == pl, "SymbolLoc drift for {tn:?}");
}
}
assert_eq!(tracked.class_like.len(), 5, "I, T, E, C, D expected");
}
}