mod document_diagnostic;
use std::path::Path;
use cpp_code_analysis::{DiskFiles, Session};
use lsp_types::{
ClientCapabilities, Diagnostic, DiagnosticOptions, DiagnosticServerCapabilities,
DiagnosticSeverity, DiagnosticTag, ServerCapabilities, Uri,
};
use super::RegisterCapabilities;
use crate::util::{path_to_uri, position_in_file};
pub use document_diagnostic::on_pull_document_diagnostic;
use log::debug;
pub fn diagnose_file(session: &Session<DiskFiles>, path: &Path) -> Option<(Uri, Vec<Diagnostic>)> {
let uri = path_to_uri(path)?;
let held = session.files().held(path)?;
let found = session.diagnostics(path)?;
debug!(
"diagnose {}: {} byte(s) held, reading {:?}, {} error(s), {} unplaced, {} note(s), {} check(s)",
path.display(),
held.text.len(),
found.reading,
found.errors.len(),
found.unplaced,
found.notes.len(),
found.checks.len(),
);
if found.unplaced > 0 {
debug!(
"{}: {} error(s) of the cooked reading are not written in this file and are not published",
path.display(),
found.unplaced
);
}
let diagnostics = found
.errors
.iter()
.map(|error| {
let range = match (
position_in_file(held, error.start),
position_in_file(held, error.end),
) {
(Some(start), Some(end)) => lsp_types::Range::new(start, end),
_ => lsp_types::Range::default(),
};
Diagnostic {
range,
severity: Some(DiagnosticSeverity::ERROR),
source: Some("cpp_ls".to_string()),
code: Some(lsp_types::NumberOrString::String(
error.code.name().to_string(),
)),
message: error.message.clone(),
..Diagnostic::default()
}
})
.chain(module_notes(&found.notes, held))
.chain(if checks_may_be_trusted(session, path) {
check_findings(&found.checks, held).collect::<Vec<Diagnostic>>()
} else {
Vec::new()
})
.collect();
Some((uri, diagnostics))
}
pub fn checks_may_be_trusted(session: &Session<DiskFiles>, path: &std::path::Path) -> bool {
session.is_closure_ready(path) && !session.its_closure_is_owed(path)
}
fn check_findings<'a>(
findings: &'a [cpp_code_analysis::Finding],
held: &'a cpp_code_analysis::VfsFile,
) -> impl Iterator<Item = Diagnostic> + 'a {
findings.iter().filter_map(move |finding| {
let start = position_in_file(held, finding.range.start_offset)?;
let end = position_in_file(held, finding.range.end_offset()).unwrap_or(start);
Some(Diagnostic {
range: lsp_types::Range::new(start, end),
severity: Some(severity_of(finding.code)),
tags: tags_of(finding.code),
source: Some("cpp_ls".to_string()),
code: Some(lsp_types::NumberOrString::String(
finding.code.name().to_string(),
)),
message: finding.message.clone(),
..Diagnostic::default()
})
})
}
fn severity_of(code: cpp_code_analysis::DiagnosticCode) -> DiagnosticSeverity {
match code.severity() {
cpp_code_analysis::sema::check::RuleSeverity::Error => DiagnosticSeverity::ERROR,
cpp_code_analysis::sema::check::RuleSeverity::Warning => DiagnosticSeverity::WARNING,
cpp_code_analysis::sema::check::RuleSeverity::Hint => DiagnosticSeverity::HINT,
cpp_code_analysis::sema::check::RuleSeverity::Information => {
DiagnosticSeverity::INFORMATION
}
}
}
fn tags_of(code: cpp_code_analysis::DiagnosticCode) -> Option<Vec<DiagnosticTag>> {
let tags: Vec<DiagnosticTag> = code
.tags()
.iter()
.map(|tag| match tag {
cpp_code_analysis::sema::check::RuleTag::Unnecessary => DiagnosticTag::UNNECESSARY,
})
.collect();
(!tags.is_empty()).then_some(tags)
}
fn module_notes<'a>(
notes: &'a [cpp_code_analysis::ModuleNote],
held: &'a cpp_code_analysis::VfsFile,
) -> impl Iterator<Item = Diagnostic> + 'a {
notes.iter().map(move |note| {
let range = match (
position_in_file(held, note.start),
position_in_file(held, note.end),
) {
(Some(start), Some(end)) => lsp_types::Range::new(start, end),
_ => lsp_types::Range::default(),
};
Diagnostic {
range,
severity: Some(DiagnosticSeverity::INFORMATION),
source: Some("cpp_ls".to_string()),
code: Some(lsp_types::NumberOrString::String(
cpp_code_analysis::sema::check::DiagnosticCode::ModuleNote
.name()
.to_string(),
)),
message: note.message.clone(),
..Diagnostic::default()
}
})
}
pub struct DiagnosticCapabilities;
impl RegisterCapabilities for DiagnosticCapabilities {
fn register_capabilities(server_capabilities: &mut ServerCapabilities, _: &ClientCapabilities) {
server_capabilities.diagnostic_provider =
Some(DiagnosticServerCapabilities::Options(DiagnosticOptions {
identifier: Some("cpp_ls".to_string()),
inter_file_dependencies: true,
workspace_diagnostics: false,
..Default::default()
}))
}
}
#[cfg(test)]
mod tests {
use super::{checks_may_be_trusted, diagnose_file};
use cpp_code_analysis::{
CompilerConfig, DiskFiles, OpenDocuments, Session, SessionFiles, WatchFilter,
};
use lsp_types::DiagnosticSeverity;
use std::path::PathBuf;
fn a_project_importing_a_module_nobody_declares(text: &str) -> (Session<DiskFiles>, PathBuf) {
a_project_in_a_scratch_directory_named("cppls-diagnostic", text)
}
fn a_project_in_a_scratch_directory_named(
name: &str,
text: &str,
) -> (Session<DiskFiles>, PathBuf) {
let root = std::env::temp_dir().join(format!("{name}-{}", std::process::id()));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(&root).expect("a scratch directory");
let file = root.join("main.cpp");
std::fs::write(&file, text).expect("the fixture");
let providers = SessionFiles::new(OpenDocuments::new(), DiskFiles);
let mut session = Session::with_config(
root.clone(),
providers,
WatchFilter::new(&root),
CompilerConfig::default(),
);
session.add_project_files([file.clone()]);
session.want_cooked_reading(&file);
session.index_everything();
(session, file)
}
#[test]
fn the_checks_are_gated_on_this_file_s_closure_and_the_errors_are_not() {
let (session, path) = a_project_in_a_scratch_directory_named(
"cppls-gate",
"namespace std { template <class T> class vector; }\n\
void f(std::vector<int>& anotherVec) { anotherVec.push_back(1231) }\n\
int g() { return 1 }\n",
);
assert!(
session.is_closure_ready(&path),
"the fixture's own file is read, so its closure is ready"
);
assert!(
checks_may_be_trusted(&session, &path),
"…so its checks are trusted"
);
let (_, found) = diagnose_file(&session, &path).expect("the file is held");
assert!(
found.iter().any(|diagnostic| matches!(
diagnostic.code.as_ref(),
Some(lsp_types::NumberOrString::String(name)) if name == "syntax-error"
)),
"a parse error is about the text and is never withheld: {found:?}"
);
}
#[test]
fn a_module_nothing_declares_becomes_an_information_diagnostic_on_its_line() {
let text = "import mylib;\n\nint main() { return 0; }\n";
let (session, path) = a_project_importing_a_module_nobody_declares(text);
let (_, diagnostics) = diagnose_file(&session, &path).expect("the file is held");
assert_eq!(
diagnostics.len(),
1,
"one import, one diagnostic: {diagnostics:?}"
);
let note = &diagnostics[0];
assert_eq!(
note.severity,
Some(DiagnosticSeverity::INFORMATION),
"the file is not wrong — the module is outside the project"
);
assert_eq!(
note.range.start.line, 0,
"on the line that wrote the import"
);
assert_eq!(note.range.start.character, 0, "and at the declaration");
assert!(
note.message.contains("mylib"),
"the message names the module: {}",
note.message
);
let (session, path) =
a_project_importing_a_module_nobody_declares("int main() { return 0; }\n");
let (_, diagnostics) = diagnose_file(&session, &path).expect("the file is held");
assert!(
diagnostics.is_empty(),
"a file that imports nothing has nothing to be told: {diagnostics:?}"
);
let _ = std::fs::remove_dir_all(path.parent().expect("the scratch root"));
}
}