diff options
author | Norbert Preining <norbert@preining.info> | 2023-07-31 03:02:46 +0000 |
---|---|---|
committer | Norbert Preining <norbert@preining.info> | 2023-07-31 03:02:46 +0000 |
commit | 17d547a1effe2cafcdfbf704bdf8cb0484790ef2 (patch) | |
tree | 30556f1ebbb411da2d3e7297c0c4a2ffbd5af4ee /support/texlab/crates/diagnostics | |
parent | 595d37aac232836c0519c45f2078c5272122eb32 (diff) |
CTAN sync 202307310302
Diffstat (limited to 'support/texlab/crates/diagnostics')
-rw-r--r-- | support/texlab/crates/diagnostics/Cargo.toml | 21 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/build_log.rs | 101 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/citations.rs | 97 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/grammar.rs | 4 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/grammar/bib.rs | 103 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/grammar/tex.rs | 132 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/labels.rs | 97 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/lib.rs | 86 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/tests.rs | 191 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/types.rs | 38 | ||||
-rw-r--r-- | support/texlab/crates/diagnostics/src/util.rs | 28 |
11 files changed, 898 insertions, 0 deletions
diff --git a/support/texlab/crates/diagnostics/Cargo.toml b/support/texlab/crates/diagnostics/Cargo.toml new file mode 100644 index 0000000000..3f87c8c879 --- /dev/null +++ b/support/texlab/crates/diagnostics/Cargo.toml @@ -0,0 +1,21 @@ +[package] +name = "diagnostics" +version = "0.0.0" +license.workspace = true +authors.workspace = true +edition.workspace = true +rust-version.workspace = true + +[dependencies] +base-db = { path = "../base-db" } +itertools = "0.11.0" +rowan = "0.15.11" +rustc-hash = "1.1.0" +syntax = { path = "../syntax" } +url = "=2.3.1" + +[dev-dependencies] +test-utils = { path = "../test-utils" } + +[lib] +doctest = false diff --git a/support/texlab/crates/diagnostics/src/build_log.rs b/support/texlab/crates/diagnostics/src/build_log.rs new file mode 100644 index 0000000000..f5770c90fb --- /dev/null +++ b/support/texlab/crates/diagnostics/src/build_log.rs @@ -0,0 +1,101 @@ +use std::borrow::Cow; + +use base_db::{Document, Workspace}; +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>>, +} + +#[derive(Debug, Default)] +pub struct BuildErrors { + logs: FxHashMap<Url, BuildLog>, +} + +impl DiagnosticSource for BuildErrors { + fn update(&mut self, workspace: &Workspace, log_document: &Document) { + let mut errors: FxHashMap<Url, Vec<Diagnostic>> = FxHashMap::default(); + + let Some(data) = log_document.data.as_log() else { return }; + + let parents = workspace.parents(log_document); + let Some(root_document) = parents.iter().next() else { return }; + + 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 }; + 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 + .newlines + .get(line as usize) + .unwrap_or(&TextSize::from(0)); + + 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 }); + } + + 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)); + } + } + } +} + +fn find_range_of_hint(document: &Document, error: &BuildError) -> Option<TextRange> { + let line = error.line? as usize; + let hint = error.hint.as_deref()?; + let line_index = &document.line_index; + + let line_start = line_index.newlines.get(line).copied()?; + let line_end = line_index + .newlines + .get(line + 1) + .copied() + .unwrap_or((&document.text).text_len()); + + let line_text = &document.text[line_start.into()..line_end.into()]; + let hint_start = line_start + TextSize::try_from(line_text.find(hint)?).unwrap(); + let hint_end = hint_start + hint.text_len(); + Some(TextRange::new(hint_start, hint_end)) +} diff --git a/support/texlab/crates/diagnostics/src/citations.rs b/support/texlab/crates/diagnostics/src/citations.rs new file mode 100644 index 0000000000..e6b4d1022a --- /dev/null +++ b/support/texlab/crates/diagnostics/src/citations.rs @@ -0,0 +1,97 @@ +use std::borrow::Cow; + +use base_db::{ + semantics::{bib::Entry, tex::Citation}, + util::queries::{self, Object}, + BibDocumentData, Document, DocumentData, Project, TexDocumentData, Workspace, +}; +use rustc_hash::FxHashSet; + +use crate::{ + types::{BibError, Diagnostic, DiagnosticData, TexError}, + DiagnosticBuilder, DiagnosticSource, +}; + +#[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); + + if let DocumentData::Tex(data) = &document.data { + detect_undefined_citations(&project, document, data, builder); + } else if let DocumentData::Bib(data) = &document.data { + detect_unused_entries(&project, document, data, builder); + } + } + + detect_duplicate_entries(workspace, builder); + } +} + +fn detect_undefined_citations<'db>( + project: &Project<'db>, + document: &'db Document, + data: &TexDocumentData, + builder: &mut DiagnosticBuilder<'db>, +) { + let entries: FxHashSet<&str> = Entry::find_all(project) + .map(|(_, entry)| entry.name_text()) + .collect(); + + for citation in &data.semantics.citations { + if !entries.contains(citation.name.text.as_str()) { + let diagnostic = Diagnostic { + range: citation.name.range, + data: DiagnosticData::Tex(TexError::UndefinedCitation), + }; + + builder.push(&document.uri, Cow::Owned(diagnostic)); + } + } +} + +fn detect_unused_entries<'db>( + project: &Project<'db>, + document: &'db Document, + data: &BibDocumentData, + builder: &mut DiagnosticBuilder<'db>, +) { + let citations: FxHashSet<&str> = Citation::find_all(project) + .map(|(_, citation)| citation.name_text()) + .collect(); + + 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)); + } + } +} + +fn detect_duplicate_entries<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) { + for conflict in queries::Conflict::find_all::<Entry>(workspace) { + let others = conflict + .rest + .iter() + .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)); + } +} diff --git a/support/texlab/crates/diagnostics/src/grammar.rs b/support/texlab/crates/diagnostics/src/grammar.rs new file mode 100644 index 0000000000..1a9e3cecda --- /dev/null +++ b/support/texlab/crates/diagnostics/src/grammar.rs @@ -0,0 +1,4 @@ +mod bib; +mod tex; + +pub use self::{bib::BibSyntaxErrors, tex::TexSyntaxErrors}; diff --git a/support/texlab/crates/diagnostics/src/grammar/bib.rs b/support/texlab/crates/diagnostics/src/grammar/bib.rs new file mode 100644 index 0000000000..e101e68f2a --- /dev/null +++ b/support/texlab/crates/diagnostics/src/grammar/bib.rs @@ -0,0 +1,103 @@ +use base_db::{Document, DocumentData, Workspace}; +use rowan::{ast::AstNode, TextRange}; +use syntax::bibtex::{self, HasDelims, HasEq, HasName, HasType, HasValue}; + +use crate::{ + types::{BibError, DiagnosticData}, + util::SimpleDiagnosticSource, + Diagnostic, DiagnosticBuilder, DiagnosticSource, +}; + +#[derive(Default)] +pub struct BibSyntaxErrors(SimpleDiagnosticSource); + +impl DiagnosticSource for BibSyntaxErrors { + fn update(&mut self, _workspace: &Workspace, document: &Document) { + let mut analyzer = Analyzer { + document, + diagnostics: Vec::new(), + }; + + analyzer.analyze_root(); + self.0 + .errors + .insert(document.uri.clone(), analyzer.diagnostics); + } + + fn publish<'db>( + &'db mut self, + workspace: &'db Workspace, + builder: &mut DiagnosticBuilder<'db>, + ) { + self.0.publish(workspace, builder); + } +} + +struct Analyzer<'a> { + document: &'a Document, + 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() { + if let Some(entry) = bibtex::Entry::cast(node.clone()) { + self.analyze_entry(entry); + } else if let Some(field) = bibtex::Field::cast(node.clone()) { + self.analyze_field(field); + } + } + } + + 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), + }); + + 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), + }); + + 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), + }); + } + } + + 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), + }); + + 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), + }); + } + } +} diff --git a/support/texlab/crates/diagnostics/src/grammar/tex.rs b/support/texlab/crates/diagnostics/src/grammar/tex.rs new file mode 100644 index 0000000000..b61ae4b020 --- /dev/null +++ b/support/texlab/crates/diagnostics/src/grammar/tex.rs @@ -0,0 +1,132 @@ +use base_db::{Config, Document, DocumentData, Workspace}; +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); + } + + fn publish<'db>( + &'db mut self, + workspace: &'db Workspace, + builder: &mut DiagnosticBuilder<'db>, + ) { + self.0.publish(workspace, builder); + } +} + +struct Analyzer<'a> { + document: &'a Document, + 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(); + while let Some(event) = traversal.next() { + match event { + rowan::WalkEvent::Enter(node) => { + if let Some(environment) = latex::Environment::cast(node.clone()) { + if environment + .begin() + .and_then(|begin| begin.name()) + .and_then(|name| name.key()) + .map_or(false, |name| verbatim_envs.contains(&name.to_string())) + { + traversal.skip_subtree(); + continue; + } + } + + self.analyze_environment(node.clone()) + .or_else(|| self.analyze_curly_group(node.clone())) + .or_else(|| self.analyze_curly_braces(node)); + } + rowan::WalkEvent::Leave(_) => { + continue; + } + }; + } + } + + fn analyze_environment(&mut self, node: latex::SyntaxNode) -> Option<()> { + let environment = latex::Environment::cast(node)?; + 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), + }); + } + + Some(()) + } + + fn analyze_curly_group(&mut self, node: latex::SyntaxNode) -> Option<()> { + if !matches!( + node.kind(), + latex::CURLY_GROUP + | latex::CURLY_GROUP_COMMAND + | latex::CURLY_GROUP_KEY_VALUE + | latex::CURLY_GROUP_WORD + | latex::CURLY_GROUP_WORD_LIST + ) { + return None; + } + + if !node + .children_with_tokens() + .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), + }); + } + + Some(()) + } + + 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), + }); + + Some(()) + } else { + None + } + } +} diff --git a/support/texlab/crates/diagnostics/src/labels.rs b/support/texlab/crates/diagnostics/src/labels.rs new file mode 100644 index 0000000000..03df664a15 --- /dev/null +++ b/support/texlab/crates/diagnostics/src/labels.rs @@ -0,0 +1,97 @@ +use std::borrow::Cow; + +use base_db::{ + semantics::tex::{Label, LabelKind}, + util::queries, + DocumentData, Workspace, +}; +use itertools::Itertools; +use rustc_hash::FxHashSet; + +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); + } +} + +fn detect_undefined_and_unused_labels<'db>( + workspace: &'db Workspace, + builder: &mut DiagnosticBuilder<'db>, +) { + let graphs: Vec<_> = workspace + .iter() + .map(|start| base_db::graph::Graph::new(workspace, start)) + .collect(); + + for document in workspace.iter() { + let DocumentData::Tex(data) = &document.data else { + continue; + }; + + let mut label_refs = FxHashSet::default(); + let mut label_defs = FxHashSet::default(); + let project = graphs + .iter() + .filter(|graph| graph.preorder().contains(&document)) + .flat_map(|graph| graph.preorder()); + + for label in project + .filter_map(|child| child.data.as_tex()) + .flat_map(|data| data.semantics.labels.iter()) + { + if label.kind == LabelKind::Definition { + label_defs.insert(&label.name.text); + } else { + label_refs.insert(&label.name.text); + } + } + + 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)); + } + + 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)); + } + } + } +} + +fn detect_duplicate_labels<'db>(workspace: &'db Workspace, builder: &mut DiagnosticBuilder<'db>) { + for conflict in queries::Conflict::find_all::<Label>(workspace) { + let others = conflict + .rest + .iter() + .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)); + } +} diff --git a/support/texlab/crates/diagnostics/src/lib.rs b/support/texlab/crates/diagnostics/src/lib.rs new file mode 100644 index 0000000000..e55ecb78e6 --- /dev/null +++ b/support/texlab/crates/diagnostics/src/lib.rs @@ -0,0 +1,86 @@ +mod build_log; +mod citations; +mod grammar; +mod labels; +pub mod types; +pub(crate) mod util; + +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 mut sources: Vec<Box<dyn DiagnosticSource>> = Vec::new(); + sources.push(Box::new(TexSyntaxErrors::default())); + sources.push(Box::new(BibSyntaxErrors::default())); + sources.push(Box::new(BuildErrors::default())); + sources.push(Box::new(LabelErrors::default())); + sources.push(Box::new(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); + } + } +} + +#[cfg(test)] +mod tests; diff --git a/support/texlab/crates/diagnostics/src/tests.rs b/support/texlab/crates/diagnostics/src/tests.rs new file mode 100644 index 0000000000..8b9534f337 --- /dev/null +++ b/support/texlab/crates/diagnostics/src/tests.rs @@ -0,0 +1,191 @@ +use std::borrow::Cow; + +use test_utils::fixture::Fixture; + +use crate::{ + types::{BibError, Diagnostic, DiagnosticData, TexError}, + DiagnosticBuilder, DiagnosticManager, DiagnosticSource, +}; + +fn check(input: &str, expected_data: &[DiagnosticData]) { + let 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); + } + + for document in fixture.workspace.iter() { + manager.update(&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())); + } + + assert_eq!(actual, expected); +} + +#[test] +fn test_bib_entry_missing_l_delim() { + check( + r#" +%! main.bib +@article + ! +"#, + &[DiagnosticData::Bib(BibError::ExpectingLCurly)], + ) +} + +#[test] +fn test_bib_entry_missing_r_delim() { + check( + r#" +%! main.bib +@article{foo, + ! + +%! main.tex +\bibliography{main} +\cite{foo} +"#, + &[DiagnosticData::Bib(BibError::ExpectingRCurly)], + ) +} + +#[test] +fn test_bib_entry_missing_name() { + check( + r#" +%! main.bib +@article{ + !"#, + &[DiagnosticData::Bib(BibError::ExpectingKey)], + ) +} + +#[test] +fn test_bib_field_missing_eq() { + check( + r#" +%! main.bib +@article{foo, + field + ! +} + +%! main.tex +\bibliography{main} +\cite{foo} +"#, + &[DiagnosticData::Bib(BibError::ExpectingEq)], + ) +} + +#[test] +fn test_bib_field_missing_value() { + check( + r#" +%! main.bib +@article{foo, + field = + ! +} + +%! main.tex +\bibliography{main} +\cite{foo} +"#, + &[DiagnosticData::Bib(BibError::ExpectingFieldValue)], + ) +} + +#[test] +fn test_tex_unmatched_braces() { + check( + r#" +%! main.tex +} +^ +{ + ! +"#, + &[ + DiagnosticData::Tex(TexError::UnexpectedRCurly), + DiagnosticData::Tex(TexError::ExpectingRCurly), + ], + ) +} + +#[test] +fn test_tex_environment_mismatched() { + check( + r#" +%! main.tex +\begin{foo} + ^^^ +\end{bar} +"#, + &[DiagnosticData::Tex(TexError::MismatchedEnvironment)], + ) +} + +#[test] +fn test_label_unused() { + check( + r#" +%! main.tex +\label{foo} + ^^^ +\label{bar}\ref{bar} +"#, + &[DiagnosticData::Tex(TexError::UnusedLabel)], + ) +} + +#[test] +fn test_label_undefined() { + check( + r#" +%! main.tex +\ref{foo} + ^^^ +"#, + &[DiagnosticData::Tex(TexError::UndefinedLabel)], + ) +} + +#[test] +fn test_citation_undefined() { + check( + r#" +%! main.tex +\cite{foo} + ^^^ +"#, + &[DiagnosticData::Tex(TexError::UndefinedCitation)], + ) +} + +#[test] +fn test_citation_unused() { + check( + r#" +%! main.bib +@article{foo,} + ^^^ +"#, + &[DiagnosticData::Bib(BibError::UnusedEntry)], + ) +} diff --git a/support/texlab/crates/diagnostics/src/types.rs b/support/texlab/crates/diagnostics/src/types.rs new file mode 100644 index 0000000000..a443245b6f --- /dev/null +++ b/support/texlab/crates/diagnostics/src/types.rs @@ -0,0 +1,38 @@ +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, + MismatchedEnvironment, + UnusedLabel, + UndefinedLabel, + UndefinedCitation, + DuplicateLabel(Vec<(Url, TextRange)>), +} + +#[derive(Debug, PartialEq, Eq, Clone)] +pub enum BibError { + ExpectingLCurly, + ExpectingKey, + ExpectingRCurly, + ExpectingEq, + ExpectingFieldValue, + UnusedEntry, + DuplicateEntry(Vec<(Url, TextRange)>), +} diff --git a/support/texlab/crates/diagnostics/src/util.rs b/support/texlab/crates/diagnostics/src/util.rs new file mode 100644 index 0000000000..fd34125dbe --- /dev/null +++ b/support/texlab/crates/diagnostics/src/util.rs @@ -0,0 +1,28 @@ +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)); + } + } + } +} |