use std::collections::{HashMap, HashSet};
use rustc_hash::FxHashMap;
use std::sync::Arc;
use mir_codebase::storage::{
Assertion, ConstantStorage, FnParam, FunctionStorage, Location, MethodStorage, PropertyStorage,
TemplateParam, Visibility,
};
use mir_codebase::StubSlice;
use mir_issues::Issue;
use mir_types::Union;
#[salsa::db]
pub trait MirDatabase: salsa::Database {
fn php_version_str(&self) -> Arc<str>;
fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode>;
fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode>;
fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode>;
fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode>;
fn lookup_class_constant_node(&self, fqcn: &str, const_name: &str)
-> Option<ClassConstantNode>;
fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode>;
fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode>;
fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode>;
fn active_class_node_fqcns(&self) -> Vec<Arc<str>>;
fn active_function_node_fqns(&self) -> Vec<Arc<str>>;
fn file_namespace(&self, file: &str) -> Option<Arc<str>>;
fn file_imports(&self, file: &str) -> HashMap<String, String>;
fn global_var_type(&self, name: &str) -> Option<Union>;
fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)>;
fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>>;
fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>>;
fn record_reference_location(&self, loc: RefLoc);
fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]);
fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)>;
fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)>;
fn has_reference(&self, symbol: &str) -> bool;
fn clear_file_references(&self, file: &str);
}
#[salsa::input]
pub struct SourceFile {
pub path: Arc<str>,
pub text: Arc<str>,
}
#[derive(Clone, Debug)]
pub struct FileDefinitions {
pub slice: Arc<StubSlice>,
pub issues: Arc<Vec<Issue>>,
}
impl PartialEq for FileDefinitions {
fn eq(&self, other: &Self) -> bool {
Arc::ptr_eq(&self.slice, &other.slice) && Arc::ptr_eq(&self.issues, &other.issues)
}
}
unsafe impl salsa::Update for FileDefinitions {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
unsafe { *old_pointer = new_value };
true
}
}
pub type ImplementsTypeArgs = Arc<[(Arc<str>, Arc<[Union]>)]>;
#[salsa::input]
pub struct ClassNode {
pub fqcn: Arc<str>,
pub active: bool,
pub is_interface: bool,
pub is_trait: bool,
pub is_enum: bool,
pub is_abstract: bool,
pub parent: Option<Arc<str>>,
pub interfaces: Arc<[Arc<str>]>,
pub traits: Arc<[Arc<str>]>,
pub extends: Arc<[Arc<str>]>,
pub template_params: Arc<[TemplateParam]>,
pub require_extends: Arc<[Arc<str>]>,
pub require_implements: Arc<[Arc<str>]>,
pub is_backed_enum: bool,
pub mixins: Arc<[Arc<str>]>,
pub deprecated: Option<Arc<str>>,
pub enum_scalar_type: Option<Union>,
pub is_final: bool,
pub is_readonly: bool,
pub location: Option<Location>,
pub extends_type_args: Arc<[Union]>,
pub implements_type_args: ImplementsTypeArgs,
}
#[derive(Debug, Clone, Copy)]
pub struct ClassKind {
pub is_interface: bool,
pub is_trait: bool,
pub is_enum: bool,
pub is_abstract: bool,
}
pub fn class_kind_via_db(db: &dyn MirDatabase, fqcn: &str) -> Option<ClassKind> {
let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
Some(ClassKind {
is_interface: node.is_interface(db),
is_trait: node.is_trait(db),
is_enum: node.is_enum(db),
is_abstract: node.is_abstract(db),
})
}
pub fn type_exists_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
db.lookup_class_node(fqcn).is_some_and(|n| n.active(db))
}
pub fn function_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
db.lookup_function_node(fqn).is_some_and(|n| n.active(db))
}
pub fn constant_exists_via_db(db: &dyn MirDatabase, fqn: &str) -> bool {
db.lookup_global_constant_node(fqn)
.is_some_and(|n| n.active(db))
}
pub fn resolve_name_via_db(db: &dyn MirDatabase, file: &str, name: &str) -> String {
if name.starts_with('\\') {
return name.trim_start_matches('\\').to_string();
}
let lower = name.to_ascii_lowercase();
if matches!(lower.as_str(), "self" | "static" | "parent") {
return name.to_string();
}
if name.contains('\\') {
if let Some(imports) = (!name.starts_with('\\')).then(|| db.file_imports(file)) {
if let Some((first, rest)) = name.split_once('\\') {
if let Some(base) = imports.get(first) {
return format!("{base}\\{rest}");
}
}
}
if type_exists_via_db(db, name) {
return name.to_string();
}
if let Some(ns) = db.file_namespace(file) {
let qualified = format!("{}\\{}", ns, name);
if type_exists_via_db(db, &qualified) {
return qualified;
}
}
return name.to_string();
}
let imports = db.file_imports(file);
if let Some(fqcn) = imports.get(name) {
return fqcn.clone();
}
if let Some((_, fqcn)) = imports
.iter()
.find(|(alias, _)| alias.eq_ignore_ascii_case(name))
{
return fqcn.clone();
}
if let Some(ns) = db.file_namespace(file) {
return format!("{}\\{}", ns, name);
}
name.to_string()
}
pub fn class_template_params_via_db(
db: &dyn MirDatabase,
fqcn: &str,
) -> Option<Arc<[TemplateParam]>> {
let node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
Some(node.template_params(db))
}
pub fn inherited_template_bindings_via_db(
db: &dyn MirDatabase,
fqcn: &str,
) -> std::collections::HashMap<Arc<str>, Union> {
let mut bindings: std::collections::HashMap<Arc<str>, Union> = std::collections::HashMap::new();
let mut visited: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
let mut current: Arc<str> = Arc::from(fqcn);
loop {
if !visited.insert(current.clone()) {
break;
}
let node = match db
.lookup_class_node(current.as_ref())
.filter(|n| n.active(db))
{
Some(n) => n,
None => break,
};
let parent = match node.parent(db) {
Some(p) => p,
None => break,
};
let extends_type_args = node.extends_type_args(db);
if !extends_type_args.is_empty() {
if let Some(parent_tps) = class_template_params_via_db(db, parent.as_ref()) {
for (tp, ty) in parent_tps.iter().zip(extends_type_args.iter()) {
bindings
.entry(tp.name.clone())
.or_insert_with(|| ty.clone());
}
}
}
current = parent;
}
bindings
}
#[salsa::input]
pub struct FunctionNode {
pub fqn: Arc<str>,
pub short_name: Arc<str>,
pub active: bool,
pub params: Arc<[FnParam]>,
pub return_type: Option<Union>,
pub inferred_return_type: Option<Union>,
pub template_params: Arc<[TemplateParam]>,
pub assertions: Arc<[Assertion]>,
pub throws: Arc<[Arc<str>]>,
pub deprecated: Option<Arc<str>>,
pub is_pure: bool,
pub location: Option<Location>,
}
#[salsa::input]
pub struct MethodNode {
pub fqcn: Arc<str>,
pub name: Arc<str>,
pub active: bool,
pub params: Arc<[FnParam]>,
pub return_type: Option<Union>,
pub inferred_return_type: Option<Union>,
pub template_params: Arc<[TemplateParam]>,
pub assertions: Arc<[Assertion]>,
pub throws: Arc<[Arc<str>]>,
pub deprecated: Option<Arc<str>>,
pub visibility: Visibility,
pub is_static: bool,
pub is_abstract: bool,
pub is_final: bool,
pub is_constructor: bool,
pub is_pure: bool,
pub location: Option<Location>,
}
#[salsa::input]
pub struct PropertyNode {
pub fqcn: Arc<str>,
pub name: Arc<str>,
pub active: bool,
pub ty: Option<Union>,
pub visibility: Visibility,
pub is_static: bool,
pub is_readonly: bool,
pub location: Option<Location>,
}
#[salsa::input]
pub struct ClassConstantNode {
pub fqcn: Arc<str>,
pub name: Arc<str>,
pub active: bool,
pub ty: Union,
pub visibility: Option<Visibility>,
pub is_final: bool,
pub location: Option<Location>,
}
#[salsa::input]
pub struct GlobalConstantNode {
pub fqn: Arc<str>,
pub active: bool,
pub ty: Union,
}
#[derive(Clone, Debug, Default)]
pub struct Ancestors(pub Vec<Arc<str>>);
impl PartialEq for Ancestors {
fn eq(&self, other: &Self) -> bool {
self.0.len() == other.0.len()
&& self
.0
.iter()
.zip(&other.0)
.all(|(a, b)| a.as_ref() == b.as_ref())
}
}
unsafe impl salsa::Update for Ancestors {
unsafe fn maybe_update(old_ptr: *mut Self, new_val: Self) -> bool {
let old = unsafe { &mut *old_ptr };
if *old == new_val {
return false;
}
*old = new_val;
true
}
}
fn ancestors_initial(_db: &dyn MirDatabase, _id: salsa::Id, _node: ClassNode) -> Ancestors {
Ancestors(vec![])
}
fn ancestors_cycle(
_db: &dyn MirDatabase,
_cycle: &salsa::Cycle,
_last: &Ancestors,
_value: Ancestors,
_node: ClassNode,
) -> Ancestors {
Ancestors(vec![])
}
#[salsa::tracked(cycle_fn = ancestors_cycle, cycle_initial = ancestors_initial)]
pub fn class_ancestors(db: &dyn MirDatabase, node: ClassNode) -> Ancestors {
if !node.active(db) {
return Ancestors(vec![]);
}
if node.is_enum(db) || node.is_trait(db) {
return Ancestors(vec![]);
}
let mut all: Vec<Arc<str>> = Vec::new();
let mut seen: rustc_hash::FxHashSet<Arc<str>> = rustc_hash::FxHashSet::default();
let add =
|fqcn: &Arc<str>, all: &mut Vec<Arc<str>>, seen: &mut rustc_hash::FxHashSet<Arc<str>>| {
if seen.insert(fqcn.clone()) {
all.push(fqcn.clone());
}
};
if node.is_interface(db) {
for e in node.extends(db).iter() {
add(e, &mut all, &mut seen);
if let Some(parent_node) = db.lookup_class_node(e) {
for a in class_ancestors(db, parent_node).0 {
add(&a, &mut all, &mut seen);
}
}
}
} else {
if let Some(ref p) = node.parent(db) {
add(p, &mut all, &mut seen);
if let Some(parent_node) = db.lookup_class_node(p) {
for a in class_ancestors(db, parent_node).0 {
add(&a, &mut all, &mut seen);
}
}
}
for iface in node.interfaces(db).iter() {
add(iface, &mut all, &mut seen);
if let Some(iface_node) = db.lookup_class_node(iface) {
for a in class_ancestors(db, iface_node).0 {
add(&a, &mut all, &mut seen);
}
}
}
for t in node.traits(db).iter() {
add(t, &mut all, &mut seen);
}
}
Ancestors(all)
}
pub fn has_unknown_ancestor_via_db(db: &dyn MirDatabase, fqcn: &str) -> bool {
let Some(node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
return false;
};
class_ancestors(db, node)
.0
.iter()
.any(|ancestor| !type_exists_via_db(db, ancestor))
}
pub fn method_is_concretely_implemented(
db: &dyn MirDatabase,
fqcn: &str,
method_name: &str,
) -> bool {
let lower = method_name.to_lowercase();
let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
return false;
};
if self_node.is_interface(db) {
return false;
}
if let Some(m) = db.lookup_method_node(fqcn, &lower).filter(|m| m.active(db)) {
if !m.is_abstract(db) {
return true;
}
}
let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
for t in self_node.traits(db).iter() {
if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
return true;
}
}
for ancestor in class_ancestors(db, self_node).0.iter() {
let Some(anc_node) = db
.lookup_class_node(ancestor.as_ref())
.filter(|n| n.active(db))
else {
continue;
};
if anc_node.is_interface(db) {
continue;
}
if !anc_node.is_trait(db) {
if let Some(m) = db
.lookup_method_node(ancestor.as_ref(), &lower)
.filter(|m| m.active(db))
{
if !m.is_abstract(db) {
return true;
}
}
}
if anc_node.is_trait(db) {
if trait_provides_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
return true;
}
} else {
for t in anc_node.traits(db).iter() {
if trait_provides_method(db, t.as_ref(), &lower, &mut visited_traits) {
return true;
}
}
}
}
false
}
fn trait_provides_method(
db: &dyn MirDatabase,
trait_fqcn: &str,
method_lower: &str,
visited: &mut rustc_hash::FxHashSet<String>,
) -> bool {
if !visited.insert(trait_fqcn.to_string()) {
return false;
}
if let Some(m) = db
.lookup_method_node(trait_fqcn, method_lower)
.filter(|m| m.active(db))
{
if !m.is_abstract(db) {
return true;
}
}
let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
return false;
};
if !node.is_trait(db) {
return false;
}
for t in node.traits(db).iter() {
if trait_provides_method(db, t.as_ref(), method_lower, visited) {
return true;
}
}
false
}
pub fn lookup_method_in_chain(
db: &dyn MirDatabase,
fqcn: &str,
method_name: &str,
) -> Option<MethodNode> {
let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
lookup_method_in_chain_inner(db, fqcn, &method_name.to_lowercase(), &mut visited_mixins)
}
fn lookup_method_in_chain_inner(
db: &dyn MirDatabase,
fqcn: &str,
lower: &str,
visited_mixins: &mut rustc_hash::FxHashSet<String>,
) -> Option<MethodNode> {
let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
if let Some(node) = db.lookup_method_node(fqcn, lower).filter(|n| n.active(db)) {
return Some(node);
}
for m in self_node.mixins(db).iter() {
if visited_mixins.insert(m.to_string()) {
if let Some(node) = lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
{
return Some(node);
}
}
}
let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
for t in self_node.traits(db).iter() {
if let Some(node) = trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits) {
return Some(node);
}
}
for ancestor in class_ancestors(db, self_node).0.iter() {
if let Some(node) = db
.lookup_method_node(ancestor.as_ref(), lower)
.filter(|n| n.active(db))
{
return Some(node);
}
if let Some(anc_node) = db
.lookup_class_node(ancestor.as_ref())
.filter(|n| n.active(db))
{
if anc_node.is_trait(db) {
if let Some(node) =
trait_provides_method_node(db, ancestor.as_ref(), lower, &mut visited_traits)
{
return Some(node);
}
} else {
for t in anc_node.traits(db).iter() {
if let Some(node) =
trait_provides_method_node(db, t.as_ref(), lower, &mut visited_traits)
{
return Some(node);
}
}
for m in anc_node.mixins(db).iter() {
if visited_mixins.insert(m.to_string()) {
if let Some(node) =
lookup_method_in_chain_inner(db, m.as_ref(), lower, visited_mixins)
{
return Some(node);
}
}
}
}
}
}
None
}
fn trait_provides_method_node(
db: &dyn MirDatabase,
trait_fqcn: &str,
method_lower: &str,
visited: &mut rustc_hash::FxHashSet<String>,
) -> Option<MethodNode> {
if !visited.insert(trait_fqcn.to_string()) {
return None;
}
if let Some(node) = db
.lookup_method_node(trait_fqcn, method_lower)
.filter(|n| n.active(db))
{
return Some(node);
}
let node = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db))?;
if !node.is_trait(db) {
return None;
}
for t in node.traits(db).iter() {
if let Some(found) = trait_provides_method_node(db, t.as_ref(), method_lower, visited) {
return Some(found);
}
}
None
}
pub fn method_exists_via_db(db: &dyn MirDatabase, fqcn: &str, method_name: &str) -> bool {
let lower = method_name.to_lowercase();
let Some(self_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
return false;
};
if db
.lookup_method_node(fqcn, &lower)
.is_some_and(|m| m.active(db))
{
return true;
}
let mut visited_traits: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
for t in self_node.traits(db).iter() {
if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
return true;
}
}
for ancestor in class_ancestors(db, self_node).0.iter() {
if db
.lookup_method_node(ancestor.as_ref(), &lower)
.is_some_and(|m| m.active(db))
{
return true;
}
if let Some(anc_node) = db
.lookup_class_node(ancestor.as_ref())
.filter(|n| n.active(db))
{
if anc_node.is_trait(db) {
if trait_declares_method(db, ancestor.as_ref(), &lower, &mut visited_traits) {
return true;
}
} else {
for t in anc_node.traits(db).iter() {
if trait_declares_method(db, t.as_ref(), &lower, &mut visited_traits) {
return true;
}
}
}
}
}
false
}
fn trait_declares_method(
db: &dyn MirDatabase,
trait_fqcn: &str,
method_lower: &str,
visited: &mut rustc_hash::FxHashSet<String>,
) -> bool {
if !visited.insert(trait_fqcn.to_string()) {
return false;
}
if db
.lookup_method_node(trait_fqcn, method_lower)
.is_some_and(|m| m.active(db))
{
return true;
}
let Some(node) = db.lookup_class_node(trait_fqcn).filter(|n| n.active(db)) else {
return false;
};
if !node.is_trait(db) {
return false;
}
for t in node.traits(db).iter() {
if trait_declares_method(db, t.as_ref(), method_lower, visited) {
return true;
}
}
false
}
pub fn lookup_property_in_chain(
db: &dyn MirDatabase,
fqcn: &str,
prop_name: &str,
) -> Option<PropertyNode> {
let mut visited_mixins: rustc_hash::FxHashSet<String> = rustc_hash::FxHashSet::default();
lookup_property_in_chain_inner(db, fqcn, prop_name, &mut visited_mixins)
}
fn lookup_property_in_chain_inner(
db: &dyn MirDatabase,
fqcn: &str,
prop_name: &str,
visited_mixins: &mut rustc_hash::FxHashSet<String>,
) -> Option<PropertyNode> {
let self_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
if let Some(node) = db
.lookup_property_node(fqcn, prop_name)
.filter(|n| n.active(db))
{
return Some(node);
}
for m in self_node.mixins(db).iter() {
if visited_mixins.insert(m.to_string()) {
if let Some(node) =
lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
{
return Some(node);
}
}
}
for ancestor in class_ancestors(db, self_node).0.iter() {
if let Some(node) = db
.lookup_property_node(ancestor.as_ref(), prop_name)
.filter(|n| n.active(db))
{
return Some(node);
}
if let Some(anc_node) = db
.lookup_class_node(ancestor.as_ref())
.filter(|n| n.active(db))
{
for m in anc_node.mixins(db).iter() {
if visited_mixins.insert(m.to_string()) {
if let Some(node) =
lookup_property_in_chain_inner(db, m.as_ref(), prop_name, visited_mixins)
{
return Some(node);
}
}
}
}
}
None
}
pub fn class_constant_exists_in_chain(db: &dyn MirDatabase, fqcn: &str, const_name: &str) -> bool {
if db
.lookup_class_constant_node(fqcn, const_name)
.is_some_and(|n| n.active(db))
{
return true;
}
let Some(class_node) = db.lookup_class_node(fqcn).filter(|n| n.active(db)) else {
return false;
};
for ancestor in class_ancestors(db, class_node).0.iter() {
if db
.lookup_class_constant_node(ancestor.as_ref(), const_name)
.is_some_and(|n| n.active(db))
{
return true;
}
}
false
}
pub fn member_location_via_db(
db: &dyn MirDatabase,
fqcn: &str,
member_name: &str,
) -> Option<Location> {
if let Some(node) = lookup_method_in_chain(db, fqcn, member_name) {
if let Some(loc) = node.location(db) {
return Some(loc);
}
}
if let Some(node) = lookup_property_in_chain(db, fqcn, member_name) {
if let Some(loc) = node.location(db) {
return Some(loc);
}
}
if let Some(node) = db
.lookup_class_constant_node(fqcn, member_name)
.filter(|n| n.active(db))
{
if let Some(loc) = node.location(db) {
return Some(loc);
}
}
let class_node = db.lookup_class_node(fqcn).filter(|n| n.active(db))?;
for ancestor in class_ancestors(db, class_node).0.iter() {
if let Some(node) = db
.lookup_class_constant_node(ancestor.as_ref(), member_name)
.filter(|n| n.active(db))
{
if let Some(loc) = node.location(db) {
return Some(loc);
}
}
}
None
}
pub fn extends_or_implements_via_db(db: &dyn MirDatabase, child: &str, ancestor: &str) -> bool {
if child == ancestor {
return true;
}
let Some(node) = db.lookup_class_node(child).filter(|n| n.active(db)) else {
return false;
};
if node.is_enum(db) {
if node.interfaces(db).iter().any(|i| i.as_ref() == ancestor) {
return true;
}
if ancestor == "UnitEnum" || ancestor == "\\UnitEnum" {
return true;
}
if (ancestor == "BackedEnum" || ancestor == "\\BackedEnum") && node.is_backed_enum(db) {
return true;
}
return false;
}
class_ancestors(db, node)
.0
.iter()
.any(|p| p.as_ref() == ancestor)
}
#[salsa::tracked]
pub fn collect_file_definitions(db: &dyn MirDatabase, file: SourceFile) -> FileDefinitions {
let path = file.path(db);
let text = file.text(db);
let arena = bumpalo::Bump::new();
let parsed = php_rs_parser::parse(&arena, &text);
let mut all_issues: Vec<Issue> = parsed
.errors
.iter()
.map(|err| {
Issue::new(
mir_issues::IssueKind::ParseError {
message: err.to_string(),
},
mir_issues::Location {
file: path.clone(),
line: 1,
line_end: 1,
col_start: 0,
col_end: 0,
},
)
})
.collect();
let collector =
crate::collector::DefinitionCollector::new_for_slice(path, &text, &parsed.source_map);
let (slice, collector_issues) = collector.collect_slice(&parsed.program);
all_issues.extend(collector_issues);
FileDefinitions {
slice: Arc::new(slice),
issues: Arc::new(all_issues),
}
}
type MemberRegistry<V> = Arc<FxHashMap<Arc<str>, FxHashMap<Arc<str>, V>>>;
type ReferenceLocations =
Arc<std::sync::Mutex<FxHashMap<Arc<str>, Vec<(Arc<str>, u32, u16, u16)>>>>;
#[salsa::db]
#[derive(Default, Clone)]
pub struct MirDb {
storage: salsa::Storage<Self>,
class_nodes: Arc<FxHashMap<Arc<str>, ClassNode>>,
function_nodes: Arc<FxHashMap<Arc<str>, FunctionNode>>,
method_nodes: MemberRegistry<MethodNode>,
property_nodes: MemberRegistry<PropertyNode>,
class_constant_nodes: MemberRegistry<ClassConstantNode>,
global_constant_nodes: Arc<FxHashMap<Arc<str>, GlobalConstantNode>>,
file_namespaces: Arc<FxHashMap<Arc<str>, Arc<str>>>,
file_imports: Arc<FxHashMap<Arc<str>, HashMap<String, String>>>,
global_vars: Arc<FxHashMap<Arc<str>, Union>>,
symbol_to_file: Arc<FxHashMap<Arc<str>, Arc<str>>>,
reference_locations: ReferenceLocations,
}
#[salsa::db]
impl salsa::Database for MirDb {}
#[salsa::db]
impl MirDatabase for MirDb {
fn php_version_str(&self) -> Arc<str> {
Arc::from("8.2")
}
fn lookup_class_node(&self, fqcn: &str) -> Option<ClassNode> {
self.class_nodes.get(fqcn).copied()
}
fn lookup_function_node(&self, fqn: &str) -> Option<FunctionNode> {
self.function_nodes.get(fqn).copied()
}
fn lookup_method_node(&self, fqcn: &str, method_name_lower: &str) -> Option<MethodNode> {
self.method_nodes
.get(fqcn)
.and_then(|m| m.get(method_name_lower).copied())
}
fn lookup_property_node(&self, fqcn: &str, prop_name: &str) -> Option<PropertyNode> {
self.property_nodes
.get(fqcn)
.and_then(|m| m.get(prop_name).copied())
}
fn lookup_class_constant_node(
&self,
fqcn: &str,
const_name: &str,
) -> Option<ClassConstantNode> {
self.class_constant_nodes
.get(fqcn)
.and_then(|m| m.get(const_name).copied())
}
fn lookup_global_constant_node(&self, fqn: &str) -> Option<GlobalConstantNode> {
self.global_constant_nodes.get(fqn).copied()
}
fn class_own_methods(&self, fqcn: &str) -> Vec<MethodNode> {
self.method_nodes
.get(fqcn)
.map(|m| m.values().copied().collect())
.unwrap_or_default()
}
fn class_own_properties(&self, fqcn: &str) -> Vec<PropertyNode> {
self.property_nodes
.get(fqcn)
.map(|m| m.values().copied().collect())
.unwrap_or_default()
}
fn active_class_node_fqcns(&self) -> Vec<Arc<str>> {
self.class_nodes
.iter()
.filter_map(|(fqcn, node)| {
if node.active(self) {
Some(fqcn.clone())
} else {
None
}
})
.collect()
}
fn active_function_node_fqns(&self) -> Vec<Arc<str>> {
self.function_nodes
.iter()
.filter_map(|(fqn, node)| {
if node.active(self) {
Some(fqn.clone())
} else {
None
}
})
.collect()
}
fn file_namespace(&self, file: &str) -> Option<Arc<str>> {
self.file_namespaces.get(file).cloned()
}
fn file_imports(&self, file: &str) -> HashMap<String, String> {
self.file_imports.get(file).cloned().unwrap_or_default()
}
fn global_var_type(&self, name: &str) -> Option<Union> {
self.global_vars.get(name).cloned()
}
fn file_import_snapshots(&self) -> Vec<(Arc<str>, HashMap<String, String>)> {
self.file_imports
.iter()
.map(|(file, imports)| (file.clone(), imports.clone()))
.collect()
}
fn symbol_defining_file(&self, symbol: &str) -> Option<Arc<str>> {
self.symbol_to_file.get(symbol).cloned()
}
fn symbols_defined_in_file(&self, file: &str) -> Vec<Arc<str>> {
self.symbol_to_file
.iter()
.filter_map(|(sym, defining_file)| {
if defining_file.as_ref() == file {
Some(sym.clone())
} else {
None
}
})
.collect()
}
fn record_reference_location(&self, loc: RefLoc) {
let mut refs = self
.reference_locations
.lock()
.expect("reference lock poisoned");
let entry = refs.entry(loc.symbol_key).or_default();
let tuple = (loc.file, loc.line, loc.col_start, loc.col_end);
if !entry.iter().any(|existing| existing == &tuple) {
entry.push(tuple);
}
}
fn replay_reference_locations(&self, file: Arc<str>, locs: &[(String, u32, u16, u16)]) {
for (symbol, line, col_start, col_end) in locs {
self.record_reference_location(RefLoc {
symbol_key: Arc::from(symbol.as_str()),
file: file.clone(),
line: *line,
col_start: *col_start,
col_end: *col_end,
});
}
}
fn extract_file_reference_locations(&self, file: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
let refs = self
.reference_locations
.lock()
.expect("reference lock poisoned");
let mut out = Vec::new();
for (symbol, locs) in refs.iter() {
for (loc_file, line, col_start, col_end) in locs {
if loc_file.as_ref() == file {
out.push((symbol.clone(), *line, *col_start, *col_end));
}
}
}
out
}
fn reference_locations(&self, symbol: &str) -> Vec<(Arc<str>, u32, u16, u16)> {
let refs = self
.reference_locations
.lock()
.expect("reference lock poisoned");
refs.get(symbol).cloned().unwrap_or_default()
}
fn has_reference(&self, symbol: &str) -> bool {
let refs = self
.reference_locations
.lock()
.expect("reference lock poisoned");
refs.get(symbol).is_some_and(|locs| !locs.is_empty())
}
fn clear_file_references(&self, file: &str) {
let mut refs = self
.reference_locations
.lock()
.expect("reference lock poisoned");
for locs in refs.values_mut() {
locs.retain(|(loc_file, _, _, _)| loc_file.as_ref() != file);
}
}
}
#[derive(Default)]
#[allow(clippy::type_complexity)]
pub struct InferredReturnTypes {
functions: std::sync::Mutex<Vec<(Arc<str>, Union)>>,
methods: std::sync::Mutex<Vec<(Arc<str>, Arc<str>, Union)>>,
}
impl InferredReturnTypes {
pub fn new() -> Self {
Self::default()
}
pub fn push_function(&self, fqn: Arc<str>, inferred: Union) {
if let Ok(mut g) = self.functions.lock() {
g.push((fqn, inferred));
}
}
pub fn push_method(&self, fqcn: Arc<str>, name: Arc<str>, inferred: Union) {
if let Ok(mut g) = self.methods.lock() {
g.push((fqcn, name, inferred));
}
}
}
#[derive(Debug, Clone, Default)]
pub struct ClassNodeFields {
pub fqcn: Arc<str>,
pub is_interface: bool,
pub is_trait: bool,
pub is_enum: bool,
pub is_abstract: bool,
pub parent: Option<Arc<str>>,
pub interfaces: Arc<[Arc<str>]>,
pub traits: Arc<[Arc<str>]>,
pub extends: Arc<[Arc<str>]>,
pub template_params: Arc<[TemplateParam]>,
pub require_extends: Arc<[Arc<str>]>,
pub require_implements: Arc<[Arc<str>]>,
pub is_backed_enum: bool,
pub mixins: Arc<[Arc<str>]>,
pub deprecated: Option<Arc<str>>,
pub enum_scalar_type: Option<Union>,
pub is_final: bool,
pub is_readonly: bool,
pub location: Option<Location>,
pub extends_type_args: Arc<[Union]>,
pub implements_type_args: ImplementsTypeArgs,
}
impl ClassNodeFields {
pub fn for_class(fqcn: Arc<str>) -> Self {
Self {
fqcn,
..Self::default()
}
}
pub fn for_interface(fqcn: Arc<str>) -> Self {
Self {
fqcn,
is_interface: true,
..Self::default()
}
}
pub fn for_trait(fqcn: Arc<str>) -> Self {
Self {
fqcn,
is_trait: true,
..Self::default()
}
}
pub fn for_enum(fqcn: Arc<str>) -> Self {
Self {
fqcn,
is_enum: true,
..Self::default()
}
}
}
impl MirDb {
pub fn remove_file_definitions(&mut self, file: &str) {
let symbols = self.symbols_defined_in_file(file);
for symbol in &symbols {
self.deactivate_class_node(symbol);
self.deactivate_function_node(symbol);
self.deactivate_class_methods(symbol);
self.deactivate_class_properties(symbol);
self.deactivate_class_constants(symbol);
self.deactivate_global_constant_node(symbol);
}
let symbol_set: HashSet<Arc<str>> = symbols.into_iter().collect();
Arc::make_mut(&mut self.symbol_to_file).retain(|sym, defining_file| {
defining_file.as_ref() != file && !symbol_set.contains(sym)
});
Arc::make_mut(&mut self.file_namespaces).retain(|path, _| path.as_ref() != file);
Arc::make_mut(&mut self.file_imports).retain(|path, _| path.as_ref() != file);
Arc::make_mut(&mut self.global_vars).retain(|name, _| !symbol_set.contains(name));
self.clear_file_references(file);
}
pub fn type_count(&self) -> usize {
self.class_nodes
.values()
.filter(|node| node.active(self))
.count()
}
pub fn function_count(&self) -> usize {
self.function_nodes
.values()
.filter(|node| node.active(self))
.count()
}
pub fn constant_count(&self) -> usize {
self.global_constant_nodes
.values()
.filter(|node| node.active(self))
.count()
}
pub fn ingest_stub_slice(&mut self, slice: &StubSlice) {
use std::collections::HashSet;
if let Some(file) = &slice.file {
if let Some(namespace) = &slice.namespace {
Arc::make_mut(&mut self.file_namespaces).insert(file.clone(), namespace.clone());
}
if !slice.imports.is_empty() {
Arc::make_mut(&mut self.file_imports).insert(file.clone(), slice.imports.clone());
}
for (name, _) in &slice.global_vars {
let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
Arc::make_mut(&mut self.symbol_to_file)
.insert(Arc::from(global_name), file.clone());
}
}
for (name, ty) in &slice.global_vars {
let global_name = name.strip_prefix('$').unwrap_or(name.as_ref());
Arc::make_mut(&mut self.global_vars).insert(Arc::from(global_name), ty.clone());
}
for cls in &slice.classes {
if let Some(file) = &slice.file {
Arc::make_mut(&mut self.symbol_to_file).insert(cls.fqcn.clone(), file.clone());
}
self.upsert_class_node(ClassNodeFields {
is_abstract: cls.is_abstract,
parent: cls.parent.clone(),
interfaces: Arc::from(cls.interfaces.as_slice()),
traits: Arc::from(cls.traits.as_slice()),
template_params: Arc::from(cls.template_params.as_slice()),
mixins: Arc::from(cls.mixins.as_slice()),
deprecated: cls.deprecated.clone(),
is_final: cls.is_final,
is_readonly: cls.is_readonly,
location: cls.location.clone(),
extends_type_args: Arc::from(cls.extends_type_args.as_slice()),
implements_type_args: Arc::from(
cls.implements_type_args
.iter()
.map(|(iface, args)| (iface.clone(), Arc::from(args.as_slice())))
.collect::<Vec<_>>(),
),
..ClassNodeFields::for_class(cls.fqcn.clone())
});
if self.method_nodes.contains_key(cls.fqcn.as_ref()) {
let method_keep: HashSet<&str> =
cls.own_methods.keys().map(|m| m.as_ref()).collect();
self.prune_class_methods(&cls.fqcn, &method_keep);
}
for method in cls.own_methods.values() {
self.upsert_method_node(method.as_ref());
}
if self.property_nodes.contains_key(cls.fqcn.as_ref()) {
let prop_keep: HashSet<&str> =
cls.own_properties.keys().map(|p| p.as_ref()).collect();
self.prune_class_properties(&cls.fqcn, &prop_keep);
}
for prop in cls.own_properties.values() {
self.upsert_property_node(&cls.fqcn, prop);
}
if self.class_constant_nodes.contains_key(cls.fqcn.as_ref()) {
let const_keep: HashSet<&str> =
cls.own_constants.keys().map(|c| c.as_ref()).collect();
self.prune_class_constants(&cls.fqcn, &const_keep);
}
for constant in cls.own_constants.values() {
self.upsert_class_constant_node(&cls.fqcn, constant);
}
}
for iface in &slice.interfaces {
if let Some(file) = &slice.file {
Arc::make_mut(&mut self.symbol_to_file).insert(iface.fqcn.clone(), file.clone());
}
self.upsert_class_node(ClassNodeFields {
extends: Arc::from(iface.extends.as_slice()),
template_params: Arc::from(iface.template_params.as_slice()),
location: iface.location.clone(),
..ClassNodeFields::for_interface(iface.fqcn.clone())
});
if self.method_nodes.contains_key(iface.fqcn.as_ref()) {
let method_keep: HashSet<&str> =
iface.own_methods.keys().map(|m| m.as_ref()).collect();
self.prune_class_methods(&iface.fqcn, &method_keep);
}
for method in iface.own_methods.values() {
self.upsert_method_node(method.as_ref());
}
if self.class_constant_nodes.contains_key(iface.fqcn.as_ref()) {
let const_keep: HashSet<&str> =
iface.own_constants.keys().map(|c| c.as_ref()).collect();
self.prune_class_constants(&iface.fqcn, &const_keep);
}
for constant in iface.own_constants.values() {
self.upsert_class_constant_node(&iface.fqcn, constant);
}
}
for tr in &slice.traits {
if let Some(file) = &slice.file {
Arc::make_mut(&mut self.symbol_to_file).insert(tr.fqcn.clone(), file.clone());
}
self.upsert_class_node(ClassNodeFields {
traits: Arc::from(tr.traits.as_slice()),
template_params: Arc::from(tr.template_params.as_slice()),
require_extends: Arc::from(tr.require_extends.as_slice()),
require_implements: Arc::from(tr.require_implements.as_slice()),
location: tr.location.clone(),
..ClassNodeFields::for_trait(tr.fqcn.clone())
});
if self.method_nodes.contains_key(tr.fqcn.as_ref()) {
let method_keep: HashSet<&str> =
tr.own_methods.keys().map(|m| m.as_ref()).collect();
self.prune_class_methods(&tr.fqcn, &method_keep);
}
for method in tr.own_methods.values() {
self.upsert_method_node(method.as_ref());
}
if self.property_nodes.contains_key(tr.fqcn.as_ref()) {
let prop_keep: HashSet<&str> =
tr.own_properties.keys().map(|p| p.as_ref()).collect();
self.prune_class_properties(&tr.fqcn, &prop_keep);
}
for prop in tr.own_properties.values() {
self.upsert_property_node(&tr.fqcn, prop);
}
if self.class_constant_nodes.contains_key(tr.fqcn.as_ref()) {
let const_keep: HashSet<&str> =
tr.own_constants.keys().map(|c| c.as_ref()).collect();
self.prune_class_constants(&tr.fqcn, &const_keep);
}
for constant in tr.own_constants.values() {
self.upsert_class_constant_node(&tr.fqcn, constant);
}
}
for en in &slice.enums {
if let Some(file) = &slice.file {
Arc::make_mut(&mut self.symbol_to_file).insert(en.fqcn.clone(), file.clone());
}
self.upsert_class_node(ClassNodeFields {
interfaces: Arc::from(en.interfaces.as_slice()),
is_backed_enum: en.scalar_type.is_some(),
enum_scalar_type: en.scalar_type.clone(),
location: en.location.clone(),
..ClassNodeFields::for_enum(en.fqcn.clone())
});
if self.method_nodes.contains_key(en.fqcn.as_ref()) {
let mut method_keep: HashSet<&str> =
en.own_methods.keys().map(|m| m.as_ref()).collect();
method_keep.insert("cases");
if en.scalar_type.is_some() {
method_keep.insert("from");
method_keep.insert("tryfrom");
}
self.prune_class_methods(&en.fqcn, &method_keep);
}
for method in en.own_methods.values() {
self.upsert_method_node(method.as_ref());
}
let synth_method = |name: &str| mir_codebase::storage::MethodStorage {
fqcn: en.fqcn.clone(),
name: Arc::from(name),
params: vec![],
return_type: Some(Union::mixed()),
inferred_return_type: None,
visibility: Visibility::Public,
is_static: true,
is_abstract: false,
is_constructor: false,
template_params: vec![],
assertions: vec![],
throws: vec![],
is_final: false,
is_internal: false,
is_pure: false,
deprecated: None,
location: None,
};
let already = |name: &str| {
en.own_methods
.keys()
.any(|k| k.as_ref().eq_ignore_ascii_case(name))
};
if !already("cases") {
self.upsert_method_node(&synth_method("cases"));
}
if en.scalar_type.is_some() {
if !already("from") {
self.upsert_method_node(&synth_method("from"));
}
if !already("tryFrom") {
self.upsert_method_node(&synth_method("tryFrom"));
}
}
if self.class_constant_nodes.contains_key(en.fqcn.as_ref()) {
let mut const_keep: HashSet<&str> =
en.own_constants.keys().map(|c| c.as_ref()).collect();
for case in en.cases.values() {
const_keep.insert(case.name.as_ref());
}
self.prune_class_constants(&en.fqcn, &const_keep);
}
for constant in en.own_constants.values() {
self.upsert_class_constant_node(&en.fqcn, constant);
}
for case in en.cases.values() {
let case_const = ConstantStorage {
name: case.name.clone(),
ty: mir_types::Union::mixed(),
visibility: None,
is_final: false,
location: case.location.clone(),
};
self.upsert_class_constant_node(&en.fqcn, &case_const);
}
}
for func in &slice.functions {
if let Some(file) = &slice.file {
Arc::make_mut(&mut self.symbol_to_file).insert(func.fqn.clone(), file.clone());
}
self.upsert_function_node(func);
}
for (fqn, ty) in &slice.constants {
self.upsert_global_constant_node(fqn.clone(), ty.clone());
}
}
#[allow(clippy::too_many_arguments)]
pub fn upsert_class_node(&mut self, fields: ClassNodeFields) -> ClassNode {
use salsa::Setter as _;
let ClassNodeFields {
fqcn,
is_interface,
is_trait,
is_enum,
is_abstract,
parent,
interfaces,
traits,
extends,
template_params,
require_extends,
require_implements,
is_backed_enum,
mixins,
deprecated,
enum_scalar_type,
is_final,
is_readonly,
location,
extends_type_args,
implements_type_args,
} = fields;
if let Some(&node) = self.class_nodes.get(&fqcn) {
if node.active(self)
&& node.is_interface(self) == is_interface
&& node.is_trait(self) == is_trait
&& node.is_enum(self) == is_enum
&& node.is_abstract(self) == is_abstract
&& node.is_backed_enum(self) == is_backed_enum
&& node.parent(self) == parent
&& *node.interfaces(self) == *interfaces
&& *node.traits(self) == *traits
&& *node.extends(self) == *extends
&& *node.template_params(self) == *template_params
&& *node.require_extends(self) == *require_extends
&& *node.require_implements(self) == *require_implements
&& *node.mixins(self) == *mixins
&& node.deprecated(self) == deprecated
&& node.enum_scalar_type(self) == enum_scalar_type
&& node.is_final(self) == is_final
&& node.is_readonly(self) == is_readonly
&& node.location(self) == location
&& *node.extends_type_args(self) == *extends_type_args
&& *node.implements_type_args(self) == *implements_type_args
{
return node;
}
node.set_active(self).to(true);
node.set_is_interface(self).to(is_interface);
node.set_is_trait(self).to(is_trait);
node.set_is_enum(self).to(is_enum);
node.set_is_abstract(self).to(is_abstract);
node.set_parent(self).to(parent);
node.set_interfaces(self).to(interfaces);
node.set_traits(self).to(traits);
node.set_extends(self).to(extends);
node.set_template_params(self).to(template_params);
node.set_require_extends(self).to(require_extends);
node.set_require_implements(self).to(require_implements);
node.set_is_backed_enum(self).to(is_backed_enum);
node.set_mixins(self).to(mixins);
node.set_deprecated(self).to(deprecated);
node.set_enum_scalar_type(self).to(enum_scalar_type);
node.set_is_final(self).to(is_final);
node.set_is_readonly(self).to(is_readonly);
node.set_location(self).to(location);
node.set_extends_type_args(self).to(extends_type_args);
node.set_implements_type_args(self).to(implements_type_args);
node
} else {
let node = ClassNode::new(
self,
fqcn.clone(),
true,
is_interface,
is_trait,
is_enum,
is_abstract,
parent,
interfaces,
traits,
extends,
template_params,
require_extends,
require_implements,
is_backed_enum,
mixins,
deprecated,
enum_scalar_type,
is_final,
is_readonly,
location,
extends_type_args,
implements_type_args,
);
Arc::make_mut(&mut self.class_nodes).insert(fqcn, node);
node
}
}
pub fn deactivate_class_node(&mut self, fqcn: &str) {
use salsa::Setter as _;
if let Some(&node) = self.class_nodes.get(fqcn) {
node.set_active(self).to(false);
}
}
pub fn upsert_function_node(&mut self, storage: &FunctionStorage) -> FunctionNode {
use salsa::Setter as _;
let fqn = &storage.fqn;
if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
if node.active(self)
&& node.short_name(self) == storage.short_name
&& node.is_pure(self) == storage.is_pure
&& node.deprecated(self) == storage.deprecated
&& node.return_type(self) == storage.return_type
&& node.location(self) == storage.location
&& *node.params(self) == *storage.params.as_slice()
&& *node.template_params(self) == *storage.template_params.as_slice()
&& *node.assertions(self) == *storage.assertions.as_slice()
&& *node.throws(self) == *storage.throws.as_slice()
{
return node;
}
node.set_active(self).to(true);
node.set_short_name(self).to(storage.short_name.clone());
node.set_params(self)
.to(Arc::from(storage.params.as_slice()));
node.set_return_type(self).to(storage.return_type.clone());
node.set_template_params(self)
.to(Arc::from(storage.template_params.as_slice()));
node.set_assertions(self)
.to(Arc::from(storage.assertions.as_slice()));
node.set_throws(self)
.to(Arc::from(storage.throws.as_slice()));
node.set_deprecated(self).to(storage.deprecated.clone());
node.set_is_pure(self).to(storage.is_pure);
node.set_location(self).to(storage.location.clone());
node
} else {
let node = FunctionNode::new(
self,
fqn.clone(),
storage.short_name.clone(),
true,
Arc::from(storage.params.as_slice()),
storage.return_type.clone(),
storage.inferred_return_type.clone(),
Arc::from(storage.template_params.as_slice()),
Arc::from(storage.assertions.as_slice()),
Arc::from(storage.throws.as_slice()),
storage.deprecated.clone(),
storage.is_pure,
storage.location.clone(),
);
Arc::make_mut(&mut self.function_nodes).insert(fqn.clone(), node);
node
}
}
pub fn commit_inferred_return_types(&mut self, buf: &InferredReturnTypes) {
use salsa::Setter as _;
let funcs = std::mem::take(&mut *buf.functions.lock().expect("inferred buffer poisoned"));
for (fqn, inferred) in funcs {
if let Some(&node) = self.function_nodes.get(fqn.as_ref()) {
if !node.active(self) {
continue;
}
let new = Some(inferred);
if node.inferred_return_type(self) == new {
continue;
}
node.set_inferred_return_type(self).to(new);
}
}
let methods = std::mem::take(&mut *buf.methods.lock().expect("inferred buffer poisoned"));
for (fqcn, name, inferred) in methods {
let name_lower: Arc<str> = if name.chars().all(|c| !c.is_uppercase()) {
name.clone()
} else {
Arc::from(name.to_lowercase().as_str())
};
let node = self
.method_nodes
.get(fqcn.as_ref())
.and_then(|m| m.get(&name_lower))
.copied();
if let Some(node) = node {
if !node.active(self) {
continue;
}
let new = Some(inferred);
if node.inferred_return_type(self) == new {
continue;
}
node.set_inferred_return_type(self).to(new);
}
}
}
pub fn deactivate_function_node(&mut self, fqn: &str) {
use salsa::Setter as _;
if let Some(&node) = self.function_nodes.get(fqn) {
node.set_active(self).to(false);
}
}
pub fn upsert_method_node(&mut self, storage: &MethodStorage) -> MethodNode {
use salsa::Setter as _;
let fqcn = &storage.fqcn;
let name_lower: Arc<str> = Arc::from(storage.name.to_lowercase().as_str());
let existing = self
.method_nodes
.get(fqcn.as_ref())
.and_then(|m| m.get(&name_lower))
.copied();
if let Some(node) = existing {
if node.active(self)
&& node.visibility(self) == storage.visibility
&& node.is_static(self) == storage.is_static
&& node.is_abstract(self) == storage.is_abstract
&& node.is_final(self) == storage.is_final
&& node.is_constructor(self) == storage.is_constructor
&& node.is_pure(self) == storage.is_pure
&& node.deprecated(self) == storage.deprecated
&& node.return_type(self) == storage.return_type
&& node.location(self) == storage.location
&& *node.params(self) == *storage.params.as_slice()
&& *node.template_params(self) == *storage.template_params.as_slice()
&& *node.assertions(self) == *storage.assertions.as_slice()
&& *node.throws(self) == *storage.throws.as_slice()
{
return node;
}
node.set_active(self).to(true);
node.set_params(self)
.to(Arc::from(storage.params.as_slice()));
node.set_return_type(self).to(storage.return_type.clone());
node.set_template_params(self)
.to(Arc::from(storage.template_params.as_slice()));
node.set_assertions(self)
.to(Arc::from(storage.assertions.as_slice()));
node.set_throws(self)
.to(Arc::from(storage.throws.as_slice()));
node.set_deprecated(self).to(storage.deprecated.clone());
node.set_visibility(self).to(storage.visibility);
node.set_is_static(self).to(storage.is_static);
node.set_is_abstract(self).to(storage.is_abstract);
node.set_is_final(self).to(storage.is_final);
node.set_is_constructor(self).to(storage.is_constructor);
node.set_is_pure(self).to(storage.is_pure);
node.set_location(self).to(storage.location.clone());
node
} else {
let node = MethodNode::new(
self,
fqcn.clone(),
storage.name.clone(),
true,
Arc::from(storage.params.as_slice()),
storage.return_type.clone(),
storage.inferred_return_type.clone(),
Arc::from(storage.template_params.as_slice()),
Arc::from(storage.assertions.as_slice()),
Arc::from(storage.throws.as_slice()),
storage.deprecated.clone(),
storage.visibility,
storage.is_static,
storage.is_abstract,
storage.is_final,
storage.is_constructor,
storage.is_pure,
storage.location.clone(),
);
Arc::make_mut(&mut self.method_nodes)
.entry(fqcn.clone())
.or_default()
.insert(name_lower, node);
node
}
}
pub fn deactivate_class_methods(&mut self, fqcn: &str) {
use salsa::Setter as _;
let nodes: Vec<MethodNode> = match self.method_nodes.get(fqcn) {
Some(methods) => methods.values().copied().collect(),
None => return,
};
for node in nodes {
node.set_active(self).to(false);
}
}
pub fn prune_class_methods<T>(&mut self, fqcn: &str, keep_lower: &std::collections::HashSet<T>)
where
T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
{
use salsa::Setter as _;
let candidates: Vec<MethodNode> = self
.method_nodes
.get(fqcn)
.map(|m| {
m.iter()
.filter(|(k, _)| !keep_lower.contains(k.as_ref()))
.map(|(_, n)| *n)
.collect()
})
.unwrap_or_default();
for node in candidates {
if node.active(self) {
node.set_active(self).to(false);
}
}
}
pub fn prune_class_properties<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
where
T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
{
use salsa::Setter as _;
let candidates: Vec<PropertyNode> = self
.property_nodes
.get(fqcn)
.map(|m| {
m.iter()
.filter(|(k, _)| !keep.contains(k.as_ref()))
.map(|(_, n)| *n)
.collect()
})
.unwrap_or_default();
for node in candidates {
if node.active(self) {
node.set_active(self).to(false);
}
}
}
pub fn prune_class_constants<T>(&mut self, fqcn: &str, keep: &std::collections::HashSet<T>)
where
T: Eq + std::hash::Hash + std::borrow::Borrow<str>,
{
use salsa::Setter as _;
let candidates: Vec<ClassConstantNode> = self
.class_constant_nodes
.get(fqcn)
.map(|m| {
m.iter()
.filter(|(k, _)| !keep.contains(k.as_ref()))
.map(|(_, n)| *n)
.collect()
})
.unwrap_or_default();
for node in candidates {
if node.active(self) {
node.set_active(self).to(false);
}
}
}
pub fn upsert_property_node(&mut self, fqcn: &Arc<str>, storage: &PropertyStorage) {
use salsa::Setter as _;
let existing = self
.property_nodes
.get(fqcn.as_ref())
.and_then(|m| m.get(storage.name.as_ref()))
.copied();
if let Some(node) = existing {
if node.active(self)
&& node.visibility(self) == storage.visibility
&& node.is_static(self) == storage.is_static
&& node.is_readonly(self) == storage.is_readonly
&& node.ty(self) == storage.ty
&& node.location(self) == storage.location
{
return;
}
node.set_active(self).to(true);
node.set_ty(self).to(storage.ty.clone());
node.set_visibility(self).to(storage.visibility);
node.set_is_static(self).to(storage.is_static);
node.set_is_readonly(self).to(storage.is_readonly);
node.set_location(self).to(storage.location.clone());
} else {
let node = PropertyNode::new(
self,
fqcn.clone(),
storage.name.clone(),
true,
storage.ty.clone(),
storage.visibility,
storage.is_static,
storage.is_readonly,
storage.location.clone(),
);
Arc::make_mut(&mut self.property_nodes)
.entry(fqcn.clone())
.or_default()
.insert(storage.name.clone(), node);
}
}
pub fn deactivate_class_properties(&mut self, fqcn: &str) {
use salsa::Setter as _;
let nodes: Vec<PropertyNode> = match self.property_nodes.get(fqcn) {
Some(props) => props.values().copied().collect(),
None => return,
};
for node in nodes {
node.set_active(self).to(false);
}
}
pub fn upsert_class_constant_node(&mut self, fqcn: &Arc<str>, storage: &ConstantStorage) {
use salsa::Setter as _;
let existing = self
.class_constant_nodes
.get(fqcn.as_ref())
.and_then(|m| m.get(storage.name.as_ref()))
.copied();
if let Some(node) = existing {
if node.active(self)
&& node.visibility(self) == storage.visibility
&& node.is_final(self) == storage.is_final
&& node.ty(self) == storage.ty
&& node.location(self) == storage.location
{
return;
}
node.set_active(self).to(true);
node.set_ty(self).to(storage.ty.clone());
node.set_visibility(self).to(storage.visibility);
node.set_is_final(self).to(storage.is_final);
node.set_location(self).to(storage.location.clone());
} else {
let node = ClassConstantNode::new(
self,
fqcn.clone(),
storage.name.clone(),
true,
storage.ty.clone(),
storage.visibility,
storage.is_final,
storage.location.clone(),
);
Arc::make_mut(&mut self.class_constant_nodes)
.entry(fqcn.clone())
.or_default()
.insert(storage.name.clone(), node);
}
}
pub fn upsert_global_constant_node(&mut self, fqn: Arc<str>, ty: Union) -> GlobalConstantNode {
use salsa::Setter as _;
if let Some(&node) = self.global_constant_nodes.get(&fqn) {
if node.active(self) && node.ty(self) == ty {
return node;
}
node.set_active(self).to(true);
node.set_ty(self).to(ty);
node
} else {
let node = GlobalConstantNode::new(self, fqn.clone(), true, ty);
Arc::make_mut(&mut self.global_constant_nodes).insert(fqn, node);
node
}
}
pub fn deactivate_global_constant_node(&mut self, fqn: &str) {
use salsa::Setter as _;
if let Some(&node) = self.global_constant_nodes.get(fqn) {
node.set_active(self).to(false);
}
}
pub fn deactivate_class_constants(&mut self, fqcn: &str) {
use salsa::Setter as _;
let nodes: Vec<ClassConstantNode> = match self.class_constant_nodes.get(fqcn) {
Some(consts) => consts.values().copied().collect(),
None => return,
};
for node in nodes {
node.set_active(self).to(false);
}
}
}
#[salsa::accumulator]
#[derive(Clone, Debug)]
pub struct IssueAccumulator(pub Issue);
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct RefLoc {
pub symbol_key: Arc<str>,
pub file: Arc<str>,
pub line: u32,
pub col_start: u16,
pub col_end: u16,
}
#[salsa::accumulator]
#[derive(Clone, Debug)]
pub struct RefLocAccumulator(pub RefLoc);
#[salsa::input]
pub struct AnalyzeFileInput {
pub php_version: Arc<str>,
}
#[salsa::tracked]
pub fn analyze_file(db: &dyn MirDatabase, file: SourceFile, _input: AnalyzeFileInput) {
use salsa::Accumulator as _;
let path = file.path(db);
let text = file.text(db);
let arena = bumpalo::Bump::new();
let parsed = php_rs_parser::parse(&arena, &text);
for err in &parsed.errors {
let issue = Issue::new(
mir_issues::IssueKind::ParseError {
message: err.to_string(),
},
mir_issues::Location {
file: path.clone(),
line: 1,
line_end: 1,
col_start: 0,
col_end: 0,
},
);
IssueAccumulator(issue).accumulate(db);
}
}
#[cfg(test)]
mod tests {
use super::*;
use salsa::Setter as _;
fn upsert_class(
db: &mut MirDb,
fqcn: &str,
parent: Option<Arc<str>>,
extends: Arc<[Arc<str>]>,
is_interface: bool,
) -> ClassNode {
db.upsert_class_node(ClassNodeFields {
is_interface,
parent,
extends,
..ClassNodeFields::for_class(Arc::from(fqcn))
})
}
#[test]
fn mirdb_constructs() {
let _db = MirDb::default();
}
#[test]
fn source_file_input_roundtrip() {
let db = MirDb::default();
let file = SourceFile::new(&db, Arc::from("/tmp/test.php"), Arc::from("<?php echo 1;"));
assert_eq!(file.path(&db).as_ref(), "/tmp/test.php");
assert_eq!(file.text(&db).as_ref(), "<?php echo 1;");
}
#[test]
fn collect_file_definitions_basic() {
let db = MirDb::default();
let src = Arc::from("<?php class Foo {}");
let file = SourceFile::new(&db, Arc::from("/tmp/foo.php"), src);
let defs = collect_file_definitions(&db, file);
assert!(defs.issues.is_empty());
assert_eq!(defs.slice.classes.len(), 1);
assert_eq!(defs.slice.classes[0].fqcn.as_ref(), "Foo");
}
#[test]
fn collect_file_definitions_memoized() {
let db = MirDb::default();
let file = SourceFile::new(
&db,
Arc::from("/tmp/memo.php"),
Arc::from("<?php class Bar {}"),
);
let defs1 = collect_file_definitions(&db, file);
let defs2 = collect_file_definitions(&db, file);
assert!(
Arc::ptr_eq(&defs1.slice, &defs2.slice),
"unchanged file must return the memoized result"
);
}
#[test]
fn analyze_file_accumulates_parse_errors() {
let db = MirDb::default();
let file = SourceFile::new(
&db,
Arc::from("/tmp/parse_err.php"),
Arc::from("<?php $x = \"unterminated"),
);
let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
analyze_file(&db, file, input);
let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
assert!(
!issues.is_empty(),
"expected parse error to surface as accumulated IssueAccumulator"
);
assert!(matches!(
issues[0].0.kind,
mir_issues::IssueKind::ParseError { .. }
));
}
#[test]
fn analyze_file_clean_input_accumulates_nothing() {
let db = MirDb::default();
let file = SourceFile::new(
&db,
Arc::from("/tmp/clean.php"),
Arc::from("<?php class Foo {}"),
);
let input = AnalyzeFileInput::new(&db, Arc::from("8.2"));
analyze_file(&db, file, input);
let issues: Vec<&IssueAccumulator> = analyze_file::accumulated(&db, file, input);
let refs: Vec<&RefLocAccumulator> = analyze_file::accumulated(&db, file, input);
assert!(issues.is_empty());
assert!(refs.is_empty());
}
#[test]
fn collect_file_definitions_recomputes_on_change() {
let mut db = MirDb::default();
let file = SourceFile::new(
&db,
Arc::from("/tmp/memo2.php"),
Arc::from("<?php class Foo {}"),
);
let defs1 = collect_file_definitions(&db, file);
file.set_text(&mut db)
.to(Arc::from("<?php class Foo {} class Bar {}"));
let defs2 = collect_file_definitions(&db, file);
assert!(
!Arc::ptr_eq(&defs1.slice, &defs2.slice),
"changed file must produce a new result"
);
assert_eq!(defs2.slice.classes.len(), 2);
}
#[test]
fn class_ancestors_empty_for_root_class() {
let mut db = MirDb::default();
let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
let ancestors = class_ancestors(&db, node);
assert!(ancestors.0.is_empty(), "root class has no ancestors");
}
#[test]
fn class_ancestors_single_parent() {
let mut db = MirDb::default();
upsert_class(&mut db, "Base", None, Arc::from([]), false);
let child = upsert_class(
&mut db,
"Child",
Some(Arc::from("Base")),
Arc::from([]),
false,
);
let ancestors = class_ancestors(&db, child);
assert_eq!(ancestors.0.len(), 1);
assert_eq!(ancestors.0[0].as_ref(), "Base");
}
#[test]
fn class_ancestors_transitive() {
let mut db = MirDb::default();
upsert_class(&mut db, "GrandParent", None, Arc::from([]), false);
upsert_class(
&mut db,
"Parent",
Some(Arc::from("GrandParent")),
Arc::from([]),
false,
);
let child = upsert_class(
&mut db,
"Child",
Some(Arc::from("Parent")),
Arc::from([]),
false,
);
let ancestors = class_ancestors(&db, child);
assert_eq!(ancestors.0.len(), 2);
assert_eq!(ancestors.0[0].as_ref(), "Parent");
assert_eq!(ancestors.0[1].as_ref(), "GrandParent");
}
#[test]
fn class_ancestors_cycle_returns_empty() {
let mut db = MirDb::default();
let node_a = upsert_class(&mut db, "A", Some(Arc::from("A")), Arc::from([]), false);
let ancestors = class_ancestors(&db, node_a);
assert!(ancestors.0.is_empty(), "cycle must yield empty ancestors");
}
#[test]
fn class_ancestors_inactive_node_returns_empty() {
let mut db = MirDb::default();
let node = upsert_class(&mut db, "Foo", None, Arc::from([]), false);
db.deactivate_class_node("Foo");
let ancestors = class_ancestors(&db, node);
assert!(ancestors.0.is_empty(), "inactive node must yield empty");
}
#[test]
fn class_ancestors_recomputes_on_parent_change() {
let mut db = MirDb::default();
upsert_class(&mut db, "Base", None, Arc::from([]), false);
let child = upsert_class(&mut db, "Child", None, Arc::from([]), false);
let before = class_ancestors(&db, child);
assert!(before.0.is_empty());
child.set_parent(&mut db).to(Some(Arc::from("Base")));
let after = class_ancestors(&db, child);
assert_eq!(after.0.len(), 1);
assert_eq!(after.0[0].as_ref(), "Base");
}
#[test]
fn interface_ancestors_via_extends() {
let mut db = MirDb::default();
upsert_class(&mut db, "Countable", None, Arc::from([]), true);
let child_iface = upsert_class(
&mut db,
"Collection",
None,
Arc::from([Arc::from("Countable")]),
true,
);
let ancestors = class_ancestors(&db, child_iface);
assert_eq!(ancestors.0.len(), 1);
assert_eq!(ancestors.0[0].as_ref(), "Countable");
}
#[test]
fn type_exists_via_db_tracks_active_state() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
assert!(type_exists_via_db(&db, "Foo"));
assert!(!type_exists_via_db(&db, "Bar"));
db.deactivate_class_node("Foo");
assert!(!type_exists_via_db(&db, "Foo"));
}
#[test]
fn clone_preserves_class_node_lookups() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
let cloned = db.clone();
assert!(
type_exists_via_db(&cloned, "Foo"),
"clone must observe nodes registered before clone()"
);
assert!(
!type_exists_via_db(&cloned, "Bar"),
"clone must not observe nodes that were never registered"
);
let foo_node = cloned.lookup_class_node("Foo").expect("registered");
let ancestors = class_ancestors(&cloned, foo_node);
assert!(ancestors.0.is_empty(), "Foo has no ancestors");
}
fn upsert_class_with_traits(
db: &mut MirDb,
fqcn: &str,
parent: Option<Arc<str>>,
traits: &[&str],
is_interface: bool,
is_trait: bool,
) -> ClassNode {
db.upsert_class_node(ClassNodeFields {
is_interface,
is_trait,
parent,
traits: Arc::from(
traits
.iter()
.map(|t| Arc::<str>::from(*t))
.collect::<Vec<_>>(),
),
..ClassNodeFields::for_class(Arc::from(fqcn))
})
}
fn upsert_method(db: &mut MirDb, fqcn: &str, name: &str, is_abstract: bool) -> MethodNode {
let storage = MethodStorage {
name: Arc::from(name),
fqcn: Arc::from(fqcn),
params: vec![],
return_type: None,
inferred_return_type: None,
visibility: Visibility::Public,
is_static: false,
is_abstract,
is_final: false,
is_constructor: name == "__construct",
template_params: vec![],
assertions: vec![],
throws: vec![],
deprecated: None,
is_internal: false,
is_pure: false,
location: None,
};
db.upsert_method_node(&storage)
}
fn upsert_enum(db: &mut MirDb, fqcn: &str, interfaces: &[&str], is_backed: bool) -> ClassNode {
db.upsert_class_node(ClassNodeFields {
interfaces: Arc::from(
interfaces
.iter()
.map(|i| Arc::<str>::from(*i))
.collect::<Vec<_>>(),
),
is_backed_enum: is_backed,
..ClassNodeFields::for_enum(Arc::from(fqcn))
})
}
#[test]
fn method_exists_via_db_finds_own_method() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_method(&mut db, "Foo", "bar", false);
assert!(method_exists_via_db(&db, "Foo", "bar"));
assert!(!method_exists_via_db(&db, "Foo", "missing"));
}
#[test]
fn method_exists_via_db_walks_parent() {
let mut db = MirDb::default();
upsert_class(&mut db, "Base", None, Arc::from([]), false);
upsert_method(&mut db, "Base", "inherited", false);
upsert_class(
&mut db,
"Child",
Some(Arc::from("Base")),
Arc::from([]),
false,
);
assert!(method_exists_via_db(&db, "Child", "inherited"));
}
#[test]
fn method_exists_via_db_walks_traits_transitively() {
let mut db = MirDb::default();
upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
upsert_method(&mut db, "InnerTrait", "deep_trait_method", false);
upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
assert!(method_exists_via_db(&db, "Foo", "deep_trait_method"));
}
#[test]
fn method_exists_via_db_is_case_insensitive() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_method(&mut db, "Foo", "doStuff", false);
assert!(method_exists_via_db(&db, "Foo", "DoStuff"));
assert!(method_exists_via_db(&db, "Foo", "DOSTUFF"));
}
#[test]
fn method_exists_via_db_unknown_class_returns_false() {
let db = MirDb::default();
assert!(!method_exists_via_db(&db, "Nope", "anything"));
}
#[test]
fn method_exists_via_db_inactive_class_returns_false() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_method(&mut db, "Foo", "bar", false);
db.deactivate_class_node("Foo");
assert!(!method_exists_via_db(&db, "Foo", "bar"));
}
#[test]
fn method_exists_via_db_finds_abstract_methods() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_method(&mut db, "Foo", "abstr", true);
assert!(method_exists_via_db(&db, "Foo", "abstr"));
}
#[test]
fn method_is_concretely_implemented_skips_abstract() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_method(&mut db, "Foo", "abstr", true);
assert!(!method_is_concretely_implemented(&db, "Foo", "abstr"));
}
#[test]
fn method_is_concretely_implemented_finds_concrete_in_trait() {
let mut db = MirDb::default();
upsert_class_with_traits(&mut db, "MyTrait", None, &[], false, true);
upsert_method(&mut db, "MyTrait", "provided", false);
upsert_class_with_traits(&mut db, "Foo", None, &["MyTrait"], false, false);
assert!(method_is_concretely_implemented(&db, "Foo", "provided"));
}
#[test]
fn method_is_concretely_implemented_skips_interface_definitions() {
let mut db = MirDb::default();
upsert_class(&mut db, "I", None, Arc::from([]), true);
upsert_method(&mut db, "I", "m", false);
upsert_class(&mut db, "C", None, Arc::from([Arc::from("I")]), false);
assert!(!method_is_concretely_implemented(&db, "C", "m"));
}
#[test]
fn extends_or_implements_via_db_self_match() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
assert!(extends_or_implements_via_db(&db, "Foo", "Foo"));
}
#[test]
fn extends_or_implements_via_db_transitive() {
let mut db = MirDb::default();
upsert_class(&mut db, "Animal", None, Arc::from([]), false);
upsert_class(
&mut db,
"Mammal",
Some(Arc::from("Animal")),
Arc::from([]),
false,
);
upsert_class(
&mut db,
"Dog",
Some(Arc::from("Mammal")),
Arc::from([]),
false,
);
assert!(extends_or_implements_via_db(&db, "Dog", "Animal"));
assert!(extends_or_implements_via_db(&db, "Dog", "Mammal"));
assert!(!extends_or_implements_via_db(&db, "Animal", "Dog"));
}
#[test]
fn extends_or_implements_via_db_unknown_returns_false() {
let db = MirDb::default();
assert!(!extends_or_implements_via_db(&db, "Nope", "Foo"));
}
#[test]
fn extends_or_implements_via_db_unit_enum_implicit() {
let mut db = MirDb::default();
upsert_enum(&mut db, "Status", &[], false);
assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
assert!(extends_or_implements_via_db(&db, "Status", "\\UnitEnum"));
assert!(!extends_or_implements_via_db(&db, "Status", "BackedEnum"));
}
#[test]
fn extends_or_implements_via_db_backed_enum_implicit() {
let mut db = MirDb::default();
upsert_enum(&mut db, "Status", &[], true);
assert!(extends_or_implements_via_db(&db, "Status", "UnitEnum"));
assert!(extends_or_implements_via_db(&db, "Status", "BackedEnum"));
assert!(extends_or_implements_via_db(&db, "Status", "\\BackedEnum"));
}
#[test]
fn extends_or_implements_via_db_enum_declared_interface() {
let mut db = MirDb::default();
upsert_class(&mut db, "Stringable", None, Arc::from([]), true);
upsert_enum(&mut db, "Status", &["Stringable"], false);
assert!(extends_or_implements_via_db(&db, "Status", "Stringable"));
}
#[test]
fn has_unknown_ancestor_via_db_clean_chain_returns_false() {
let mut db = MirDb::default();
upsert_class(&mut db, "Base", None, Arc::from([]), false);
upsert_class(
&mut db,
"Child",
Some(Arc::from("Base")),
Arc::from([]),
false,
);
assert!(!has_unknown_ancestor_via_db(&db, "Child"));
}
#[test]
fn has_unknown_ancestor_via_db_missing_parent_returns_true() {
let mut db = MirDb::default();
upsert_class(
&mut db,
"Child",
Some(Arc::from("Missing")),
Arc::from([]),
false,
);
assert!(has_unknown_ancestor_via_db(&db, "Child"));
}
#[test]
fn class_template_params_via_db_returns_registered_params() {
use mir_types::Variance;
let mut db = MirDb::default();
let tp = TemplateParam {
name: Arc::from("T"),
bound: None,
defining_entity: Arc::from("Box"),
variance: Variance::Invariant,
};
db.upsert_class_node(ClassNodeFields {
template_params: Arc::from([tp.clone()]),
..ClassNodeFields::for_class(Arc::from("Box"))
});
let got = class_template_params_via_db(&db, "Box").expect("registered");
assert_eq!(got.len(), 1);
assert_eq!(got[0].name.as_ref(), "T");
assert!(class_template_params_via_db(&db, "Missing").is_none());
db.deactivate_class_node("Box");
assert!(class_template_params_via_db(&db, "Box").is_none());
}
fn upsert_class_with_mixins(
db: &mut MirDb,
fqcn: &str,
parent: Option<Arc<str>>,
mixins: &[&str],
) -> ClassNode {
db.upsert_class_node(ClassNodeFields {
parent,
mixins: Arc::from(
mixins
.iter()
.map(|m| Arc::<str>::from(*m))
.collect::<Vec<_>>(),
),
..ClassNodeFields::for_class(Arc::from(fqcn))
})
}
#[test]
fn lookup_method_in_chain_finds_own_then_ancestor() {
let mut db = MirDb::default();
upsert_class(&mut db, "Base", None, Arc::from([]), false);
upsert_method(&mut db, "Base", "shared", false);
upsert_class(
&mut db,
"Child",
Some(Arc::from("Base")),
Arc::from([]),
false,
);
upsert_method(&mut db, "Child", "shared", false);
let found = lookup_method_in_chain(&db, "Child", "shared").expect("own");
assert_eq!(found.fqcn(&db).as_ref(), "Child");
upsert_method(&mut db, "Base", "only_in_base", false);
let found = lookup_method_in_chain(&db, "Child", "only_in_base").expect("ancestor");
assert_eq!(found.fqcn(&db).as_ref(), "Base");
}
#[test]
fn lookup_method_in_chain_walks_trait_of_traits() {
let mut db = MirDb::default();
upsert_class_with_traits(&mut db, "InnerTrait", None, &[], false, true);
upsert_method(&mut db, "InnerTrait", "deep", false);
upsert_class_with_traits(&mut db, "OuterTrait", None, &["InnerTrait"], false, true);
upsert_class_with_traits(&mut db, "Foo", None, &["OuterTrait"], false, false);
let found = lookup_method_in_chain(&db, "Foo", "deep").expect("transitive trait");
assert_eq!(found.fqcn(&db).as_ref(), "InnerTrait");
}
#[test]
fn lookup_method_in_chain_walks_mixins() {
let mut db = MirDb::default();
upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
upsert_method(&mut db, "MixinTarget", "magic", false);
upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
let found = lookup_method_in_chain(&db, "Host", "magic").expect("via @mixin");
assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
}
#[test]
fn lookup_method_in_chain_mixin_cycle_does_not_hang() {
let mut db = MirDb::default();
upsert_class_with_mixins(&mut db, "A", None, &["B"]);
upsert_class_with_mixins(&mut db, "B", None, &["A"]);
assert!(lookup_method_in_chain(&db, "A", "missing").is_none());
}
#[test]
fn lookup_method_in_chain_is_case_insensitive() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_method(&mut db, "Foo", "doStuff", false);
assert!(lookup_method_in_chain(&db, "Foo", "DOSTUFF").is_some());
assert!(lookup_method_in_chain(&db, "Foo", "dostuff").is_some());
}
#[test]
fn lookup_method_in_chain_unknown_returns_none() {
let db = MirDb::default();
assert!(lookup_method_in_chain(&db, "Nope", "anything").is_none());
}
fn upsert_property(db: &mut MirDb, fqcn: &str, name: &str, is_readonly: bool) -> PropertyNode {
let storage = PropertyStorage {
name: Arc::from(name),
ty: None,
inferred_ty: None,
visibility: Visibility::Public,
is_static: false,
is_readonly,
default: None,
location: None,
};
let owner = Arc::<str>::from(fqcn);
db.upsert_property_node(&owner, &storage);
db.lookup_property_node(fqcn, name).expect("registered")
}
#[test]
fn lookup_property_in_chain_own_then_ancestor() {
let mut db = MirDb::default();
upsert_class(&mut db, "Base", None, Arc::from([]), false);
upsert_property(&mut db, "Base", "x", false);
upsert_class(
&mut db,
"Child",
Some(Arc::from("Base")),
Arc::from([]),
false,
);
let found = lookup_property_in_chain(&db, "Child", "x").expect("ancestor");
assert_eq!(found.fqcn(&db).as_ref(), "Base");
upsert_property(&mut db, "Child", "x", true);
let found = lookup_property_in_chain(&db, "Child", "x").expect("own");
assert_eq!(found.fqcn(&db).as_ref(), "Child");
assert!(found.is_readonly(&db));
}
#[test]
fn lookup_property_in_chain_walks_mixins() {
let mut db = MirDb::default();
upsert_class(&mut db, "MixinTarget", None, Arc::from([]), false);
upsert_property(&mut db, "MixinTarget", "exposed", false);
upsert_class_with_mixins(&mut db, "Host", None, &["MixinTarget"]);
let found = lookup_property_in_chain(&db, "Host", "exposed").expect("via @mixin");
assert_eq!(found.fqcn(&db).as_ref(), "MixinTarget");
}
#[test]
fn lookup_property_in_chain_mixin_cycle_does_not_hang() {
let mut db = MirDb::default();
upsert_class_with_mixins(&mut db, "A", None, &["B"]);
upsert_class_with_mixins(&mut db, "B", None, &["A"]);
assert!(lookup_property_in_chain(&db, "A", "missing").is_none());
}
#[test]
fn lookup_property_in_chain_is_case_sensitive() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_property(&mut db, "Foo", "myProp", false);
assert!(lookup_property_in_chain(&db, "Foo", "myProp").is_some());
assert!(lookup_property_in_chain(&db, "Foo", "MyProp").is_none());
}
#[test]
fn lookup_property_in_chain_inactive_returns_none() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_property(&mut db, "Foo", "x", false);
db.deactivate_class_node("Foo");
assert!(lookup_property_in_chain(&db, "Foo", "x").is_none());
}
fn upsert_constant(db: &mut MirDb, fqcn: &str, name: &str) {
let storage = ConstantStorage {
name: Arc::from(name),
ty: mir_types::Union::mixed(),
visibility: None,
is_final: false,
location: None,
};
let owner = Arc::<str>::from(fqcn);
db.upsert_class_constant_node(&owner, &storage);
}
#[test]
fn class_constant_exists_in_chain_finds_own() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_constant(&mut db, "Foo", "MAX");
assert!(class_constant_exists_in_chain(&db, "Foo", "MAX"));
assert!(!class_constant_exists_in_chain(&db, "Foo", "MIN"));
}
#[test]
fn class_constant_exists_in_chain_walks_parent() {
let mut db = MirDb::default();
upsert_class(&mut db, "Base", None, Arc::from([]), false);
upsert_constant(&mut db, "Base", "VERSION");
upsert_class(
&mut db,
"Child",
Some(Arc::from("Base")),
Arc::from([]),
false,
);
assert!(class_constant_exists_in_chain(&db, "Child", "VERSION"));
}
#[test]
fn class_constant_exists_in_chain_walks_interface() {
let mut db = MirDb::default();
upsert_class(&mut db, "I", None, Arc::from([]), true);
upsert_constant(&mut db, "I", "TYPE");
db.upsert_class_node(ClassNodeFields {
interfaces: Arc::from([Arc::from("I")]),
..ClassNodeFields::for_class(Arc::from("Impl"))
});
assert!(class_constant_exists_in_chain(&db, "Impl", "TYPE"));
}
#[test]
fn class_constant_exists_in_chain_walks_direct_trait() {
let mut db = MirDb::default();
upsert_class_with_traits(&mut db, "T", None, &[], false, true);
upsert_constant(&mut db, "T", "FROM_TRAIT");
upsert_class_with_traits(&mut db, "Foo", None, &["T"], false, false);
assert!(class_constant_exists_in_chain(&db, "Foo", "FROM_TRAIT"));
}
#[test]
fn class_constant_exists_in_chain_unknown_class_returns_false() {
let db = MirDb::default();
assert!(!class_constant_exists_in_chain(&db, "Nope", "ANY"));
}
#[test]
fn class_constant_exists_in_chain_inactive_returns_false() {
let mut db = MirDb::default();
upsert_class(&mut db, "Foo", None, Arc::from([]), false);
upsert_constant(&mut db, "Foo", "X");
db.deactivate_class_node("Foo");
db.deactivate_class_constants("Foo");
assert!(!class_constant_exists_in_chain(&db, "Foo", "X"));
}
#[test]
fn parallel_reads_then_serial_write_does_not_deadlock() {
use rayon::prelude::*;
use std::sync::mpsc;
use std::time::Duration;
let (tx, rx) = mpsc::channel::<()>();
std::thread::spawn(move || {
let mut db = MirDb::default();
let storage = mir_codebase::storage::FunctionStorage {
fqn: Arc::from("foo"),
short_name: Arc::from("foo"),
params: vec![],
return_type: None,
inferred_return_type: None,
template_params: vec![],
assertions: vec![],
throws: vec![],
deprecated: None,
is_pure: false,
location: None,
};
let node = db.upsert_function_node(&storage);
let db_for_sweep = db.clone();
(0..256u32)
.into_par_iter()
.for_each_with(db_for_sweep, |db, _| {
let _ = node.return_type(&*db as &dyn MirDatabase);
});
node.set_return_type(&mut db).to(Some(Union::mixed()));
assert_eq!(node.return_type(&db), Some(Union::mixed()));
tx.send(()).unwrap();
});
match rx.recv_timeout(Duration::from_secs(30)) {
Ok(()) => {}
Err(_) => {
panic!("S3 deadlock repro: setter after for_each_with did not return within 30s")
}
}
}
#[test]
fn sibling_clone_blocks_setter_until_dropped() {
use std::sync::mpsc;
use std::time::Duration;
let mut db = MirDb::default();
let storage = mir_codebase::storage::FunctionStorage {
fqn: Arc::from("foo"),
short_name: Arc::from("foo"),
params: vec![],
return_type: None,
inferred_return_type: None,
template_params: vec![],
assertions: vec![],
throws: vec![],
deprecated: None,
is_pure: false,
location: None,
};
let node = db.upsert_function_node(&storage);
let sibling = db.clone();
let (tx, rx) = mpsc::channel::<()>();
let writer = std::thread::spawn(move || {
node.set_return_type(&mut db).to(Some(Union::mixed()));
tx.send(()).unwrap();
});
match rx.recv_timeout(Duration::from_millis(500)) {
Err(mpsc::RecvTimeoutError::Timeout) => { }
Ok(()) => panic!(
"setter completed while sibling clone was alive — strong-count==1 \
invariant of `cancel_others` is broken; commit_inferred_return_types \
cannot rely on tight-scoping clones"
),
Err(e) => panic!("unexpected channel error: {e:?}"),
}
drop(sibling);
match rx.recv_timeout(Duration::from_secs(5)) {
Ok(()) => {}
Err(_) => panic!("setter did not complete within 5s after sibling clone dropped"),
}
writer.join().expect("writer thread panicked");
}
}