mirror of
https://github.com/rust-lang/rust-analyzer.git
synced 2025-10-02 14:51:48 +00:00

The previous analysis was top-down, and worked on a single file (expanding macros). The new analysis is bottom-up, starting from the diagnostics and climbing up the syntax and module tree. While this is more efficient (and in fact, efficiency was the motivating reason to work on this), unfortunately the code was already fast enough. But luckily, it also fixes a correctness problem: outline parent modules' attributes were not respected for the previous analysis. Case lints specifically did their own analysis to accommodate that, but it was limited to only them. The new analysis works on all kinds of lints, present and future. It was basically impossible to fix the old analysis without rewriting it because navigating the module hierarchy must come bottom-up, and if we already have a bottom-up analysis (including syntax analysis because modules can be nested in other syntax elements, including macros), it makes sense to use only this kind of analysis. Few other bugs (not fundamental ti the previous analysis) are also fixed, e.g. overwriting of lint levels (i.e. `#[allow(lint)] mod foo { #[warn(lint)] mod bar; }`.
823 lines
33 KiB
Rust
823 lines
33 KiB
Rust
//! Diagnostics rendering and fixits.
|
|
//!
|
|
//! Most of the diagnostics originate from the dark depth of the compiler, and
|
|
//! are originally expressed in term of IR. When we emit the diagnostic, we are
|
|
//! usually not in the position to decide how to best "render" it in terms of
|
|
//! user-authored source code. We are especially not in the position to offer
|
|
//! fixits, as the compiler completely lacks the infrastructure to edit the
|
|
//! source code.
|
|
//!
|
|
//! Instead, we "bubble up" raw, structured diagnostics until the `hir` crate,
|
|
//! where we "cook" them so that each diagnostic is formulated in terms of `hir`
|
|
//! types. Well, at least that's the aspiration, the "cooking" is somewhat
|
|
//! ad-hoc at the moment. Anyways, we get a bunch of ide-friendly diagnostic
|
|
//! structs from hir, and we want to render them to unified serializable
|
|
//! representation (span, level, message) here. If we can, we also provide
|
|
//! fixits. By the way, that's why we want to keep diagnostics structured
|
|
//! internally -- so that we have all the info to make fixes.
|
|
//!
|
|
//! We have one "handler" module per diagnostic code. Such a module contains
|
|
//! rendering, optional fixes and tests. It's OK if some low-level compiler
|
|
//! functionality ends up being tested via a diagnostic.
|
|
//!
|
|
//! There are also a couple of ad-hoc diagnostics implemented directly here, we
|
|
//! don't yet have a great pattern for how to do them properly.
|
|
|
|
mod handlers {
|
|
pub(crate) mod await_outside_of_async;
|
|
pub(crate) mod break_outside_of_loop;
|
|
pub(crate) mod expected_function;
|
|
pub(crate) mod inactive_code;
|
|
pub(crate) mod incoherent_impl;
|
|
pub(crate) mod incorrect_case;
|
|
pub(crate) mod invalid_cast;
|
|
pub(crate) mod invalid_derive_target;
|
|
pub(crate) mod macro_error;
|
|
pub(crate) mod malformed_derive;
|
|
pub(crate) mod mismatched_arg_count;
|
|
pub(crate) mod missing_fields;
|
|
pub(crate) mod missing_match_arms;
|
|
pub(crate) mod missing_unsafe;
|
|
pub(crate) mod moved_out_of_ref;
|
|
pub(crate) mod mutability_errors;
|
|
pub(crate) mod no_such_field;
|
|
pub(crate) mod non_exhaustive_let;
|
|
pub(crate) mod private_assoc_item;
|
|
pub(crate) mod private_field;
|
|
pub(crate) mod remove_trailing_return;
|
|
pub(crate) mod remove_unnecessary_else;
|
|
pub(crate) mod replace_filter_map_next_with_find_map;
|
|
pub(crate) mod trait_impl_incorrect_safety;
|
|
pub(crate) mod trait_impl_missing_assoc_item;
|
|
pub(crate) mod trait_impl_orphan;
|
|
pub(crate) mod trait_impl_redundant_assoc_item;
|
|
pub(crate) mod type_mismatch;
|
|
pub(crate) mod typed_hole;
|
|
pub(crate) mod undeclared_label;
|
|
pub(crate) mod unimplemented_builtin_macro;
|
|
pub(crate) mod unreachable_label;
|
|
pub(crate) mod unresolved_assoc_item;
|
|
pub(crate) mod unresolved_extern_crate;
|
|
pub(crate) mod unresolved_field;
|
|
pub(crate) mod unresolved_ident;
|
|
pub(crate) mod unresolved_import;
|
|
pub(crate) mod unresolved_macro_call;
|
|
pub(crate) mod unresolved_method;
|
|
pub(crate) mod unresolved_module;
|
|
pub(crate) mod unused_variables;
|
|
|
|
// The handlers below are unusual, the implement the diagnostics as well.
|
|
pub(crate) mod field_shorthand;
|
|
pub(crate) mod json_is_not_rust;
|
|
pub(crate) mod unlinked_file;
|
|
pub(crate) mod useless_braces;
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests;
|
|
|
|
use std::{collections::hash_map, sync::LazyLock};
|
|
|
|
use hir::{diagnostics::AnyDiagnostic, HirFileId, InFile, Semantics};
|
|
use ide_db::{
|
|
assists::{Assist, AssistId, AssistKind, AssistResolveStrategy},
|
|
base_db::SourceDatabase,
|
|
generated::lints::{LintGroup, CLIPPY_LINT_GROUPS, DEFAULT_LINT_GROUPS},
|
|
imports::insert_use::InsertUseConfig,
|
|
label::Label,
|
|
source_change::SourceChange,
|
|
syntax_helpers::node_ext::parse_tt_as_comma_sep_paths,
|
|
EditionedFileId, FileId, FileRange, FxHashMap, FxHashSet, RootDatabase, SnippetCap,
|
|
};
|
|
use itertools::Itertools;
|
|
use syntax::{
|
|
ast::{self, AstNode, HasAttrs},
|
|
AstPtr, Edition, SmolStr, SyntaxNode, SyntaxNodePtr, TextRange,
|
|
};
|
|
|
|
// FIXME: Make this an enum
|
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
|
pub enum DiagnosticCode {
|
|
RustcHardError(&'static str),
|
|
SyntaxError,
|
|
RustcLint(&'static str),
|
|
Clippy(&'static str),
|
|
Ra(&'static str, Severity),
|
|
}
|
|
|
|
impl DiagnosticCode {
|
|
pub fn url(&self) -> String {
|
|
match self {
|
|
DiagnosticCode::RustcHardError(e) => {
|
|
format!("https://doc.rust-lang.org/stable/error_codes/{e}.html")
|
|
}
|
|
DiagnosticCode::SyntaxError => {
|
|
String::from("https://doc.rust-lang.org/stable/reference/")
|
|
}
|
|
DiagnosticCode::RustcLint(e) => {
|
|
format!("https://doc.rust-lang.org/rustc/?search={e}")
|
|
}
|
|
DiagnosticCode::Clippy(e) => {
|
|
format!("https://rust-lang.github.io/rust-clippy/master/#/{e}")
|
|
}
|
|
DiagnosticCode::Ra(e, _) => {
|
|
format!("https://rust-analyzer.github.io/manual.html#{e}")
|
|
}
|
|
}
|
|
}
|
|
|
|
pub fn as_str(&self) -> &'static str {
|
|
match self {
|
|
DiagnosticCode::RustcHardError(r)
|
|
| DiagnosticCode::RustcLint(r)
|
|
| DiagnosticCode::Clippy(r)
|
|
| DiagnosticCode::Ra(r, _) => r,
|
|
DiagnosticCode::SyntaxError => "syntax-error",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug)]
|
|
pub struct Diagnostic {
|
|
pub code: DiagnosticCode,
|
|
pub message: String,
|
|
pub range: FileRange,
|
|
pub severity: Severity,
|
|
pub unused: bool,
|
|
pub experimental: bool,
|
|
pub fixes: Option<Vec<Assist>>,
|
|
// The node that will be affected by `#[allow]` and similar attributes.
|
|
pub main_node: Option<InFile<SyntaxNodePtr>>,
|
|
}
|
|
|
|
impl Diagnostic {
|
|
fn new(
|
|
code: DiagnosticCode,
|
|
message: impl Into<String>,
|
|
range: impl Into<FileRange>,
|
|
) -> Diagnostic {
|
|
let message = message.into();
|
|
Diagnostic {
|
|
code,
|
|
message,
|
|
range: range.into(),
|
|
severity: match code {
|
|
DiagnosticCode::RustcHardError(_) | DiagnosticCode::SyntaxError => Severity::Error,
|
|
// FIXME: Rustc lints are not always warning, but the ones that are currently implemented are all warnings.
|
|
DiagnosticCode::RustcLint(_) => Severity::Warning,
|
|
// FIXME: We can make this configurable, and if the user uses `cargo clippy` on flycheck, we can
|
|
// make it normal warning.
|
|
DiagnosticCode::Clippy(_) => Severity::WeakWarning,
|
|
DiagnosticCode::Ra(_, s) => s,
|
|
},
|
|
unused: false,
|
|
experimental: false,
|
|
fixes: None,
|
|
main_node: None,
|
|
}
|
|
}
|
|
|
|
fn new_with_syntax_node_ptr(
|
|
ctx: &DiagnosticsContext<'_>,
|
|
code: DiagnosticCode,
|
|
message: impl Into<String>,
|
|
node: InFile<SyntaxNodePtr>,
|
|
) -> Diagnostic {
|
|
Diagnostic::new(code, message, ctx.sema.diagnostics_display_range(node))
|
|
.with_main_node(node)
|
|
}
|
|
|
|
fn experimental(mut self) -> Diagnostic {
|
|
self.experimental = true;
|
|
self
|
|
}
|
|
|
|
fn with_main_node(mut self, main_node: InFile<SyntaxNodePtr>) -> Diagnostic {
|
|
self.main_node = Some(main_node);
|
|
self
|
|
}
|
|
|
|
fn with_fixes(mut self, fixes: Option<Vec<Assist>>) -> Diagnostic {
|
|
self.fixes = fixes;
|
|
self
|
|
}
|
|
|
|
fn with_unused(mut self, unused: bool) -> Diagnostic {
|
|
self.unused = unused;
|
|
self
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash)]
|
|
pub enum Severity {
|
|
Error,
|
|
Warning,
|
|
WeakWarning,
|
|
Allow,
|
|
}
|
|
|
|
#[derive(Clone, Debug, PartialEq, Eq)]
|
|
pub enum ExprFillDefaultMode {
|
|
Todo,
|
|
Default,
|
|
}
|
|
impl Default for ExprFillDefaultMode {
|
|
fn default() -> Self {
|
|
Self::Todo
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone)]
|
|
pub struct DiagnosticsConfig {
|
|
/// Whether native diagnostics are enabled.
|
|
pub enabled: bool,
|
|
pub proc_macros_enabled: bool,
|
|
pub proc_attr_macros_enabled: bool,
|
|
pub disable_experimental: bool,
|
|
pub disabled: FxHashSet<String>,
|
|
pub expr_fill_default: ExprFillDefaultMode,
|
|
pub style_lints: bool,
|
|
// FIXME: We may want to include a whole `AssistConfig` here
|
|
pub snippet_cap: Option<SnippetCap>,
|
|
pub insert_use: InsertUseConfig,
|
|
pub prefer_no_std: bool,
|
|
pub prefer_prelude: bool,
|
|
pub prefer_absolute: bool,
|
|
pub term_search_fuel: u64,
|
|
pub term_search_borrowck: bool,
|
|
}
|
|
|
|
impl DiagnosticsConfig {
|
|
pub fn test_sample() -> Self {
|
|
use hir::PrefixKind;
|
|
use ide_db::imports::insert_use::ImportGranularity;
|
|
|
|
Self {
|
|
enabled: true,
|
|
proc_macros_enabled: Default::default(),
|
|
proc_attr_macros_enabled: Default::default(),
|
|
disable_experimental: Default::default(),
|
|
disabled: Default::default(),
|
|
expr_fill_default: Default::default(),
|
|
style_lints: true,
|
|
snippet_cap: SnippetCap::new(true),
|
|
insert_use: InsertUseConfig {
|
|
granularity: ImportGranularity::Preserve,
|
|
enforce_granularity: false,
|
|
prefix_kind: PrefixKind::Plain,
|
|
group: false,
|
|
skip_glob_imports: false,
|
|
},
|
|
prefer_no_std: false,
|
|
prefer_prelude: true,
|
|
prefer_absolute: false,
|
|
term_search_fuel: 400,
|
|
term_search_borrowck: true,
|
|
}
|
|
}
|
|
}
|
|
|
|
struct DiagnosticsContext<'a> {
|
|
config: &'a DiagnosticsConfig,
|
|
sema: Semantics<'a, RootDatabase>,
|
|
resolve: &'a AssistResolveStrategy,
|
|
edition: Edition,
|
|
}
|
|
|
|
impl DiagnosticsContext<'_> {
|
|
fn resolve_precise_location(
|
|
&self,
|
|
node: &InFile<SyntaxNodePtr>,
|
|
precise_location: Option<TextRange>,
|
|
) -> FileRange {
|
|
let sema = &self.sema;
|
|
(|| {
|
|
let precise_location = precise_location?;
|
|
let root = sema.parse_or_expand(node.file_id);
|
|
match root.covering_element(precise_location) {
|
|
syntax::NodeOrToken::Node(it) => Some(sema.original_range(&it)),
|
|
syntax::NodeOrToken::Token(it) => {
|
|
node.with_value(it).original_file_range_opt(sema.db)
|
|
}
|
|
}
|
|
})()
|
|
.unwrap_or_else(|| sema.diagnostics_display_range(*node))
|
|
.into()
|
|
}
|
|
}
|
|
|
|
/// Request parser level diagnostics for the given [`FileId`].
|
|
pub fn syntax_diagnostics(
|
|
db: &RootDatabase,
|
|
config: &DiagnosticsConfig,
|
|
file_id: FileId,
|
|
) -> Vec<Diagnostic> {
|
|
let _p = tracing::info_span!("syntax_diagnostics").entered();
|
|
|
|
if config.disabled.contains("syntax-error") {
|
|
return Vec::new();
|
|
}
|
|
|
|
let sema = Semantics::new(db);
|
|
let file_id = sema
|
|
.attach_first_edition(file_id)
|
|
.unwrap_or_else(|| EditionedFileId::current_edition(file_id));
|
|
|
|
// [#3434] Only take first 128 errors to prevent slowing down editor/ide, the number 128 is chosen arbitrarily.
|
|
db.parse_errors(file_id)
|
|
.as_deref()
|
|
.into_iter()
|
|
.flatten()
|
|
.take(128)
|
|
.map(|err| {
|
|
Diagnostic::new(
|
|
DiagnosticCode::SyntaxError,
|
|
format!("Syntax Error: {err}"),
|
|
FileRange { file_id: file_id.into(), range: err.range() },
|
|
)
|
|
})
|
|
.collect()
|
|
}
|
|
|
|
/// Request semantic diagnostics for the given [`FileId`]. The produced diagnostics may point to other files
|
|
/// due to macros.
|
|
pub fn semantic_diagnostics(
|
|
db: &RootDatabase,
|
|
config: &DiagnosticsConfig,
|
|
resolve: &AssistResolveStrategy,
|
|
file_id: FileId,
|
|
) -> Vec<Diagnostic> {
|
|
let _p = tracing::info_span!("semantic_diagnostics").entered();
|
|
let sema = Semantics::new(db);
|
|
let file_id = sema
|
|
.attach_first_edition(file_id)
|
|
.unwrap_or_else(|| EditionedFileId::current_edition(file_id));
|
|
let mut res = Vec::new();
|
|
|
|
let parse = sema.parse(file_id);
|
|
|
|
// FIXME: This iterates the entire file which is a rather expensive operation.
|
|
// We should implement these differently in some form?
|
|
// Salsa caching + incremental re-parse would be better here
|
|
for node in parse.syntax().descendants() {
|
|
handlers::useless_braces::useless_braces(&mut res, file_id, &node);
|
|
handlers::field_shorthand::field_shorthand(&mut res, file_id, &node);
|
|
handlers::json_is_not_rust::json_in_items(
|
|
&sema,
|
|
&mut res,
|
|
file_id,
|
|
&node,
|
|
config,
|
|
file_id.edition(),
|
|
);
|
|
}
|
|
|
|
let module = sema.file_to_module_def(file_id);
|
|
|
|
let ctx = DiagnosticsContext { config, sema, resolve, edition: file_id.edition() };
|
|
|
|
let mut diags = Vec::new();
|
|
match module {
|
|
// A bunch of parse errors in a file indicate some bigger structural parse changes in the
|
|
// file, so we skip semantic diagnostics so we can show these faster.
|
|
Some(m) => {
|
|
if !db.parse_errors(file_id).as_deref().is_some_and(|es| es.len() >= 16) {
|
|
m.diagnostics(db, &mut diags, config.style_lints);
|
|
}
|
|
}
|
|
None => handlers::unlinked_file::unlinked_file(&ctx, &mut res, file_id.file_id()),
|
|
}
|
|
|
|
for diag in diags {
|
|
let d = match diag {
|
|
AnyDiagnostic::AwaitOutsideOfAsync(d) => handlers::await_outside_of_async::await_outside_of_async(&ctx, &d),
|
|
AnyDiagnostic::CastToUnsized(d) => handlers::invalid_cast::cast_to_unsized(&ctx, &d),
|
|
AnyDiagnostic::ExpectedFunction(d) => handlers::expected_function::expected_function(&ctx, &d),
|
|
AnyDiagnostic::InactiveCode(d) => match handlers::inactive_code::inactive_code(&ctx, &d) {
|
|
Some(it) => it,
|
|
None => continue,
|
|
}
|
|
AnyDiagnostic::IncoherentImpl(d) => handlers::incoherent_impl::incoherent_impl(&ctx, &d),
|
|
AnyDiagnostic::IncorrectCase(d) => handlers::incorrect_case::incorrect_case(&ctx, &d),
|
|
AnyDiagnostic::InvalidCast(d) => handlers::invalid_cast::invalid_cast(&ctx, &d),
|
|
AnyDiagnostic::InvalidDeriveTarget(d) => handlers::invalid_derive_target::invalid_derive_target(&ctx, &d),
|
|
AnyDiagnostic::MacroDefError(d) => handlers::macro_error::macro_def_error(&ctx, &d),
|
|
AnyDiagnostic::MacroError(d) => handlers::macro_error::macro_error(&ctx, &d),
|
|
AnyDiagnostic::MacroExpansionParseError(d) => {
|
|
// FIXME: Point to the correct error span here, not just the macro-call name
|
|
res.extend(d.errors.iter().take(16).map(|err| {
|
|
{
|
|
Diagnostic::new(
|
|
DiagnosticCode::SyntaxError,
|
|
format!("Syntax Error in Expansion: {err}"),
|
|
ctx.resolve_precise_location(&d.node.clone(), d.precise_location),
|
|
)
|
|
}
|
|
.experimental()
|
|
}));
|
|
continue;
|
|
},
|
|
AnyDiagnostic::MalformedDerive(d) => handlers::malformed_derive::malformed_derive(&ctx, &d),
|
|
AnyDiagnostic::MismatchedArgCount(d) => handlers::mismatched_arg_count::mismatched_arg_count(&ctx, &d),
|
|
AnyDiagnostic::MissingFields(d) => handlers::missing_fields::missing_fields(&ctx, &d),
|
|
AnyDiagnostic::MissingMatchArms(d) => handlers::missing_match_arms::missing_match_arms(&ctx, &d),
|
|
AnyDiagnostic::MissingUnsafe(d) => handlers::missing_unsafe::missing_unsafe(&ctx, &d),
|
|
AnyDiagnostic::MovedOutOfRef(d) => handlers::moved_out_of_ref::moved_out_of_ref(&ctx, &d),
|
|
AnyDiagnostic::NeedMut(d) => match handlers::mutability_errors::need_mut(&ctx, &d) {
|
|
Some(it) => it,
|
|
None => continue,
|
|
},
|
|
AnyDiagnostic::NonExhaustiveLet(d) => handlers::non_exhaustive_let::non_exhaustive_let(&ctx, &d),
|
|
AnyDiagnostic::NoSuchField(d) => handlers::no_such_field::no_such_field(&ctx, &d),
|
|
AnyDiagnostic::PrivateAssocItem(d) => handlers::private_assoc_item::private_assoc_item(&ctx, &d),
|
|
AnyDiagnostic::PrivateField(d) => handlers::private_field::private_field(&ctx, &d),
|
|
AnyDiagnostic::ReplaceFilterMapNextWithFindMap(d) => handlers::replace_filter_map_next_with_find_map::replace_filter_map_next_with_find_map(&ctx, &d),
|
|
AnyDiagnostic::TraitImplIncorrectSafety(d) => handlers::trait_impl_incorrect_safety::trait_impl_incorrect_safety(&ctx, &d),
|
|
AnyDiagnostic::TraitImplMissingAssocItems(d) => handlers::trait_impl_missing_assoc_item::trait_impl_missing_assoc_item(&ctx, &d),
|
|
AnyDiagnostic::TraitImplRedundantAssocItems(d) => handlers::trait_impl_redundant_assoc_item::trait_impl_redundant_assoc_item(&ctx, &d),
|
|
AnyDiagnostic::TraitImplOrphan(d) => handlers::trait_impl_orphan::trait_impl_orphan(&ctx, &d),
|
|
AnyDiagnostic::TypedHole(d) => handlers::typed_hole::typed_hole(&ctx, &d),
|
|
AnyDiagnostic::TypeMismatch(d) => handlers::type_mismatch::type_mismatch(&ctx, &d),
|
|
AnyDiagnostic::UndeclaredLabel(d) => handlers::undeclared_label::undeclared_label(&ctx, &d),
|
|
AnyDiagnostic::UnimplementedBuiltinMacro(d) => handlers::unimplemented_builtin_macro::unimplemented_builtin_macro(&ctx, &d),
|
|
AnyDiagnostic::UnreachableLabel(d) => handlers::unreachable_label::unreachable_label(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedAssocItem(d) => handlers::unresolved_assoc_item::unresolved_assoc_item(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedExternCrate(d) => handlers::unresolved_extern_crate::unresolved_extern_crate(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedField(d) => handlers::unresolved_field::unresolved_field(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedIdent(d) => handlers::unresolved_ident::unresolved_ident(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedImport(d) => handlers::unresolved_import::unresolved_import(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedMacroCall(d) => handlers::unresolved_macro_call::unresolved_macro_call(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedMethodCall(d) => handlers::unresolved_method::unresolved_method(&ctx, &d),
|
|
AnyDiagnostic::UnresolvedModule(d) => handlers::unresolved_module::unresolved_module(&ctx, &d),
|
|
AnyDiagnostic::UnusedMut(d) => match handlers::mutability_errors::unused_mut(&ctx, &d) {
|
|
Some(it) => it,
|
|
None => continue,
|
|
},
|
|
AnyDiagnostic::UnusedVariable(d) => match handlers::unused_variables::unused_variables(&ctx, &d) {
|
|
Some(it) => it,
|
|
None => continue,
|
|
},
|
|
AnyDiagnostic::BreakOutsideOfLoop(d) => handlers::break_outside_of_loop::break_outside_of_loop(&ctx, &d),
|
|
AnyDiagnostic::MismatchedTupleStructPatArgCount(d) => handlers::mismatched_arg_count::mismatched_tuple_struct_pat_arg_count(&ctx, &d),
|
|
AnyDiagnostic::RemoveTrailingReturn(d) => match handlers::remove_trailing_return::remove_trailing_return(&ctx, &d) {
|
|
Some(it) => it,
|
|
None => continue,
|
|
},
|
|
AnyDiagnostic::RemoveUnnecessaryElse(d) => match handlers::remove_unnecessary_else::remove_unnecessary_else(&ctx, &d) {
|
|
Some(it) => it,
|
|
None => continue,
|
|
},
|
|
};
|
|
res.push(d)
|
|
}
|
|
|
|
res.retain(|d| {
|
|
!(ctx.config.disabled.contains(d.code.as_str())
|
|
|| ctx.config.disable_experimental && d.experimental)
|
|
});
|
|
|
|
let mut lints = res
|
|
.iter_mut()
|
|
.filter(|it| matches!(it.code, DiagnosticCode::Clippy(_) | DiagnosticCode::RustcLint(_)))
|
|
.filter_map(|it| {
|
|
Some((
|
|
it.main_node.map(|ptr| {
|
|
ptr.map(|node| node.to_node(&ctx.sema.parse_or_expand(ptr.file_id)))
|
|
})?,
|
|
it,
|
|
))
|
|
})
|
|
.collect::<Vec<_>>();
|
|
|
|
// The edition isn't accurate (each diagnostics may have its own edition due to macros),
|
|
// but it's okay as it's only being used for error recovery.
|
|
handle_lint_attributes(
|
|
&ctx.sema,
|
|
&mut FxHashMap::default(),
|
|
&mut lints,
|
|
&mut Vec::new(),
|
|
file_id.edition(),
|
|
);
|
|
|
|
res.retain(|d| d.severity != Severity::Allow);
|
|
|
|
res
|
|
}
|
|
|
|
/// Request both syntax and semantic diagnostics for the given [`FileId`].
|
|
pub fn full_diagnostics(
|
|
db: &RootDatabase,
|
|
config: &DiagnosticsConfig,
|
|
resolve: &AssistResolveStrategy,
|
|
file_id: FileId,
|
|
) -> Vec<Diagnostic> {
|
|
let mut res = syntax_diagnostics(db, config, file_id);
|
|
let sema = semantic_diagnostics(db, config, resolve, file_id);
|
|
res.extend(sema);
|
|
res
|
|
}
|
|
|
|
// `__RA_EVERY_LINT` is a fake lint group to allow every lint in proc macros
|
|
|
|
static RUSTC_LINT_GROUPS_DICT: LazyLock<FxHashMap<&str, Vec<&str>>> =
|
|
LazyLock::new(|| build_group_dict(DEFAULT_LINT_GROUPS, &["warnings", "__RA_EVERY_LINT"], ""));
|
|
|
|
static CLIPPY_LINT_GROUPS_DICT: LazyLock<FxHashMap<&str, Vec<&str>>> =
|
|
LazyLock::new(|| build_group_dict(CLIPPY_LINT_GROUPS, &["__RA_EVERY_LINT"], "clippy::"));
|
|
|
|
fn build_group_dict(
|
|
lint_group: &'static [LintGroup],
|
|
all_groups: &'static [&'static str],
|
|
prefix: &'static str,
|
|
) -> FxHashMap<&'static str, Vec<&'static str>> {
|
|
let mut map_with_prefixes: FxHashMap<&str, Vec<&str>> = FxHashMap::default();
|
|
for g in lint_group {
|
|
let mut add_children = |label: &'static str| {
|
|
for child in g.children {
|
|
map_with_prefixes.entry(child).or_default().push(label);
|
|
}
|
|
};
|
|
add_children(g.lint.label);
|
|
|
|
if g.lint.label == "nonstandard_style" {
|
|
// Also add `bad_style`, which for some reason isn't listed in the groups.
|
|
add_children("bad_style");
|
|
}
|
|
}
|
|
for (lint, groups) in map_with_prefixes.iter_mut() {
|
|
groups.push(lint);
|
|
groups.extend_from_slice(all_groups);
|
|
}
|
|
map_with_prefixes.into_iter().map(|(k, v)| (k.strip_prefix(prefix).unwrap(), v)).collect()
|
|
}
|
|
|
|
fn handle_lint_attributes(
|
|
sema: &Semantics<'_, RootDatabase>,
|
|
cache: &mut FxHashMap<HirFileId, FxHashMap<SmolStr, SeverityAttr>>,
|
|
diagnostics: &mut [(InFile<SyntaxNode>, &mut Diagnostic)],
|
|
cache_stack: &mut Vec<HirFileId>,
|
|
edition: Edition,
|
|
) {
|
|
for (node, diag) in diagnostics {
|
|
let mut diag_severity = fill_lint_attrs(sema, node, cache, cache_stack, diag, edition);
|
|
|
|
if let outline_diag_severity @ Some(_) =
|
|
find_outline_mod_lint_severity(sema, node, diag, edition)
|
|
{
|
|
diag_severity = outline_diag_severity;
|
|
}
|
|
|
|
if let Some(diag_severity) = diag_severity {
|
|
diag.severity = diag_severity;
|
|
}
|
|
}
|
|
}
|
|
|
|
fn find_outline_mod_lint_severity(
|
|
sema: &Semantics<'_, RootDatabase>,
|
|
node: &InFile<SyntaxNode>,
|
|
diag: &Diagnostic,
|
|
edition: Edition,
|
|
) -> Option<Severity> {
|
|
let mod_node = node.value.ancestors().find_map(ast::Module::cast)?;
|
|
if mod_node.item_list().is_some() {
|
|
// Inline modules will be handled by `fill_lint_attrs()`.
|
|
return None;
|
|
}
|
|
|
|
let mod_def = sema.to_module_def(&mod_node)?;
|
|
let module_source_file = sema.module_definition_node(mod_def);
|
|
let mut result = None;
|
|
let lint_groups = lint_groups(&diag.code);
|
|
lint_attrs(
|
|
ast::AnyHasAttrs::cast(module_source_file.value).expect("SourceFile always has attrs"),
|
|
edition,
|
|
)
|
|
.for_each(|(lint, severity)| {
|
|
if lint_groups.contains(&&*lint) {
|
|
result = Some(severity);
|
|
}
|
|
});
|
|
result
|
|
}
|
|
|
|
#[derive(Debug, Clone, Copy)]
|
|
struct SeverityAttr {
|
|
severity: Severity,
|
|
/// This field counts how far we are from the main node. Bigger values mean more far.
|
|
///
|
|
/// Note this isn't accurate: there can be gaps between values (created when merging severity maps).
|
|
/// The important thing is that if an attr is closer to the main node, it will have smaller value.
|
|
///
|
|
/// This is necessary even though we take care to never overwrite a value from deeper nesting
|
|
/// because of lint groups. For example, in the following code:
|
|
/// ```
|
|
/// #[warn(non_snake_case)]
|
|
/// mod foo {
|
|
/// #[allow(nonstandard_style)]
|
|
/// mod bar;
|
|
/// }
|
|
/// ```
|
|
/// We want to not warn on non snake case inside `bar`. If we are traversing this for the first
|
|
/// time, everything will be fine, because we will set `diag_severity` on the first matching group
|
|
/// and never overwrite it since then. But if `bar` is cached, the cache will contain both
|
|
/// `#[warn(non_snake_case)]` and `#[allow(nonstandard_style)]`, and without this field, we have
|
|
/// no way of differentiating between the two.
|
|
depth: u32,
|
|
}
|
|
|
|
fn fill_lint_attrs(
|
|
sema: &Semantics<'_, RootDatabase>,
|
|
node: &InFile<SyntaxNode>,
|
|
cache: &mut FxHashMap<HirFileId, FxHashMap<SmolStr, SeverityAttr>>,
|
|
cache_stack: &mut Vec<HirFileId>,
|
|
diag: &Diagnostic,
|
|
edition: Edition,
|
|
) -> Option<Severity> {
|
|
let mut collected_lint_attrs = FxHashMap::<SmolStr, SeverityAttr>::default();
|
|
let mut diag_severity = None;
|
|
|
|
let mut ancestors = node.value.ancestors().peekable();
|
|
let mut depth = 0;
|
|
loop {
|
|
let ancestor = ancestors.next().expect("we always return from top-level nodes");
|
|
depth += 1;
|
|
|
|
if ancestors.peek().is_none() {
|
|
// We don't want to insert too many nodes into cache, but top level nodes (aka. outline modules
|
|
// or macro expansions) need to touch the database so they seem like a good fit to cache.
|
|
|
|
if let Some(cached) = cache.get_mut(&node.file_id) {
|
|
// This node (and everything above it) is already cached; the attribute is either here or nowhere.
|
|
|
|
// Workaround for the borrow checker.
|
|
let cached = std::mem::take(cached);
|
|
|
|
cached.iter().for_each(|(lint, severity)| {
|
|
for item in &*cache_stack {
|
|
let node_cache_entry = cache
|
|
.get_mut(item)
|
|
.expect("we always insert cached nodes into the cache map");
|
|
let lint_cache_entry = node_cache_entry.entry(lint.clone());
|
|
if let hash_map::Entry::Vacant(lint_cache_entry) = lint_cache_entry {
|
|
// Do not overwrite existing lint attributes, as we go bottom to top and bottom attrs
|
|
// overwrite top attrs.
|
|
lint_cache_entry.insert(SeverityAttr {
|
|
severity: severity.severity,
|
|
depth: severity.depth + depth,
|
|
});
|
|
}
|
|
}
|
|
});
|
|
|
|
let all_matching_groups = lint_groups(&diag.code)
|
|
.iter()
|
|
.filter_map(|lint_group| cached.get(&**lint_group));
|
|
let cached_severity =
|
|
all_matching_groups.min_by_key(|it| it.depth).map(|it| it.severity);
|
|
|
|
cache.insert(node.file_id, cached);
|
|
|
|
return diag_severity.or(cached_severity);
|
|
}
|
|
|
|
// Insert this node's descendants' attributes into any outline descendant, but not including this node.
|
|
// This must come before inserting this node's own attributes to preserve order.
|
|
collected_lint_attrs.drain().for_each(|(lint, severity)| {
|
|
if diag_severity.is_none() && lint_groups(&diag.code).contains(&&*lint) {
|
|
diag_severity = Some(severity.severity);
|
|
}
|
|
|
|
for item in &*cache_stack {
|
|
let node_cache_entry = cache
|
|
.get_mut(item)
|
|
.expect("we always insert cached nodes into the cache map");
|
|
let lint_cache_entry = node_cache_entry.entry(lint.clone());
|
|
if let hash_map::Entry::Vacant(lint_cache_entry) = lint_cache_entry {
|
|
// Do not overwrite existing lint attributes, as we go bottom to top and bottom attrs
|
|
// overwrite top attrs.
|
|
lint_cache_entry.insert(severity);
|
|
}
|
|
}
|
|
});
|
|
|
|
cache_stack.push(node.file_id);
|
|
cache.insert(node.file_id, FxHashMap::default());
|
|
|
|
if let Some(ancestor) = ast::AnyHasAttrs::cast(ancestor) {
|
|
// Insert this node's attributes into any outline descendant, including this node.
|
|
lint_attrs(ancestor, edition).for_each(|(lint, severity)| {
|
|
if diag_severity.is_none() && lint_groups(&diag.code).contains(&&*lint) {
|
|
diag_severity = Some(severity);
|
|
}
|
|
|
|
for item in &*cache_stack {
|
|
let node_cache_entry = cache
|
|
.get_mut(item)
|
|
.expect("we always insert cached nodes into the cache map");
|
|
let lint_cache_entry = node_cache_entry.entry(lint.clone());
|
|
if let hash_map::Entry::Vacant(lint_cache_entry) = lint_cache_entry {
|
|
// Do not overwrite existing lint attributes, as we go bottom to top and bottom attrs
|
|
// overwrite top attrs.
|
|
lint_cache_entry.insert(SeverityAttr { severity, depth });
|
|
}
|
|
}
|
|
});
|
|
}
|
|
|
|
let parent_node = sema.find_parent_file(node.file_id);
|
|
if let Some(parent_node) = parent_node {
|
|
let parent_severity =
|
|
fill_lint_attrs(sema, &parent_node, cache, cache_stack, diag, edition);
|
|
if diag_severity.is_none() {
|
|
diag_severity = parent_severity;
|
|
}
|
|
}
|
|
cache_stack.pop();
|
|
return diag_severity;
|
|
} else if let Some(ancestor) = ast::AnyHasAttrs::cast(ancestor) {
|
|
lint_attrs(ancestor, edition).for_each(|(lint, severity)| {
|
|
if diag_severity.is_none() && lint_groups(&diag.code).contains(&&*lint) {
|
|
diag_severity = Some(severity);
|
|
}
|
|
|
|
let lint_cache_entry = collected_lint_attrs.entry(lint);
|
|
if let hash_map::Entry::Vacant(lint_cache_entry) = lint_cache_entry {
|
|
// Do not overwrite existing lint attributes, as we go bottom to top and bottom attrs
|
|
// overwrite top attrs.
|
|
lint_cache_entry.insert(SeverityAttr { severity, depth });
|
|
}
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
fn lint_attrs(
|
|
ancestor: ast::AnyHasAttrs,
|
|
edition: Edition,
|
|
) -> impl Iterator<Item = (SmolStr, Severity)> {
|
|
ancestor
|
|
.attrs_including_inner()
|
|
.filter_map(|attr| {
|
|
attr.as_simple_call().and_then(|(name, value)| match &*name {
|
|
"allow" | "expect" => Some((Severity::Allow, value)),
|
|
"warn" => Some((Severity::Warning, value)),
|
|
"forbid" | "deny" => Some((Severity::Error, value)),
|
|
_ => None,
|
|
})
|
|
})
|
|
.flat_map(move |(severity, lints)| {
|
|
parse_tt_as_comma_sep_paths(lints, edition).into_iter().flat_map(move |lints| {
|
|
// Rejoin the idents with `::`, so we have no spaces in between.
|
|
lints.into_iter().map(move |lint| {
|
|
(
|
|
lint.segments().filter_map(|segment| segment.name_ref()).join("::").into(),
|
|
severity,
|
|
)
|
|
})
|
|
})
|
|
})
|
|
}
|
|
|
|
fn lint_groups(lint: &DiagnosticCode) -> &'static [&'static str] {
|
|
match lint {
|
|
DiagnosticCode::RustcLint(name) => {
|
|
RUSTC_LINT_GROUPS_DICT.get(name).map(|it| &**it).unwrap_or_default()
|
|
}
|
|
DiagnosticCode::Clippy(name) => {
|
|
CLIPPY_LINT_GROUPS_DICT.get(name).map(|it| &**it).unwrap_or_default()
|
|
}
|
|
_ => &[],
|
|
}
|
|
}
|
|
|
|
fn fix(id: &'static str, label: &str, source_change: SourceChange, target: TextRange) -> Assist {
|
|
let mut res = unresolved_fix(id, label, target);
|
|
res.source_change = Some(source_change);
|
|
res
|
|
}
|
|
|
|
fn unresolved_fix(id: &'static str, label: &str, target: TextRange) -> Assist {
|
|
assert!(!id.contains(' '));
|
|
Assist {
|
|
id: AssistId(id, AssistKind::QuickFix),
|
|
label: Label::new(label.to_owned()),
|
|
group: None,
|
|
target,
|
|
source_change: None,
|
|
command: None,
|
|
}
|
|
}
|
|
|
|
fn adjusted_display_range<N: AstNode>(
|
|
ctx: &DiagnosticsContext<'_>,
|
|
diag_ptr: InFile<AstPtr<N>>,
|
|
adj: &dyn Fn(N) -> Option<TextRange>,
|
|
) -> FileRange {
|
|
let source_file = ctx.sema.parse_or_expand(diag_ptr.file_id);
|
|
let node = diag_ptr.value.to_node(&source_file);
|
|
diag_ptr
|
|
.with_value(adj(node).unwrap_or_else(|| diag_ptr.value.text_range()))
|
|
.original_node_file_range_rooted(ctx.sema.db)
|
|
.into()
|
|
}
|