use std::collections::BTreeMap;
use std::path::{Path, PathBuf};
use lsp_types::{Diagnostic, DiagnosticSeverity, NumberOrString, Range};
use crate::environment::{Environment, EnvironmentError};
use crate::linter::Severity;
use crate::project_files::{self, ProjectFinding};
use crate::text::{LineIndex, PositionEncoding};
pub(crate) type EnvironmentFindings = BTreeMap<PathBuf, Vec<Diagnostic>>;
pub(crate) fn environment_diagnostics(
env: &Environment,
encoding: PositionEncoding,
mut source: impl FnMut(&Path) -> Option<String>,
) -> EnvironmentFindings {
let mut out = EnvironmentFindings::new();
if let Some(text) = source(&env.project_file) {
let findings = project_files::semantic_findings(env, &text);
insert(&mut out, &env.project_file, &text, findings, encoding);
}
out
}
pub(crate) fn buffer_diagnostics(
path: &Path,
text: &str,
encoding: PositionEncoding,
) -> Vec<Diagnostic> {
let findings = project_files::syntax_findings(path, text);
if findings.is_empty() {
return Vec::new();
}
let line_index = LineIndex::new(text);
findings
.iter()
.map(|finding| to_lsp(finding, &line_index, encoding))
.collect()
}
pub(crate) fn resolve_failure_diagnostics(
error: &EnvironmentError,
encoding: PositionEncoding,
source: impl FnOnce(&Path) -> Option<String>,
) -> (PathBuf, Vec<Diagnostic>) {
let path = match error {
EnvironmentError::Read { path, .. } | EnvironmentError::Parse { path, .. } => path.clone(),
};
let text = source(&path).unwrap_or_default();
let finding = project_files::syntax_finding(&path, &text, error);
let line_index = LineIndex::new(&text);
(path, vec![to_lsp(&finding, &line_index, encoding)])
}
fn insert(
out: &mut EnvironmentFindings,
path: &Path,
text: &str,
findings: Vec<ProjectFinding>,
encoding: PositionEncoding,
) {
if findings.is_empty() {
return;
}
let line_index = LineIndex::new(text);
out.entry(path.to_path_buf()).or_default().extend(
findings
.iter()
.map(|finding| to_lsp(finding, &line_index, encoding)),
);
}
fn to_lsp(
finding: &ProjectFinding,
line_index: &LineIndex,
encoding: PositionEncoding,
) -> Diagnostic {
Diagnostic {
range: Range::new(
line_index.byte_to_position(finding.range.start().into(), encoding),
line_index.byte_to_position(finding.range.end().into(), encoding),
),
severity: Some(severity_to_lsp(finding.severity)),
code: Some(NumberOrString::String(finding.check.to_string())),
source: Some("fatou".to_string()),
message: finding.message.clone(),
..Default::default()
}
}
fn severity_to_lsp(severity: Severity) -> DiagnosticSeverity {
match severity {
Severity::Error => DiagnosticSeverity::ERROR,
Severity::Warning => DiagnosticSeverity::WARNING,
Severity::Info => DiagnosticSeverity::INFORMATION,
Severity::Hint => DiagnosticSeverity::HINT,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_parse_failure_maps_its_span_to_a_range() {
let text = "name = \"Demo\"\nuuid = \n";
let error = EnvironmentError::Parse {
path: PathBuf::from("/work/Project.toml"),
message: "invalid string".to_string(),
span: Some(21..22),
};
let (path, diags) = resolve_failure_diagnostics(&error, PositionEncoding::Utf16, |_| {
Some(text.to_string())
});
assert_eq!(path, PathBuf::from("/work/Project.toml"));
let [diag] = &diags[..] else {
panic!("expected one diagnostic, got {diags:?}");
};
assert_eq!(diag.range.start.line, 1, "the second line");
assert_eq!(diag.severity, Some(DiagnosticSeverity::ERROR));
assert_eq!(diag.source.as_deref(), Some("fatou"));
assert_eq!(
diag.code,
Some(NumberOrString::String("toml-syntax".to_string()))
);
assert!(
diag.code_description.is_none(),
"a check ID names no reference section"
);
}
#[test]
fn the_buffer_route_reports_syntax_alone() {
let path = PathBuf::from("/work/Project.toml");
let diags =
buffer_diagnostics(&path, "name = \"Demo\"\nuuid = \n", PositionEncoding::Utf16);
let [diag] = &diags[..] else {
panic!("expected one diagnostic, got {diags:?}");
};
assert_eq!(diag.range.start.line, 1);
assert_eq!(
diag.code,
Some(NumberOrString::String("toml-syntax".to_string()))
);
assert!(
buffer_diagnostics(
&path,
"name = \"Demo\"\nuuid = \"00000000-0000-0000-0000-000000000001\"\n",
PositionEncoding::Utf16,
)
.is_empty()
);
}
#[test]
fn an_unreadable_file_still_reports() {
let error = EnvironmentError::Read {
path: PathBuf::from("/work/Project.toml"),
message: "permission denied".to_string(),
};
let (_, diags) = resolve_failure_diagnostics(&error, PositionEncoding::Utf16, |_| None);
assert_eq!(diags.len(), 1);
assert_eq!(diags[0].message, "permission denied");
}
}