use std::sync::Arc;
use mir_issues::Issue;
use salsa::{Database, Update};
use crate::db::codebase::codebase;
use crate::db::input::{SourceFile, Workspace};
use crate::db::parse::parsed_doc;
#[derive(Clone)]
pub struct IssuesArc(pub Arc<[Issue]>);
impl IssuesArc {
pub fn get(&self) -> &[Issue] {
&self.0
}
}
unsafe impl Update for IssuesArc {
unsafe fn maybe_update(old_pointer: *mut Self, new_value: Self) -> bool {
let old_ref = unsafe { &mut *old_pointer };
if Arc::ptr_eq(&old_ref.0, &new_value.0) {
false
} else {
*old_ref = new_value;
true
}
}
}
#[salsa::tracked(no_eq)]
pub fn semantic_issues(db: &dyn Database, ws: Workspace, file: SourceFile) -> IssuesArc {
let cb = codebase(db, ws);
let doc_arc = parsed_doc(db, file);
let doc = doc_arc.get();
let uri_arc: Arc<str> = file.uri(db);
let source = doc.source();
let source_map = php_rs_parser::source_map::SourceMap::new(source);
let mut issue_buffer = mir_issues::IssueBuffer::new();
let mut symbols = Vec::new();
let php_version = ws.php_version(db);
let mut analyzer = mir_analyzer::stmt::StatementsAnalyzer::new(
cb.get(),
uri_arc,
source,
&source_map,
&mut issue_buffer,
&mut symbols,
php_version,
);
let mut ctx = mir_analyzer::context::Context::new();
analyzer.analyze_stmts(&doc.program().stmts, &mut ctx);
let issues: Vec<Issue> = issue_buffer
.into_issues()
.into_iter()
.filter(|i| !i.suppressed)
.collect();
IssuesArc(Arc::from(issues))
}
#[cfg(test)]
mod tests {
use std::sync::Arc;
use super::*;
use crate::db::analysis::AnalysisHost;
use crate::db::input::{FileId, SourceFile};
use salsa::Setter;
fn new_file(host: &AnalysisHost, id: u32, uri: &str, src: &str) -> SourceFile {
SourceFile::new(
host.db(),
FileId(id),
Arc::<str>::from(uri),
Arc::<str>::from(src),
None,
)
}
#[test]
fn semantic_issues_flags_undefined_function() {
let host = AnalysisHost::new();
let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
let ws = Workspace::new(
host.db(),
Arc::from([file]),
mir_analyzer::PhpVersion::LATEST,
);
let issues = semantic_issues(host.db(), ws, file);
assert!(
issues
.get()
.iter()
.any(|i| matches!(i.kind, mir_issues::IssueKind::UndefinedFunction { .. })),
"expected an UndefinedFunction issue, got {:?}",
issues.get()
);
}
#[test]
fn semantic_issues_memoizes_across_calls() {
let host = AnalysisHost::new();
let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
let ws = Workspace::new(
host.db(),
Arc::from([file]),
mir_analyzer::PhpVersion::LATEST,
);
let a = semantic_issues(host.db(), ws, file);
let b = semantic_issues(host.db(), ws, file);
assert!(
Arc::ptr_eq(&a.0, &b.0),
"second call with unchanged inputs should return the memoized Arc"
);
}
#[test]
fn semantic_issues_reruns_after_edit() {
let mut host = AnalysisHost::new();
let file = new_file(&host, 0, "file:///a.php", "<?php\nfoo_bar_baz();");
let ws = Workspace::new(
host.db(),
Arc::from([file]),
mir_analyzer::PhpVersion::LATEST,
);
let a = semantic_issues(host.db(), ws, file);
let first_ptr = Arc::as_ptr(&a.0);
file.set_text(host.db_mut())
.to(Arc::<str>::from("<?php\necho 1;"));
let b = semantic_issues(host.db(), ws, file);
assert_ne!(
first_ptr,
Arc::as_ptr(&b.0),
"edit should invalidate memoized issues"
);
}
}