use std::panic::AssertUnwindSafe;
use std::path::Path;
use lsp_types::Diagnostic;
use crate::incremental::{Analysis, normalize_path};
use crate::parser::parse;
use crate::text::{PositionEncoding, TextBuffer};
use super::format::parse_diagnostics_to_lsp;
use super::graph_diagnostics::{
graph_diagnostics, merge_script_diagnostics, script_graph_diagnostics,
};
use super::lint::{ServerRules, lint_diagnostics_via_db};
pub(crate) fn document_diagnostics_via_db(
snapshot: &Analysis,
path: &Path,
text: &TextBuffer,
encoding: PositionEncoding,
rules: &ServerRules,
) -> Vec<Diagnostic> {
let cached = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let file = snapshot.lookup_file(path)?;
if snapshot.file_text(file) != text {
return None;
}
Some(parse_diagnostics_to_lsp(
snapshot.parse_diagnostics(file),
text,
encoding,
))
}));
let mut diags = match cached {
Ok(Some(diags)) => diags,
Ok(None) | Err(_) => parse_diagnostics_to_lsp(&parse(text).diagnostics, text, encoding),
};
if diags.is_empty() {
diags.extend(lint_diagnostics_via_db(
snapshot, path, text, encoding, rules,
));
}
diags.extend(graph_diagnostics_for(snapshot, path, text, encoding, rules));
diags
}
fn graph_diagnostics_for(
snapshot: &Analysis,
path: &Path,
text: &TextBuffer,
encoding: PositionEncoding,
rules: &ServerRules,
) -> Vec<Diagnostic> {
let computed = salsa::Cancelled::catch(AssertUnwindSafe(|| {
let graph = snapshot.project_graph();
let package = graph_diagnostics(graph, encoding, |member| {
if member != path {
return None;
}
let file = snapshot.lookup_file(member)?;
Some((
snapshot.file_text_of(file).to_string(),
snapshot.parsed_tree(file),
))
});
let scripts = snapshot.scripts(&rules.entry_points);
let script = script_graph_diagnostics(&scripts, encoding, |member| {
if member != path {
return None;
}
let file = snapshot.lookup_file(member)?;
(snapshot.file_text(file) == text.text()).then(|| {
(
std::sync::Arc::new(text.clone()),
snapshot.parsed_tree(file),
)
})
});
let sources = scripts
.programs
.iter()
.flat_map(|p| p.members.keys().cloned())
.collect();
merge_script_diagnostics(package, &script, &sources)
.remove(&normalize_path(path))
.unwrap_or_default()
}));
computed.unwrap_or_default()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::incremental::IncrementalDatabase;
use lsp_types::{DiagnosticSeverity, NumberOrString};
use std::path::PathBuf;
#[test]
fn script_include_failures_are_reported_until_repaired() {
use std::collections::BTreeMap;
use std::sync::Arc;
let dir = tempfile::tempdir().unwrap();
let entry = dir.path().join("main.jl");
let helper = dir.path().join("helper.jl");
let mut db = IncrementalDatabase::default();
let (mut rules, _) = ServerRules::from_config(&Default::default());
rules.entry_points = vec![entry.clone()];
for target in ["", "missing.jl", "helper.jl"] {
let text = TextBuffer::new(format!("include(\"{target}\")\nf() = typo\n"));
let sources = BTreeMap::from([
(entry.clone(), Ok(text.text_arc())),
(dir.path().join(target), Err("unreadable".into())),
]);
db.set_script_sources(&rules.entry_points, &sources, &Default::default());
let diagnostics = document_diagnostics_via_db(
&db.snapshot(),
&entry,
&text,
PositionEncoding::Utf16,
&rules,
);
assert_eq!(diagnostics.len(), 1, "{target:?}: {diagnostics:?}");
assert_eq!(diagnostics[0].severity, Some(DiagnosticSeverity::ERROR));
assert_eq!(diagnostics[0].range.start.line, 0);
assert!(diagnostics[0].message.contains("include"));
}
let text = TextBuffer::new("include(\"helper.jl\")\nf() = provided + typo\n");
let sources = BTreeMap::from([
(entry.clone(), Ok(text.text_arc())),
(helper, Ok(Arc::from("provided = 1\n"))),
]);
db.set_script_sources(&rules.entry_points, &sources, &Default::default());
let diagnostics = document_diagnostics_via_db(
&db.snapshot(),
&entry,
&text,
PositionEncoding::Utf16,
&rules,
);
assert_eq!(diagnostics.len(), 1, "{diagnostics:?}");
assert!(diagnostics[0].message.contains("`typo`"));
}
fn report_for(text: &str) -> Vec<Diagnostic> {
let path = PathBuf::from("/work/a.jl");
let mut db = IncrementalDatabase::default();
db.upsert_file(&path, text.to_string());
document_diagnostics_via_db(
&db.snapshot(),
&path,
&TextBuffer::new(text.to_string()),
PositionEncoding::Utf16,
&ServerRules::defaults(),
)
}
#[test]
fn a_clean_document_reports_nothing() {
assert_eq!(report_for("x = 1\n"), Vec::new());
}
#[test]
fn lint_findings_join_the_report() {
let diags = report_for("function f(x)\n tmp = x + 1\n return x\nend\n");
assert_eq!(diags.len(), 1);
assert_eq!(
diags[0].code,
Some(NumberOrString::String("unused-binding".to_string()))
);
}
#[test]
fn parse_errors_suppress_lint_findings() {
let diags = report_for("function f(x)\n tmp = x + 1\n return x\n");
assert!(!diags.is_empty());
assert!(
diags
.iter()
.all(|d| d.severity == Some(DiagnosticSeverity::ERROR) && d.code.is_none()),
"a parse-broken buffer must report parse errors only, got {diags:?}"
);
}
#[test]
fn falls_back_when_the_db_lags() {
let text = "function f(x)\n tmp = x + 1\n return x\nend\n";
let db = IncrementalDatabase::default();
let diags = document_diagnostics_via_db(
&db.snapshot(),
Path::new("/work/never-seen.jl"),
&TextBuffer::new(text.to_string()),
PositionEncoding::Utf16,
&ServerRules::defaults(),
);
assert_eq!(diags.len(), 1);
assert_eq!(
diags[0].code,
Some(NumberOrString::String("unused-binding".to_string()))
);
}
}