use std::sync::Arc;
use mir_issues::Issue;
use rustc_hash::FxHashSet;
use crate::body_analysis::{BodyAnalyzer, InferredTypes};
use super::*;
#[derive(Clone, PartialEq, Eq, Hash, Debug)]
pub enum ScopeKey {
FileHeader,
Function(Arc<str>, u32),
ClassLike(Arc<str>, u32),
FileExec,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ScopeInferenceResult {
pub issues: Arc<[Issue]>,
pub ref_locs: Arc<[RefLoc]>,
pub(crate) inferred_types: Arc<crate::body_analysis::InferredTypes>,
}
impl ScopeInferenceResult {
fn empty() -> Arc<Self> {
Arc::new(Self {
issues: Arc::from([]),
ref_locs: Arc::from([]),
inferred_types: Arc::new(crate::body_analysis::InferredTypes {
functions: Vec::new(),
methods: Vec::new(),
properties: Vec::new(),
}),
})
}
}
#[salsa::tracked]
pub fn file_scopes(db: &dyn MirDatabase, file: SourceFile) -> Arc<[ScopeKey]> {
let path = file.path(db);
let parsed_file = super::queries::parse_file(db, file);
let parsed = &*parsed_file.0;
let mut keys: Vec<ScopeKey> = Vec::new();
let mut occurrences: rustc_hash::FxHashMap<(bool, Arc<str>), u32> =
rustc_hash::FxHashMap::default();
for_each_top_level_decl(&parsed.program.stmts, &mut |stmt| {
use php_ast::owned::StmtKind;
let (is_fn, name) = match &stmt.kind {
StmtKind::Function(decl) => (true, decl.name.as_deref().unwrap_or("")),
StmtKind::Class(decl) => (
false,
decl.name
.as_ref()
.and_then(|i| i.as_deref())
.unwrap_or("<anonymous>"),
),
StmtKind::Enum(decl) => (false, decl.name.as_deref().unwrap_or("<anonymous>")),
StmtKind::Interface(decl) => (false, decl.name.as_deref().unwrap_or("<anonymous>")),
StmtKind::Trait(decl) => (false, decl.name.as_deref().unwrap_or("")),
_ => return,
};
let fqn: Arc<str> = Arc::from(resolve_name(db, path.as_ref(), name));
let occ = occurrences.entry((is_fn, fqn.clone())).or_insert(0);
let key = if is_fn {
ScopeKey::Function(fqn, *occ)
} else {
ScopeKey::ClassLike(fqn, *occ)
};
*occ += 1;
keys.push(key);
});
keys.into()
}
fn for_each_top_level_decl<'a>(
stmts: &'a [php_ast::owned::Stmt],
f: &mut impl FnMut(&'a php_ast::owned::Stmt),
) {
use php_ast::owned::StmtKind;
for stmt in stmts.iter() {
match &stmt.kind {
StmtKind::Function(_)
| StmtKind::Class(_)
| StmtKind::Enum(_)
| StmtKind::Interface(_)
| StmtKind::Trait(_) => f(stmt),
StmtKind::Namespace(ns) => {
if let php_ast::owned::NamespaceBody::Braced(inner) = &ns.body {
for_each_top_level_decl(&inner.stmts, f);
}
}
_ => {}
}
}
}
fn for_each_top_level_decl_guarded<'a>(
db: &dyn MirDatabase,
file: &Arc<str>,
stmts: &'a [php_ast::owned::Stmt],
f: &mut impl FnMut(&'a php_ast::owned::Stmt, &FxHashSet<Arc<str>>),
) {
use php_ast::owned::StmtKind;
let mut guards: FxHashSet<Arc<str>> = FxHashSet::default();
for stmt in stmts.iter() {
match &stmt.kind {
StmtKind::Function(_)
| StmtKind::Class(_)
| StmtKind::Enum(_)
| StmtKind::Interface(_)
| StmtKind::Trait(_) => f(stmt, &guards),
StmtKind::Namespace(ns) => {
if let php_ast::owned::NamespaceBody::Braced(inner) = &ns.body {
for_each_top_level_decl_guarded(db, file, &inner.stmts, f);
}
}
_ => {}
}
let _scope = crate::db::ns_scope::enter(db, file, stmt.span.start);
crate::body_analysis::accumulate_class_exists_guard(stmt, db, file, &mut guards);
}
}
#[salsa::tracked(cycle_fn = infer_scope_cycle, cycle_initial = infer_scope_initial)]
pub fn infer_scope(
db: &dyn MirDatabase,
file: SourceFile,
scope: ScopeKey,
) -> Arc<ScopeInferenceResult> {
crate::recursion::ensure_phase_stack(|| infer_scope_body(db, file, scope))
}
fn infer_scope_body(
db: &dyn MirDatabase,
file: SourceFile,
scope: ScopeKey,
) -> Arc<ScopeInferenceResult> {
let prepared = super::queries::prepare_analysis_file(db, file);
let path = prepared.path;
if prepared.has_hard_parse_errors {
return ScopeInferenceResult::empty();
}
let driver = BodyAnalyzer::new(db, prepared.php_version);
let mut driver = driver;
driver.collect_inferred_types = true;
driver.collect_symbols = false;
let mut issues: Vec<Issue> = Vec::new();
db.push_ref_loc_frame();
analyze_scope(db, &driver, &prepared, &scope, &mut issues);
let ref_locs = db.pop_ref_loc_frame();
let inferred_types = Arc::new(driver.take_inferred_types());
crate::metrics::record_scope_analysis(path.as_ref(), 0);
db.note_work(crate::db::Work::ScopeAnalysis, 1);
crate::metrics::record_infer_scope_retained(
issues.len(),
ref_locs.len(),
inferred_types.functions.len(),
inferred_types.methods.len(),
inferred_types.properties.len(),
);
Arc::new(ScopeInferenceResult {
issues: issues.into(),
ref_locs: ref_locs.into(),
inferred_types,
})
}
#[salsa::tracked(cycle_fn = infer_member_cycle, cycle_initial = infer_member_initial)]
pub(crate) fn infer_member(
db: &dyn MirDatabase,
file: SourceFile,
scope: ScopeKey,
method: Option<Arc<str>>,
) -> Arc<InferredTypes> {
crate::recursion::ensure_phase_stack(|| {
let prepared = super::queries::prepare_analysis_file(db, file);
if prepared.has_hard_parse_errors {
return Arc::new(InferredTypes::default());
}
let mut driver = BodyAnalyzer::new_inference_only(db, prepared.php_version);
driver.collect_symbols = false;
driver.only_method = method;
let mut issues: Vec<Issue> = Vec::new();
db.push_ref_loc_frame();
analyze_scope(db, &driver, &prepared, &scope, &mut issues);
db.pop_ref_loc_frame();
crate::metrics::record_scope_analysis(prepared.path.as_ref(), 0);
db.note_work(crate::db::Work::ScopeAnalysis, 1);
Arc::new(driver.take_inferred_types())
})
}
fn infer_member_initial(
_db: &dyn MirDatabase,
_id: salsa::Id,
_file: SourceFile,
_scope: ScopeKey,
_method: Option<Arc<str>>,
) -> Arc<InferredTypes> {
Arc::new(InferredTypes::default())
}
fn infer_member_cycle(
_db: &dyn MirDatabase,
cycle: &salsa::Cycle,
last: &Arc<InferredTypes>,
value: Arc<InferredTypes>,
_file: SourceFile,
_scope: ScopeKey,
_method: Option<Arc<str>>,
) -> Arc<InferredTypes> {
if !super::inferred_types::should_widen(cycle) {
return value;
}
use super::inferred_types::widen_unstable;
let mut next = (*value).clone();
widen_unstable(&last.functions, &mut next.functions, |e| &mut e.1);
widen_unstable(&last.methods, &mut next.methods, |e| &mut e.2);
widen_unstable(&last.properties, &mut next.properties, |e| &mut e.2);
Arc::new(next)
}
fn analyze_scope(
db: &dyn MirDatabase,
driver: &BodyAnalyzer<'_>,
prepared: &super::queries::PreparedAnalysisFile<'_>,
scope: &ScopeKey,
issues: &mut Vec<Issue>,
) {
let path = prepared.path;
let text = prepared.text;
let parsed = prepared.parse_result();
match scope {
ScopeKey::FileHeader => {
crate::body_analysis::check_duplicate_declarations(
&parsed.program.stmts,
path,
text.as_ref(),
&parsed.source_map,
issues,
);
check_use_decls(
&parsed.program.stmts,
db,
path,
text.as_ref(),
&parsed.source_map,
issues,
true,
);
}
ScopeKey::FileExec => {
driver.analyze_global_exec(
&parsed.program,
path,
text.as_ref(),
&parsed.source_map,
issues,
None,
);
}
ScopeKey::Function(fqn, occ) | ScopeKey::ClassLike(fqn, occ) => {
let want_fn = matches!(scope, ScopeKey::Function(..));
let mut seen: u32 = 0;
let mut found = false;
for_each_top_level_decl_guarded(
db,
path,
&parsed.program.stmts,
&mut |stmt, guards| {
use php_ast::owned::StmtKind;
if found {
return;
}
let _scope = crate::db::ns_scope::enter(db, path, stmt.span.start);
let (is_fn, name) = match &stmt.kind {
StmtKind::Function(decl) => (true, decl.name.as_deref().unwrap_or("")),
StmtKind::Class(decl) => (
false,
decl.name
.as_ref()
.and_then(|i| i.as_deref())
.unwrap_or("<anonymous>"),
),
StmtKind::Enum(decl) => {
(false, decl.name.as_deref().unwrap_or("<anonymous>"))
}
StmtKind::Interface(decl) => {
(false, decl.name.as_deref().unwrap_or("<anonymous>"))
}
StmtKind::Trait(decl) => (false, decl.name.as_deref().unwrap_or("")),
_ => return,
};
if is_fn != want_fn || resolve_name(db, path.as_ref(), name) != fqn.as_ref() {
return;
}
if seen != *occ {
seen += 1;
return;
}
found = true;
match &stmt.kind {
StmtKind::Function(decl) => driver.analyze_fn_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
issues,
None,
),
StmtKind::Class(decl) => driver.analyze_class_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
issues,
None,
guards,
),
StmtKind::Enum(decl) => driver.analyze_enum_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
issues,
None,
),
StmtKind::Interface(decl) => driver.analyze_interface_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
issues,
guards,
None,
),
StmtKind::Trait(decl) => driver.analyze_trait_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
issues,
None,
),
_ => {}
}
},
);
}
}
}
fn infer_scope_initial(
_db: &dyn MirDatabase,
_id: salsa::Id,
_file: SourceFile,
_scope: ScopeKey,
) -> Arc<ScopeInferenceResult> {
ScopeInferenceResult::empty()
}
fn infer_scope_cycle(
_db: &dyn MirDatabase,
cycle: &salsa::Cycle,
last: &Arc<ScopeInferenceResult>,
value: Arc<ScopeInferenceResult>,
_file: SourceFile,
_scope: ScopeKey,
) -> Arc<ScopeInferenceResult> {
if !super::inferred_types::should_widen(cycle) {
return value;
}
use super::inferred_types::widen_unstable;
let (prev, mut next) = (&last.inferred_types, (*value.inferred_types).clone());
widen_unstable(&prev.functions, &mut next.functions, |e| &mut e.1);
widen_unstable(&prev.methods, &mut next.methods, |e| &mut e.2);
widen_unstable(&prev.properties, &mut next.properties, |e| &mut e.2);
Arc::new(ScopeInferenceResult {
issues: value.issues.clone(),
ref_locs: value.ref_locs.clone(),
inferred_types: Arc::new(next),
})
}
fn check_use_decls(
stmts: &[php_ast::owned::Stmt],
db: &dyn MirDatabase,
file: &Arc<str>,
source: &str,
source_map: &php_rs_parser::source_map::SourceMap,
issues: &mut Vec<Issue>,
is_full: bool,
) {
use php_ast::owned::StmtKind;
for stmt in stmts.iter() {
let _scope = crate::db::ns_scope::enter(db, file, stmt.span.start);
match &stmt.kind {
StmtKind::Use(use_decl) => {
crate::body_analysis::check_use_decl_casing(
use_decl, db, file, source, source_map, issues, None, None, None, false,
is_full, true,
);
}
StmtKind::Namespace(ns) => {
if let php_ast::owned::NamespaceBody::Braced(inner) = &ns.body {
check_use_decls(&inner.stmts, db, file, source, source_map, issues, is_full);
}
}
_ => {}
}
}
}
pub fn analyze_file_per_scope(db: &dyn MirDatabase, file: SourceFile) -> (Vec<Issue>, Vec<RefLoc>) {
let mut issues: Vec<Issue> = Vec::new();
let mut ref_locs: Vec<RefLoc> = Vec::new();
let mut merge = |r: &ScopeInferenceResult| {
issues.extend(r.issues.iter().cloned());
ref_locs.extend(r.ref_locs.iter().cloned());
};
merge(infer_scope(db, file, ScopeKey::FileHeader));
for key in file_scopes(db, file).iter() {
merge(infer_scope(db, file, key.clone()));
}
merge(infer_scope(db, file, ScopeKey::FileExec));
(issues, ref_locs)
}