From 759b1668e7d893fac83ea394f91b8fe3b3ccaf19 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 6 May 2026 23:23:59 +0300 Subject: [PATCH 1/2] Diagnose inactive code in macros But only if the source is code passed to the macro, not code inside the macro. --- .../src/handlers/inactive_code.rs | 59 +++++++++++++++++-- 1 file changed, 55 insertions(+), 4 deletions(-) diff --git a/crates/ide-diagnostics/src/handlers/inactive_code.rs b/crates/ide-diagnostics/src/handlers/inactive_code.rs index 09f3e8bfb319..71cac6af1346 100644 --- a/crates/ide-diagnostics/src/handlers/inactive_code.rs +++ b/crates/ide-diagnostics/src/handlers/inactive_code.rs @@ -10,10 +10,8 @@ pub(crate) fn inactive_code( ctx: &DiagnosticsContext<'_, '_>, d: &hir::InactiveCode, ) -> Option { - // If there's inactive code somewhere in a macro, don't propagate to the call-site. - if d.node.file_id.is_macro() { - return None; - } + // If there's inactive code somewhere in a macro that doesn't map to something in the call, don't propagate to the call-site. + d.node.map(|it| it.text_range()).original_node_file_range_rooted_opt(ctx.db())?; let inactive = DnfExpr::new(&d.cfg).why_inactive(&d.opts); let mut message = "code is inactive due to #[cfg] directives".to_owned(); @@ -252,4 +250,57 @@ fn foo() {} ide_db::FileRange { file_id: file_id.file_id(&db), range: full_file_range }, ); } + + #[test] + fn cfg_in_macro_does_not_diagnose_the_whole_call() { + check( + r#" +macro_rules! m { + ($e:item) => { + #[cfg(false)] + const _: () = (); + + $e + }; +} + +m! { + fn foo() {} +} + "#, + ); + } + + #[test] + fn in_macro() { + check( + r#" +macro_rules! m { + ($e:item) => { + $e + }; +} + +m! { + #[cfg(false)] fn foo() {} + // ^^^^^^^^^^^^^^^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: false is disabled +} + "#, + ); + check( + r#" +macro_rules! m { + ($e:item) => { + #[cfg(false)] + $e + }; +} + +m! { + fn foo() {} + // ^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: false is disabled +} + "#, + ); + } } From 17ab3c530b76eee1cf3b400e27c8e9a40f12ef52 Mon Sep 17 00:00:00 2001 From: Chayim Refael Friedman Date: Wed, 6 May 2026 23:48:08 +0300 Subject: [PATCH 2/2] Allow disabling the inactive-code diagnostic in code With `#[allow(rust_analyzer::inactive_code)]`. --- Cargo.lock | 1 + crates/ide-diagnostics/Cargo.toml | 1 + .../src/handlers/inactive_code.rs | 30 +++++++++-- .../src/handlers/mismatched_arg_count.rs | 4 ++ .../src/handlers/no_such_field.rs | 8 +++ crates/ide-diagnostics/src/lib.rs | 54 +++++++++++++------ .../src/tests/overly_long_real_world_cases.rs | 1 + 7 files changed, 79 insertions(+), 20 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index be9a8c491572..00a212f7c4c2 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1151,6 +1151,7 @@ dependencies = [ "itertools 0.14.0", "paths", "serde_json", + "smallvec", "stdx", "syntax", "test-fixture", diff --git a/crates/ide-diagnostics/Cargo.toml b/crates/ide-diagnostics/Cargo.toml index ddf5999036d2..ce836197571a 100644 --- a/crates/ide-diagnostics/Cargo.toml +++ b/crates/ide-diagnostics/Cargo.toml @@ -18,6 +18,7 @@ either.workspace = true itertools.workspace = true serde_json.workspace = true tracing.workspace = true +smallvec.workspace = true # local deps stdx.workspace = true diff --git a/crates/ide-diagnostics/src/handlers/inactive_code.rs b/crates/ide-diagnostics/src/handlers/inactive_code.rs index 71cac6af1346..9e38d8f1b9e4 100644 --- a/crates/ide-diagnostics/src/handlers/inactive_code.rs +++ b/crates/ide-diagnostics/src/handlers/inactive_code.rs @@ -6,6 +6,8 @@ use crate::{Diagnostic, DiagnosticCode, DiagnosticsContext, Severity}; // Diagnostic: inactive-code // // This diagnostic is shown for code with inactive `#[cfg]` attributes. +// +// It can be disabled selectively with `#[allow(rust_analyzer::inactive_code)]`. pub(crate) fn inactive_code( ctx: &DiagnosticsContext<'_, '_>, d: &hir::InactiveCode, @@ -26,10 +28,11 @@ pub(crate) fn inactive_code( } } // FIXME: This shouldn't be a diagnostic - let res = Diagnostic::new( - DiagnosticCode::Ra("inactive-code", Severity::WeakWarning), + let res = Diagnostic::new_with_syntax_node_ptr( + ctx, + DiagnosticCode::RaLint("inactive_code", Severity::WeakWarning), message, - ctx.sema.diagnostics_display_range(d.node), + d.node, ) .stable() .with_unused(true); @@ -237,7 +240,7 @@ fn foo() {} }; assert_eq!( inactive_code.code, - DiagnosticCode::Ra("inactive-code", ide_db::Severity::WeakWarning) + DiagnosticCode::RaLint("inactive_code", ide_db::Severity::WeakWarning) ); assert_eq!( inactive_code.message, @@ -299,6 +302,25 @@ macro_rules! m { m! { fn foo() {} // ^^^^^^^^^^^ weak: code is inactive due to #[cfg] directives: false is disabled +} + "#, + ); + } + + #[test] + fn allow() { + check( + r#" +macro_rules! m { + ($e:item) => { + #[cfg(false)] + #[allow(rust_analyzer::inactive_code)] + $e + }; +} + +m! { + fn foo() {} } "#, ); diff --git a/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs b/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs index f6293e35d0c3..754d90d112b8 100644 --- a/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs +++ b/crates/ide-diagnostics/src/handlers/mismatched_arg_count.rs @@ -375,6 +375,8 @@ fn main() { fn cfgd_out_call_arguments() { check_diagnostics( r#" +#![allow(rust_analyzer::inactive_code)] + struct C(#[cfg(FALSE)] ()); impl C { fn new() -> Self { @@ -398,6 +400,8 @@ fn main() { fn cfgd_out_fn_params() { check_diagnostics( r#" +#![allow(rust_analyzer::inactive_code)] + fn foo(#[cfg(NEVER)] x: ()) {} struct S; diff --git a/crates/ide-diagnostics/src/handlers/no_such_field.rs b/crates/ide-diagnostics/src/handlers/no_such_field.rs index 7959fddc757f..a96b92dd4a12 100644 --- a/crates/ide-diagnostics/src/handlers/no_such_field.rs +++ b/crates/ide-diagnostics/src/handlers/no_such_field.rs @@ -153,6 +153,8 @@ mod tests { fn dont_work_for_field_with_disabled_cfg() { check_diagnostics( r#" +#![allow(rust_analyzer::inactive_code)] + struct Test { #[cfg(feature = "hello")] test: u32, @@ -224,6 +226,8 @@ impl S { check_diagnostics( r#" //- /lib.rs crate:foo cfg:feature=foo +#![allow(rust_analyzer::inactive_code)] + struct MyStruct { my_val: usize, #[cfg(feature = "foo")] @@ -249,6 +253,8 @@ impl MyStruct { check_diagnostics( r#" //- /lib.rs crate:foo cfg:feature=foo +#![allow(rust_analyzer::inactive_code)] + enum Foo { #[cfg(not(feature = "foo"))] Buz, @@ -272,6 +278,8 @@ fn test_fn(f: Foo) { check_diagnostics( r#" //- /lib.rs crate:foo cfg:feature=foo +#![allow(rust_analyzer::inactive_code)] + struct S { #[cfg(feature = "foo")] foo: u32, diff --git a/crates/ide-diagnostics/src/lib.rs b/crates/ide-diagnostics/src/lib.rs index e2e465e26c78..f77c20b08543 100644 --- a/crates/ide-diagnostics/src/lib.rs +++ b/crates/ide-diagnostics/src/lib.rs @@ -113,9 +113,11 @@ use ide_db::{ rename::RenameConfig, source_change::SourceChange, }; +use smallvec::{SmallVec, smallvec}; use syntax::{ AstPtr, Edition, SmolStr, SyntaxNode, SyntaxNodePtr, TextRange, ast::{self, AstNode}, + format_smolstr, }; #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)] @@ -125,6 +127,7 @@ pub enum DiagnosticCode { RustcLint(&'static str), Clippy(&'static str), Ra(&'static str, Severity), + RaLint(&'static str, Severity), } impl DiagnosticCode { @@ -142,7 +145,7 @@ impl DiagnosticCode { DiagnosticCode::Clippy(e) => { format!("https://rust-lang.github.io/rust-clippy/master/#/{e}") } - DiagnosticCode::Ra(e, _) => { + DiagnosticCode::Ra(e, _) | DiagnosticCode::RaLint(e, _) => { format!("https://rust-analyzer.github.io/book/diagnostics.html#{e}") } } @@ -153,7 +156,8 @@ impl DiagnosticCode { DiagnosticCode::RustcHardError(r) | DiagnosticCode::RustcLint(r) | DiagnosticCode::Clippy(r) - | DiagnosticCode::Ra(r, _) => r, + | DiagnosticCode::Ra(r, _) + | DiagnosticCode::RaLint(r, _) => r, DiagnosticCode::SyntaxError => "syntax-error", } } @@ -190,7 +194,7 @@ impl Diagnostic { // 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, + DiagnosticCode::Ra(_, s) | DiagnosticCode::RaLint(_, s) => s, }, unused: false, experimental: true, @@ -529,7 +533,14 @@ pub fn semantic_diagnostics( let mut lints = res .iter_mut() - .filter(|it| matches!(it.code, DiagnosticCode::Clippy(_) | DiagnosticCode::RustcLint(_))) + .filter(|it| { + matches!( + it.code, + DiagnosticCode::Clippy(_) + | DiagnosticCode::RustcLint(_) + | DiagnosticCode::RaLint(..) + ) + }) .filter_map(|it| Some((it.main_node(&ctx.sema)?, it))) .collect::>(); @@ -602,7 +613,7 @@ fn handle_diag_from_macros( struct BuiltLint { lint: &'static Lint, - groups: Vec<&'static str>, + groups: SmallVec<[SmolStr; 5]>, } static RUSTC_LINTS: LazyLock> = @@ -623,12 +634,17 @@ fn build_lints_map( ) -> FxHashMap<&'static str, BuiltLint> { let mut map_with_prefixes: FxHashMap<_, _> = lints .iter() - .map(|lint| (lint.label, BuiltLint { lint, groups: vec![lint.label, "__RA_EVERY_LINT"] })) + .map(|lint| { + ( + lint.label, + BuiltLint { lint, groups: smallvec![lint.label.into(), "__RA_EVERY_LINT".into()] }, + ) + }) .collect(); for g in lint_group { let mut add_children = |label: &'static str| { for child in g.children { - map_with_prefixes.get_mut(child).unwrap().groups.push(label); + map_with_prefixes.get_mut(child).unwrap().groups.push(label.into()); } }; add_children(g.lint.label); @@ -649,12 +665,15 @@ fn handle_lints( edition: Edition, ) { for (node, diag) in diagnostics { - let lint = match diag.code { - DiagnosticCode::RustcLint(lint) => RUSTC_LINTS[lint].lint, - DiagnosticCode::Clippy(lint) => CLIPPY_LINTS[lint].lint, - _ => panic!("non-lint passed to `handle_lints()`"), + let default_severity = 'find_severity: { + let lint = match diag.code { + DiagnosticCode::RustcLint(lint) => RUSTC_LINTS[lint].lint, + DiagnosticCode::Clippy(lint) => CLIPPY_LINTS[lint].lint, + DiagnosticCode::RaLint(_, severity) => break 'find_severity severity, + _ => panic!("non-lint passed to `handle_lints()`"), + }; + default_lint_severity(lint, edition) }; - let default_severity = default_lint_severity(lint, edition); if !(default_severity == Severity::Allow && diag.severity == Severity::WeakWarning) { diag.severity = default_severity; } @@ -754,13 +773,13 @@ fn lint_attrs( #[derive(Debug)] struct LintGroups { - groups: &'static [&'static str], + groups: SmallVec<[SmolStr; 5]>, inside_warnings: bool, } impl LintGroups { fn contains(&self, group: &str) -> bool { - self.groups.contains(&group) || (self.inside_warnings && group == "warnings") + self.groups.iter().any(|g| g == group) || (self.inside_warnings && group == "warnings") } } @@ -769,12 +788,15 @@ fn lint_groups(lint: &DiagnosticCode, edition: Edition) -> LintGroups { DiagnosticCode::RustcLint(name) => { let lint = &RUSTC_LINTS[name]; let inside_warnings = default_lint_severity(lint.lint, edition) == Severity::Warning; - (&lint.groups, inside_warnings) + (lint.groups.clone(), inside_warnings) } DiagnosticCode::Clippy(name) => { let lint = &CLIPPY_LINTS[name]; let inside_warnings = default_lint_severity(lint.lint, edition) == Severity::Warning; - (&lint.groups, inside_warnings) + (lint.groups.clone(), inside_warnings) + } + DiagnosticCode::RaLint(name, severity) => { + (smallvec![format_smolstr!("rust_analyzer::{name}")], *severity == Severity::Warning) } _ => panic!("non-lint passed to `handle_lints()`"), }; diff --git a/crates/ide-diagnostics/src/tests/overly_long_real_world_cases.rs b/crates/ide-diagnostics/src/tests/overly_long_real_world_cases.rs index 301613e92019..34cf80a85b98 100644 --- a/crates/ide-diagnostics/src/tests/overly_long_real_world_cases.rs +++ b/crates/ide-diagnostics/src/tests/overly_long_real_world_cases.rs @@ -2729,6 +2729,7 @@ tracing::error!(); "unresolved-macro-call", "syntax-error", "macro-error", + "inactive_code", ], ); }