diff options
author | Norbert Preining <norbert@preining.info> | 2024-01-28 03:00:53 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2024-01-28 03:00:53 +0000 |
commit | 7084e3008c8fc947579f46c6b8a08dfd180e72ef (patch) | |
tree | 9c99041cba4afefac3859d6a2fe4cea289458468 /support/texlab/crates/diagnostics | |
parent | b93d257f657e619e22b8b7a27446118ce041727e (diff) |
CTAN sync 202401280300
Diffstat (limited to 'support/texlab/crates/diagnostics')
-rw-r--r-- | support/texlab/crates/diagnostics/Cargo.toml | 10 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/build_log.rs | 104 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/chktex.rs | 121 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/citations.rs | 91 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/grammar.rs | 6 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/grammar/bib.rs | 87 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/grammar/tex.rs | 91 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/labels.rs | 53 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/lib.rs | 85 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/manager.rs | 75 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/tests.rs | 201 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/types.rs | 65 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/util.rs | 28 |
13 files changed, 578 insertions, 439 deletions
diff --git a/support/texlab/crates/diagnostics/Cargo.toml b/support/texlab/crates/diagnostics/Cargo.toml index 7de9c6042d..1fb4b2171d 100644 --- a/support/texlab/crates/diagnostics/Cargo.toml +++ b/support/texlab/crates/diagnostics/Cargo.toml @@ -7,15 +7,25 @@ edition.workspace = true rust-version.workspace = true [dependencies] +anyhow = "1.0.75" base-db = { path = "../base-db" } +dirs = "5.0.1" +encoding_rs = "0.8.33" +encoding_rs_io = "0.1.7" itertools = "0.12.0" line-index = { path = "../line-index" } +log = "0.4.20" +multimap = "0.9.1" +once_cell = "1.19.0" +parking_lot = "0.12.1" +regex = "1.10.2" rowan = "0.15.15" rustc-hash = "1.1.0" syntax = { path = "../syntax" } url = "2.5.0" [dev-dependencies] +expect-test = "1.4.1" test-utils = { path = "../test-utils" } [lib] diff --git a/support/texlab/crates/diagnostics/src/build_log.rs b/support/texlab/crates/diagnostics/src/build_log.rs index 4c53a35ff8..352e18d0d1 100644 --- a/support/texlab/crates/diagnostics/src/build_log.rs +++ b/support/texlab/crates/diagnostics/src/build_log.rs @@ -1,94 +1,54 @@ -use std::borrow::Cow; - use base_db::{Document, Workspace}; use line_index::LineCol; +use multimap::MultiMap; use rowan::{TextLen, TextRange, TextSize}; use rustc_hash::FxHashMap; use syntax::BuildError; use url::Url; -use crate::{ - types::{Diagnostic, DiagnosticData}, - DiagnosticBuilder, DiagnosticSource, -}; - -#[derive(Debug, Default)] -struct BuildLog { - errors: FxHashMap<Url, Vec<Diagnostic>>, -} +use crate::types::Diagnostic; -#[derive(Debug, Default)] -pub struct BuildErrors { - logs: FxHashMap<Url, BuildLog>, -} +pub fn update( + workspace: &Workspace, + log_document: &Document, + results: &mut FxHashMap<Url, MultiMap<Url, Diagnostic>>, +) -> Option<()> { + let mut errors = MultiMap::default(); -impl DiagnosticSource for BuildErrors { - fn update(&mut self, workspace: &Workspace, log_document: &Document) { - let mut errors: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default(); + let data = log_document.data.as_log()?; - let Some(data) = log_document.data.as_log() else { - return; - }; + let parents = workspace.parents(log_document); + let root_document = parents.iter().next()?; - let parents = workspace.parents(log_document); - let Some(root_document) = parents.iter().next() else { - return; - }; + let base_path = root_document + .path + .as_deref() + .and_then(|path| path.parent())?; - let Some(base_path) = root_document.path.as_deref().and_then(|path| path.parent()) else { - return; + for error in &data.errors { + let full_path = base_path.join(&error.relative_path); + let Ok(full_path_uri) = Url::from_file_path(&full_path) else { + continue; }; - for error in &data.errors { - let full_path = base_path.join(&error.relative_path); - let Ok(full_path_uri) = Url::from_file_path(&full_path) else { - continue; - }; + let tex_document = workspace.lookup(&full_path_uri).unwrap_or(root_document); - let tex_document = workspace.lookup(&full_path_uri).unwrap_or(root_document); + let range = find_range_of_hint(tex_document, error).unwrap_or_else(|| { + let line = error.line.unwrap_or(0); + let offset = tex_document + .line_index + .offset(LineCol { line, col: 0 }) + .unwrap_or(TextSize::from(0)); - let range = find_range_of_hint(tex_document, error).unwrap_or_else(|| { - let line = error.line.unwrap_or(0); - let offset = tex_document - .line_index - .offset(LineCol { line, col: 0 }) - .unwrap_or(TextSize::from(0)); + TextRange::empty(offset) + }); - TextRange::empty(offset) - }); - - let diagnostic = Diagnostic { - range, - data: DiagnosticData::Build(error.clone()), - }; - - errors - .entry(tex_document.uri.clone()) - .or_default() - .push(diagnostic); - } - - self.logs - .insert(log_document.uri.clone(), BuildLog { errors }); + let diagnostic = Diagnostic::Build(range, error.clone()); + errors.insert(tex_document.uri.clone(), diagnostic); } - fn publish<'db>( - &'db mut self, - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, - ) { - self.logs.retain(|uri, _| workspace.lookup(uri).is_some()); - - for document in workspace.iter() { - let Some(log) = self.logs.get(&document.uri) else { - continue; - }; - - for (uri, errors) in &log.errors { - builder.push_many(uri, errors.iter().map(Cow::Borrowed)); - } - } - } + results.insert(log_document.uri.clone(), errors); + Some(()) } fn find_range_of_hint(document: &Document, error: &BuildError) -> Option<TextRange> { diff --git a/support/texlab/crates/diagnostics/src/chktex.rs b/support/texlab/crates/diagnostics/src/chktex.rs new file mode 100644 index 0000000000..a342f78349 --- /dev/null +++ b/support/texlab/crates/diagnostics/src/chktex.rs @@ -0,0 +1,121 @@ +use std::{ + io::{BufRead, BufReader, Write}, + path::PathBuf, + process::Stdio, +}; + +use base_db::{Document, Workspace}; +use encoding_rs_io::DecodeReaderBytesBuilder; +use line_index::LineCol; +use once_cell::sync::Lazy; +use regex::Regex; + +use crate::{types::Diagnostic, ChktexError, ChktexSeverity}; + +#[derive(Debug)] +pub struct Command { + text: String, + working_dir: PathBuf, + additional_args: Vec<String>, +} + +impl Command { + pub fn new(workspace: &Workspace, document: &Document) -> Option<Self> { + document.data.as_tex()?; + + let parent = workspace + .parents(document) + .into_iter() + .next() + .unwrap_or(document); + + if parent.path.is_none() { + log::warn!("Calling ChkTeX on non-local files is not supported yet."); + return None; + } + + let working_dir = workspace.current_dir(&parent.dir).to_file_path().ok()?; + log::debug!("Calling ChkTeX from directory: {}", working_dir.display()); + + let text = document.text.clone(); + let config = &workspace.config().diagnostics.chktex; + let additional_args = config.additional_args.clone(); + Some(Self { + text, + working_dir, + additional_args, + }) + } + + pub fn run(mut self) -> std::io::Result<Vec<Diagnostic>> { + let mut args = vec!["-I0".into(), "-f%l:%c:%d:%k:%n:%m\n".into()]; + args.append(&mut self.additional_args); + + let mut child = std::process::Command::new("chktex") + .args(args) + .stdin(Stdio::piped()) + .stdout(Stdio::piped()) + .stderr(Stdio::null()) + .current_dir(self.working_dir) + .spawn()?; + + let stdout = child.stdout.take().unwrap(); + let reader = std::thread::spawn(move || { + let mut diagnostics = Vec::new(); + let reader = BufReader::new( + DecodeReaderBytesBuilder::new() + .encoding(Some(encoding_rs::UTF_8)) + .utf8_passthru(true) + .strip_bom(true) + .build(stdout), + ); + + for line in reader.lines().flatten() { + let captures = LINE_REGEX.captures(&line).unwrap(); + let line = captures[1].parse::<u32>().unwrap() - 1; + let character = captures[2].parse::<u32>().unwrap() - 1; + let digit = captures[3].parse::<u32>().unwrap(); + let kind = &captures[4]; + let code = String::from(&captures[5]); + let message = captures[6].into(); + let start = LineCol { + line, + col: character, + }; + + let end = LineCol { + line, + col: character + digit, + }; + + let severity = match kind { + "Message" => ChktexSeverity::Message, + "Warning" => ChktexSeverity::Warning, + _ => ChktexSeverity::Error, + }; + + diagnostics.push(Diagnostic::Chktex(ChktexError { + start, + end, + message, + severity, + code, + })); + } + + diagnostics + }); + + let mut stdin = child.stdin.take().unwrap(); + let bytes = self.text.into_bytes(); + let writer = std::thread::spawn(move || stdin.write_all(&bytes)); + + child.wait()?; + writer.join().unwrap()?; + let diagnostics = reader.join().unwrap(); + Ok(diagnostics) + } +} + +static LINE_REGEX: Lazy<Regex> = + Lazy::new(|| Regex::new("(\\d+):(\\d+):(\\d+):(\\w+):(\\w+):(.*)").unwrap()); diff --git a/support/texlab/crates/diagnostics/src/citations.rs b/support/texlab/crates/diagnostics/src/citations.rs index 0b750aaadb..403d2f5bfc 100644 --- a/support/texlab/crates/diagnostics/src/citations.rs +++ b/support/texlab/crates/diagnostics/src/citations.rs @@ -1,46 +1,22 @@ -use std::borrow::Cow; - use base_db::{ semantics::{bib::Entry, tex::Citation}, util::queries::{self, Object}, Document, Project, Workspace, }; +use multimap::MultiMap; use rustc_hash::FxHashSet; +use url::Url; -use crate::{ - types::{BibError, Diagnostic, DiagnosticData, TexError}, - DiagnosticBuilder, DiagnosticSource, -}; +use crate::types::{BibError, Diagnostic, TexError}; const MAX_UNUSED_ENTRIES: usize = 1000; -#[derive(Default)] -pub struct CitationErrors; - -impl DiagnosticSource for CitationErrors { - fn publish<'db>( - &'db mut self, - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, - ) { - for document in workspace.iter() { - let project = workspace.project(document); - detect_undefined_citations(&project, document, builder); - detect_unused_entries(&project, document, builder); - } - - detect_duplicate_entries(workspace, builder); - } -} - -fn detect_undefined_citations<'db>( - project: &Project<'db>, - document: &'db Document, - builder: &mut DiagnosticBuilder<'db>, -) { - let Some(data) = document.data.as_tex() else { - return; - }; +pub fn detect_undefined_citations<'a>( + project: &Project<'a>, + document: &'a Document, + results: &mut MultiMap<Url, Diagnostic>, +) -> Option<()> { + let data = document.data.as_tex()?; let entries: FxHashSet<&str> = Entry::find_all(project) .map(|(_, entry)| entry.name_text()) @@ -49,28 +25,24 @@ fn detect_undefined_citations<'db>( for citation in &data.semantics.citations { let name = citation.name_text(); if name != "*" && !entries.contains(name) { - let diagnostic = Diagnostic { - range: citation.name.range, - data: DiagnosticData::Tex(TexError::UndefinedCitation), - }; - - builder.push(&document.uri, Cow::Owned(diagnostic)); + let diagnostic = Diagnostic::Tex(citation.name.range, TexError::UndefinedCitation); + results.insert(document.uri.clone(), diagnostic); } } + + Some(()) } -fn detect_unused_entries<'db>( - project: &Project<'db>, - document: &'db Document, - builder: &mut DiagnosticBuilder<'db>, -) { - let Some(data) = document.data.as_bib() else { - return; - }; +pub fn detect_unused_entries<'a>( + project: &Project<'a>, + document: &'a Document, + results: &mut MultiMap<Url, Diagnostic>, +) -> Option<()> { + let data = document.data.as_bib()?; // If this is a huge bibliography, then don't bother checking for unused entries. if data.semantics.entries.len() > MAX_UNUSED_ENTRIES { - return; + return None; } let citations: FxHashSet<&str> = Citation::find_all(project) @@ -79,17 +51,18 @@ fn detect_unused_entries<'db>( for entry in &data.semantics.entries { if !citations.contains(entry.name.text.as_str()) { - let diagnostic = Diagnostic { - range: entry.name.range, - data: DiagnosticData::Bib(BibError::UnusedEntry), - }; - - builder.push(&document.uri, Cow::Owned(diagnostic)); + let diagnostic = Diagnostic::Bib(entry.name.range, BibError::UnusedEntry); + results.insert(document.uri.clone(), diagnostic); } } + + Some(()) } -fn detect_duplicate_entries<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) { +pub fn detect_duplicate_entries<'a>( + workspace: &'a Workspace, + results: &mut MultiMap<Url, Diagnostic>, +) { for conflict in queries::Conflict::find_all::<Entry>(workspace) { let others = conflict .rest @@ -97,11 +70,7 @@ fn detect_duplicate_entries<'db>(workspace: &'db Workspace, builder: &mut Diagno .map(|location| (location.document.uri.clone(), location.range)) .collect(); - let diagnostic = Diagnostic { - range: conflict.main.range, - data: DiagnosticData::Bib(BibError::DuplicateEntry(others)), - }; - - builder.push(&conflict.main.document.uri, Cow::Owned(diagnostic)); + let diagnostic = Diagnostic::Bib(conflict.main.range, BibError::DuplicateEntry(others)); + results.insert(conflict.main.document.uri.clone(), diagnostic); } } diff --git a/support/texlab/crates/diagnostics/src/grammar.rs b/support/texlab/crates/diagnostics/src/grammar.rs index 1a9e3cecda..df05ee6d14 100644 --- a/support/texlab/crates/diagnostics/src/grammar.rs +++ b/support/texlab/crates/diagnostics/src/grammar.rs @@ -1,4 +1,2 @@ -mod bib; -mod tex; - -pub use self::{bib::BibSyntaxErrors, tex::TexSyntaxErrors}; +pub mod bib; +pub mod tex; diff --git a/support/texlab/crates/diagnostics/src/grammar/bib.rs b/support/texlab/crates/diagnostics/src/grammar/bib.rs index e101e68f2a..494cdbe4b1 100644 --- a/support/texlab/crates/diagnostics/src/grammar/bib.rs +++ b/support/texlab/crates/diagnostics/src/grammar/bib.rs @@ -1,48 +1,35 @@ -use base_db::{Document, DocumentData, Workspace}; +use base_db::{BibDocumentData, Document}; +use multimap::MultiMap; use rowan::{ast::AstNode, TextRange}; use syntax::bibtex::{self, HasDelims, HasEq, HasName, HasType, HasValue}; +use url::Url; -use crate::{ - types::{BibError, DiagnosticData}, - util::SimpleDiagnosticSource, - Diagnostic, DiagnosticBuilder, DiagnosticSource, -}; +use crate::types::{BibError, Diagnostic}; -#[derive(Default)] -pub struct BibSyntaxErrors(SimpleDiagnosticSource); +pub fn update(document: &Document, results: &mut MultiMap<Url, Diagnostic>) -> Option<()> { + let data = document.data.as_bib()?; + let mut analyzer = Analyzer { + data, + diagnostics: Vec::new(), + }; -impl DiagnosticSource for BibSyntaxErrors { - fn update(&mut self, _workspace: &Workspace, document: &Document) { - let mut analyzer = Analyzer { - document, - diagnostics: Vec::new(), - }; + analyzer.analyze_root(); - analyzer.analyze_root(); - self.0 - .errors - .insert(document.uri.clone(), analyzer.diagnostics); - } + *results + .entry(document.uri.clone()) + .or_insert_vec(Vec::new()) = analyzer.diagnostics; - fn publish<'db>( - &'db mut self, - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, - ) { - self.0.publish(workspace, builder); - } + Some(()) } struct Analyzer<'a> { - document: &'a Document, + data: &'a BibDocumentData, diagnostics: Vec<Diagnostic>, } impl<'a> Analyzer<'a> { fn analyze_root(&mut self) { - let DocumentData::Bib(data) = &self.document.data else { return }; - - for node in bibtex::SyntaxNode::new_root(data.green.clone()).descendants() { + for node in self.data.root_node().descendants() { if let Some(entry) = bibtex::Entry::cast(node.clone()) { self.analyze_entry(entry); } else if let Some(field) = bibtex::Field::cast(node.clone()) { @@ -54,50 +41,50 @@ impl<'a> Analyzer<'a> { fn analyze_entry(&mut self, entry: bibtex::Entry) { if entry.left_delim_token().is_none() { let offset = entry.type_token().unwrap().text_range().end(); - self.diagnostics.push(Diagnostic { - range: TextRange::empty(offset), - data: DiagnosticData::Bib(BibError::ExpectingLCurly), - }); + self.diagnostics.push(Diagnostic::Bib( + TextRange::empty(offset), + BibError::ExpectingLCurly, + )); return; } if entry.name_token().is_none() { let offset = entry.left_delim_token().unwrap().text_range().end(); - self.diagnostics.push(Diagnostic { - range: TextRange::empty(offset), - data: DiagnosticData::Bib(BibError::ExpectingKey), - }); + self.diagnostics.push(Diagnostic::Bib( + TextRange::empty(offset), + BibError::ExpectingKey, + )); return; } if entry.right_delim_token().is_none() { let offset = entry.syntax().text_range().end(); - self.diagnostics.push(Diagnostic { - range: TextRange::empty(offset), - data: DiagnosticData::Bib(BibError::ExpectingRCurly), - }); + self.diagnostics.push(Diagnostic::Bib( + TextRange::empty(offset), + BibError::ExpectingRCurly, + )); } } fn analyze_field(&mut self, field: bibtex::Field) { if field.eq_token().is_none() { let offset = field.name_token().unwrap().text_range().end(); - self.diagnostics.push(Diagnostic { - range: TextRange::empty(offset), - data: DiagnosticData::Bib(BibError::ExpectingEq), - }); + self.diagnostics.push(Diagnostic::Bib( + TextRange::empty(offset), + BibError::ExpectingEq, + )); return; } if field.value().is_none() { let offset = field.eq_token().unwrap().text_range().end(); - self.diagnostics.push(Diagnostic { - range: TextRange::empty(offset), - data: DiagnosticData::Bib(BibError::ExpectingFieldValue), - }); + self.diagnostics.push(Diagnostic::Bib( + TextRange::empty(offset), + BibError::ExpectingFieldValue, + )); } } } diff --git a/support/texlab/crates/diagnostics/src/grammar/tex.rs b/support/texlab/crates/diagnostics/src/grammar/tex.rs index b61ae4b020..fb2fa5b5fd 100644 --- a/support/texlab/crates/diagnostics/src/grammar/tex.rs +++ b/support/texlab/crates/diagnostics/src/grammar/tex.rs @@ -1,56 +1,47 @@ -use base_db::{Config, Document, DocumentData, Workspace}; +use base_db::{Config, Document, TexDocumentData}; +use multimap::MultiMap; use rowan::{ast::AstNode, NodeOrToken, TextRange}; use syntax::latex; - -use crate::{ - types::{DiagnosticData, TexError}, - util::SimpleDiagnosticSource, - Diagnostic, DiagnosticBuilder, DiagnosticSource, -}; - -#[derive(Default)] -pub struct TexSyntaxErrors(SimpleDiagnosticSource); - -impl DiagnosticSource for TexSyntaxErrors { - fn update(&mut self, workspace: &Workspace, document: &Document) { - let mut analyzer = Analyzer { - document, - config: workspace.config(), - diagnostics: Vec::new(), - }; - - analyzer.analyze_root(); - self.0 - .errors - .insert(document.uri.clone(), analyzer.diagnostics); +use url::Url; + +use crate::types::{Diagnostic, TexError}; + +pub fn update( + document: &Document, + config: &Config, + results: &mut MultiMap<Url, Diagnostic>, +) -> Option<()> { + let data = document.data.as_tex()?; + if !document.uri.as_str().ends_with(".tex") { + return None; } - fn publish<'db>( - &'db mut self, - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, - ) { - self.0.publish(workspace, builder); - } + let mut analyzer = Analyzer { + data, + config, + diagnostics: Vec::new(), + }; + + analyzer.analyze_root(); + + *results + .entry(document.uri.clone()) + .or_insert_vec(Vec::new()) = analyzer.diagnostics; + + Some(()) } struct Analyzer<'a> { - document: &'a Document, + data: &'a TexDocumentData, config: &'a Config, diagnostics: Vec<Diagnostic>, } impl<'a> Analyzer<'a> { fn analyze_root(&mut self) { - if !self.document.uri.as_str().ends_with(".tex") { - return; - } - - let DocumentData::Tex(data) = &self.document.data else { return }; - let verbatim_envs = &self.config.syntax.verbatim_environments; - let mut traversal = latex::SyntaxNode::new_root(data.green.clone()).preorder(); + let mut traversal = self.data.root_node().preorder(); while let Some(event) = traversal.next() { match event { rowan::WalkEvent::Enter(node) => { @@ -82,10 +73,10 @@ impl<'a> Analyzer<'a> { let begin = environment.begin()?.name()?.key()?; let end = environment.end()?.name()?.key()?; if begin != end { - self.diagnostics.push(Diagnostic { - range: latex::small_range(&begin), - data: DiagnosticData::Tex(TexError::MismatchedEnvironment), - }); + self.diagnostics.push(Diagnostic::Tex( + latex::small_range(&begin), + TexError::MismatchedEnvironment, + )); } Some(()) @@ -108,10 +99,10 @@ impl<'a> Analyzer<'a> { .filter_map(NodeOrToken::into_token) .any(|token| token.kind() == latex::R_CURLY) { - self.diagnostics.push(Diagnostic { - range: TextRange::empty(node.text_range().end()), - data: DiagnosticData::Tex(TexError::ExpectingRCurly), - }); + self.diagnostics.push(Diagnostic::Tex( + TextRange::empty(node.text_range().end()), + TexError::ExpectingRCurly, + )); } Some(()) @@ -119,10 +110,10 @@ impl<'a> Analyzer<'a> { fn analyze_curly_braces(&mut self, node: latex::SyntaxNode) -> Option<()> { if node.kind() == latex::ERROR && node.first_token()?.text() == "}" { - self.diagnostics.push(Diagnostic { - range: node.text_range(), - data: DiagnosticData::Tex(TexError::UnexpectedRCurly), - }); + self.diagnostics.push(Diagnostic::Tex( + node.text_range(), + TexError::UnexpectedRCurly, + )); Some(()) } else { diff --git a/support/texlab/crates/diagnostics/src/labels.rs b/support/texlab/crates/diagnostics/src/labels.rs index 03df664a15..92f9072918 100644 --- a/support/texlab/crates/diagnostics/src/labels.rs +++ b/support/texlab/crates/diagnostics/src/labels.rs @@ -1,35 +1,18 @@ -use std::borrow::Cow; - use base_db::{ semantics::tex::{Label, LabelKind}, util::queries, DocumentData, Workspace, }; use itertools::Itertools; +use multimap::MultiMap; use rustc_hash::FxHashSet; +use url::Url; -use crate::{ - types::{DiagnosticData, TexError}, - Diagnostic, DiagnosticBuilder, DiagnosticSource, -}; - -#[derive(Default)] -pub struct LabelErrors; - -impl DiagnosticSource for LabelErrors { - fn publish<'db>( - &'db mut self, - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, - ) { - detect_undefined_and_unused_labels(workspace, builder); - detect_duplicate_labels(workspace, builder); - } -} +use crate::types::{Diagnostic, TexError}; -fn detect_undefined_and_unused_labels<'db>( - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, +pub fn detect_undefined_and_unused_labels( + workspace: &Workspace, + results: &mut MultiMap<Url, Diagnostic>, ) { let graphs: Vec<_> = workspace .iter() @@ -61,25 +44,19 @@ fn detect_undefined_and_unused_labels<'db>( for label in &data.semantics.labels { if label.kind != LabelKind::Definition && !label_defs.contains(&label.name.text) { - let diagnostic = Diagnostic { - range: label.name.range, - data: DiagnosticData::Tex(TexError::UndefinedLabel), - }; - builder.push(&document.uri, Cow::Owned(diagnostic)); + let diagnostic = Diagnostic::Tex(label.name.range, TexError::UndefinedLabel); + results.insert(document.uri.clone(), diagnostic); } if label.kind == LabelKind::Definition && !label_refs.contains(&label.name.text) { - let diagnostic = Diagnostic { - range: label.name.range, - data: DiagnosticData::Tex(TexError::UnusedLabel), - }; - builder.push(&document.uri, Cow::Owned(diagnostic)); + let diagnostic = Diagnostic::Tex(label.name.range, TexError::UnusedLabel); + results.insert(document.uri.clone(), diagnostic); } } } } -fn detect_duplicate_labels<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) { +pub fn detect_duplicate_labels(workspace: &Workspace, results: &mut MultiMap<Url, Diagnostic>) { for conflict in queries::Conflict::find_all::<Label>(workspace) { let others = conflict .rest @@ -87,11 +64,7 @@ fn detect_duplicate_labels<'db>(workspace: &'db Workspace, builder: &mut Diagnos .map(|location| (location.document.uri.clone(), location.range)) .collect(); - let diagnostic = Diagnostic { - range: conflict.main.range, - data: DiagnosticData::Tex(TexError::DuplicateLabel(others)), - }; - - builder.push(&conflict.main.document.uri, Cow::Owned(diagnostic)); + let diagnostic = Diagnostic::Tex(conflict.main.range, TexError::DuplicateLabel(others)); + results.insert(conflict.main.document.uri.clone(), diagnostic); } } diff --git a/support/texlab/crates/diagnostics/src/lib.rs b/support/texlab/crates/diagnostics/src/lib.rs index d0be31a0ba..e5baf9ebdb 100644 --- a/support/texlab/crates/diagnostics/src/lib.rs +++ b/support/texlab/crates/diagnostics/src/lib.rs @@ -1,88 +1,13 @@ mod build_log; +pub mod chktex; mod citations; mod grammar; mod labels; -pub mod types; -pub(crate) mod util; +mod manager; +mod types; -use std::borrow::Cow; - -use base_db::{Document, Workspace}; -use build_log::BuildErrors; -use citations::CitationErrors; -use grammar::{BibSyntaxErrors, TexSyntaxErrors}; -use labels::LabelErrors; -use rustc_hash::FxHashMap; -use types::Diagnostic; -use url::Url; - -#[derive(Debug, PartialEq, Eq, Clone, Default)] -pub struct DiagnosticBuilder<'db> { - inner: FxHashMap<&'db Url, Vec<Cow<'db, Diagnostic>>>, -} - -impl<'db> DiagnosticBuilder<'db> { - pub fn push(&mut self, uri: &'db Url, diagnostic: Cow<'db, Diagnostic>) { - self.inner.entry(uri).or_default().push(diagnostic); - } - - pub fn push_many( - &mut self, - uri: &'db Url, - diagnostics: impl Iterator<Item = Cow<'db, Diagnostic>>, - ) { - self.inner.entry(uri).or_default().extend(diagnostics); - } - - pub fn iter(&self) -> impl Iterator<Item = (&'db Url, impl Iterator<Item = &Diagnostic>)> { - self.inner - .iter() - .map(|(uri, diagnostics)| (*uri, diagnostics.iter().map(|diag| diag.as_ref()))) - } -} - -pub trait DiagnosticSource { - #[allow(unused_variables)] - fn update(&mut self, workspace: &Workspace, document: &Document) {} - - fn publish<'db>(&'db mut self, workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>); -} - -pub struct DiagnosticManager { - sources: Vec<Box<dyn DiagnosticSource>>, -} - -impl Default for DiagnosticManager { - fn default() -> Self { - let sources: Vec<Box<dyn DiagnosticSource>> = vec![ - Box::<TexSyntaxErrors>::default(), - Box::<BibSyntaxErrors>::default(), - Box::<BuildErrors>::default(), - Box::<LabelErrors>::default(), - Box::<CitationErrors>::default(), - ]; - - Self { sources } - } -} - -impl DiagnosticSource for DiagnosticManager { - fn update(&mut self, workspace: &Workspace, document: &Document) { - for source in &mut self.sources { - source.update(workspace, document); - } - } - - fn publish<'db>( - &'db mut self, - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, - ) { - for source in &mut self.sources { - source.publish(workspace, builder); - } - } -} +pub use manager::Manager; +pub use types::*; #[cfg(test)] mod tests; diff --git a/support/texlab/crates/diagnostics/src/manager.rs b/support/texlab/crates/diagnostics/src/manager.rs new file mode 100644 index 0000000000..1ce5e194b8 --- /dev/null +++ b/support/texlab/crates/diagnostics/src/manager.rs @@ -0,0 +1,75 @@ +use base_db::{util::filter_regex_patterns, Document, Owner, Workspace}; +use multimap::MultiMap; +use rustc_hash::FxHashMap; +use url::Url; + +use crate::types::Diagnostic; + +/// Manages all diagnostics for a workspace. +#[derive(Debug, Default)] +pub struct Manager { + grammar: MultiMap<Url, Diagnostic>, + chktex: FxHashMap<Url, Vec<Diagnostic>>, + build_log: FxHashMap<Url, MultiMap<Url, Diagnostic>>, +} + +impl Manager { + /// Updates the syntax-based diagnostics for the given document. + pub fn update_syntax(&mut self, workspace: &Workspace, document: &Document) { + self.grammar.remove(&document.uri); + super::grammar::tex::update(document, workspace.config(), &mut self.grammar); + super::grammar::bib::update(document, &mut self.grammar); + + self.build_log.remove(&document.uri); + super::build_log::update(workspace, document, &mut self.build_log); + } + + /// Updates the ChkTeX diagnostics for the given document. + pub fn update_chktex(&mut self, uri: Url, diagnostics: Vec<Diagnostic>) { + self.chktex.insert(uri, diagnostics); + } + + /// Returns all filtered diagnostics for the given workspace. + pub fn get(&self, workspace: &Workspace) -> MultiMap<Url, Diagnostic> { + let mut results = MultiMap::default(); + for (uri, diagnostics) in &self.grammar { + results.insert_many_from_slice(uri.clone(), diagnostics); + } + + for (uri, diagnostics) in self.build_log.values().flatten() { + results.insert_many_from_slice(uri.clone(), diagnostics); + } + + for (uri, diagnostics) in &self.chktex { + if workspace + .lookup(uri) + .map_or(false, |document| document.owner == Owner::Client) + { + results.insert_many_from_slice(uri.clone(), diagnostics); + } + } + + for document in workspace.iter() { + let project = workspace.project(document); + super::citations::detect_undefined_citations(&project, document, &mut results); + super::citations::detect_unused_entries(&project, document, &mut results); + } + + super::citations::detect_duplicate_entries(workspace, &mut results); + super::labels::detect_duplicate_labels(workspace, &mut results); + super::labels::detect_undefined_and_unused_labels(workspace, &mut results); + + let config = &workspace.config().diagnostics; + for (_, diagnostics) in &mut results { + diagnostics.retain(|diagnostic| { + filter_regex_patterns( + &diagnostic.message(), + &config.allowed_patterns, + &config.ignored_patterns, + ) + }); + } + + results + } +} diff --git a/support/texlab/crates/diagnostics/src/tests.rs b/support/texlab/crates/diagnostics/src/tests.rs index 539e3ba765..b7c6004d19 100644 --- a/support/texlab/crates/diagnostics/src/tests.rs +++ b/support/texlab/crates/diagnostics/src/tests.rs @@ -1,37 +1,23 @@ -use std::borrow::Cow; +use expect_test::{expect, Expect}; +use itertools::Itertools; -use crate::{ - types::{BibError, Diagnostic, DiagnosticData, TexError}, - DiagnosticBuilder, DiagnosticManager, DiagnosticSource, -}; - -fn check(input: &str, expected_data: &[DiagnosticData]) { +fn check(input: &str, expect: Expect) { let fixture = test_utils::fixture::Fixture::parse(input); - let mut manager = DiagnosticManager::default(); - - let mut expected = DiagnosticBuilder::default(); - let mut expected_data = expected_data.iter(); - for document in &fixture.documents { - let diagnostics = document.ranges.iter().copied().map(|range| { - let data = expected_data.next().unwrap().clone(); - Cow::Owned(Diagnostic { range, data }) - }); - - expected.push_many(&document.uri, diagnostics); - } + let mut manager = crate::Manager::default(); for document in fixture.workspace.iter() { - manager.update(&fixture.workspace, &document); + manager.update_syntax(&fixture.workspace, &document); } - let mut actual = DiagnosticBuilder::default(); - manager.publish(&fixture.workspace, &mut actual); - - for diagnostics in actual.inner.values_mut() { - diagnostics.sort_by_key(|diag| (diag.range.start(), diag.range.len())); - } + let results = manager.get(&fixture.workspace); + let results = results + .iter_all() + .filter(|(_, diags)| !diags.is_empty()) + .sorted_by(|(uri1, _), (uri2, _)| uri1.cmp(&uri2)) + .map(|(uri, diags)| (uri.as_str(), diags)) + .collect_vec(); - assert_eq!(actual, expected); + expect.assert_debug_eq(&results); } #[test] @@ -42,7 +28,19 @@ fn test_bib_entry_missing_l_delim() { @article ! "#, - &[DiagnosticData::Bib(BibError::ExpectingLCurly)], + expect![[r#" + [ + ( + "file:///texlab/main.bib", + [ + Bib( + 8..8, + ExpectingLCurly, + ), + ], + ), + ] + "#]], ) } @@ -58,7 +56,19 @@ fn test_bib_entry_missing_r_delim() { \bibliography{main} \cite{foo} "#, - &[DiagnosticData::Bib(BibError::ExpectingRCurly)], + expect![[r#" + [ + ( + "file:///texlab/main.bib", + [ + Bib( + 14..14, + ExpectingRCurly, + ), + ], + ), + ] + "#]], ) } @@ -69,7 +79,19 @@ fn test_bib_entry_missing_name() { %! main.bib @article{ !"#, - &[DiagnosticData::Bib(BibError::ExpectingKey)], + expect![[r#" + [ + ( + "file:///texlab/main.bib", + [ + Bib( + 9..9, + ExpectingKey, + ), + ], + ), + ] + "#]], ) } @@ -87,7 +109,19 @@ fn test_bib_field_missing_eq() { \bibliography{main} \cite{foo} "#, - &[DiagnosticData::Bib(BibError::ExpectingEq)], + expect![[r#" + [ + ( + "file:///texlab/main.bib", + [ + Bib( + 23..23, + ExpectingEq, + ), + ], + ), + ] + "#]], ) } @@ -105,7 +139,19 @@ fn test_bib_field_missing_value() { \bibliography{main} \cite{foo} "#, - &[DiagnosticData::Bib(BibError::ExpectingFieldValue)], + expect![[r#" + [ + ( + "file:///texlab/main.bib", + [ + Bib( + 25..25, + ExpectingFieldValue, + ), + ], + ), + ] + "#]], ) } @@ -119,10 +165,23 @@ fn test_tex_unmatched_braces() { { ! "#, - &[ - DiagnosticData::Tex(TexError::UnexpectedRCurly), - DiagnosticData::Tex(TexError::ExpectingRCurly), - ], + expect![[r#" + [ + ( + "file:///texlab/main.tex", + [ + Tex( + 0..1, + UnexpectedRCurly, + ), + Tex( + 4..4, + ExpectingRCurly, + ), + ], + ), + ] + "#]], ) } @@ -135,7 +194,19 @@ fn test_tex_environment_mismatched() { ^^^ \end{bar} "#, - &[DiagnosticData::Tex(TexError::MismatchedEnvironment)], + expect![[r#" + [ + ( + "file:///texlab/main.tex", + [ + Tex( + 7..10, + MismatchedEnvironment, + ), + ], + ), + ] + "#]], ) } @@ -148,7 +219,19 @@ fn test_label_unused() { ^^^ \label{bar}\ref{bar} "#, - &[DiagnosticData::Tex(TexError::UnusedLabel)], + expect![[r#" + [ + ( + "file:///texlab/main.tex", + [ + Tex( + 7..10, + UnusedLabel, + ), + ], + ), + ] + "#]], ) } @@ -160,7 +243,19 @@ fn test_label_undefined() { \ref{foo} ^^^ "#, - &[DiagnosticData::Tex(TexError::UndefinedLabel)], + expect![[r#" + [ + ( + "file:///texlab/main.tex", + [ + Tex( + 5..8, + UndefinedLabel, + ), + ], + ), + ] + "#]], ) } @@ -172,7 +267,19 @@ fn test_citation_undefined() { \cite{foo} ^^^ "#, - &[DiagnosticData::Tex(TexError::UndefinedCitation)], + expect![[r#" + [ + ( + "file:///texlab/main.tex", + [ + Tex( + 6..9, + UndefinedCitation, + ), + ], + ), + ] + "#]], ) } @@ -184,6 +291,18 @@ fn test_citation_unused() { @article{foo,} ^^^ "#, - &[DiagnosticData::Bib(BibError::UnusedEntry)], + expect![[r#" + [ + ( + "file:///texlab/main.bib", + [ + Bib( + 9..12, + UnusedEntry, + ), + ], + ), + ] + "#]], ) } diff --git a/support/texlab/crates/diagnostics/src/types.rs b/support/texlab/crates/diagnostics/src/types.rs index a443245b6f..9cbbf8936f 100644 --- a/support/texlab/crates/diagnostics/src/types.rs +++ b/support/texlab/crates/diagnostics/src/types.rs @@ -1,21 +1,9 @@ +use line_index::LineCol; use rowan::TextRange; use syntax::BuildError; use url::Url; #[derive(Debug, PartialEq, Eq, Clone)] -pub struct Diagnostic { - pub range: TextRange, - pub data: DiagnosticData, -} - -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum DiagnosticData { - Tex(TexError), - Bib(BibError), - Build(BuildError), -} - -#[derive(Debug, PartialEq, Eq, Clone)] pub enum TexError { UnexpectedRCurly, ExpectingRCurly, @@ -36,3 +24,54 @@ pub enum BibError { UnusedEntry, DuplicateEntry(Vec<(Url, TextRange)>), } + +#[derive(Debug, PartialEq, Eq, Clone)] +pub struct ChktexError { + pub start: LineCol, + pub end: LineCol, + pub message: String, + pub severity: ChktexSeverity, + pub code: String, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum ChktexSeverity { + Error, + Warning, + Message, +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum Diagnostic { + Tex(TextRange, TexError), + Bib(TextRange, BibError), + Build(TextRange, BuildError), + Chktex(ChktexError), +} + +impl Diagnostic { + pub fn message(&self) -> &str { + match self { + Diagnostic::Tex(_, error) => match error { + TexError::UnexpectedRCurly => "Unexpected \"}\"", + TexError::ExpectingRCurly => "Expecting a curly bracket: \"}\"", + TexError::MismatchedEnvironment => "Mismatched environment", + TexError::UnusedLabel => "Unused label", + TexError::UndefinedLabel => "Undefined reference", + TexError::UndefinedCitation => "Undefined reference", + TexError::DuplicateLabel(_) => "Duplicate label", + }, + Diagnostic::Bib(_, error) => match error { + BibError::ExpectingLCurly => "Expecting a curly bracket: \"{\"", + BibError::ExpectingKey => "Expecting a key", + BibError::ExpectingRCurly => "Expecting a curly bracket: \"}\"", + BibError::ExpectingEq => "Expecting an equality sign: \"=\"", + BibError::ExpectingFieldValue => "Expecting a field value", + BibError::UnusedEntry => "Unused entry", + BibError::DuplicateEntry(_) => "Duplicate entry key", + }, + Diagnostic::Build(_, error) => &error.message, + Diagnostic::Chktex(error) => &error.message, + } + } +} diff --git a/support/texlab/crates/diagnostics/src/util.rs b/support/texlab/crates/diagnostics/src/util.rs deleted file mode 100644 index fd34125dbe..0000000000 --- a/support/texlab/crates/diagnostics/src/util.rs +++ /dev/null @@ -1,28 +0,0 @@ -use std::borrow::Cow; - -use base_db::Workspace; -use rustc_hash::FxHashMap; -use url::Url; - -use crate::{Diagnostic, DiagnosticBuilder, DiagnosticSource}; - -#[derive(Default)] -pub struct SimpleDiagnosticSource { - pub errors: FxHashMap<Url, Vec<Diagnostic>>, -} - -impl DiagnosticSource for SimpleDiagnosticSource { - fn publish<'db>( - &'db mut self, - workspace: &'db Workspace, - builder: &mut DiagnosticBuilder<'db>, - ) { - self.errors.retain(|uri, _| workspace.lookup(uri).is_some()); - - for document in workspace.iter() { - if let Some(diagnostics) = self.errors.get(&document.uri) { - builder.push_many(&document.uri, diagnostics.iter().map(Cow::Borrowed)); - } - } - } -} |