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),

View file

@ -609,6 +609,15 @@
},
"description": "List of warnings that should be displayed with hint severity.\nThe warnings will be indicated by faded text or three dots in code and will not show up in the problems panel.",
"default": []
},
"rust-analyzer.analysis.disabledDiagnostics": {
"type": "array",
"uniqueItems": true,
"items": {
"type": "string"
},
"description": "List of rust-analyzer diagnostics to disable",
"default": []
}
}
},