use std::sync::Arc;
use mir_issues::Issue;
use crate::body_analysis::BodyAnalyzer;
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>,
}
#[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);
}
}
_ => {}
}
}
}
#[salsa::tracked(lru = 4096)]
pub fn infer_scope(
db: &dyn MirDatabase,
file: SourceFile,
scope: ScopeKey,
) -> Arc<ScopeInferenceResult> {
let prepared = super::queries::prepare_analysis_file(db, file);
let _guard = super::inferred_types::try_mark_infer_in_progress(prepared.path.clone());
let path = &prepared.path;
let text = &prepared.text;
let parsed = prepared.parse_result();
let empty = || {
Arc::new(ScopeInferenceResult {
issues: Arc::from([]),
ref_locs: Arc::from([]),
inferred_types: Arc::new(crate::body_analysis::InferredTypes {
functions: Vec::new(),
methods: Vec::new(),
properties: Vec::new(),
}),
})
};
if prepared.has_hard_parse_errors {
return 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();
match &scope {
ScopeKey::FileHeader => {
crate::body_analysis::check_duplicate_declarations(
&parsed.program.stmts,
path,
text.as_ref(),
&parsed.source_map,
&mut issues,
);
check_use_decls(
&parsed.program.stmts,
db,
path,
text.as_ref(),
&parsed.source_map,
&mut issues,
true,
);
}
ScopeKey::FileExec => {
driver.analyze_global_exec(
&parsed.program,
path,
text.as_ref(),
&parsed.source_map,
&mut 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(&parsed.program.stmts, &mut |stmt| {
use php_ast::owned::StmtKind;
if found {
return;
}
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,
&mut issues,
None,
),
StmtKind::Class(decl) => driver.analyze_class_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
&mut issues,
None,
&Default::default(),
),
StmtKind::Enum(decl) => driver.analyze_enum_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
&mut issues,
None,
),
StmtKind::Interface(decl) => driver.analyze_interface_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
&mut issues,
&Default::default(),
None,
),
StmtKind::Trait(decl) => driver.analyze_trait_decl(
decl,
path,
text.as_ref(),
&parsed.source_map,
&mut issues,
None,
),
_ => {}
}
});
}
}
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);
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,
})
}
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() {
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)
}