Skip to content

Commit 5538f5d

Browse files
authored
fix(ide): mark inactive macro branches as unused (#144)
1 parent 1f13652 commit 5538f5d

11 files changed

Lines changed: 313 additions & 18 deletions

File tree

crates/base-db/src/preproc_index.rs

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,7 @@ pub struct PreprocFileIndex {
1313
pub includes: Vec<MacroInclude>,
1414
pub conditionals: Vec<MacroConditional>,
1515
pub usages: Vec<MacroUsage>,
16+
pub inactive_ranges: Vec<TextRange>,
1617
}
1718

1819
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -118,6 +119,11 @@ fn range_to_text_range(range: std::ops::Range<usize>) -> Option<TextRange> {
118119
}
119120

120121
fn collect_preprocessor_directive(index: &mut PreprocFileIndex, directive: PreprocessorDirective) {
122+
index.inactive_ranges.extend(directive.disabled_ranges.iter().filter_map(|range| {
123+
let range = range_to_text_range(range.clone())?;
124+
(!range.is_empty()).then_some(range)
125+
}));
126+
121127
let kind = directive.kind;
122128
match kind {
123129
SyntaxKind::DEFINE_DIRECTIVE => {
@@ -420,4 +426,29 @@ endmodule
420426
}
421427
);
422428
}
429+
430+
#[test]
431+
fn records_inactive_preprocessor_branch_ranges() {
432+
let text = r#"`ifdef USE_A
433+
logic active;
434+
`else
435+
logic inactive;
436+
`endif
437+
"#;
438+
439+
let without_define = index_with_predefines(text, Vec::new());
440+
let with_define = index_with_predefines(text, vec!["USE_A=1".to_owned()]);
441+
442+
let inactive_range = without_define.inactive_ranges[0];
443+
assert_eq!(
444+
&text[usize::from(inactive_range.start())..usize::from(inactive_range.end())],
445+
"logic active;"
446+
);
447+
448+
let inactive_range = with_define.inactive_ranges[0];
449+
assert_eq!(
450+
&text[usize::from(inactive_range.start())..usize::from(inactive_range.end())],
451+
"logic inactive;"
452+
);
453+
}
423454
}

crates/ide/src/diagnostics.rs

Lines changed: 160 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -17,8 +17,11 @@ use crate::module_resolution::{ModuleResolution, ModuleResolutionAmbiguity, reso
1717

1818
const AMBIGUOUS_MODULE_INSTANTIATION: VizslaDiagnosticDescriptor =
1919
VizslaDiagnosticDescriptor { code: 1, subsystem: 0, name: "ambiguous-module-instantiation" };
20+
const INACTIVE_PREPROCESSOR_BRANCH: VizslaDiagnosticDescriptor =
21+
VizslaDiagnosticDescriptor { code: 2, subsystem: 0, name: "inactive-preprocessor-branch" };
2022
pub const DIAGNOSTIC_AMBIGUOUS_MODULE_STRICT: &str = "diagnostic.ambiguous_module.strict";
2123
pub const DIAGNOSTIC_AMBIGUOUS_MODULE_BEST_EFFORT: &str = "diagnostic.ambiguous_module.best_effort";
24+
pub const DIAGNOSTIC_INACTIVE_PREPROCESSOR_BRANCH: &str = "diagnostic.inactive_preprocessor_branch";
2225

2326
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2427
pub enum DiagnosticSource {
@@ -41,6 +44,12 @@ pub struct Diagnostic {
4144
pub message: String,
4245
pub message_key: Option<&'static str>,
4346
pub message_args: Vec<(&'static str, String)>,
47+
pub tags: Vec<DiagnosticTag>,
48+
}
49+
50+
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
51+
pub enum DiagnosticTag {
52+
Unnecessary,
4453
}
4554

4655
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
@@ -50,6 +59,12 @@ struct VizslaDiagnosticDescriptor {
5059
name: &'static str,
5160
}
5261

62+
#[derive(Debug, Clone, Default)]
63+
struct VizslaDiagnosticMetadata {
64+
message_args: Vec<(&'static str, String)>,
65+
tags: Vec<DiagnosticTag>,
66+
}
67+
5368
impl VizslaDiagnosticDescriptor {
5469
fn diagnostic(
5570
self,
@@ -59,6 +74,44 @@ impl VizslaDiagnosticDescriptor {
5974
message: String,
6075
message_key: &'static str,
6176
message_args: Vec<(&'static str, String)>,
77+
) -> Diagnostic {
78+
self.diagnostic_with_metadata(
79+
file_id,
80+
range,
81+
severity,
82+
message,
83+
message_key,
84+
VizslaDiagnosticMetadata { message_args, tags: Vec::new() },
85+
)
86+
}
87+
88+
fn diagnostic_with_tags(
89+
self,
90+
file_id: FileId,
91+
range: TextRange,
92+
severity: DiagnosticSeverity,
93+
message: String,
94+
message_key: &'static str,
95+
tags: Vec<DiagnosticTag>,
96+
) -> Diagnostic {
97+
self.diagnostic_with_metadata(
98+
file_id,
99+
range,
100+
severity,
101+
message,
102+
message_key,
103+
VizslaDiagnosticMetadata { message_args: Vec::new(), tags },
104+
)
105+
}
106+
107+
fn diagnostic_with_metadata(
108+
self,
109+
file_id: FileId,
110+
range: TextRange,
111+
severity: DiagnosticSeverity,
112+
message: String,
113+
message_key: &'static str,
114+
metadata: VizslaDiagnosticMetadata,
62115
) -> Diagnostic {
63116
Diagnostic {
64117
file_id,
@@ -72,7 +125,8 @@ impl VizslaDiagnosticDescriptor {
72125
severity,
73126
message,
74127
message_key: Some(message_key),
75-
message_args,
128+
message_args: metadata.message_args,
129+
tags: metadata.tags,
76130
}
77131
}
78132
}
@@ -95,23 +149,37 @@ pub(crate) fn compilation_profile_diagnostics(
95149
db: &RootDb,
96150
profile_id: CompilationProfileId,
97151
) -> Vec<Diagnostic> {
98-
db.compilation_profile_diagnostics(profile_id)
152+
let mut diagnostics = db
153+
.compilation_profile_diagnostics(profile_id)
99154
.iter()
100155
.map(|diag| slang_diagnostic(diag.file_id, diag.source, &diag.diagnostic))
101-
.collect()
156+
.collect::<Vec<_>>();
157+
158+
diagnostics.extend(
159+
compilation_profile_file_ids(db, profile_id)
160+
.into_iter()
161+
.flat_map(|file_id| inactive_preprocessor_branch_diagnostics(db, file_id)),
162+
);
163+
diagnostics
102164
}
103165

104166
pub(crate) fn compilation_profile_syntax_diagnostics(
105167
db: &RootDb,
106168
profile_id: CompilationProfileId,
107169
) -> Vec<Diagnostic> {
170+
compilation_profile_file_ids(db, profile_id)
171+
.into_iter()
172+
.flat_map(|file_id| syntax_diagnostics(db, file_id))
173+
.collect()
174+
}
175+
176+
fn compilation_profile_file_ids(db: &RootDb, profile_id: CompilationProfileId) -> Vec<FileId> {
108177
let plan = db.compilation_plan_for_profile(Some(profile_id));
109178
let mut file_ids = plan.roots.clone();
110179
file_ids.extend(plan.include_only.iter().copied());
111180
file_ids.sort_unstable_by_key(|file_id| file_id.0);
112181
file_ids.dedup();
113-
114-
file_ids.into_iter().flat_map(|file_id| syntax_diagnostics(db, file_id)).collect()
182+
file_ids
115183
}
116184

117185
fn syntax_diagnostics(db: &RootDb, file_id: FileId) -> Vec<Diagnostic> {
@@ -141,6 +209,7 @@ fn slang_diagnostic(
141209
message: diag.message.clone(),
142210
message_key: None,
143211
message_args: Vec::new(),
212+
tags: Vec::new(),
144213
}
145214
}
146215

@@ -157,7 +226,7 @@ pub(crate) fn diagnostics(db: &RootDb, file_id: FileId) -> Vec<Diagnostic> {
157226
}
158227

159228
let mut diagnostics = if slang_semantic_diagnostics_active(db, file_id) {
160-
Vec::new()
229+
inactive_preprocessor_branch_diagnostics(db, file_id)
161230
} else {
162231
syntax_diagnostics(db, file_id)
163232
};
@@ -184,6 +253,11 @@ pub(crate) fn source_root_diagnostics(db: &RootDb, file_id: FileId) -> Vec<Diagn
184253

185254
if slang_semantic_diagnostics_active(db, file_id) {
186255
diagnostics.extend(compilation_diagnostics(db, file_id));
256+
diagnostics.extend(
257+
source_root
258+
.iter()
259+
.flat_map(|file_id| inactive_preprocessor_branch_diagnostics(db, file_id)),
260+
);
187261
} else {
188262
for file_id in source_root.iter() {
189263
diagnostics.extend(syntax_diagnostics(db, file_id));
@@ -214,11 +288,21 @@ pub(crate) fn source_root_role(db: &RootDb, file_id: FileId) -> SourceRootRole {
214288
}
215289

216290
fn vizsla_diagnostics(db: &RootDb, file_id: FileId) -> Vec<Diagnostic> {
217-
if slang_semantic_diagnostics_active(db, file_id) {
291+
if !vizsla_diagnostics_enabled(db) {
218292
return Vec::new();
219293
}
220294

221-
module_instantiation_resolution_diagnostics(db, file_id)
295+
let mut diagnostics = inactive_preprocessor_branch_diagnostics(db, file_id);
296+
297+
if !slang_semantic_diagnostics_active(db, file_id) {
298+
diagnostics.extend(module_instantiation_resolution_diagnostics(db, file_id));
299+
}
300+
301+
diagnostics
302+
}
303+
304+
fn vizsla_diagnostics_enabled(db: &RootDb) -> bool {
305+
db.diagnostics_config().enabled
222306
}
223307

224308
fn slang_semantic_diagnostics_active(db: &RootDb, file_id: FileId) -> bool {
@@ -273,6 +357,28 @@ fn module_instantiation_resolution_diagnostics(db: &RootDb, file_id: FileId) ->
273357
diagnostics
274358
}
275359

360+
fn inactive_preprocessor_branch_diagnostics(db: &RootDb, file_id: FileId) -> Vec<Diagnostic> {
361+
if !vizsla_diagnostics_enabled(db) {
362+
return Vec::new();
363+
}
364+
365+
db.preproc_file_index(file_id)
366+
.inactive_ranges
367+
.iter()
368+
.copied()
369+
.map(|range| {
370+
INACTIVE_PREPROCESSOR_BRANCH.diagnostic_with_tags(
371+
file_id,
372+
range,
373+
DiagnosticSeverity::Note,
374+
"code is inactive due to preprocessor conditionals".to_owned(),
375+
DIAGNOSTIC_INACTIVE_PREPROCESSOR_BRANCH,
376+
vec![DiagnosticTag::Unnecessary],
377+
)
378+
})
379+
.collect()
380+
}
381+
276382
fn ambiguous_module_instantiation_diagnostic(
277383
module_name: &str,
278384
candidate_count: usize,
@@ -324,8 +430,10 @@ fn to_text_range(diag: &SyntaxDiagnostic) -> TextRange {
324430
mod tests {
325431
use base_db::{
326432
change::Change,
433+
diagnostics_config::DiagnosticsConfig,
327434
project::{CompilationProfile, CompilationProfileId, PreprocessConfig, ProjectConfig},
328-
source_db::SourceRootDb,
435+
salsa::Durability,
436+
source_db::{SourceDb, SourceRootDb},
329437
source_root::{SourceRoot, SourceRootId, SourceRootRole},
330438
};
331439
use ide_db::root_db::RootDb;
@@ -334,13 +442,21 @@ mod tests {
334442
use vfs::{ChangeKind, ChangedFile, FileId, FileSet, VfsPath};
335443

336444
use super::{
337-
AMBIGUOUS_MODULE_INSTANTIATION, DiagnosticSource, diagnostics, source_root_diagnostics,
445+
AMBIGUOUS_MODULE_INSTANTIATION, DIAGNOSTIC_INACTIVE_PREPROCESSOR_BRANCH, DiagnosticSource,
446+
DiagnosticTag, INACTIVE_PREPROCESSOR_BRANCH, diagnostics, source_root_diagnostics,
338447
};
339448

340449
fn db_with_files(files: &[(&str, &str)], configured: bool) -> RootDb {
341450
db_with_files_in_role(files, SourceRootRole::Local, configured)
342451
}
343452

453+
fn disable_diagnostics(db: &mut RootDb) {
454+
db.set_diagnostics_config_with_durability(
455+
Arc::new(DiagnosticsConfig { enabled: false, ..DiagnosticsConfig::default() }),
456+
Durability::HIGH,
457+
);
458+
}
459+
344460
fn db_with_files_in_role(
345461
files: &[(&str, &str)],
346462
role: SourceRootRole,
@@ -463,6 +579,40 @@ mod tests {
463579
);
464580
}
465581

582+
#[test]
583+
fn inactive_preprocessor_branch_reports_unnecessary_hint() {
584+
let db = db_with_files(
585+
&[("/top.sv", "`ifdef USE_IMPL\nlogic active;\n`else\nlogic inactive;\n`endif\n")],
586+
false,
587+
);
588+
589+
let diagnostics = diagnostics(&db, FileId(0));
590+
let inactive = diagnostics
591+
.iter()
592+
.find(|diag| diag.name == INACTIVE_PREPROCESSOR_BRANCH.name)
593+
.expect("expected inactive preprocessor branch diagnostic");
594+
595+
assert_eq!(inactive.severity, syntax::DiagnosticSeverity::Note);
596+
assert_eq!(inactive.tags, vec![DiagnosticTag::Unnecessary]);
597+
assert_eq!(inactive.message_key, Some(DIAGNOSTIC_INACTIVE_PREPROCESSOR_BRANCH));
598+
}
599+
600+
#[test]
601+
fn inactive_preprocessor_branch_respects_global_diagnostics_switch() {
602+
let mut db = db_with_files(
603+
&[("/top.sv", "`ifdef USE_IMPL\nlogic active;\n`else\nlogic inactive;\n`endif\n")],
604+
false,
605+
);
606+
disable_diagnostics(&mut db);
607+
608+
let diagnostics = diagnostics(&db, FileId(0));
609+
610+
assert!(
611+
diagnostics.is_empty(),
612+
"global diagnostics switch must suppress Vizsla inactive diagnostics: {diagnostics:?}"
613+
);
614+
}
615+
466616
#[test]
467617
fn semantic_diagnostics_include_other_workspace_files() {
468618
let db = db_with_files(

crates/slang/bindings/rust/ffi.rs

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,13 @@ mod slang_ffi {
8282
has_token: bool,
8383
}
8484

85+
#[derive(Debug, Clone, PartialEq, Eq)]
86+
struct RawTextRange {
87+
range_start: usize,
88+
range_end: usize,
89+
has_range: bool,
90+
}
91+
8592
#[derive(Debug, Clone, PartialEq, Eq)]
8693
struct RawPreprocessorMacroParam {
8794
name: RawPreprocessorToken,
@@ -104,6 +111,7 @@ mod slang_ffi {
104111
params: Vec<RawPreprocessorMacroParam>,
105112
body_tokens: Vec<RawPreprocessorToken>,
106113
expr_tokens: Vec<RawPreprocessorToken>,
114+
disabled_ranges: Vec<RawTextRange>,
107115
}
108116

109117
#[namespace = "slang"]

0 commit comments

Comments
 (0)