5682: Add an option to disable diagnostics r=matklad a=popzxc

As far as I know, currently it's not possible to disable a selected type of diagnostics provided by `rust-analyzer`.

This causes an inconvenient situation with a false-positive warnings: you either have to disable all the diagnostics, or you have to ignore these warnings.

There are some open issues related to this problem, e.g.: https://github.com/rust-analyzer/rust-analyzer/issues/5412, https://github.com/rust-analyzer/rust-analyzer/issues/5502

This PR attempts to make it possible to selectively disable some diagnostics on per-project basis.

Co-authored-by: Igor Aleksanov <popzxc@yandex.ru>
This commit is contained in:
bors[bot] 2020-08-18 12:04:49 +00:00 committed by GitHub
commit b8dfc331ab
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 155 additions and 15 deletions

View file

@ -15,6 +15,9 @@ pub struct UnresolvedModule {
}
impl Diagnostic for UnresolvedModule {
fn name(&self) -> &'static str {
"unresolved-module"
}
fn message(&self) -> String {
"unresolved module".to_string()
}

View file

@ -21,6 +21,7 @@ use syntax::SyntaxNodePtr;
use crate::InFile;
pub trait Diagnostic: Any + Send + Sync + fmt::Debug + 'static {
fn name(&self) -> &'static str;
fn message(&self) -> String;
/// Used in highlighting and related purposes
fn display_source(&self) -> InFile<SyntaxNodePtr>;

View file

@ -32,6 +32,10 @@ pub struct NoSuchField {
}
impl Diagnostic for NoSuchField {
fn name(&self) -> &'static str {
"no-such-field"
}
fn message(&self) -> String {
"no such field".to_string()
}
@ -54,6 +58,9 @@ pub struct MissingFields {
}
impl Diagnostic for MissingFields {
fn name(&self) -> &'static str {
"missing-structure-fields"
}
fn message(&self) -> String {
let mut buf = String::from("Missing structure fields:\n");
for field in &self.missed_fields {
@ -87,6 +94,9 @@ pub struct MissingPatFields {
}
impl Diagnostic for MissingPatFields {
fn name(&self) -> &'static str {
"missing-pat-fields"
}
fn message(&self) -> String {
let mut buf = String::from("Missing structure fields:\n");
for field in &self.missed_fields {
@ -117,6 +127,9 @@ pub struct MissingMatchArms {
}
impl Diagnostic for MissingMatchArms {
fn name(&self) -> &'static str {
"missing-match-arm"
}
fn message(&self) -> String {
String::from("Missing match arm")
}
@ -135,6 +148,9 @@ pub struct MissingOkInTailExpr {
}
impl Diagnostic for MissingOkInTailExpr {
fn name(&self) -> &'static str {
"missing-ok-in-tail-expr"
}
fn message(&self) -> String {
"wrap return expression in Ok".to_string()
}
@ -153,6 +169,9 @@ pub struct BreakOutsideOfLoop {
}
impl Diagnostic for BreakOutsideOfLoop {
fn name(&self) -> &'static str {
"break-outside-of-loop"
}
fn message(&self) -> String {
"break outside of loop".to_string()
}
@ -171,6 +190,9 @@ pub struct MissingUnsafe {
}
impl Diagnostic for MissingUnsafe {
fn name(&self) -> &'static str {
"missing-unsafe"
}
fn message(&self) -> String {
format!("This operation is unsafe and requires an unsafe function or block")
}
@ -191,6 +213,9 @@ pub struct MismatchedArgCount {
}
impl Diagnostic for MismatchedArgCount {
fn name(&self) -> &'static str {
"mismatched-arg-count"
}
fn message(&self) -> String {
let s = if self.expected == 1 { "" } else { "s" };
format!("Expected {} argument{}, found {}", self.expected, s, self.found)

View file

@ -4,7 +4,7 @@
//! macro-expanded files, but we need to present them to the users in terms of
//! original files. So we need to map the ranges.
use std::cell::RefCell;
use std::{cell::RefCell, collections::HashSet};
use base_db::SourceDatabase;
use hir::{diagnostics::DiagnosticSinkBuilder, Semantics};
@ -31,6 +31,7 @@ pub(crate) fn diagnostics(
db: &RootDatabase,
file_id: FileId,
enable_experimental: bool,
disabled_diagnostics: Option<HashSet<String>>,
) -> Vec<Diagnostic> {
let _p = profile::span("diagnostics");
let sema = Semantics::new(db);
@ -39,6 +40,7 @@ pub(crate) fn diagnostics(
// [#34344] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
res.extend(parse.errors().iter().take(128).map(|err| Diagnostic {
name: None,
range: err.range(),
message: format!("Syntax Error: {}", err),
severity: Severity::Error,
@ -50,7 +52,7 @@ pub(crate) fn diagnostics(
check_struct_shorthand_initialization(&mut res, file_id, &node);
}
let res = RefCell::new(res);
let mut sink = DiagnosticSinkBuilder::new()
let mut sink_builder = DiagnosticSinkBuilder::new()
.on::<hir::diagnostics::UnresolvedModule, _>(|d| {
res.borrow_mut().push(diagnostic_with_fix(d, &sema));
})
@ -64,10 +66,19 @@ pub(crate) fn diagnostics(
res.borrow_mut().push(diagnostic_with_fix(d, &sema));
})
// Only collect experimental diagnostics when they're enabled.
.filter(|diag| !diag.is_experimental() || enable_experimental)
.filter(|diag| !diag.is_experimental() || enable_experimental);
if let Some(disabled_diagnostics) = disabled_diagnostics {
// Do not collect disabled diagnostics.
sink_builder = sink_builder.filter(move |diag| !disabled_diagnostics.contains(diag.name()));
}
// Finalize the `DiagnosticSink` building process.
let mut sink = sink_builder
// Diagnostics not handled above get no fix and default treatment.
.build(|d| {
res.borrow_mut().push(Diagnostic {
name: Some(d.name().into()),
message: d.message(),
range: sema.diagnostics_display_range(d).range,
severity: Severity::Error,
@ -84,6 +95,7 @@ pub(crate) fn diagnostics(
fn diagnostic_with_fix<D: DiagnosticWithFix>(d: &D, sema: &Semantics<RootDatabase>) -> Diagnostic {
Diagnostic {
name: Some(d.name().into()),
range: sema.diagnostics_display_range(d).range,
message: d.message(),
severity: Severity::Error,
@ -110,6 +122,7 @@ fn check_unnecessary_braces_in_use_statement(
});
acc.push(Diagnostic {
name: None,
range: use_range,
message: "Unnecessary braces in use statement".to_string(),
severity: Severity::WeakWarning,
@ -156,6 +169,7 @@ fn check_struct_shorthand_initialization(
let field_range = record_field.syntax().text_range();
acc.push(Diagnostic {
name: None,
range: field_range,
message: "Shorthand struct initialization".to_string(),
severity: Severity::WeakWarning,
@ -173,6 +187,7 @@ fn check_struct_shorthand_initialization(
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use stdx::trim_indent;
use test_utils::assert_eq_text;
@ -188,7 +203,8 @@ mod tests {
let after = trim_indent(ra_fixture_after);
let (analysis, file_position) = analysis_and_position(ra_fixture_before);
let diagnostic = analysis.diagnostics(file_position.file_id, true).unwrap().pop().unwrap();
let diagnostic =
analysis.diagnostics(file_position.file_id, true, None).unwrap().pop().unwrap();
let mut fix = diagnostic.fix.unwrap();
let edit = fix.source_change.source_file_edits.pop().unwrap().edit;
let target_file_contents = analysis.file_text(file_position.file_id).unwrap();
@ -214,7 +230,7 @@ mod tests {
let ra_fixture_after = &trim_indent(ra_fixture_after);
let (analysis, file_pos) = analysis_and_position(ra_fixture_before);
let current_file_id = file_pos.file_id;
let diagnostic = analysis.diagnostics(current_file_id, true).unwrap().pop().unwrap();
let diagnostic = analysis.diagnostics(current_file_id, true, None).unwrap().pop().unwrap();
let mut fix = diagnostic.fix.unwrap();
let edit = fix.source_change.source_file_edits.pop().unwrap();
let changed_file_id = edit.file_id;
@ -235,14 +251,58 @@ mod tests {
let analysis = mock.analysis();
let diagnostics = files
.into_iter()
.flat_map(|file_id| analysis.diagnostics(file_id, true).unwrap())
.flat_map(|file_id| analysis.diagnostics(file_id, true, None).unwrap())
.collect::<Vec<_>>();
assert_eq!(diagnostics.len(), 0, "unexpected diagnostics:\n{:#?}", diagnostics);
}
/// Takes a multi-file input fixture with annotated cursor position and the list of disabled diagnostics,
/// and checks that provided diagnostics aren't spawned during analysis.
fn check_disabled_diagnostics(ra_fixture: &str, disabled_diagnostics: &[&'static str]) {
let disabled_diagnostics: HashSet<_> =
disabled_diagnostics.into_iter().map(|diag| diag.to_string()).collect();
let mock = MockAnalysis::with_files(ra_fixture);
let files = mock.files().map(|(it, _)| it).collect::<Vec<_>>();
let analysis = mock.analysis();
let diagnostics = files
.clone()
.into_iter()
.flat_map(|file_id| {
analysis.diagnostics(file_id, true, Some(disabled_diagnostics.clone())).unwrap()
})
.collect::<Vec<_>>();
// First, we have to check that diagnostic is not emitted when it's added to the disabled diagnostics list.
for diagnostic in diagnostics {
if let Some(name) = diagnostic.name {
assert!(!disabled_diagnostics.contains(&name), "Diagnostic {} is disabled", name);
}
}
// Then, we must reset the config and repeat the check, so that we'll be sure that without
// config these diagnostics are emitted.
// This is required for tests to not become outdated if e.g. diagnostics name changes:
// without this additional run the test will pass simply because a diagnostic with an old name
// will no longer exist.
let diagnostics = files
.into_iter()
.flat_map(|file_id| analysis.diagnostics(file_id, true, None).unwrap())
.collect::<Vec<_>>();
assert!(
diagnostics
.into_iter()
.filter_map(|diag| diag.name)
.any(|name| disabled_diagnostics.contains(&name)),
"At least one of the diagnostics was not emitted even without config; are the diagnostics names correct?"
);
}
fn check_expect(ra_fixture: &str, expect: Expect) {
let (analysis, file_id) = single_file(ra_fixture);
let diagnostics = analysis.diagnostics(file_id, true).unwrap();
let diagnostics = analysis.diagnostics(file_id, true, None).unwrap();
expect.assert_debug_eq(&diagnostics)
}
@ -502,6 +562,9 @@ fn test_fn() {
expect![[r#"
[
Diagnostic {
name: Some(
"unresolved-module",
),
message: "unresolved module",
range: 0..8,
severity: Error,
@ -675,4 +738,9 @@ struct Foo {
",
)
}
#[test]
fn test_disabled_diagnostics() {
check_disabled_diagnostics(r#"mod foo;"#, &["unresolved-module"]);
}
}

View file

@ -44,7 +44,7 @@ mod syntax_highlighting;
mod syntax_tree;
mod typing;
use std::sync::Arc;
use std::{collections::HashSet, sync::Arc};
use base_db::{
salsa::{self, ParallelDatabase},
@ -101,6 +101,7 @@ pub type Cancelable<T> = Result<T, Canceled>;
#[derive(Debug)]
pub struct Diagnostic {
pub name: Option<String>,
pub message: String,
pub range: TextRange,
pub severity: Severity,
@ -147,7 +148,7 @@ pub struct AnalysisHost {
}
impl AnalysisHost {
pub fn new(lru_capacity: Option<usize>) -> AnalysisHost {
pub fn new(lru_capacity: Option<usize>) -> Self {
AnalysisHost { db: RootDatabase::new(lru_capacity) }
}
@ -496,8 +497,11 @@ impl Analysis {
&self,
file_id: FileId,
enable_experimental: bool,
disabled_diagnostics: Option<HashSet<String>>,
) -> Cancelable<Vec<Diagnostic>> {
self.with_db(|db| diagnostics::diagnostics(db, file_id, enable_experimental))
self.with_db(|db| {
diagnostics::diagnostics(db, file_id, enable_experimental, disabled_diagnostics)
})
}
/// Returns the edit required to rename reference at the position to the new

View file

@ -71,7 +71,7 @@ impl BenchCmd {
match &self.what {
BenchWhat::Highlight { .. } => {
let res = do_work(&mut host, file_id, |analysis| {
analysis.diagnostics(file_id, true).unwrap();
analysis.diagnostics(file_id, true, None).unwrap();
analysis.highlight_as_html(file_id, false).unwrap()
});
if verbosity.is_verbose() {

View file

@ -47,7 +47,7 @@ pub fn diagnostics(
String::from("unknown")
};
println!("processing crate: {}, module: {}", crate_name, _vfs.file_path(file_id));
for diagnostic in analysis.diagnostics(file_id, true).unwrap() {
for diagnostic in analysis.diagnostics(file_id, true, None).unwrap() {
if matches!(diagnostic.severity, Severity::Error) {
found_error = true;
}

View file

@ -7,7 +7,7 @@
//! configure the server itself, feature flags are passed into analysis, and
//! tweak things like automatic insertion of `()` in completions.
use std::{ffi::OsString, path::PathBuf};
use std::{collections::HashSet, ffi::OsString, path::PathBuf};
use flycheck::FlycheckConfig;
use ide::{AssistConfig, CompletionConfig, HoverConfig, InlayHintsConfig};
@ -45,6 +45,14 @@ pub struct Config {
pub with_sysroot: bool,
pub linked_projects: Vec<LinkedProject>,
pub root_path: AbsPathBuf,
pub analysis: AnalysisConfig,
}
/// Configuration parameters for the analysis run.
#[derive(Debug, Default, Clone)]
pub struct AnalysisConfig {
pub disabled_diagnostics: HashSet<String>,
}
#[derive(Debug, Clone, Eq, PartialEq)]
@ -176,6 +184,8 @@ impl Config {
hover: HoverConfig::default(),
linked_projects: Vec::new(),
root_path,
analysis: AnalysisConfig::default(),
}
}
@ -293,6 +303,8 @@ impl Config {
goto_type_def: data.hoverActions_enable && data.hoverActions_gotoTypeDef,
};
self.analysis = AnalysisConfig { disabled_diagnostics: data.analysis_disabledDiagnostics };
log::info!("Config::update() = {:#?}", self);
}
@ -357,6 +369,14 @@ impl Config {
self.client_caps.status_notification = get_bool("statusNotification");
}
}
pub fn disabled_diagnostics(&self) -> Option<HashSet<String>> {
if self.analysis.disabled_diagnostics.is_empty() {
None
} else {
Some(self.analysis.disabled_diagnostics.clone())
}
}
}
#[derive(Deserialize)]
@ -444,5 +464,7 @@ config_data! {
rustfmt_overrideCommand: Option<Vec<String>> = None,
withSysroot: bool = true,
analysis_disabledDiagnostics: HashSet<String> = HashSet::new(),
}
}

View file

@ -775,7 +775,11 @@ fn handle_fixes(
None => {}
};
let diagnostics = snap.analysis.diagnostics(file_id, snap.config.experimental_diagnostics)?;
let diagnostics = snap.analysis.diagnostics(
file_id,
snap.config.experimental_diagnostics,
snap.config.disabled_diagnostics(),
)?;
for fix in diagnostics
.into_iter()
@ -1049,7 +1053,11 @@ pub(crate) fn publish_diagnostics(
let line_index = snap.analysis.file_line_index(file_id)?;
let diagnostics: Vec<Diagnostic> = snap
.analysis
.diagnostics(file_id, snap.config.experimental_diagnostics)?
.diagnostics(
file_id,
snap.config.experimental_diagnostics,
snap.config.disabled_diagnostics(),
)?
.into_iter()
.map(|d| Diagnostic {
range: to_proto::range(&line_index, d.range),