From c330340b8ce5a6927c0a5587ee1345d372833478 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Thu, 4 Jun 2026 23:11:12 +0800 Subject: [PATCH 1/8] refactor(preproc): add query model --- crates/hir/src/base_db/compilation_plan.rs | 2 +- crates/hir/src/base_db/source_db.rs | 4 +- crates/ide/src/verilog_2005.rs | 2 +- crates/preproc/src/index.rs | 457 ++++++++++++++++++ crates/preproc/src/lib.rs | 456 +----------------- crates/preproc/src/model.rs | 518 +++++++++++++++++++++ 6 files changed, 981 insertions(+), 458 deletions(-) create mode 100644 crates/preproc/src/index.rs create mode 100644 crates/preproc/src/model.rs diff --git a/crates/hir/src/base_db/compilation_plan.rs b/crates/hir/src/base_db/compilation_plan.rs index 01b1bef5..e76bdc12 100644 --- a/crates/hir/src/base_db/compilation_plan.rs +++ b/crates/hir/src/base_db/compilation_plan.rs @@ -1,4 +1,4 @@ -use preproc::MacroIncludeTarget; +use preproc::index::MacroIncludeTarget; use rustc_hash::FxHashSet; use syntax::SyntaxTreeBuffer; use utils::{ diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index a6d1d445..e4903846 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,4 +1,4 @@ -use preproc::PreprocFileIndex; +use preproc::index::{PreprocFileIndex, preproc_file_index_from_text}; use rustc_hash::{FxHashMap, FxHashSet}; use syntax::{ Compilation, ParserExpectedSyntax, SyntaxDiagnostic, SyntaxTree, SyntaxTreeBuffer, @@ -132,7 +132,7 @@ fn preproc_file_index_with_predefines( predefines, ..syntax::SyntaxTreeOptions::without_include_expansion() }; - Arc::new(preproc::preproc_file_index_from_text(&db.file_text(file_id), &options)) + Arc::new(preproc_file_index_from_text(&db.file_text(file_id), &options)) } SourceFileKind::LibraryMap | SourceFileKind::ProjectManifest => { Arc::new(PreprocFileIndex::default()) diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index 59f52676..b937087d 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -14,7 +14,7 @@ use hir::{ semantics::Semantics, }; use insta::assert_snapshot; -use preproc::MacroIncludeTarget; +use preproc::index::MacroIncludeTarget; use triomphe::Arc; use utils::{ lines::LineEnding, diff --git a/crates/preproc/src/index.rs b/crates/preproc/src/index.rs new file mode 100644 index 00000000..d8b2c1ee --- /dev/null +++ b/crates/preproc/src/index.rs @@ -0,0 +1,457 @@ +use smol_str::{SmolStr, ToSmolStr}; +use syntax::{ + PreprocessorDirective, PreprocessorDirectiveToken, PreprocessorMacroParam, SyntaxKind, + SyntaxTree, SyntaxTreeOptions, +}; +use utils::line_index::{TextRange, TextSize}; + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct PreprocFileIndex { + pub directives: Vec, + pub defines: Vec, + pub undefs: Vec, + pub includes: Vec, + pub conditionals: Vec, + pub usages: Vec, + pub inactive_ranges: Vec, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MacroDirectiveKind { + Define, + Undef, + Include, + Conditional, + Branch, + Usage, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroDirective { + pub kind: MacroDirectiveKind, + pub range: Option, + pub index: usize, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroDefine { + pub name: Option, + pub params: Option>, + pub body: Vec, + pub range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroParam { + pub name: Option, + pub default: Option>, + pub range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroUndef { + pub name: Option, + pub range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroInclude { + pub target: MacroIncludeTarget, + pub range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum MacroIncludeTarget { + Literal { path: SmolStr, raw: SmolStr }, + Token { raw: SmolStr }, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum MacroConditionalKind { + IfDef, + IfNDef, + ElsIf, + Else, + EndIf, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroConditional { + pub kind: MacroConditionalKind, + pub expr: Vec, + pub range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroUsage { + pub name: Option, + pub range: Option, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroToken { + pub raw: SmolStr, + pub value: SmolStr, + pub range: Option, +} + +pub fn preproc_file_index_from_text(text: &str, options: &SyntaxTreeOptions) -> PreprocFileIndex { + let mut index = PreprocFileIndex::default(); + for directive in SyntaxTree::preprocessor_directives(text, "source", "", options) { + collect_preprocessor_directive(&mut index, directive); + } + index +} + +pub fn literal_include_directives(text: &str) -> Vec { + preproc_file_index_from_text(text, &SyntaxTreeOptions::without_include_expansion()) + .includes + .into_iter() + .filter(|include| matches!(include.target, MacroIncludeTarget::Literal { .. })) + .collect() +} + +fn range_to_text_range(range: std::ops::Range) -> Option { + Some(TextRange::new( + TextSize::from(u32::try_from(range.start).ok()?), + TextSize::from(u32::try_from(range.end).ok()?), + )) +} + +fn collect_preprocessor_directive(index: &mut PreprocFileIndex, directive: PreprocessorDirective) { + index.inactive_ranges.extend(directive.disabled_ranges.iter().filter_map(|range| { + let range = range_to_text_range(range.clone())?; + (!range.is_empty()).then_some(range) + })); + + let kind = directive.kind; + match kind { + SyntaxKind::DEFINE_DIRECTIVE => { + let directive_index = index.defines.len(); + let define = collect_preprocessor_define(directive); + let range = define.range; + index.defines.push(define); + push_preprocessor_directive(index, MacroDirectiveKind::Define, directive_index, range); + } + SyntaxKind::UNDEF_DIRECTIVE => { + let directive_index = index.undefs.len(); + let range = directive.range.and_then(range_to_text_range); + index.undefs.push(MacroUndef { + name: directive.name.as_ref().map(preprocessor_token_value), + range, + }); + push_preprocessor_directive(index, MacroDirectiveKind::Undef, directive_index, range); + } + SyntaxKind::INCLUDE_DIRECTIVE => { + let directive_index = index.includes.len(); + let range = directive.range.and_then(range_to_text_range); + let target = directive + .include_file_name + .map(|token| include_target_from_raw(token.raw_text.to_smolstr())) + .unwrap_or_else(|| MacroIncludeTarget::Token { raw: SmolStr::new("") }); + index.includes.push(MacroInclude { target, range }); + push_preprocessor_directive(index, MacroDirectiveKind::Include, directive_index, range); + } + SyntaxKind::IF_DEF_DIRECTIVE + | SyntaxKind::IF_N_DEF_DIRECTIVE + | SyntaxKind::ELS_IF_DIRECTIVE => { + let directive_index = index.conditionals.len(); + let range = directive.range.and_then(range_to_text_range); + index.conditionals.push(MacroConditional { + kind: preprocessor_conditional_kind(kind), + expr: directive + .expr_tokens + .into_iter() + .map(macro_token_from_preprocessor) + .collect(), + range, + }); + push_preprocessor_directive( + index, + MacroDirectiveKind::Conditional, + directive_index, + range, + ); + } + SyntaxKind::ELSE_DIRECTIVE | SyntaxKind::END_IF_DIRECTIVE => { + let directive_index = index.conditionals.len(); + let range = directive.range.and_then(range_to_text_range); + index.conditionals.push(MacroConditional { + kind: preprocessor_conditional_kind(kind), + expr: Vec::new(), + range, + }); + push_preprocessor_directive(index, MacroDirectiveKind::Branch, directive_index, range); + } + SyntaxKind::MACRO_USAGE => { + let directive_index = index.usages.len(); + let name = directive.name; + let range = directive + .range + .and_then(range_to_text_range) + .or_else(|| name.as_ref()?.range.clone().and_then(range_to_text_range)); + index + .usages + .push(MacroUsage { name: name.map(|token| macro_name(token.value_text)), range }); + push_preprocessor_directive(index, MacroDirectiveKind::Usage, directive_index, range); + } + _ => {} + } +} + +fn collect_preprocessor_define(directive: PreprocessorDirective) -> MacroDefine { + MacroDefine { + name: directive.name.as_ref().map(preprocessor_token_value), + params: (!directive.params.is_empty()) + .then(|| directive.params.into_iter().map(macro_param_from_preprocessor).collect()), + body: directive.body_tokens.into_iter().map(macro_token_from_preprocessor).collect(), + range: directive.range.and_then(range_to_text_range), + } +} + +fn macro_param_from_preprocessor(param: PreprocessorMacroParam) -> MacroParam { + MacroParam { + name: param.name.as_ref().map(preprocessor_token_value), + default: param + .default_tokens + .map(|tokens| tokens.into_iter().map(macro_token_from_preprocessor).collect()), + range: param.range.and_then(range_to_text_range), + } +} + +fn macro_token_from_preprocessor(token: PreprocessorDirectiveToken) -> MacroToken { + MacroToken { + raw: token.raw_text.to_smolstr(), + value: token.value_text.to_smolstr(), + range: token.range.and_then(range_to_text_range), + } +} + +fn preprocessor_token_value(token: &PreprocessorDirectiveToken) -> SmolStr { + token.value_text.to_smolstr() +} + +fn preprocessor_conditional_kind(kind: SyntaxKind) -> MacroConditionalKind { + match kind { + SyntaxKind::IF_DEF_DIRECTIVE => MacroConditionalKind::IfDef, + SyntaxKind::IF_N_DEF_DIRECTIVE => MacroConditionalKind::IfNDef, + SyntaxKind::ELS_IF_DIRECTIVE => MacroConditionalKind::ElsIf, + SyntaxKind::ELSE_DIRECTIVE => MacroConditionalKind::Else, + SyntaxKind::END_IF_DIRECTIVE => MacroConditionalKind::EndIf, + _ => unreachable!(), + } +} + +fn push_preprocessor_directive( + index: &mut PreprocFileIndex, + kind: MacroDirectiveKind, + directive_index: usize, + range: Option, +) { + index.directives.push(MacroDirective { kind, range, index: directive_index }); +} + +fn include_target_from_raw(raw: SmolStr) -> MacroIncludeTarget { + if let Some(path) = strip_include_delimiters(&raw) { + MacroIncludeTarget::Literal { path: path.to_smolstr(), raw } + } else { + MacroIncludeTarget::Token { raw } + } +} + +fn strip_include_delimiters(raw: &str) -> Option<&str> { + let bytes = raw.as_bytes(); + let (first, last) = (*bytes.first()?, *bytes.last()?); + match (first, last) { + (b'"', b'"') | (b'<', b'>') if raw.len() >= 2 => Some(&raw[1..raw.len() - 1]), + _ => None, + } +} + +fn macro_name(name: String) -> SmolStr { + name.strip_prefix('`').unwrap_or(&name).to_smolstr() +} + +#[cfg(test)] +mod tests { + use super::*; + + fn index(text: &str) -> PreprocFileIndex { + preproc_file_index_from_text(text, &SyntaxTreeOptions::without_include_expansion()) + } + + fn literal_includes(text: &str) -> Vec { + literal_include_directives(text) + } + + fn index_with_predefines(text: &str, predefines: Vec) -> PreprocFileIndex { + preproc_file_index_from_text( + text, + &SyntaxTreeOptions { predefines, ..SyntaxTreeOptions::without_include_expansion() }, + ) + } + + #[test] + fn indexes_define_include_undef_and_usage_directives() { + let index = index( + r#"`define WIDTH(W=8) logic [W-1:0] +`include "defs.svh" +`undef WIDTH +`WIDTH +module top; +endmodule +"#, + ); + + assert_eq!(index.defines.len(), 1); + assert_eq!(index.defines[0].name.as_deref(), Some("WIDTH")); + assert_eq!(index.defines[0].params.as_ref().unwrap()[0].name.as_deref(), Some("W")); + assert_eq!( + index.defines[0].params.as_ref().unwrap()[0].default.as_ref().unwrap()[0].raw.as_str(), + "8" + ); + assert!(index.defines[0].body.iter().any(|token| token.value == "logic")); + + assert_eq!(index.includes.len(), 1); + assert_eq!( + index.includes[0].target, + MacroIncludeTarget::Literal { + path: SmolStr::new("defs.svh"), + raw: SmolStr::new("\"defs.svh\"") + } + ); + + assert_eq!(index.undefs[0].name.as_deref(), Some("WIDTH")); + assert_eq!(index.usages[0].name.as_deref(), Some("WIDTH")); + assert_eq!( + index.directives.iter().map(|directive| directive.kind).collect::>(), + vec![ + MacroDirectiveKind::Define, + MacroDirectiveKind::Include, + MacroDirectiveKind::Undef, + MacroDirectiveKind::Usage, + ] + ); + } + + #[test] + fn indexes_conditional_directive_nodes() { + let index = index( + r#"`ifdef USE_A +`include "a.sv" +`else +`include "b.sv" +`endif +"#, + ); + + assert_eq!( + index.conditionals.iter().map(|conditional| conditional.kind).collect::>(), + vec![ + MacroConditionalKind::IfDef, + MacroConditionalKind::Else, + MacroConditionalKind::EndIf, + ] + ); + assert_eq!(index.conditionals[0].expr[0].value.as_str(), "USE_A"); + } + + #[test] + fn scans_literal_include_directives_without_full_parse() { + let includes = literal_includes( + r#"`include "defs.svh" +`include +`include SOME_MACRO +"`include \"string.svh\"" +// `include "comment.svh" +/* `include "block_comment.svh" */ +"#, + ); + + assert_eq!( + includes.iter().map(|include| &include.target).collect::>(), + vec![ + &MacroIncludeTarget::Literal { + path: SmolStr::new("defs.svh"), + raw: SmolStr::new("\"defs.svh\"") + }, + &MacroIncludeTarget::Literal { + path: SmolStr::new("vendor/pkg.svh"), + raw: SmolStr::new("") + }, + ] + ); + } + + #[test] + fn does_not_scan_include_target_past_line_end() { + let includes = literal_includes( + r#"`include +"next_line.svh" +`include "same_line.svh" +"#, + ); + + assert_eq!(includes.len(), 1); + assert_eq!( + includes[0].target, + MacroIncludeTarget::Literal { + path: SmolStr::new("same_line.svh"), + raw: SmolStr::new("\"same_line.svh\"") + } + ); + } + + #[test] + fn preprocessor_index_honors_predefined_conditional_includes() { + let text = r#"`ifdef USE_A +`include "a.svh" +`else +`include "b.svh" +`endif +"#; + + let without_define = index_with_predefines(text, Vec::new()); + let with_define = index_with_predefines(text, vec!["USE_A=1".to_owned()]); + + assert_eq!( + without_define.includes[0].target, + MacroIncludeTarget::Literal { + path: SmolStr::new("b.svh"), + raw: SmolStr::new("\"b.svh\"") + } + ); + assert_eq!( + with_define.includes[0].target, + MacroIncludeTarget::Literal { + path: SmolStr::new("a.svh"), + raw: SmolStr::new("\"a.svh\"") + } + ); + } + + #[test] + fn records_inactive_preprocessor_branch_ranges() { + let text = r#"`ifdef USE_A +logic active; +`else +logic inactive; +`endif +"#; + + let without_define = index_with_predefines(text, Vec::new()); + let with_define = index_with_predefines(text, vec!["USE_A=1".to_owned()]); + + let inactive_range = without_define.inactive_ranges[0]; + assert_eq!( + &text[usize::from(inactive_range.start())..usize::from(inactive_range.end())], + "logic active;" + ); + + let inactive_range = with_define.inactive_ranges[0]; + assert_eq!( + &text[usize::from(inactive_range.start())..usize::from(inactive_range.end())], + "logic inactive;" + ); + } +} diff --git a/crates/preproc/src/lib.rs b/crates/preproc/src/lib.rs index dfbacbc3..afbe7558 100644 --- a/crates/preproc/src/lib.rs +++ b/crates/preproc/src/lib.rs @@ -1,454 +1,2 @@ -use smol_str::{SmolStr, ToSmolStr}; -use syntax::{ - PreprocessorDirective, PreprocessorDirectiveToken, PreprocessorMacroParam, SyntaxKind, - SyntaxTree, SyntaxTreeOptions, -}; -use utils::line_index::{TextRange, TextSize}; - -#[derive(Debug, Clone, PartialEq, Eq, Default)] -pub struct PreprocFileIndex { - pub directives: Vec, - pub defines: Vec, - pub undefs: Vec, - pub includes: Vec, - pub conditionals: Vec, - pub usages: Vec, - pub inactive_ranges: Vec, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MacroDirectiveKind { - Define, - Undef, - Include, - Conditional, - Branch, - Usage, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroDirective { - pub kind: MacroDirectiveKind, - pub range: Option, - pub index: usize, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroDefine { - pub name: Option, - pub params: Option>, - pub body: Vec, - pub range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroParam { - pub name: Option, - pub default: Option>, - pub range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroUndef { - pub name: Option, - pub range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroInclude { - pub target: MacroIncludeTarget, - pub range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub enum MacroIncludeTarget { - Literal { path: SmolStr, raw: SmolStr }, - Token { raw: SmolStr }, -} - -#[derive(Debug, Clone, Copy, PartialEq, Eq)] -pub enum MacroConditionalKind { - IfDef, - IfNDef, - ElsIf, - Else, - EndIf, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroConditional { - pub kind: MacroConditionalKind, - pub expr: Vec, - pub range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroUsage { - pub name: Option, - pub range: Option, -} - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct MacroToken { - pub raw: SmolStr, - pub value: SmolStr, - pub range: Option, -} - -pub fn preproc_file_index_from_text(text: &str, options: &SyntaxTreeOptions) -> PreprocFileIndex { - let mut index = PreprocFileIndex::default(); - for directive in SyntaxTree::preprocessor_directives(text, "source", "", options) { - collect_preprocessor_directive(&mut index, directive); - } - index -} - -pub fn literal_include_directives(text: &str) -> Vec { - preproc_file_index_from_text(text, &SyntaxTreeOptions::without_include_expansion()) - .includes - .into_iter() - .filter(|include| matches!(include.target, MacroIncludeTarget::Literal { .. })) - .collect() -} - -fn range_to_text_range(range: std::ops::Range) -> Option { - Some(TextRange::new( - TextSize::from(u32::try_from(range.start).ok()?), - TextSize::from(u32::try_from(range.end).ok()?), - )) -} - -fn collect_preprocessor_directive(index: &mut PreprocFileIndex, directive: PreprocessorDirective) { - index.inactive_ranges.extend(directive.disabled_ranges.iter().filter_map(|range| { - let range = range_to_text_range(range.clone())?; - (!range.is_empty()).then_some(range) - })); - - let kind = directive.kind; - match kind { - SyntaxKind::DEFINE_DIRECTIVE => { - let directive_index = index.defines.len(); - let define = collect_preprocessor_define(directive); - let range = define.range; - index.defines.push(define); - push_preprocessor_directive(index, MacroDirectiveKind::Define, directive_index, range); - } - SyntaxKind::UNDEF_DIRECTIVE => { - let directive_index = index.undefs.len(); - let range = directive.range.and_then(range_to_text_range); - index.undefs.push(MacroUndef { - name: directive.name.as_ref().map(preprocessor_token_value), - range, - }); - push_preprocessor_directive(index, MacroDirectiveKind::Undef, directive_index, range); - } - SyntaxKind::INCLUDE_DIRECTIVE => { - let directive_index = index.includes.len(); - let range = directive.range.and_then(range_to_text_range); - let target = directive - .include_file_name - .map(|token| include_target_from_raw(token.raw_text.to_smolstr())) - .unwrap_or_else(|| MacroIncludeTarget::Token { raw: SmolStr::new("") }); - index.includes.push(MacroInclude { target, range }); - push_preprocessor_directive(index, MacroDirectiveKind::Include, directive_index, range); - } - SyntaxKind::IF_DEF_DIRECTIVE - | SyntaxKind::IF_N_DEF_DIRECTIVE - | SyntaxKind::ELS_IF_DIRECTIVE => { - let directive_index = index.conditionals.len(); - let range = directive.range.and_then(range_to_text_range); - index.conditionals.push(MacroConditional { - kind: preprocessor_conditional_kind(kind), - expr: directive - .expr_tokens - .into_iter() - .map(macro_token_from_preprocessor) - .collect(), - range, - }); - push_preprocessor_directive( - index, - MacroDirectiveKind::Conditional, - directive_index, - range, - ); - } - SyntaxKind::ELSE_DIRECTIVE | SyntaxKind::END_IF_DIRECTIVE => { - let directive_index = index.conditionals.len(); - let range = directive.range.and_then(range_to_text_range); - index.conditionals.push(MacroConditional { - kind: preprocessor_conditional_kind(kind), - expr: Vec::new(), - range, - }); - push_preprocessor_directive(index, MacroDirectiveKind::Branch, directive_index, range); - } - SyntaxKind::MACRO_USAGE => { - let directive_index = index.usages.len(); - let range = directive.range.and_then(range_to_text_range); - index.usages.push(MacroUsage { - name: directive.name.map(|token| macro_name(token.value_text)), - range, - }); - push_preprocessor_directive(index, MacroDirectiveKind::Usage, directive_index, range); - } - _ => {} - } -} - -fn collect_preprocessor_define(directive: PreprocessorDirective) -> MacroDefine { - MacroDefine { - name: directive.name.as_ref().map(preprocessor_token_value), - params: (!directive.params.is_empty()) - .then(|| directive.params.into_iter().map(macro_param_from_preprocessor).collect()), - body: directive.body_tokens.into_iter().map(macro_token_from_preprocessor).collect(), - range: directive.range.and_then(range_to_text_range), - } -} - -fn macro_param_from_preprocessor(param: PreprocessorMacroParam) -> MacroParam { - MacroParam { - name: param.name.as_ref().map(preprocessor_token_value), - default: param - .default_tokens - .map(|tokens| tokens.into_iter().map(macro_token_from_preprocessor).collect()), - range: param.range.and_then(range_to_text_range), - } -} - -fn macro_token_from_preprocessor(token: PreprocessorDirectiveToken) -> MacroToken { - MacroToken { - raw: token.raw_text.to_smolstr(), - value: token.value_text.to_smolstr(), - range: token.range.and_then(range_to_text_range), - } -} - -fn preprocessor_token_value(token: &PreprocessorDirectiveToken) -> SmolStr { - token.value_text.to_smolstr() -} - -fn preprocessor_conditional_kind(kind: SyntaxKind) -> MacroConditionalKind { - match kind { - SyntaxKind::IF_DEF_DIRECTIVE => MacroConditionalKind::IfDef, - SyntaxKind::IF_N_DEF_DIRECTIVE => MacroConditionalKind::IfNDef, - SyntaxKind::ELS_IF_DIRECTIVE => MacroConditionalKind::ElsIf, - SyntaxKind::ELSE_DIRECTIVE => MacroConditionalKind::Else, - SyntaxKind::END_IF_DIRECTIVE => MacroConditionalKind::EndIf, - _ => unreachable!(), - } -} - -fn push_preprocessor_directive( - index: &mut PreprocFileIndex, - kind: MacroDirectiveKind, - directive_index: usize, - range: Option, -) { - index.directives.push(MacroDirective { kind, range, index: directive_index }); -} - -fn include_target_from_raw(raw: SmolStr) -> MacroIncludeTarget { - if let Some(path) = strip_include_delimiters(&raw) { - MacroIncludeTarget::Literal { path: path.to_smolstr(), raw } - } else { - MacroIncludeTarget::Token { raw } - } -} - -fn strip_include_delimiters(raw: &str) -> Option<&str> { - let bytes = raw.as_bytes(); - let (first, last) = (*bytes.first()?, *bytes.last()?); - match (first, last) { - (b'"', b'"') | (b'<', b'>') if raw.len() >= 2 => Some(&raw[1..raw.len() - 1]), - _ => None, - } -} - -fn macro_name(name: String) -> SmolStr { - name.strip_prefix('`').unwrap_or(&name).to_smolstr() -} - -#[cfg(test)] -mod tests { - use super::*; - - fn index(text: &str) -> PreprocFileIndex { - preproc_file_index_from_text(text, &SyntaxTreeOptions::without_include_expansion()) - } - - fn literal_includes(text: &str) -> Vec { - literal_include_directives(text) - } - - fn index_with_predefines(text: &str, predefines: Vec) -> PreprocFileIndex { - preproc_file_index_from_text( - text, - &SyntaxTreeOptions { predefines, ..SyntaxTreeOptions::without_include_expansion() }, - ) - } - - #[test] - fn indexes_define_include_undef_and_usage_directives() { - let index = index( - r#"`define WIDTH(W=8) logic [W-1:0] -`include "defs.svh" -`undef WIDTH -`WIDTH -module top; -endmodule -"#, - ); - - assert_eq!(index.defines.len(), 1); - assert_eq!(index.defines[0].name.as_deref(), Some("WIDTH")); - assert_eq!(index.defines[0].params.as_ref().unwrap()[0].name.as_deref(), Some("W")); - assert_eq!( - index.defines[0].params.as_ref().unwrap()[0].default.as_ref().unwrap()[0].raw.as_str(), - "8" - ); - assert!(index.defines[0].body.iter().any(|token| token.value == "logic")); - - assert_eq!(index.includes.len(), 1); - assert_eq!( - index.includes[0].target, - MacroIncludeTarget::Literal { - path: SmolStr::new("defs.svh"), - raw: SmolStr::new("\"defs.svh\"") - } - ); - - assert_eq!(index.undefs[0].name.as_deref(), Some("WIDTH")); - assert_eq!(index.usages[0].name.as_deref(), Some("WIDTH")); - assert_eq!( - index.directives.iter().map(|directive| directive.kind).collect::>(), - vec![ - MacroDirectiveKind::Define, - MacroDirectiveKind::Include, - MacroDirectiveKind::Undef, - MacroDirectiveKind::Usage, - ] - ); - } - - #[test] - fn indexes_conditional_directive_nodes() { - let index = index( - r#"`ifdef USE_A -`include "a.sv" -`else -`include "b.sv" -`endif -"#, - ); - - assert_eq!( - index.conditionals.iter().map(|conditional| conditional.kind).collect::>(), - vec![ - MacroConditionalKind::IfDef, - MacroConditionalKind::Else, - MacroConditionalKind::EndIf, - ] - ); - assert_eq!(index.conditionals[0].expr[0].value.as_str(), "USE_A"); - } - - #[test] - fn scans_literal_include_directives_without_full_parse() { - let includes = literal_includes( - r#"`include "defs.svh" -`include -`include SOME_MACRO -"`include \"string.svh\"" -// `include "comment.svh" -/* `include "block_comment.svh" */ -"#, - ); - - assert_eq!( - includes.iter().map(|include| &include.target).collect::>(), - vec![ - &MacroIncludeTarget::Literal { - path: SmolStr::new("defs.svh"), - raw: SmolStr::new("\"defs.svh\"") - }, - &MacroIncludeTarget::Literal { - path: SmolStr::new("vendor/pkg.svh"), - raw: SmolStr::new("") - }, - ] - ); - } - - #[test] - fn does_not_scan_include_target_past_line_end() { - let includes = literal_includes( - r#"`include -"next_line.svh" -`include "same_line.svh" -"#, - ); - - assert_eq!(includes.len(), 1); - assert_eq!( - includes[0].target, - MacroIncludeTarget::Literal { - path: SmolStr::new("same_line.svh"), - raw: SmolStr::new("\"same_line.svh\"") - } - ); - } - - #[test] - fn preprocessor_index_honors_predefined_conditional_includes() { - let text = r#"`ifdef USE_A -`include "a.svh" -`else -`include "b.svh" -`endif -"#; - - let without_define = index_with_predefines(text, Vec::new()); - let with_define = index_with_predefines(text, vec!["USE_A=1".to_owned()]); - - assert_eq!( - without_define.includes[0].target, - MacroIncludeTarget::Literal { - path: SmolStr::new("b.svh"), - raw: SmolStr::new("\"b.svh\"") - } - ); - assert_eq!( - with_define.includes[0].target, - MacroIncludeTarget::Literal { - path: SmolStr::new("a.svh"), - raw: SmolStr::new("\"a.svh\"") - } - ); - } - - #[test] - fn records_inactive_preprocessor_branch_ranges() { - let text = r#"`ifdef USE_A -logic active; -`else -logic inactive; -`endif -"#; - - let without_define = index_with_predefines(text, Vec::new()); - let with_define = index_with_predefines(text, vec!["USE_A=1".to_owned()]); - - let inactive_range = without_define.inactive_ranges[0]; - assert_eq!( - &text[usize::from(inactive_range.start())..usize::from(inactive_range.end())], - "logic active;" - ); - - let inactive_range = with_define.inactive_ranges[0]; - assert_eq!( - &text[usize::from(inactive_range.start())..usize::from(inactive_range.end())], - "logic inactive;" - ); - } -} +pub mod index; +pub mod model; diff --git a/crates/preproc/src/model.rs b/crates/preproc/src/model.rs new file mode 100644 index 00000000..59f6f948 --- /dev/null +++ b/crates/preproc/src/model.rs @@ -0,0 +1,518 @@ +use std::collections::BTreeMap; + +use smol_str::SmolStr; +use syntax::SyntaxTreeOptions; +use utils::line_index::{TextRange, TextSize}; + +use crate::index::{ + MacroConditional, MacroDefine, MacroDirective, MacroDirectiveKind, MacroInclude, MacroUndef, + MacroUsage, PreprocFileIndex, preproc_file_index_from_text, +}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocModel { + index: PreprocFileIndex, +} + +#[derive(Debug, Clone, PartialEq, Eq, Default)] +pub struct MacroEnvironment { + definitions: BTreeMap, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroBinding<'a> { + pub name: SmolStr, + pub define_index: usize, + pub define: &'a MacroDefine, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PreprocEntity { + Define(usize), + Undef(usize), + Usage(usize), + Include(usize), + Conditional(usize), +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct PreprocProvenance { + pub entity: PreprocEntity, + pub name: Option, + pub range: Option, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum PreprocEvent<'a> { + Define { source_order: usize, index: usize, define: &'a MacroDefine }, + Undef { source_order: usize, index: usize, undef: &'a MacroUndef }, + Include { source_order: usize, index: usize, include: &'a MacroInclude }, + Conditional { source_order: usize, index: usize, conditional: &'a MacroConditional }, + Branch { source_order: usize, index: usize, conditional: &'a MacroConditional }, + Usage { source_order: usize, index: usize, usage: &'a MacroUsage }, +} + +impl PreprocModel { + pub fn new(index: PreprocFileIndex) -> Self { + Self { index } + } + + pub fn from_text(text: &str, options: &SyntaxTreeOptions) -> Self { + Self::new(preproc_file_index_from_text(text, options)) + } + + pub fn index(&self) -> &PreprocFileIndex { + &self.index + } + + pub fn into_index(self) -> PreprocFileIndex { + self.index + } + + pub fn defines(&self) -> &[MacroDefine] { + &self.index.defines + } + + pub fn undefs(&self) -> &[MacroUndef] { + &self.index.undefs + } + + pub fn usages(&self) -> &[MacroUsage] { + &self.index.usages + } + + pub fn includes(&self) -> &[MacroInclude] { + &self.index.includes + } + + pub fn conditionals(&self) -> &[MacroConditional] { + &self.index.conditionals + } + + pub fn inactive_ranges(&self) -> &[TextRange] { + &self.index.inactive_ranges + } + + pub fn events(&self) -> impl Iterator> + '_ { + self.index.directives.iter().enumerate().filter_map(|(source_order, directive)| { + self.event_from_directive(source_order, directive) + }) + } + + pub fn macro_environment_at(&self, offset: TextSize) -> MacroEnvironment { + let mut environment = MacroEnvironment::default(); + for directive in &self.index.directives { + if directive_applies_at_offset(directive, offset) { + self.apply_macro_state(directive, &mut environment); + } + } + environment + } + + pub fn visible_macros_at(&self, offset: TextSize) -> Vec> { + let environment = self.macro_environment_at(offset); + self.bindings_for_environment(&environment) + } + + pub fn definition_for_usage(&self, usage_index: usize) -> Option> { + let usage = self.index.usages.get(usage_index)?; + let name = usage.name.as_ref()?; + let environment = self.macro_environment_before(PreprocEntity::Usage(usage_index))?; + let define_index = environment.define_index(name.as_str())?; + let define = self.index.defines.get(define_index)?; + Some(MacroBinding { name: name.clone(), define_index, define }) + } + + pub fn provenance(&self, entity: PreprocEntity) -> Option { + let (name, range) = match entity { + PreprocEntity::Define(index) => { + let define = self.index.defines.get(index)?; + (define.name.clone(), define.range) + } + PreprocEntity::Undef(index) => { + let undef = self.index.undefs.get(index)?; + (undef.name.clone(), undef.range) + } + PreprocEntity::Usage(index) => { + let usage = self.index.usages.get(index)?; + (usage.name.clone(), usage.range) + } + PreprocEntity::Include(index) => { + let include = self.index.includes.get(index)?; + (None, include.range) + } + PreprocEntity::Conditional(index) => { + let conditional = self.index.conditionals.get(index)?; + (None, conditional.range) + } + }; + Some(PreprocProvenance { entity, name, range }) + } + + pub fn source_range(&self, entity: PreprocEntity) -> Option { + self.provenance(entity).and_then(|provenance| provenance.range) + } + + pub fn define(&self, index: usize) -> Option<&MacroDefine> { + self.index.defines.get(index) + } + + pub fn undef(&self, index: usize) -> Option<&MacroUndef> { + self.index.undefs.get(index) + } + + pub fn usage(&self, index: usize) -> Option<&MacroUsage> { + self.index.usages.get(index) + } + + pub fn include(&self, index: usize) -> Option<&MacroInclude> { + self.index.includes.get(index) + } + + pub fn conditional(&self, index: usize) -> Option<&MacroConditional> { + self.index.conditionals.get(index) + } + + fn macro_environment_before(&self, entity: PreprocEntity) -> Option { + let mut environment = MacroEnvironment::default(); + for directive in &self.index.directives { + if directive_matches_entity(directive, entity) { + return Some(environment); + } + self.apply_macro_state(directive, &mut environment); + } + None + } + + fn bindings_for_environment(&self, environment: &MacroEnvironment) -> Vec> { + environment + .definitions + .iter() + .filter_map(|(name, define_index)| { + let define = self.index.defines.get(*define_index)?; + Some(MacroBinding { name: name.clone(), define_index: *define_index, define }) + }) + .collect() + } + + fn apply_macro_state(&self, directive: &MacroDirective, environment: &mut MacroEnvironment) { + match directive.kind { + MacroDirectiveKind::Define => { + if let Some(define) = self.index.defines.get(directive.index) + && let Some(name) = define.name.as_ref() + { + environment.definitions.insert(name.clone(), directive.index); + } + } + MacroDirectiveKind::Undef => { + if let Some(undef) = self.index.undefs.get(directive.index) + && let Some(name) = undef.name.as_ref() + { + environment.definitions.remove(name.as_str()); + } + } + MacroDirectiveKind::Include + | MacroDirectiveKind::Conditional + | MacroDirectiveKind::Branch + | MacroDirectiveKind::Usage => {} + } + } + + fn event_from_directive( + &self, + source_order: usize, + directive: &MacroDirective, + ) -> Option> { + match directive.kind { + MacroDirectiveKind::Define => { + let define = self.index.defines.get(directive.index)?; + Some(PreprocEvent::Define { source_order, index: directive.index, define }) + } + MacroDirectiveKind::Undef => { + let undef = self.index.undefs.get(directive.index)?; + Some(PreprocEvent::Undef { source_order, index: directive.index, undef }) + } + MacroDirectiveKind::Include => { + let include = self.index.includes.get(directive.index)?; + Some(PreprocEvent::Include { source_order, index: directive.index, include }) + } + MacroDirectiveKind::Conditional => { + let conditional = self.index.conditionals.get(directive.index)?; + Some(PreprocEvent::Conditional { + source_order, + index: directive.index, + conditional, + }) + } + MacroDirectiveKind::Branch => { + let conditional = self.index.conditionals.get(directive.index)?; + Some(PreprocEvent::Branch { source_order, index: directive.index, conditional }) + } + MacroDirectiveKind::Usage => { + let usage = self.index.usages.get(directive.index)?; + Some(PreprocEvent::Usage { source_order, index: directive.index, usage }) + } + } + } +} + +impl MacroEnvironment { + pub fn define_index(&self, name: &str) -> Option { + self.definitions.get(name).copied() + } + + pub fn contains(&self, name: &str) -> bool { + self.definitions.contains_key(name) + } + + pub fn len(&self) -> usize { + self.definitions.len() + } + + pub fn is_empty(&self) -> bool { + self.definitions.is_empty() + } + + pub fn names(&self) -> impl Iterator { + self.definitions.keys() + } + + pub fn definitions(&self) -> &BTreeMap { + &self.definitions + } +} + +impl PreprocEvent<'_> { + pub fn source_order(&self) -> usize { + match self { + PreprocEvent::Define { source_order, .. } + | PreprocEvent::Undef { source_order, .. } + | PreprocEvent::Include { source_order, .. } + | PreprocEvent::Conditional { source_order, .. } + | PreprocEvent::Branch { source_order, .. } + | PreprocEvent::Usage { source_order, .. } => *source_order, + } + } + + pub fn kind(&self) -> MacroDirectiveKind { + match self { + PreprocEvent::Define { .. } => MacroDirectiveKind::Define, + PreprocEvent::Undef { .. } => MacroDirectiveKind::Undef, + PreprocEvent::Include { .. } => MacroDirectiveKind::Include, + PreprocEvent::Conditional { .. } => MacroDirectiveKind::Conditional, + PreprocEvent::Branch { .. } => MacroDirectiveKind::Branch, + PreprocEvent::Usage { .. } => MacroDirectiveKind::Usage, + } + } + + pub fn entity(&self) -> PreprocEntity { + match self { + PreprocEvent::Define { index, .. } => PreprocEntity::Define(*index), + PreprocEvent::Undef { index, .. } => PreprocEntity::Undef(*index), + PreprocEvent::Include { index, .. } => PreprocEntity::Include(*index), + PreprocEvent::Conditional { index, .. } | PreprocEvent::Branch { index, .. } => { + PreprocEntity::Conditional(*index) + } + PreprocEvent::Usage { index, .. } => PreprocEntity::Usage(*index), + } + } + + pub fn range(&self) -> Option { + match self { + PreprocEvent::Define { define, .. } => define.range, + PreprocEvent::Undef { undef, .. } => undef.range, + PreprocEvent::Include { include, .. } => include.range, + PreprocEvent::Conditional { conditional, .. } + | PreprocEvent::Branch { conditional, .. } => conditional.range, + PreprocEvent::Usage { usage, .. } => usage.range, + } + } +} + +fn directive_applies_at_offset(directive: &MacroDirective, offset: TextSize) -> bool { + directive.range.is_none_or(|range| range.end() <= offset) +} + +fn directive_matches_entity(directive: &MacroDirective, entity: PreprocEntity) -> bool { + match (directive.kind, entity) { + (MacroDirectiveKind::Define, PreprocEntity::Define(index)) + | (MacroDirectiveKind::Undef, PreprocEntity::Undef(index)) + | (MacroDirectiveKind::Usage, PreprocEntity::Usage(index)) + | (MacroDirectiveKind::Include, PreprocEntity::Include(index)) => directive.index == index, + ( + MacroDirectiveKind::Conditional | MacroDirectiveKind::Branch, + PreprocEntity::Conditional(index), + ) => directive.index == index, + _ => false, + } +} + +#[cfg(test)] +mod tests { + use smol_str::SmolStr; + + use super::*; + use crate::index::{MacroConditionalKind, MacroIncludeTarget}; + + fn model(text: &str) -> PreprocModel { + PreprocModel::from_text(text, &SyntaxTreeOptions::without_include_expansion()) + } + + fn model_with_predefines(text: &str, predefines: Vec) -> PreprocModel { + PreprocModel::from_text( + text, + &SyntaxTreeOptions { predefines, ..SyntaxTreeOptions::without_include_expansion() }, + ) + } + + fn offset_after(text: &str, needle: &str) -> TextSize { + TextSize::from(u32::try_from(text.find(needle).unwrap() + needle.len()).unwrap()) + } + + fn text_at_range(text: &str, range: TextRange) -> &str { + &text[usize::from(range.start())..usize::from(range.end())] + } + + #[test] + fn preproc_model_reports_define_visible_after_directive() { + let text = r#"`define WIDTH 8 +module top; +endmodule +"#; + let model = model(text); + let environment = model.macro_environment_at(offset_after(text, "`define WIDTH 8\n")); + + assert_eq!(environment.define_index("WIDTH"), Some(0)); + assert_eq!( + environment.names().map(|name| name.as_str()).collect::>(), + vec!["WIDTH"] + ); + + let visible = model.visible_macros_at(offset_after(text, "`define WIDTH 8\n")); + assert_eq!(visible.len(), 1); + assert_eq!(visible[0].name.as_str(), "WIDTH"); + assert_eq!(visible[0].define_index, 0); + + let provenance = model.provenance(PreprocEntity::Define(0)).unwrap(); + assert_eq!(provenance.name.as_deref(), Some("WIDTH")); + assert_eq!(text_at_range(text, provenance.range.unwrap()).trim(), "WIDTH"); + } + + #[test] + fn preproc_model_removes_macro_after_undef() { + let text = r#"`define WIDTH 8 +`undef WIDTH +module top; +endmodule +"#; + let model = model(text); + let environment = model.macro_environment_at(offset_after(text, "`undef WIDTH\n")); + + assert!(!environment.contains("WIDTH")); + assert_eq!(environment.define_index("WIDTH"), None); + + let provenance = model.provenance(PreprocEntity::Undef(0)).unwrap(); + assert_eq!(provenance.name.as_deref(), Some("WIDTH")); + assert_eq!(text_at_range(text, provenance.range.unwrap()), "WIDTH"); + } + + #[test] + fn preproc_model_uses_latest_define_for_same_macro_name() { + let text = r#"`define WIDTH 8 +`define WIDTH 16 +module top; +endmodule +"#; + let model = model(text); + let environment = model.macro_environment_at(offset_after(text, "`define WIDTH 16\n")); + + assert_eq!(environment.define_index("WIDTH"), Some(1)); + let binding = + model.visible_macros_at(offset_after(text, "`define WIDTH 16\n")).pop().unwrap(); + assert_eq!(binding.define_index, 1); + assert_eq!(binding.define.body[0].value.as_str(), "16"); + } + + #[test] + fn preproc_model_resolves_usage_to_nearest_effective_define() { + let text = r#"`define WIDTH 8 +logic [`WIDTH-1:0] a; +`define WIDTH 16 +logic [`WIDTH-1:0] b; +"#; + let model = model(text); + + assert_eq!(model.usages().len(), 2); + + let first_binding = model.definition_for_usage(0).unwrap(); + assert_eq!(first_binding.name.as_str(), "WIDTH"); + assert_eq!(first_binding.define_index, 0); + assert_eq!(first_binding.define.body[0].value.as_str(), "8"); + + let second_binding = model.definition_for_usage(1).unwrap(); + assert_eq!(second_binding.name.as_str(), "WIDTH"); + assert_eq!(second_binding.define_index, 1); + assert_eq!(second_binding.define.body[0].value.as_str(), "16"); + + let provenance = model.provenance(PreprocEntity::Usage(1)).unwrap(); + assert_eq!(provenance.name.as_deref(), Some("WIDTH")); + assert_eq!(text_at_range(text, provenance.range.unwrap()), "`WIDTH"); + } + + #[test] + fn preproc_model_preserves_conditional_branch_ranges() { + let text = r#"`ifdef USE_A +logic active; +`else +logic inactive; +`endif +"#; + let model = model_with_predefines(text, vec!["USE_A=1".to_owned()]); + + assert_eq!( + model.conditionals().iter().map(|conditional| conditional.kind).collect::>(), + vec![ + MacroConditionalKind::IfDef, + MacroConditionalKind::Else, + MacroConditionalKind::EndIf, + ] + ); + assert_eq!( + model.events().map(|event| event.kind()).collect::>(), + vec![ + MacroDirectiveKind::Conditional, + MacroDirectiveKind::Branch, + MacroDirectiveKind::Branch, + ] + ); + + let else_range = model.source_range(PreprocEntity::Conditional(1)).unwrap(); + assert_eq!(text_at_range(text, else_range), "logic inactive"); + let inactive_range = model.inactive_ranges()[0]; + assert_eq!(text_at_range(text, inactive_range), "logic inactive;"); + } + + #[test] + fn preproc_model_exposes_include_targets() { + let text = r#"`include "defs.svh" +module top; +endmodule +"#; + let model = model(text); + + assert_eq!( + model.includes()[0].target, + MacroIncludeTarget::Literal { + path: SmolStr::new("defs.svh"), + raw: SmolStr::new("\"defs.svh\"") + } + ); + assert_eq!( + model.events().map(|event| event.kind()).collect::>(), + vec![MacroDirectiveKind::Include] + ); + + let include_range = model.source_range(PreprocEntity::Include(0)).unwrap(); + assert_eq!(text_at_range(text, include_range), "\"defs.svh\""); + } +} From 7035374d4c46b161668dbb999890bbb366d0e72e Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 5 Jun 2026 01:19:40 +0800 Subject: [PATCH 2/8] feat(hir): expose preprocessor query facts --- crates/hir/src/base_db/source_db.rs | 10 +- crates/hir/src/lib.rs | 1 + crates/hir/src/preproc.rs | 161 ++++++++++++++++++++++++++++ 3 files changed, 171 insertions(+), 1 deletion(-) create mode 100644 crates/hir/src/preproc.rs diff --git a/crates/hir/src/base_db/source_db.rs b/crates/hir/src/base_db/source_db.rs index e4903846..e7404186 100644 --- a/crates/hir/src/base_db/source_db.rs +++ b/crates/hir/src/base_db/source_db.rs @@ -1,4 +1,7 @@ -use preproc::index::{PreprocFileIndex, preproc_file_index_from_text}; +use preproc::{ + index::{PreprocFileIndex, preproc_file_index_from_text}, + model::PreprocModel, +}; use rustc_hash::{FxHashMap, FxHashSet}; use syntax::{ Compilation, ParserExpectedSyntax, SyntaxDiagnostic, SyntaxTree, SyntaxTreeBuffer, @@ -42,6 +45,7 @@ pub trait SourceDb: FileLoader + std::fmt::Debug { file_id: FileId, predefines: Vec, ) -> Arc; + fn preproc_model(&self, file_id: FileId) -> Arc; #[salsa::input] fn files(&self) -> Box>; @@ -140,6 +144,10 @@ fn preproc_file_index_with_predefines( } } +fn preproc_model(db: &dyn SourceDb, file_id: FileId) -> Arc { + Arc::new(PreprocModel::new((*db.preproc_file_index(file_id)).clone())) +} + struct SourceFileIdentity { name: String, path: String, diff --git a/crates/hir/src/lib.rs b/crates/hir/src/lib.rs index a8eb4b30..3fcf3243 100644 --- a/crates/hir/src/lib.rs +++ b/crates/hir/src/lib.rs @@ -7,6 +7,7 @@ pub mod display; pub mod file; pub mod has_source; pub mod hir_def; +pub mod preproc; pub mod region_tree; pub mod scope; pub mod semantics; diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs new file mode 100644 index 00000000..f9d99d53 --- /dev/null +++ b/crates/hir/src/preproc.rs @@ -0,0 +1,161 @@ +use smol_str::SmolStr; +use utils::{ + line_index::{TextRange, TextSize}, + path_identity::PathIdentityIndex, + paths::{AbsPathBuf, Utf8Path}, +}; +use vfs::FileId; + +use crate::base_db::source_db::{SourceDb, SourceRootDb}; + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroDefinition { + pub file_id: FileId, + pub name: SmolStr, + pub define_index: usize, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroUsage { + pub file_id: FileId, + pub name: SmolStr, + pub usage_index: usize, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroUsageResolution { + pub usage: MacroUsage, + pub definition: MacroDefinition, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct IncludeDirective { + pub file_id: FileId, + pub include_index: usize, + pub range: TextRange, + pub target: IncludeTarget, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum IncludeTarget { + Literal { path: SmolStr, resolved_file: Option }, + Token { raw: SmolStr }, +} + +pub fn visible_macros_at( + db: &dyn SourceDb, + file_id: FileId, + offset: TextSize, +) -> Vec { + db.preproc_model(file_id) + .visible_macros_at(offset) + .into_iter() + .filter_map(|binding| { + let range = binding.define.range?; + Some(MacroDefinition { + file_id, + name: binding.name, + define_index: binding.define_index, + range, + }) + }) + .collect() +} + +pub fn macro_usage_resolution_at( + db: &dyn SourceDb, + file_id: FileId, + offset: TextSize, +) -> Option { + let model = db.preproc_model(file_id); + let (usage_index, usage) = + model.usages().iter().enumerate().find(|(_, usage)| { + usage.range.is_some_and(|range| range_contains_offset(range, offset)) + })?; + let usage = MacroUsage { file_id, name: usage.name.clone()?, usage_index, range: usage.range? }; + let binding = model.definition_for_usage(usage_index)?; + let definition = MacroDefinition { + file_id, + name: binding.name, + define_index: binding.define_index, + range: binding.define.range?, + }; + Some(MacroUsageResolution { usage, definition }) +} + +pub fn include_directive_at( + db: &dyn SourceRootDb, + file_id: FileId, + offset: TextSize, +) -> Option { + let model = db.preproc_model(file_id); + let (include_index, include) = model.includes().iter().enumerate().find(|(_, include)| { + include.range.is_some_and(|range| range_contains_offset(range, offset)) + })?; + let range = include.range?; + let target = match &include.target { + ::preproc::index::MacroIncludeTarget::Literal { path, .. } => IncludeTarget::Literal { + path: path.clone(), + resolved_file: resolve_literal_include(db, file_id, path), + }, + ::preproc::index::MacroIncludeTarget::Token { raw } => { + IncludeTarget::Token { raw: raw.clone() } + } + }; + Some(IncludeDirective { file_id, include_index, range, target }) +} + +fn resolve_literal_include(db: &dyn SourceRootDb, file_id: FileId, path: &str) -> Option { + let includer_path = db.file_path(file_id)?; + let include_dirs = db.file_preprocess_config(file_id).include_dirs.clone(); + let path_file_ids = path_file_ids(db); + resolve_include_target(path, &includer_path, &include_dirs, &path_file_ids) +} + +fn path_file_ids(db: &dyn SourceRootDb) -> PathIdentityIndex { + let mut index = PathIdentityIndex::default(); + for file_id in db.files().iter().copied() { + if db.file_is_project_ignored(file_id) { + continue; + } + if let Some(path) = db.file_path(file_id) { + index.insert_path(&path, file_id); + } + } + index +} + +fn resolve_include_target( + path: &str, + includer_path: &AbsPathBuf, + include_dirs: &[AbsPathBuf], + path_file_ids: &PathIdentityIndex, +) -> Option { + let include_path = Utf8Path::new(path); + if include_path.is_absolute() { + let abs_path = AbsPathBuf::try_from(include_path.to_path_buf()).ok()?.normalize(); + return path_file_ids.get_path(abs_path.as_path()); + } + + if let Some(parent) = includer_path.parent() { + let candidate = parent.absolutize(include_path); + if let Some(file_id) = path_file_ids.get_path(candidate.as_path()) { + return Some(file_id); + } + } + + for include_dir in include_dirs { + let candidate = include_dir.absolutize(include_path); + if let Some(file_id) = path_file_ids.get_path(candidate.as_path()) { + return Some(file_id); + } + } + + None +} + +fn range_contains_offset(range: TextRange, offset: TextSize) -> bool { + range.start() <= offset && offset <= range.end() +} From 7f26532220891d9828b7e9b2fdb2d69476733e7b Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 5 Jun 2026 01:25:05 +0800 Subject: [PATCH 3/8] feat(ide): navigate preprocessor includes --- crates/hir/src/preproc.rs | 15 ++++ crates/ide/src/diagnostics.rs | 8 +-- crates/ide/src/goto_definition.rs | 39 +++++++++- crates/ide/src/hover.rs | 44 +++++++++++- crates/ide/src/verilog_2005.rs | 115 ++++++++++++++++++++---------- 5 files changed, 177 insertions(+), 44 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index f9d99d53..681727cc 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -38,6 +38,12 @@ pub struct IncludeDirective { pub target: IncludeTarget, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct InactiveBranch { + pub file_id: FileId, + pub range: TextRange, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub enum IncludeTarget { Literal { path: SmolStr, resolved_file: Option }, @@ -107,6 +113,15 @@ pub fn include_directive_at( Some(IncludeDirective { file_id, include_index, range, target }) } +pub fn inactive_branches(db: &dyn SourceDb, file_id: FileId) -> Vec { + db.preproc_model(file_id) + .inactive_ranges() + .iter() + .copied() + .map(|range| InactiveBranch { file_id, range }) + .collect() +} + fn resolve_literal_include(db: &dyn SourceRootDb, file_id: FileId, path: &str) -> Option { let includer_path = db.file_path(file_id)?; let include_dirs = db.file_preprocess_config(file_id).include_dirs.clone(); diff --git a/crates/ide/src/diagnostics.rs b/crates/ide/src/diagnostics.rs index da96a7e4..ce589ef9 100644 --- a/crates/ide/src/diagnostics.rs +++ b/crates/ide/src/diagnostics.rs @@ -371,14 +371,12 @@ fn inactive_preprocessor_branch_diagnostics(db: &RootDb, file_id: FileId) -> Vec return Vec::new(); } - db.preproc_file_index(file_id) - .inactive_ranges + hir::preproc::inactive_branches(db, file_id) .iter() - .copied() - .map(|range| { + .map(|branch| { INACTIVE_PREPROCESSOR_BRANCH.diagnostic_with_tags( file_id, - range, + branch.range, DiagnosticSeverity::Note, "code is inactive due to preprocessor conditionals".to_owned(), DIAGNOSTIC_INACTIVE_PREPROCESSOR_BRANCH, diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index b59983aa..abb06d08 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -1,10 +1,18 @@ -use hir::{container::InFile, file::HirFileId, semantics::Semantics}; +use hir::{ + base_db::source_db::SourceDb, + container::InFile, + file::HirFileId, + preproc::{IncludeTarget, include_directive_at}, + semantics::Semantics, +}; use itertools::Itertools; use syntax::{ SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, has_text_range::HasTextRange, token::{TokenKindExt, pair_token}, }; +use utils::line_index::{TextRange, TextSize}; +use vfs::FileId; use crate::{ FilePosition, RangeInfo, @@ -17,6 +25,10 @@ pub(crate) fn goto_definition( db: &RootDb, FilePosition { file_id, offset }: FilePosition, ) -> Option>> { + if let Some(include) = handle_preproc_include(db, file_id, offset) { + return Some(include); + } + let sema = Semantics::new(db); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); @@ -36,6 +48,31 @@ pub(crate) fn goto_definition( Some(RangeInfo::new(token.text_range()?, navs)) } +fn handle_preproc_include( + db: &RootDb, + file_id: FileId, + offset: TextSize, +) -> Option>> { + let include = include_directive_at(db, file_id, offset)?; + let IncludeTarget::Literal { path, resolved_file: Some(target_file_id) } = include.target + else { + return None; + }; + let target_range = TextRange::empty(TextSize::new(0)); + Some(RangeInfo::new( + include.range, + vec![NavTarget { + file_id: target_file_id, + full_range: target_range, + focus_range: Some(target_range), + name: Some(path), + kind: None, + container_name: None, + description: db.file_path(target_file_id).map(|path| path.to_string()), + }], + )) +} + fn handle_ctrl_flow_kw( sema: &Semantics, file_id: HirFileId, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 38925457..cf95e9fe 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -1,11 +1,19 @@ -use hir::{container::InContainer, file::HirFileId, hir_def::expr::Expr, semantics::Semantics}; +use hir::{ + base_db::source_db::SourceDb, + container::InContainer, + file::HirFileId, + hir_def::expr::Expr, + preproc::{IncludeTarget, include_directive_at}, + semantics::Semantics, +}; use syntax::{ SyntaxNodeExt, SyntaxTokenWithParent, TokenKind, ast::{self, AstNode}, has_text_range::HasTextRange, token::TokenKindExt, }; -use utils::get::GetRef; +use utils::{get::GetRef, line_index::TextSize}; +use vfs::FileId; use crate::{ FilePosition, RangeInfo, db::root_db::RootDb, definitions::DefinitionClass, markup::Markup, @@ -28,6 +36,10 @@ pub(crate) fn hover( FilePosition { file_id, offset }: FilePosition, _config: HoverConfig, ) -> Option> { + if let Some(include) = handle_preproc_include(db, file_id, offset) { + return Some(include); + } + let sema = Semantics::new(db); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); @@ -66,6 +78,34 @@ fn handle_literal( render::render_literal(literal) } +fn handle_preproc_include( + db: &RootDb, + file_id: FileId, + offset: TextSize, +) -> Option> { + let include = include_directive_at(db, file_id, offset)?; + let mut markup = Markup::new(); + match include.target { + IncludeTarget::Literal { path, resolved_file } => { + markup.print("Include"); + markup.newline(); + markup.push_with_backticks(path.as_str()); + if let Some(target_file_id) = resolved_file + && let Some(path) = db.file_path(target_file_id) + { + markup.newline(); + markup.print(&path.to_string()); + } + } + IncludeTarget::Token { raw } => { + markup.print("Include"); + markup.newline(); + markup.push_with_backticks(raw.as_str()); + } + } + Some(RangeInfo::new(include.range, markup)) +} + fn handle_definition( sema: &Semantics, file_id: HirFileId, diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index b937087d..acfb486a 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -18,6 +18,7 @@ use preproc::index::MacroIncludeTarget; use triomphe::Arc; use utils::{ lines::LineEnding, + test_support::TestDir, text_edit::{TextRange, TextSize}, }; use vfs::{ChangeKind, ChangedFile, FileId, FileSet, VfsPath}; @@ -177,24 +178,7 @@ fn setup_marked_with_path( text: &str, path: &str, ) -> (AnalysisHost, FileId, String, HashMap) { - let mut text = normalize_fixture_text(text); - let mut markers = HashMap::new(); - let mut cursor = 0; - let prefix = "/*marker:"; - - while let Some(rel_start) = text[cursor..].find(prefix) { - let start = cursor + rel_start; - let name_start = start + prefix.len(); - let Some(rel_end) = text[name_start..].find("*/") else { - panic!("unterminated marker in fixture"); - }; - let name_end = name_start + rel_end; - let name = text[name_start..name_end].to_string(); - let end = name_end + 2; - text.replace_range(start..end, ""); - markers.insert(name, TextSize::from(start as u32)); - cursor = start; - } + let (text, markers) = strip_markers(normalize_fixture_text(text)); let (host, file_id) = setup_with_path(&text, path); (host, file_id, text, markers) @@ -204,24 +188,7 @@ fn setup_marked_with_predefines( text: &str, predefines: Vec, ) -> (AnalysisHost, FileId, String, HashMap) { - let mut text = normalize_fixture_text(text); - let mut markers = HashMap::new(); - let mut cursor = 0; - let prefix = "/*marker:"; - - while let Some(rel_start) = text[cursor..].find(prefix) { - let start = cursor + rel_start; - let name_start = start + prefix.len(); - let Some(rel_end) = text[name_start..].find("*/") else { - panic!("unterminated marker in fixture"); - }; - let name_end = name_start + rel_end; - let name = text[name_start..name_end].to_string(); - let end = name_end + 2; - text.replace_range(start..end, ""); - markers.insert(name, TextSize::from(start as u32)); - cursor = start; - } + let (text, markers) = strip_markers(normalize_fixture_text(text)); let file_id = FileId(0); let mut file_set = FileSet::default(); @@ -247,6 +214,28 @@ fn setup_marked_with_predefines( (host, file_id, text, markers) } +fn strip_markers(mut text: String) -> (String, HashMap) { + let mut markers = HashMap::new(); + let mut cursor = 0; + let prefix = "/*marker:"; + + while let Some(rel_start) = text[cursor..].find(prefix) { + let start = cursor + rel_start; + let name_start = start + prefix.len(); + let Some(rel_end) = text[name_start..].find("*/") else { + panic!("unterminated marker in fixture"); + }; + let name_end = name_start + rel_end; + let name = text[name_start..name_end].to_string(); + let end = name_end + 2; + text.replace_range(start..end, ""); + markers.insert(name, TextSize::from(start as u32)); + cursor = start; + } + + (text, markers) +} + fn position(file_id: FileId, markers: &HashMap, name: &str) -> FilePosition { FilePosition { file_id, @@ -870,6 +859,60 @@ endmodule assert_eq!(literal_include_paths, vec!["active.svh"]); } +#[test] +fn preproc_include_literal_supports_navigation_and_hover() { + let dir = TestDir::new("preproc-include-nav-hover"); + let top_path = dir.path().join("top.sv"); + let defs_path = dir.path().join("defs.svh"); + let marked_top_text = normalize_fixture_text( + r#" +`include "/*marker:include*/defs.svh" +module top; +endmodule +"#, + ); + let (top_text, markers) = strip_markers(marked_top_text); + let defs_text = "module defs; endmodule\n"; + std::fs::write(&top_path, &top_text).unwrap(); + std::fs::write(&defs_path, defs_text).unwrap(); + + let top_file_id = FileId(0); + let defs_file_id = FileId(1); + + let mut file_set = FileSet::default(); + file_set.insert(top_file_id, VfsPath::from(top_path)); + file_set.insert(defs_file_id, VfsPath::from(defs_path)); + + let mut change = Change::new(); + change.set_roots(vec![SourceRoot::new_local(file_set)]); + change.add_changed_file(ChangedFile { + file_id: top_file_id, + change_kind: ChangeKind::Create(Arc::from(top_text.as_str()), LineEnding::Unix), + }); + change.add_changed_file(ChangedFile { + file_id: defs_file_id, + change_kind: ChangeKind::Create(Arc::from(defs_text), LineEnding::Unix), + }); + + let mut host = AnalysisHost::default(); + host.apply_change(change); + let position = position(top_file_id, &markers, "include"); + let analysis = host.make_analysis(); + + let nav = + analysis.goto_definition(position).unwrap().expect("include target navigation expected"); + assert!( + nav.info.iter().any(|target| target.file_id == defs_file_id), + "include should navigate to defs.svh: {nav:?}" + ); + + let hover = analysis + .hover(position, HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("include hover expected"); + assert!(hover.info.as_str().contains("defs.svh"), "hover should mention include target"); +} + #[test] fn verilog_2005_genvar_declaration_lowers_without_fallback() { let text = r#" From 71a116efbe8c203a8128239fad22ea4e1ae8ce34 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 5 Jun 2026 01:32:05 +0800 Subject: [PATCH 4/8] feat(ide): surface preprocessor macros --- crates/ide/src/completion/engine/plan.rs | 2 +- crates/ide/src/completion/engine/preproc.rs | 20 ++++++++++++-- crates/ide/src/completion/engine/tests.rs | 25 +++++++++++++++++ crates/ide/src/goto_definition.rs | 28 ++++++++++++++++++- crates/ide/src/hover.rs | 23 +++++++++++++++- crates/ide/src/verilog_2005.rs | 30 +++++++++++++++++++++ 6 files changed, 123 insertions(+), 5 deletions(-) diff --git a/crates/ide/src/completion/engine/plan.rs b/crates/ide/src/completion/engine/plan.rs index 605208f0..cc1c7cb5 100644 --- a/crates/ide/src/completion/engine/plan.rs +++ b/crates/ide/src/completion/engine/plan.rs @@ -30,7 +30,7 @@ fn complete_provider( provider: CompletionProvider, ) -> Vec { match provider { - CompletionProvider::Directives => preproc::complete_directives(ctx), + CompletionProvider::Directives => preproc::complete_directives(db, position, ctx), CompletionProvider::Keywords(provider) => { keywords::complete_keywords(db, position, &ctx.prefix, ctx, provider) } diff --git a/crates/ide/src/completion/engine/preproc.rs b/crates/ide/src/completion/engine/preproc.rs index a19973be..bea3ed94 100644 --- a/crates/ide/src/completion/engine/preproc.rs +++ b/crates/ide/src/completion/engine/preproc.rs @@ -1,9 +1,19 @@ use std::collections::HashMap; +use hir::preproc::visible_macros_at; + use super::candidate::CompletionCandidate; -use crate::completion::{context::CompletionContext, directives, engine::snippets}; +use crate::{ + FilePosition, + completion::{context::CompletionContext, directives, engine::snippets}, + db::root_db::RootDb, +}; -pub(super) fn complete_directives(ctx: &CompletionContext) -> Vec { +pub(super) fn complete_directives( + db: &RootDb, + position: FilePosition, + ctx: &CompletionContext, +) -> Vec { let snippet_entries = snippets::entries(&snippets::snippet_config().directives); let mut snippet_map = HashMap::new(); for entry in snippet_entries { @@ -23,5 +33,11 @@ pub(super) fn complete_directives(ctx: &CompletionContext) -> Vec Option>> { + if let Some(macro_definition) = handle_preproc_macro(db, file_id, offset) { + return Some(macro_definition); + } + if let Some(include) = handle_preproc_include(db, file_id, offset) { return Some(include); } @@ -48,6 +52,28 @@ pub(crate) fn goto_definition( Some(RangeInfo::new(token.text_range()?, navs)) } +fn handle_preproc_macro( + db: &RootDb, + file_id: FileId, + offset: TextSize, +) -> Option>> { + let resolution = macro_usage_resolution_at(db, file_id, offset)?; + let usage_range = resolution.usage.range; + let definition = resolution.definition; + Some(RangeInfo::new( + usage_range, + vec![NavTarget { + file_id: definition.file_id, + full_range: definition.range, + focus_range: Some(definition.range), + name: Some(definition.name), + kind: None, + container_name: None, + description: Some("macro definition".to_owned()), + }], + )) +} + fn handle_preproc_include( db: &RootDb, file_id: FileId, diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index cf95e9fe..12f58eb8 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -3,7 +3,7 @@ use hir::{ container::InContainer, file::HirFileId, hir_def::expr::Expr, - preproc::{IncludeTarget, include_directive_at}, + preproc::{IncludeTarget, include_directive_at, macro_usage_resolution_at}, semantics::Semantics, }; use syntax::{ @@ -36,6 +36,10 @@ pub(crate) fn hover( FilePosition { file_id, offset }: FilePosition, _config: HoverConfig, ) -> Option> { + if let Some(macro_hover) = handle_preproc_macro(db, file_id, offset) { + return Some(macro_hover); + } + if let Some(include) = handle_preproc_include(db, file_id, offset) { return Some(include); } @@ -78,6 +82,23 @@ fn handle_literal( render::render_literal(literal) } +fn handle_preproc_macro( + db: &RootDb, + file_id: FileId, + offset: TextSize, +) -> Option> { + let resolution = macro_usage_resolution_at(db, file_id, offset)?; + let mut markup = Markup::new(); + markup.print("Macro"); + markup.newline(); + markup.push_with_backticks(resolution.usage.name.as_str()); + markup.newline(); + markup.print("Definition"); + markup.newline(); + markup.push_with_backticks(resolution.definition.name.as_str()); + Some(RangeInfo::new(resolution.usage.range, markup)) +} + fn handle_preproc_include( db: &RootDb, file_id: FileId, diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index acfb486a..bfd9aa01 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -913,6 +913,36 @@ endmodule assert!(hover.info.as_str().contains("defs.svh"), "hover should mention include target"); } +#[test] +fn preproc_macro_usage_supports_navigation_and_hover() { + let text = r#" +`define WIDTH 8 +module top; + logic [`/*marker:usage*/WIDTH-1:0] data; +endmodule +"#; + let (host, file_id, clean_text, markers) = setup_marked(text); + let position = position(file_id, &markers, "usage"); + let analysis = host.make_analysis(); + + let nav = + analysis.goto_definition(position).unwrap().expect("macro definition navigation expected"); + let target = nav + .info + .iter() + .find(|target| target.name.as_deref() == Some("WIDTH")) + .expect("WIDTH definition target expected"); + let range = target.focus_or_full_range(); + assert_eq!(clean_text[usize::from(range.start())..usize::from(range.end())].trim(), "WIDTH"); + + let hover = analysis + .hover(position, HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("macro hover expected"); + assert!(hover.info.as_str().contains("Macro"), "hover should identify macro"); + assert!(hover.info.as_str().contains("WIDTH"), "hover should mention macro name"); +} + #[test] fn verilog_2005_genvar_declaration_lowers_without_fallback() { let text = r#" From 9c78586fdde5fa54623d2b78448cb5b2d9c7bb67 Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 5 Jun 2026 01:34:17 +0800 Subject: [PATCH 5/8] chore(ide): keep preproc access behind hir --- crates/ide/Cargo.toml | 1 - crates/ide/src/verilog_2005.rs | 27 ++++++++++++--------------- 2 files changed, 12 insertions(+), 16 deletions(-) diff --git a/crates/ide/Cargo.toml b/crates/ide/Cargo.toml index fa5e9c44..44097b28 100644 --- a/crates/ide/Cargo.toml +++ b/crates/ide/Cargo.toml @@ -15,7 +15,6 @@ itertools.workspace = true la-arena.workspace = true memchr.workspace = true nohash-hasher.workspace = true -preproc.workspace = true regex.workspace = true rustc-hash.workspace = true serde.workspace = true diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index bfd9aa01..d4a8a550 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -11,10 +11,10 @@ use hir::{ source_db::SourceDb, source_root::{SourceRoot, SourceRootId}, }, + preproc::{IncludeTarget, include_directive_at}, semantics::Semantics, }; use insta::assert_snapshot; -use preproc::index::MacroIncludeTarget; use triomphe::Arc; use utils::{ lines::LineEnding, @@ -833,30 +833,27 @@ endmodule } #[test] -fn manifest_predefines_feed_default_preproc_file_index() { +fn manifest_predefines_feed_preproc_include_query() { let text = r#" `ifdef USE_IMPL -`include "active.svh" +`include "/*marker:active*/active.svh" `else -`include "inactive.svh" +`include "/*marker:inactive*/inactive.svh" `endif module manifest_preproc_index_ctx; endmodule "#; - let (host, file_id, _clean_text, _markers) = + let (host, file_id, _clean_text, markers) = setup_marked_with_predefines(text, vec!["USE_IMPL=1".to_owned()]); - let index = host.raw_db().preproc_file_index(file_id); - let literal_include_paths = index - .includes - .iter() - .filter_map(|include| match &include.target { - MacroIncludeTarget::Literal { path, .. } => Some(path.as_str()), - MacroIncludeTarget::Token { .. } => None, - }) - .collect::>(); + let include = include_directive_at(host.raw_db(), file_id, markers["active"]) + .expect("active include should be queryable"); + let IncludeTarget::Literal { path, .. } = include.target else { + panic!("active include should be literal: {include:?}"); + }; + assert_eq!(path.as_str(), "active.svh"); - assert_eq!(literal_include_paths, vec!["active.svh"]); + assert!(include_directive_at(host.raw_db(), file_id, markers["inactive"]).is_none()); } #[test] From 40123382271b679c2bd59eda7ecc6e2f68e5505f Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 5 Jun 2026 11:13:43 +0800 Subject: [PATCH 6/8] feat(ide): find preprocessor macro references --- crates/hir/src/preproc.rs | 46 +++++++++++++++++++++++ crates/ide/src/references.rs | 47 +++++++++++++++++++++++- crates/ide/src/references/search.rs | 4 ++ crates/ide/src/verilog_2005.rs | 57 +++++++++++++++++++++++++++++ 4 files changed, 152 insertions(+), 2 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index 681727cc..a85ca780 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -70,6 +70,23 @@ pub fn visible_macros_at( .collect() } +pub fn macro_definition_at( + db: &dyn SourceDb, + file_id: FileId, + offset: TextSize, +) -> Option { + let model = db.preproc_model(file_id); + let (define_index, define) = model.defines().iter().enumerate().find(|(_, define)| { + define.range.is_some_and(|range| range_contains_offset(range, offset)) + })?; + Some(MacroDefinition { + file_id, + name: define.name.clone()?, + define_index, + range: define.range?, + }) +} + pub fn macro_usage_resolution_at( db: &dyn SourceDb, file_id: FileId, @@ -91,6 +108,35 @@ pub fn macro_usage_resolution_at( Some(MacroUsageResolution { usage, definition }) } +pub fn macro_references( + db: &dyn SourceDb, + file_id: FileId, + definition: &MacroDefinition, +) -> Vec { + if definition.file_id != file_id { + return Vec::new(); + } + + let model = db.preproc_model(file_id); + model + .usages() + .iter() + .enumerate() + .filter_map(|(usage_index, usage)| { + let binding = model.definition_for_usage(usage_index)?; + if binding.define_index != definition.define_index { + return None; + } + Some(MacroUsage { + file_id, + name: usage.name.clone()?, + usage_index, + range: usage.range?, + }) + }) + .collect() +} + pub fn include_directive_at( db: &dyn SourceRootDb, file_id: FileId, diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index feb2e71f..adac5e2d 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -1,4 +1,8 @@ -use hir::{file::HirFileId, semantics::Semantics}; +use hir::{ + file::HirFileId, + preproc::{MacroDefinition, macro_definition_at, macro_references, macro_usage_resolution_at}, + semantics::Semantics, +}; use itertools::Itertools; use nohash_hasher::IntMap; use search::{ReferencesCtx, SearchScope}; @@ -7,7 +11,7 @@ use syntax::{ has_text_range::HasTextRange, token::{TokenKindExt, pair_token}, }; -use utils::line_index::TextRange; +use utils::line_index::{TextRange, TextSize}; use vfs::FileId; use crate::{ @@ -61,6 +65,10 @@ pub(crate) fn references( FilePosition { file_id, offset }: FilePosition, config: ReferencesConfig, ) -> Option> { + if let Some(macro_refs) = handle_preproc_macro(db, file_id, offset, &config) { + return Some(macro_refs); + } + let sema = Semantics::new(db); let hir_file_id = file_id.into(); let parsed_file = sema.parse_file(file_id); @@ -77,6 +85,41 @@ pub(crate) fn references( }) } +fn handle_preproc_macro( + db: &RootDb, + file_id: FileId, + offset: TextSize, + config: &ReferencesConfig, +) -> Option> { + let definition = macro_definition_at(db, file_id, offset).or_else(|| { + macro_usage_resolution_at(db, file_id, offset).map(|resolution| resolution.definition) + })?; + let search_range = match &config.search_scope { + Some(scope) => scope.range_for_file(file_id)?, + None => None, + }; + let refs = macro_references(db, file_id, &definition) + .into_iter() + .filter(|usage| search_range.is_none_or(|range| range.intersect(usage.range).is_some())) + .map(|usage| (usage.range, ReferenceCategory::empty())) + .collect_vec(); + let refs = + if refs.is_empty() { IntMap::default() } else { IntMap::from_iter([(file_id, refs)]) }; + Some(vec![References { def: Some(vec![macro_nav_target(definition)]), refs }]) +} + +fn macro_nav_target(definition: MacroDefinition) -> NavTarget { + NavTarget { + file_id: definition.file_id, + full_range: definition.range, + focus_range: Some(definition.range), + name: Some(definition.name), + kind: None, + container_name: None, + description: Some("macro definition".to_owned()), + } +} + pub(crate) fn handle_ctrl_flow_kw( _sema: &Semantics<'_, RootDb>, file_id: HirFileId, diff --git a/crates/ide/src/references/search.rs b/crates/ide/src/references/search.rs index 90ccadd5..911c8d17 100644 --- a/crates/ide/src/references/search.rs +++ b/crates/ide/src/references/search.rs @@ -131,6 +131,10 @@ impl SearchScope { pub(crate) fn is_within_file(&self, file_id: FileId) -> bool { self.0.keys().all(|candidate| *candidate == file_id) } + + pub(crate) fn range_for_file(&self, file_id: FileId) -> Option> { + self.0.get(&file_id).copied() + } } pub(crate) struct ReferencesCtx<'a, 'b> { diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index d4a8a550..b9f23563 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -940,6 +940,63 @@ endmodule assert!(hover.info.as_str().contains("WIDTH"), "hover should mention macro name"); } +#[test] +fn preproc_macro_definition_supports_references() { + let text = r#" +`define /*marker:first_def*/WIDTH 8 +module top; + logic [/*marker:first_use*/`WIDTH-1:0] narrow_data; +`define /*marker:second_def*/WIDTH 16 + logic [/*marker:second_use*/`WIDTH-1:0] wide_data; +endmodule +"#; + let (host, file_id, _clean_text, markers) = setup_marked(text); + let analysis = host.make_analysis(); + let width_use_len = TextSize::of("`WIDTH"); + let first_use = TextRange::new(markers["first_use"], markers["first_use"] + width_use_len); + let second_use = TextRange::new(markers["second_use"], markers["second_use"] + width_use_len); + + let reference_ranges = |marker: &str| { + analysis + .references( + position(file_id, &markers, marker), + ReferencesConfig::new( + ScopeVisibility::Public, + Some(SearchScope::single_file(file_id)), + ), + ) + .unwrap() + .unwrap_or_else(|| panic!("{marker} references expected")) + .into_iter() + .flat_map(|mut refs| refs.refs.remove(&file_id).unwrap_or_default()) + .map(|(range, _)| range) + .collect::>() + }; + + let first_ranges = reference_ranges("first_def"); + assert!( + first_ranges.contains(&first_use), + "first define should reference first use: {first_ranges:?}" + ); + assert!( + !first_ranges.contains(&second_use), + "first define should not reference use after override: {first_ranges:?}" + ); + + let second_ranges = reference_ranges("second_def"); + assert!( + second_ranges.contains(&second_use), + "second define should reference second use: {second_ranges:?}" + ); + assert!( + !second_ranges.contains(&first_use), + "second define should not reference use before override: {second_ranges:?}" + ); + + let use_ranges = reference_ranges("second_use"); + assert_eq!(use_ranges, second_ranges); +} + #[test] fn verilog_2005_genvar_declaration_lowers_without_fallback() { let text = r#" From 0b95c6e409ab8334a9882ba485b802f3f6562bab Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 5 Jun 2026 11:28:13 +0800 Subject: [PATCH 7/8] fix(ide): link preprocessor macro definitions --- crates/ide/src/goto_definition.rs | 35 ++++++++++++++++++------------- crates/ide/src/hover.rs | 21 ++++++++++++------- crates/ide/src/verilog_2005.rs | 33 +++++++++++++++++++++++++++++ 3 files changed, 68 insertions(+), 21 deletions(-) diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 06c00f62..4eea57ca 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -2,7 +2,10 @@ use hir::{ base_db::source_db::SourceDb, container::InFile, file::HirFileId, - preproc::{IncludeTarget, include_directive_at, macro_usage_resolution_at}, + preproc::{ + IncludeTarget, MacroDefinition, include_directive_at, macro_definition_at, + macro_usage_resolution_at, + }, semantics::Semantics, }; use itertools::Itertools; @@ -57,21 +60,25 @@ fn handle_preproc_macro( file_id: FileId, offset: TextSize, ) -> Option>> { + if let Some(definition) = macro_definition_at(db, file_id, offset) { + return Some(RangeInfo::new(definition.range, vec![macro_nav_target(definition)])); + } + let resolution = macro_usage_resolution_at(db, file_id, offset)?; let usage_range = resolution.usage.range; - let definition = resolution.definition; - Some(RangeInfo::new( - usage_range, - vec![NavTarget { - file_id: definition.file_id, - full_range: definition.range, - focus_range: Some(definition.range), - name: Some(definition.name), - kind: None, - container_name: None, - description: Some("macro definition".to_owned()), - }], - )) + Some(RangeInfo::new(usage_range, vec![macro_nav_target(resolution.definition)])) +} + +fn macro_nav_target(definition: MacroDefinition) -> NavTarget { + NavTarget { + file_id: definition.file_id, + full_range: definition.range, + focus_range: Some(definition.range), + name: Some(definition.name), + kind: None, + container_name: None, + description: Some("macro definition".to_owned()), + } } fn handle_preproc_include( diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 12f58eb8..1cb67d3d 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -3,7 +3,10 @@ use hir::{ container::InContainer, file::HirFileId, hir_def::expr::Expr, - preproc::{IncludeTarget, include_directive_at, macro_usage_resolution_at}, + preproc::{ + IncludeTarget, MacroDefinition, include_directive_at, macro_definition_at, + macro_usage_resolution_at, + }, semantics::Semantics, }; use syntax::{ @@ -87,16 +90,20 @@ fn handle_preproc_macro( file_id: FileId, offset: TextSize, ) -> Option> { + if let Some(definition) = macro_definition_at(db, file_id, offset) { + return Some(RangeInfo::new(definition.range, macro_definition_markup(&definition))); + } + let resolution = macro_usage_resolution_at(db, file_id, offset)?; + Some(RangeInfo::new(resolution.usage.range, macro_definition_markup(&resolution.definition))) +} + +fn macro_definition_markup(definition: &MacroDefinition) -> Markup { let mut markup = Markup::new(); markup.print("Macro"); markup.newline(); - markup.push_with_backticks(resolution.usage.name.as_str()); - markup.newline(); - markup.print("Definition"); - markup.newline(); - markup.push_with_backticks(resolution.definition.name.as_str()); - Some(RangeInfo::new(resolution.usage.range, markup)) + markup.push_with_backticks(definition.name.as_str()); + markup } fn handle_preproc_include( diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index b9f23563..b2e8f745 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -940,6 +940,39 @@ endmodule assert!(hover.info.as_str().contains("WIDTH"), "hover should mention macro name"); } +#[test] +fn preproc_macro_definition_supports_navigation_and_hover() { + let text = r#" +`define /*marker:definition*/LOCAL_WIDTH 8 +module top; + logic [`/*marker:usage*/LOCAL_WIDTH-1:0] data; +endmodule +"#; + let (host, file_id, clean_text, markers) = setup_marked(text); + let definition = position(file_id, &markers, "definition"); + let analysis = host.make_analysis(); + + let nav = + analysis.goto_definition(definition).unwrap().expect("macro definition link expected"); + let target = nav + .info + .iter() + .find(|target| target.name.as_deref() == Some("LOCAL_WIDTH")) + .expect("LOCAL_WIDTH definition target expected"); + let range = target.focus_or_full_range(); + assert_eq!( + clean_text[usize::from(range.start())..usize::from(range.end())].trim(), + "LOCAL_WIDTH" + ); + + let hover = analysis + .hover(definition, HoverConfig { format: HoverFormat::PlainText }) + .unwrap() + .expect("macro definition hover expected"); + assert!(hover.info.as_str().contains("Macro"), "hover should identify macro"); + assert!(hover.info.as_str().contains("LOCAL_WIDTH"), "hover should mention macro name"); +} + #[test] fn preproc_macro_definition_supports_references() { let text = r#" From da0afcba01f05371f29ebb0155e0578892e50e0c Mon Sep 17 00:00:00 2001 From: hongjr03 Date: Fri, 5 Jun 2026 11:34:03 +0800 Subject: [PATCH 8/8] fix(ide): resolve preprocessor conditional macros --- crates/hir/src/preproc.rs | 92 ++++++++++++++++++++++++++++--- crates/ide/src/goto_definition.rs | 8 +-- crates/ide/src/hover.rs | 9 ++- crates/ide/src/references.rs | 6 +- crates/ide/src/verilog_2005.rs | 21 +++++++ 5 files changed, 120 insertions(+), 16 deletions(-) diff --git a/crates/hir/src/preproc.rs b/crates/hir/src/preproc.rs index a85ca780..0dcc24e3 100644 --- a/crates/hir/src/preproc.rs +++ b/crates/hir/src/preproc.rs @@ -30,6 +30,19 @@ pub struct MacroUsageResolution { pub definition: MacroDefinition, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroReference { + pub file_id: FileId, + pub name: SmolStr, + pub range: TextRange, +} + +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct MacroReferenceResolution { + pub reference: MacroReference, + pub definition: MacroDefinition, +} + #[derive(Debug, Clone, PartialEq, Eq)] pub struct IncludeDirective { pub file_id: FileId, @@ -108,17 +121,52 @@ pub fn macro_usage_resolution_at( Some(MacroUsageResolution { usage, definition }) } +pub fn macro_reference_resolution_at( + db: &dyn SourceDb, + file_id: FileId, + offset: TextSize, +) -> Option { + if let Some(resolution) = macro_usage_resolution_at(db, file_id, offset) { + return Some(MacroReferenceResolution { + reference: MacroReference { + file_id, + name: resolution.usage.name, + range: resolution.usage.range, + }, + definition: resolution.definition, + }); + } + + let model = db.preproc_model(file_id); + for conditional in model.conditionals() { + for token in &conditional.expr { + let range = token.range?; + if !range_contains_offset(range, offset) { + continue; + } + let definition = + macro_definition_for_name_at(db, file_id, token.value.as_str(), range.start())?; + return Some(MacroReferenceResolution { + reference: MacroReference { file_id, name: token.value.clone(), range }, + definition, + }); + } + } + + None +} + pub fn macro_references( db: &dyn SourceDb, file_id: FileId, definition: &MacroDefinition, -) -> Vec { +) -> Vec { if definition.file_id != file_id { return Vec::new(); } let model = db.preproc_model(file_id); - model + let mut refs = model .usages() .iter() .enumerate() @@ -127,14 +175,24 @@ pub fn macro_references( if binding.define_index != definition.define_index { return None; } - Some(MacroUsage { + Some(MacroReference { file_id, name: usage.name.clone()?, range: usage.range? }) + }) + .collect::>(); + + refs.extend(model.conditionals().iter().flat_map(|conditional| { + conditional.expr.iter().filter_map(|token| { + let range = token.range?; + let resolved = + macro_definition_for_name_at(db, file_id, token.value.as_str(), range.start())?; + (resolved.define_index == definition.define_index).then(|| MacroReference { file_id, - name: usage.name.clone()?, - usage_index, - range: usage.range?, + name: token.value.clone(), + range, }) }) - .collect() + })); + + refs } pub fn include_directive_at( @@ -175,6 +233,26 @@ fn resolve_literal_include(db: &dyn SourceRootDb, file_id: FileId, path: &str) - resolve_include_target(path, &includer_path, &include_dirs, &path_file_ids) } +fn macro_definition_for_name_at( + db: &dyn SourceDb, + file_id: FileId, + name: &str, + offset: TextSize, +) -> Option { + db.preproc_model(file_id) + .visible_macros_at(offset) + .into_iter() + .find(|binding| binding.name.as_str() == name) + .and_then(|binding| { + Some(MacroDefinition { + file_id, + name: binding.name, + define_index: binding.define_index, + range: binding.define.range?, + }) + }) +} + fn path_file_ids(db: &dyn SourceRootDb) -> PathIdentityIndex { let mut index = PathIdentityIndex::default(); for file_id in db.files().iter().copied() { diff --git a/crates/ide/src/goto_definition.rs b/crates/ide/src/goto_definition.rs index 4eea57ca..afa13eb4 100644 --- a/crates/ide/src/goto_definition.rs +++ b/crates/ide/src/goto_definition.rs @@ -4,7 +4,7 @@ use hir::{ file::HirFileId, preproc::{ IncludeTarget, MacroDefinition, include_directive_at, macro_definition_at, - macro_usage_resolution_at, + macro_reference_resolution_at, }, semantics::Semantics, }; @@ -64,9 +64,9 @@ fn handle_preproc_macro( return Some(RangeInfo::new(definition.range, vec![macro_nav_target(definition)])); } - let resolution = macro_usage_resolution_at(db, file_id, offset)?; - let usage_range = resolution.usage.range; - Some(RangeInfo::new(usage_range, vec![macro_nav_target(resolution.definition)])) + let resolution = macro_reference_resolution_at(db, file_id, offset)?; + let reference_range = resolution.reference.range; + Some(RangeInfo::new(reference_range, vec![macro_nav_target(resolution.definition)])) } fn macro_nav_target(definition: MacroDefinition) -> NavTarget { diff --git a/crates/ide/src/hover.rs b/crates/ide/src/hover.rs index 1cb67d3d..a0ee69a3 100644 --- a/crates/ide/src/hover.rs +++ b/crates/ide/src/hover.rs @@ -5,7 +5,7 @@ use hir::{ hir_def::expr::Expr, preproc::{ IncludeTarget, MacroDefinition, include_directive_at, macro_definition_at, - macro_usage_resolution_at, + macro_reference_resolution_at, }, semantics::Semantics, }; @@ -94,8 +94,11 @@ fn handle_preproc_macro( return Some(RangeInfo::new(definition.range, macro_definition_markup(&definition))); } - let resolution = macro_usage_resolution_at(db, file_id, offset)?; - Some(RangeInfo::new(resolution.usage.range, macro_definition_markup(&resolution.definition))) + let resolution = macro_reference_resolution_at(db, file_id, offset)?; + Some(RangeInfo::new( + resolution.reference.range, + macro_definition_markup(&resolution.definition), + )) } fn macro_definition_markup(definition: &MacroDefinition) -> Markup { diff --git a/crates/ide/src/references.rs b/crates/ide/src/references.rs index adac5e2d..cc7b0dce 100644 --- a/crates/ide/src/references.rs +++ b/crates/ide/src/references.rs @@ -1,6 +1,8 @@ use hir::{ file::HirFileId, - preproc::{MacroDefinition, macro_definition_at, macro_references, macro_usage_resolution_at}, + preproc::{ + MacroDefinition, macro_definition_at, macro_reference_resolution_at, macro_references, + }, semantics::Semantics, }; use itertools::Itertools; @@ -92,7 +94,7 @@ fn handle_preproc_macro( config: &ReferencesConfig, ) -> Option> { let definition = macro_definition_at(db, file_id, offset).or_else(|| { - macro_usage_resolution_at(db, file_id, offset).map(|resolution| resolution.definition) + macro_reference_resolution_at(db, file_id, offset).map(|resolution| resolution.definition) })?; let search_range = match &config.search_scope { Some(scope) => scope.range_for_file(file_id)?, diff --git a/crates/ide/src/verilog_2005.rs b/crates/ide/src/verilog_2005.rs index b2e8f745..71fb3606 100644 --- a/crates/ide/src/verilog_2005.rs +++ b/crates/ide/src/verilog_2005.rs @@ -946,6 +946,9 @@ fn preproc_macro_definition_supports_navigation_and_hover() { `define /*marker:definition*/LOCAL_WIDTH 8 module top; logic [`/*marker:usage*/LOCAL_WIDTH-1:0] data; +`ifdef /*marker:conditional*/LOCAL_WIDTH + assign data = '0; +`endif endmodule "#; let (host, file_id, clean_text, markers) = setup_marked(text); @@ -971,6 +974,15 @@ endmodule .expect("macro definition hover expected"); assert!(hover.info.as_str().contains("Macro"), "hover should identify macro"); assert!(hover.info.as_str().contains("LOCAL_WIDTH"), "hover should mention macro name"); + + let conditional_nav = analysis + .goto_definition(position(file_id, &markers, "conditional")) + .unwrap() + .expect("conditional macro definition navigation expected"); + assert!( + conditional_nav.info.iter().any(|target| target.name.as_deref() == Some("LOCAL_WIDTH")), + "conditional macro should navigate to LOCAL_WIDTH definition: {conditional_nav:?}" + ); } #[test] @@ -979,6 +991,9 @@ fn preproc_macro_definition_supports_references() { `define /*marker:first_def*/WIDTH 8 module top; logic [/*marker:first_use*/`WIDTH-1:0] narrow_data; +`ifdef /*marker:first_cond*/WIDTH + assign narrow_data = '0; +`endif `define /*marker:second_def*/WIDTH 16 logic [/*marker:second_use*/`WIDTH-1:0] wide_data; endmodule @@ -986,7 +1001,9 @@ endmodule let (host, file_id, _clean_text, markers) = setup_marked(text); let analysis = host.make_analysis(); let width_use_len = TextSize::of("`WIDTH"); + let width_name_len = TextSize::of("WIDTH"); let first_use = TextRange::new(markers["first_use"], markers["first_use"] + width_use_len); + let first_cond = TextRange::new(markers["first_cond"], markers["first_cond"] + width_name_len); let second_use = TextRange::new(markers["second_use"], markers["second_use"] + width_use_len); let reference_ranges = |marker: &str| { @@ -1011,6 +1028,10 @@ endmodule first_ranges.contains(&first_use), "first define should reference first use: {first_ranges:?}" ); + assert!( + first_ranges.contains(&first_cond), + "first define should reference conditional use: {first_ranges:?}" + ); assert!( !first_ranges.contains(&second_use), "first define should not reference use after override: {first_ranges:?}"