Skip to content

Commit

Permalink
Allow ignoring diagnostics with magic comments (#1211)
Browse files Browse the repository at this point in the history
With this changeset, using `%texlab: ignore` magic comment suppresses diagnostics from all sources for the current and the next line.

Fixes #1092.
Fixes #1131.
  • Loading branch information
pfoerster authored Oct 7, 2024
1 parent a5edc54 commit 3d6def1
Show file tree
Hide file tree
Showing 5 changed files with 230 additions and 21 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0
### Added

- Add `texlab.inlayHints.maxLength` setting to allow limiting inlay hint text length ([#1212](https://github.com/latex-lsp/texlab/issues/1212))
- Allow suppressing diagnostics using `% texlab: ignore` magic comments ([#1211](https://github.com/latex-lsp/texlab/pull/1211))

### Fixed

Expand Down
11 changes: 8 additions & 3 deletions crates/base-db/src/semantics/tex.rs
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,7 @@ pub struct Semantics {
pub graphics_paths: FxHashSet<String>,
pub can_be_root: bool,
pub can_be_compiled: bool,
pub diagnostic_suppressions: Vec<TextRange>,
}

impl Semantics {
Expand All @@ -46,11 +47,15 @@ impl Semantics {
latex::SyntaxElement::Node(node) => {
self.process_node(conf, &node);
}
latex::SyntaxElement::Token(token) => {
if token.kind() == latex::COMMAND_NAME {
latex::SyntaxElement::Token(token) => match token.kind() {
latex::COMMAND_NAME => {
self.commands.push(Span::command(&token));
}
}
latex::COMMENT if token.text().contains("texlab: ignore") => {
self.diagnostic_suppressions.push(token.text_range());
}
_ => {}
},
};
}

Expand Down
83 changes: 68 additions & 15 deletions crates/diagnostics/src/manager.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
use base_db::{deps::Project, util::filter_regex_patterns, Document, Owner, Workspace};
use base_db::{
deps::Project, util::filter_regex_patterns, Document, DocumentData, Owner, Workspace,
};
use multimap::MultiMap;
use rowan::TextRange;
use rustc_hash::{FxHashMap, FxHashSet};
use url::Url;

Expand All @@ -16,7 +19,7 @@ pub struct Manager {
impl Manager {
/// Updates the syntax-based diagnostics for the given document.
pub fn update_syntax(&mut self, workspace: &Workspace, document: &Document) {
if !Self::is_relevant(document) {
if !Self::is_relevant_document(document) {
return;
}

Expand Down Expand Up @@ -76,7 +79,7 @@ impl Manager {

for document in workspace
.iter()
.filter(|document| Self::is_relevant(document))
.filter(|document| Self::is_relevant_document(document))
{
let project = Project::from_child(workspace, document);
super::citations::detect_undefined_citations(&project, document, &mut results);
Expand All @@ -87,28 +90,78 @@ impl Manager {
super::labels::detect_duplicate_labels(workspace, &mut results);
super::labels::detect_undefined_and_unused_labels(workspace, &mut results);

let config = &workspace.config().diagnostics;

results.retain(|uri, _| workspace.lookup(uri).map_or(false, Self::is_relevant));
results.retain(|uri, _| {
workspace
.lookup(uri)
.map_or(false, Self::is_relevant_document)
});

for diagnostics in results.values_mut() {
diagnostics.retain(|diagnostic| {
filter_regex_patterns(
diagnostic.message(),
&config.allowed_patterns,
&config.ignored_patterns,
)
});
for (uri, diagnostics) in results.iter_mut() {
diagnostics
.retain_mut(|diagnostic| Self::filter_diagnostic(workspace, uri, diagnostic));
}

results
}

fn is_relevant(document: &Document) -> bool {
fn is_relevant_document(document: &Document) -> bool {
match document.owner {
Owner::Client => true,
Owner::Server => true,
Owner::Distro => false,
}
}

fn filter_diagnostic(workspace: &Workspace, uri: &Url, diagnostic: &mut Diagnostic) -> bool {
let config = &workspace.config().diagnostics;

if !filter_regex_patterns(
diagnostic.message(),
&config.allowed_patterns,
&config.ignored_patterns,
) {
return false;
}

let Some(document) = workspace.lookup(uri) else {
return false;
};

let Some(primary_range) = diagnostic.range(&document.line_index) else {
return false;
};

if Self::is_ignored(workspace, &document.uri, &primary_range) {
return false;
}

let Some(additional_locations) = diagnostic.additional_locations_mut() else {
return true;
};

additional_locations.retain(|(uri, range)| !Self::is_ignored(workspace, uri, range));
if additional_locations.is_empty() {
return false;
}

true
}

fn is_ignored(workspace: &Workspace, uri: &Url, diag_range: &TextRange) -> bool {
let Some(document) = workspace.lookup(uri) else {
return false;
};

let DocumentData::Tex(data) = &document.data else {
return false;
};

let diag_line_col = document.line_index.line_col(diag_range.start());

data.semantics
.diagnostic_suppressions
.iter()
.map(|r| document.line_index.line_col(r.start()))
.any(|r| r.line == diag_line_col.line || r.line + 1 == diag_line_col.line)
}
}
70 changes: 70 additions & 0 deletions crates/diagnostics/src/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,33 @@ fn test_citation_undefined() {
)
}

#[test]
fn test_citation_undefined_ignore() {
check(
r#"
%! main.tex
% texlab: ignore
\cite{foo}
"#,
expect![[r#"
[]
"#]],
)
}

#[test]
fn test_citation_undefined_ignore_single_line() {
check(
r#"
%! main.tex
\cite{foo} % texlab: ignore
"#,
expect![[r#"
[]
"#]],
)
}

#[test]
fn test_citation_unused() {
check(
Expand All @@ -306,3 +333,46 @@ fn test_citation_unused() {
"#]],
)
}

#[test]
fn test_label_duplicate() {
check(
r#"
%! main.tex
\label{foo}
^^^
\label{foo}
^^^
\label{foo} % texlab: ignore
\ref{foo}
"#,
expect![[r#"
[
(
"file:///texlab/main.tex",
[
Tex(
7..10,
DuplicateLabel(
(
"file:///texlab/main.tex",
19..22,
),
),
),
Tex(
19..22,
DuplicateLabel(
(
"file:///texlab/main.tex",
7..10,
),
),
),
],
),
]
"#]],
)
}
86 changes: 83 additions & 3 deletions crates/diagnostics/src/types.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
use line_index::LineCol;
use line_index::{LineCol, LineIndex};
use rowan::TextRange;
use syntax::BuildError;
use url::Url;

#[derive(Debug, PartialEq, Eq, Clone)]
#[derive(PartialEq, Eq, Clone)]
pub enum TexError {
UnexpectedRCurly,
ExpectingRCurly,
Expand All @@ -14,7 +14,28 @@ pub enum TexError {
DuplicateLabel(Vec<(Url, TextRange)>),
}

#[derive(Debug, PartialEq, Eq, Clone)]
impl std::fmt::Debug for TexError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::UnexpectedRCurly => write!(f, "UnexpectedRCurly"),
Self::ExpectingRCurly => write!(f, "ExpectingRCurly"),
Self::MismatchedEnvironment => write!(f, "MismatchedEnvironment"),
Self::UnusedLabel => write!(f, "UnusedLabel"),
Self::UndefinedLabel => write!(f, "UndefinedLabel"),
Self::UndefinedCitation => write!(f, "UndefinedCitation"),
Self::DuplicateLabel(locations) => {
let mut t = f.debug_tuple("DuplicateLabel");
for (uri, range) in locations {
t.field(&(uri.as_str(), range));
}

t.finish()
}
}
}
}

#[derive(PartialEq, Eq, Clone)]
pub enum BibError {
ExpectingLCurly,
ExpectingKey,
Expand All @@ -25,6 +46,27 @@ pub enum BibError {
DuplicateEntry(Vec<(Url, TextRange)>),
}

impl std::fmt::Debug for BibError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::ExpectingLCurly => write!(f, "ExpectingLCurly"),
Self::ExpectingKey => write!(f, "ExpectingKey"),
Self::ExpectingRCurly => write!(f, "ExpectingRCurly"),
Self::ExpectingEq => write!(f, "ExpectingEq"),
Self::ExpectingFieldValue => write!(f, "ExpectingFieldValue"),
Self::UnusedEntry => write!(f, "UnusedEntry"),
Self::DuplicateEntry(locations) => {
let mut t = f.debug_tuple("DuplicateEntry");
for (uri, range) in locations {
t.field(&(uri.as_str(), range));
}

t.finish()
}
}
}
}

#[derive(Debug, PartialEq, Eq, Clone)]
pub struct ChktexError {
pub start: LineCol,
Expand Down Expand Up @@ -74,4 +116,42 @@ impl Diagnostic {
Diagnostic::Chktex(error) => &error.message,
}
}

pub fn range(&self, line_index: &LineIndex) -> Option<TextRange> {
Some(match self {
Diagnostic::Tex(range, _) => *range,
Diagnostic::Bib(range, _) => *range,
Diagnostic::Build(range, _) => *range,
Diagnostic::Chktex(error) => {
let start = line_index.offset(error.start)?;
let end = line_index.offset(error.end)?;
TextRange::new(start, end)
}
})
}

pub fn additional_locations_mut(&mut self) -> Option<&mut Vec<(Url, TextRange)>> {
match self {
Diagnostic::Tex(_, err) => match err {
TexError::UnexpectedRCurly
| TexError::ExpectingRCurly
| TexError::MismatchedEnvironment
| TexError::UnusedLabel
| TexError::UndefinedLabel
| TexError::UndefinedCitation => None,
TexError::DuplicateLabel(locations) => Some(locations),
},
Diagnostic::Bib(_, err) => match err {
BibError::ExpectingLCurly
| BibError::ExpectingKey
| BibError::ExpectingRCurly
| BibError::ExpectingEq
| BibError::ExpectingFieldValue
| BibError::UnusedEntry => None,
BibError::DuplicateEntry(locations) => Some(locations),
},
Diagnostic::Chktex(_) => None,
Diagnostic::Build(_, _) => None,
}
}
}

0 comments on commit 3d6def1

Please sign in to comment.