use std::sync::Arc;
use mir_issues::Issue;
use crate::body_analysis::BodyAnalyzer;
use crate::PhpVersion;
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]>,
}
unsafe impl salsa::Update for ScopeInferenceResult {
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
}
}
#[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]
pub fn infer_scope(
db: &dyn MirDatabase,
file: SourceFile,
scope: ScopeKey,
) -> Arc<ScopeInferenceResult> {
use std::str::FromStr as _;
let path = file.path(db);
let text = file.text(db);
let parsed_file = super::queries::parse_file(db, file);
let parsed = &*parsed_file.0;
let empty = || {
Arc::new(ScopeInferenceResult {
issues: Arc::from([]),
ref_locs: Arc::from([]),
})
};
if parsed.errors.iter().any(crate::parser::is_hard_parse_error) {
return empty();
}
let php_version_str = db.analyze_config().php_version(db);
let php_version = PhpVersion::from_str(php_version_str.as_ref()).unwrap_or(PhpVersion::LATEST);
let driver = BodyAnalyzer::new(db, php_version);
let mut issues: Vec<Issue> = Vec::new();
let mut symbols: Vec<crate::symbol::ResolvedSymbol> = 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,
);
}
ScopeKey::FileExec => {
driver.analyze_global_exec(
&parsed.program,
&path,
text.as_ref(),
&parsed.source_map,
&mut issues,
&mut symbols,
);
}
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,
&mut symbols,
),
StmtKind::Class(decl) => driver.analyze_class_decl(
decl,
&path,
text.as_ref(),
&parsed.source_map,
&mut issues,
&mut symbols,
),
StmtKind::Enum(decl) => driver.analyze_enum_decl(
decl,
&path,
text.as_ref(),
&parsed.source_map,
&mut issues,
&mut symbols,
),
StmtKind::Interface(decl) => driver.analyze_interface_decl(
decl,
&path,
text.as_ref(),
&parsed.source_map,
&mut issues,
),
StmtKind::Trait(decl) => driver.analyze_trait_decl(
decl,
&path,
text.as_ref(),
&parsed.source_map,
&mut issues,
&mut symbols,
),
_ => {}
}
});
}
}
let ref_locs = db.pop_ref_loc_frame();
Arc::new(ScopeInferenceResult {
issues: issues.into(),
ref_locs: ref_locs.into(),
})
}
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>,
) {
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,
);
}
StmtKind::Namespace(ns) => {
if let php_ast::owned::NamespaceBody::Braced(inner) = &ns.body {
check_use_decls(&inner.stmts, db, file, source, source_map, issues);
}
}
_ => {}
}
}
}
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: Arc<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)
}