From 180dcce31d448487f952af681e295cc7c00ddf4e Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Mon, 24 Aug 2026 16:48:47 +0000 Subject: [PATCH 1/7] Support BA_REL relation attributes in attribute_structs --- README.md | 36 ++++++++ src/lib.rs | 181 ++++++++++++++++++++++++++++++++++--- tests/attribute_structs.rs | 149 +++++++++++++++++++++++++++++- 3 files changed, 352 insertions(+), 14 deletions(-) diff --git a/README.md b/README.md index 8efe03cd..e4472a00 100644 --- a/README.md +++ b/README.md @@ -153,6 +153,42 @@ impl SomeMessage { } ``` +DBCs can also attach attributes to relations between a node and a signal (`BA_DEF_REL_ BU_SG_REL_` / `BA_REL_`) or a node and a message (`BA_DEF_REL_ BU_BO_REL_` / `BA_REL_`), for example, a signal timeout. `AttributeScope::NodeSignal` emits one const per receiving node of a matching signal, and `AttributeScope::NodeMessage` emits one const per node related to a matching message. `FieldSource::NodeName` exposes the related node's name and is only valid with these two scopes: + +```rust,no_run +use dbc_codegen::{AttributeField, AttributeScope, AttributeStruct, Config, FieldSource}; + +// `relation::SigTimeoutInfo { node, timeout_ms }` is a type defined in your crate. +let sig_timeout = AttributeStruct { + type_path: "relation::SigTimeoutInfo", + const_name: "SIG_TIMEOUT", + scope: AttributeScope::NodeSignal, // One const per receiving node of a matching signal + require: "GenSigTimeoutTime", // Only nodes carrying this relation attribute + fields: &[ + AttributeField { name: "node", source: FieldSource::NodeName }, + AttributeField { name: "timeout_ms", source: FieldSource::Attr("GenSigTimeoutTime") }, + ], +}; + +let dbc_file = ""; +Config::builder() + .dbc_name("example.dbc") + .dbc_content(dbc_file) + .attribute_structs(&[sig_timeout]) + .build() + .generate() + .unwrap(); +``` + +For every signal and receiving node pair that carries a `GenSigTimeoutTime` relation attribute, this generates: + +```rust,ignore +impl SomeMessage { + pub const SOME_SIGNAL_ECU2_SIG_TIMEOUT: relation::SigTimeoutInfo = + relation::SigTimeoutInfo { node: "ECU2", timeout_ms: 60 }; +} +``` + ### `no_std` The generated code is `no_std` compatible. diff --git a/src/lib.rs b/src/lib.rs index 07daa560..05123f21 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -24,7 +24,10 @@ use can_dbc::MultiplexIndicator::{ MultiplexedSignal, Multiplexor, MultiplexorAndMultiplexedSignal, Plain, }; use can_dbc::ValueType::Signed; -use can_dbc::{AttributeValue, Dbc, Message, MessageId, Signal, ValDescription, ValueDescription}; +use can_dbc::{ + AttributeValue, AttributeValueForRelationType, Dbc, Message, MessageId, Signal, + ValDescription, ValueDescription, +}; use heck::ToSnakeCase; use quote::ToTokens; use typed_builder::TypedBuilder; @@ -35,7 +38,7 @@ use crate::signal_type::{IntSize, ValType}; use crate::utils::{ enum_name, enum_variant_name, is_screaming_snake_case, is_valid_ident, is_valid_type_path, multiplex_enum_name, multiplexed_enum_variant_name, multiplexed_enum_variant_wrapper_name, - MessageExt as _, SignalExt as _, + sanitize_name, MessageExt as _, SignalExt as _, }; static ALLOW_DEADCODE: &str = "#[allow(dead_code)]"; @@ -132,10 +135,14 @@ pub struct AttributeStruct<'a> { #[derive(Debug, Clone, Copy)] #[non_exhaustive] pub enum AttributeScope { - /// One const per message (BO_ attribute). + /// One const per message (`BO_` attribute). Message, - /// One const per signal (SG_ attribute). + /// One const per signal (`SG_` attribute). Signal, + /// One const per node related to a message (`BU_BO_REL_` relation attribute). + NodeMessage, + /// One const per receiving node of a signal (`BU_SG_REL_` relation attribute). + NodeSignal, } /// One field of an [`AttributeStruct`] and where its value comes from. @@ -169,6 +176,9 @@ pub enum FieldSource<'a> { Int(i64), /// A literal string. Str(&'a str), + /// The name of the related node. + /// [`AttributeScope::NodeSignal`] and [`AttributeScope::NodeMessage`] only. + NodeName, } impl Config<'_> { @@ -531,7 +541,12 @@ impl Config<'_> { spec.const_name ); - let message_scope = matches!(spec.scope, AttributeScope::Message); + let has_signal = + matches!(spec.scope, AttributeScope::Signal | AttributeScope::NodeSignal); + let has_node = matches!( + spec.scope, + AttributeScope::NodeSignal | AttributeScope::NodeMessage + ); let mut seen = BTreeSet::new(); for field in spec.fields { ensure!( @@ -546,14 +561,23 @@ impl Config<'_> { field.name, spec.const_name ); - if message_scope { + if !has_signal { ensure!( !matches!( field.source, FieldSource::StartBit | FieldSource::StartByte | FieldSource::BitWidth ), "attribute_structs: field {:?} of {:?} uses a signal-only source \ - (StartBit/StartByte/BitWidth) but the struct has message scope", + (StartBit/StartByte/BitWidth) but the struct has no associated signal", + field.name, + spec.const_name + ); + } + if !has_node { + ensure!( + !matches!(field.source, FieldSource::NodeName), + "attribute_structs: field {:?} of {:?} uses NodeName but the struct has \ + no associated node (scope must be NodeSignal or NodeMessage)", field.name, spec.const_name ); @@ -595,6 +619,7 @@ impl Config<'_> { msg, dbc, None, + None, &mut used, )?; } @@ -615,6 +640,56 @@ impl Config<'_> { msg, dbc, Some(signal), + None, + &mut used, + )?; + } + } + AttributeScope::NodeSignal => { + for signal in &msg.signals { + for node in &signal.receivers { + if node_signal_attribute(dbc, node, msg.id, &signal.name, spec.require) + .is_none() + { + continue; + } + let node_ident = + sanitize_name(node, "x", ToSnakeCase::to_snake_case).to_uppercase(); + let name = format!( + "{}_{}_{}", + signal.field_name().to_uppercase(), + node_ident, + spec.const_name + ); + Self::render_attribute_struct( + w, + spec, + &name, + msg, + dbc, + Some(signal), + Some(node), + &mut used, + )?; + } + } + } + AttributeScope::NodeMessage => { + for node in &dbc.nodes { + if node_message_attribute(dbc, &node.0, msg.id, spec.require).is_none() { + continue; + } + let node_ident = + sanitize_name(&node.0, "x", ToSnakeCase::to_snake_case).to_uppercase(); + let name = format!("{node_ident}_{}", spec.const_name); + Self::render_attribute_struct( + w, + spec, + &name, + msg, + dbc, + None, + Some(&node.0), &mut used, )?; } @@ -632,6 +707,7 @@ impl Config<'_> { msg: &Message, dbc: &Dbc, signal: Option<&Signal>, + node: Option<&str>, used: &mut BTreeSet, ) -> Result<()> { ensure!( @@ -643,7 +719,7 @@ impl Config<'_> { let mut fields = Vec::with_capacity(spec.fields.len()); for field in spec.fields { - let lit = resolve_field_source(&field.source, msg, dbc, signal).ok_or_else(|| { + let lit = resolve_field_source(&field.source, msg, dbc, signal, node).ok_or_else(|| { anyhow!( "attribute_structs: const '{const_name}' field {:?} has no value in the DBC \ and no default (source: {:?})", @@ -1641,11 +1717,14 @@ fn resolve_field_source( msg: &Message, dbc: &Dbc, signal: Option<&Signal>, + node: Option<&str>, ) -> Option { match source { - FieldSource::Attr(name) => match signal { - Some(s) => dbc.resolved_signal_attribute(msg.id, &s.name, name), - None => dbc.resolved_message_attribute(msg.id, name), + FieldSource::Attr(name) => match (signal, node) { + (Some(s), None) => dbc.resolved_signal_attribute(msg.id, &s.name, name), + (None, None) => dbc.resolved_message_attribute(msg.id, name), + (Some(s), Some(n)) => resolved_node_signal_attribute(dbc, n, msg.id, &s.name, name), + (None, Some(n)) => resolved_node_message_attribute(dbc, n, msg.id, name), } .map(attr_value_literal), FieldSource::MessageAttr(name) => dbc @@ -1657,9 +1736,89 @@ fn resolve_field_source( FieldSource::MessageSize => Some(msg.size.to_string()), FieldSource::Int(v) => Some(v.to_string()), FieldSource::Str(v) => Some(format!("{v:?}")), + FieldSource::NodeName => node.map(|n| format!("{n:?}")), } } +/// Lookup an assigned node-to-signal relation attribute value (`BA_REL_ ... BU_SG_REL_`). +fn node_signal_attribute<'a>( + dbc: &'a Dbc, + node_name: &str, + message_id: MessageId, + signal_name: &str, + name: &str, +) -> Option<&'a AttributeValue> { + dbc.relation_attribute_values.iter().find_map(|rel| { + if rel.name != name { + return None; + } + match &rel.details { + AttributeValueForRelationType::NodeToSignal { + node_name: n, + message_id: mid, + signal_name: sig, + value, + } if n == node_name && *mid == message_id && sig == signal_name => Some(value), + _ => None, + } + }) +} + +/// Lookup a node-to-signal relation attribute value. +/// Uses the default (`BA_DEF_DEF_REL_`) if a value is not assigned. +fn resolved_node_signal_attribute<'a>( + dbc: &'a Dbc, + node_name: &str, + message_id: MessageId, + signal_name: &str, + name: &str, +) -> Option<&'a AttributeValue> { + node_signal_attribute(dbc, node_name, message_id, signal_name, name).or_else(|| { + dbc.relation_attribute_defaults + .iter() + .find(|d| d.name == name) + .map(|d| &d.value) + }) +} + +/// Lookup an assigned node-to-message relation attribute value (`BA_REL_ ... BU_BO_REL_`). +fn node_message_attribute<'a>( + dbc: &'a Dbc, + node_name: &str, + message_id: MessageId, + name: &str, +) -> Option<&'a AttributeValue> { + dbc.relation_attribute_values.iter().find_map(|rel| { + if rel.name != name { + return None; + } + match &rel.details { + AttributeValueForRelationType::NodeToMessage { + node_name: n, + message_id: mid, + value, + } if n == node_name && *mid == message_id => Some(value), + _ => None, + } + }) +} + +/// Lookup a node-to-message relation attribute value. +/// Uses the default (`BA_DEF_DEF_REL_`) if a value is not assigned. +fn resolved_node_message_attribute<'a>( + dbc: &'a Dbc, + node_name: &str, + message_id: MessageId, + name: &str, +) -> Option<&'a AttributeValue> { + node_message_attribute(dbc, node_name, message_id, name).or_else(|| { + dbc.relation_attribute_defaults + .iter() + .find(|d| d.name == name) + .map(|d| &d.value) + }) +} + /// Byte index of a signal's start bit. fn signal_start_byte(signal: &Signal) -> u64 { signal.start_bit.checked_div(8).unwrap_or(0) diff --git a/tests/attribute_structs.rs b/tests/attribute_structs.rs index 53be2b31..c010954f 100644 --- a/tests/attribute_structs.rs +++ b/tests/attribute_structs.rs @@ -11,7 +11,7 @@ NS_ : BS_: -BU_: ECU1 +BU_: ECU1 ECU2 BO_ 256 Protected: 8 ECU1 SG_ TestSig : 16|8@1+ (1,0) [0|255] "" ECU1 @@ -22,6 +22,9 @@ BO_ 257 Plain: 8 ECU1 BO_ 258 BeProtected: 8 ECU1 SG_ BeSig : 23|8@0+ (1,0) [0|255] "" ECU1 +BO_ 259 Related: 8 ECU1 + SG_ RelSig : 0|8@1+ (1,0) [0|255] "" ECU2 + BA_DEF_ BO_ "SC_Message" ENUM "0","1","2"; BA_DEF_ BO_ "SCP_FreshnessValueId" INT 0 65535; BA_DEF_ BO_ "TxOffset" INT -128 127; @@ -44,11 +47,20 @@ BA_ "E2EDataId" SG_ 256 TestSig 373; BA_ "E2EDataLength" SG_ 256 TestSig 48; BA_ "E2EDataId" SG_ 258 BeSig 500; BA_ "E2EDataLength" SG_ 258 BeSig 16; +BA_DEF_REL_ BU_SG_REL_ "GenSigTimeoutTime" INT 0 65535; +BA_DEF_REL_ BU_SG_REL_ "GenSigFirstTimeoutTime" INT 0 65535; +BA_DEF_DEF_REL_ "GenSigTimeoutTime" 0; +BA_DEF_DEF_REL_ "GenSigFirstTimeoutTime" 240; +BA_REL_ "GenSigTimeoutTime" BU_SG_REL_ ECU2 SG_ 259 RelSig 60; +BA_DEF_REL_ BU_BO_REL_ "MsgProject" ENUM "A","B","C"; +BA_DEF_DEF_REL_ "MsgProject" "A"; +BA_REL_ "MsgProject" BU_BO_REL_ ECU1 259 0; +BA_REL_ "MsgProject" BU_BO_REL_ ECU2 259 1; "#; const FIXTURE: &str = "tests/fixtures/shared-test-files/dbc-cantools/attributes.dbc"; -/// Signal-scoped +/// Signal-scoped attributes const E2E: AttributeStruct = AttributeStruct { type_path: "data_protection::E2EDataIdInfo", const_name: "E2E", @@ -74,7 +86,7 @@ const E2E: AttributeStruct = AttributeStruct { ], }; -/// Message-scoped +/// Message-scoped attributes const SEC_OC: AttributeStruct = AttributeStruct { type_path: "data_protection::SecOcInfo", const_name: "SEC_OC", @@ -130,6 +142,50 @@ const LAYOUT: AttributeStruct = AttributeStruct { ], }; +/// Node-signal relation attributes +const SIG_TIMEOUT: AttributeStruct = AttributeStruct { + type_path: "relation::SigTimeoutInfo", + const_name: "SIG_TIMEOUT", + scope: AttributeScope::NodeSignal, + require: "GenSigTimeoutTime", + fields: &[ + AttributeField { + name: "node", + source: FieldSource::NodeName, + }, + AttributeField { + name: "timeout_ms", + source: FieldSource::Attr("GenSigTimeoutTime"), + }, + AttributeField { + name: "first_timeout_ms", + source: FieldSource::Attr("GenSigFirstTimeoutTime"), + }, + AttributeField { + name: "start_byte", + source: FieldSource::StartByte, + }, + ], +}; + +/// Node-message relation attributes +const MSG_PROJECT: AttributeStruct = AttributeStruct { + type_path: "relation::MsgProjectInfo", + const_name: "MSG_PROJECT", + scope: AttributeScope::NodeMessage, + require: "MsgProject", + fields: &[ + AttributeField { + name: "node", + source: FieldSource::NodeName, + }, + AttributeField { + name: "project", + source: FieldSource::Attr("MsgProject"), + }, + ], +}; + #[test] fn default_emits_nothing() { let out = Config::builder() @@ -592,3 +648,90 @@ fn fixture_definition_without_default_is_an_error() { .unwrap_err(); assert!(format!("{err:#}").contains("has no value"), "{err:#}"); } + +#[test] +fn node_signal_scope_emits_const_per_receiving_node() { + let out = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[SIG_TIMEOUT]) + .build() + .generate() + .unwrap(); + assert!( + out.contains("pub const REL_SIG_ECU2_SIG_TIMEOUT: relation::SigTimeoutInfo"), + "{out}" + ); + assert!(out.contains(r#"node: "ECU2""#), "{out}"); + assert!(out.contains("timeout_ms: 60"), "{out}"); + assert!(out.contains("first_timeout_ms: 240"), "{out}"); // BA_DEF_DEF_REL_ default + assert!(out.contains("start_byte: 0"), "{out}"); +} + +#[test] +fn node_message_scope_emits_const_per_related_node() { + let out = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[MSG_PROJECT]) + .build() + .generate() + .unwrap(); + assert!( + out.contains("pub const ECU1_MSG_PROJECT: relation::MsgProjectInfo"), + "{out}" + ); + assert!( + out.contains("pub const ECU2_MSG_PROJECT: relation::MsgProjectInfo"), + "{out}" + ); + assert!(out.contains(r#"node: "ECU1""#), "{out}"); + assert!(out.contains(r#"node: "ECU2""#), "{out}"); + // The enum value is emitted as its index (0 for "A", 1 for "B"), not the label. + assert!(out.contains("project: 0"), "{out}"); + assert!(out.contains("project: 1"), "{out}"); +} + +#[test] +fn node_name_source_is_rejected_outside_node_scopes() { + let bad = AttributeStruct { + type_path: "foo::Bar", + const_name: "BAD", + scope: AttributeScope::Signal, + require: "E2EDataId", + fields: &[AttributeField { + name: "node", + source: FieldSource::NodeName, + }], + }; + let err = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[bad]) + .build() + .generate() + .unwrap_err(); + assert!(format!("{err:#}").contains("NodeName"), "{err:#}"); +} + +#[test] +fn node_message_scope_with_signal_source_is_rejected() { + let bad = AttributeStruct { + type_path: "foo::Bar", + const_name: "BAD", + scope: AttributeScope::NodeMessage, + require: "MsgProject", + fields: &[AttributeField { + name: "x", + source: FieldSource::StartByte, + }], + }; + let err = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[bad]) + .build() + .generate() + .unwrap_err(); + assert!(format!("{err:#}").contains("signal-only source"), "{err:#}"); +} From 4822b862ab5768065685223b8aff3f755053b9d7 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Mon, 24 Aug 2026 16:49:14 +0000 Subject: [PATCH 2/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lib.rs | 17 ++++++++++------- 1 file changed, 10 insertions(+), 7 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 05123f21..38aef833 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -25,8 +25,8 @@ use can_dbc::MultiplexIndicator::{ }; use can_dbc::ValueType::Signed; use can_dbc::{ - AttributeValue, AttributeValueForRelationType, Dbc, Message, MessageId, Signal, - ValDescription, ValueDescription, + AttributeValue, AttributeValueForRelationType, Dbc, Message, MessageId, Signal, ValDescription, + ValueDescription, }; use heck::ToSnakeCase; use quote::ToTokens; @@ -541,8 +541,10 @@ impl Config<'_> { spec.const_name ); - let has_signal = - matches!(spec.scope, AttributeScope::Signal | AttributeScope::NodeSignal); + let has_signal = matches!( + spec.scope, + AttributeScope::Signal | AttributeScope::NodeSignal + ); let has_node = matches!( spec.scope, AttributeScope::NodeSignal | AttributeScope::NodeMessage @@ -719,14 +721,15 @@ impl Config<'_> { let mut fields = Vec::with_capacity(spec.fields.len()); for field in spec.fields { - let lit = resolve_field_source(&field.source, msg, dbc, signal, node).ok_or_else(|| { - anyhow!( + let lit = + resolve_field_source(&field.source, msg, dbc, signal, node).ok_or_else(|| { + anyhow!( "attribute_structs: const '{const_name}' field {:?} has no value in the DBC \ and no default (source: {:?})", field.name, field.source ) - })?; + })?; fields.push((field.name, lit)); } From 2cb8be840e44106914e16fc1cf69ccf62a2c884b Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Mon, 24 Aug 2026 16:56:25 +0000 Subject: [PATCH 3/7] Resolve Clippy warnings --- src/lib.rs | 79 +++++++++++++++++++++++++++++++----------------------- 1 file changed, 45 insertions(+), 34 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 38aef833..7a17ed9c 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -181,6 +181,14 @@ pub enum FieldSource<'a> { NodeName, } +/// Message/signal/node context used to resolve an [`AttributeStruct`]'s fields. +struct AttrTarget<'a> { + msg: &'a Message, + dbc: &'a Dbc, + signal: Option<&'a Signal>, + node: Option<&'a str>, +} + impl Config<'_> { /// Write Rust structs matching DBC input description to `out` buffer fn codegen(&self, out: impl Write) -> Result<()> { @@ -618,10 +626,12 @@ impl Config<'_> { w, spec, spec.const_name, - msg, - dbc, - None, - None, + &AttrTarget { + msg, + dbc, + signal: None, + node: None, + }, &mut used, )?; } @@ -639,10 +649,12 @@ impl Config<'_> { w, spec, &name, - msg, - dbc, - Some(signal), - None, + &AttrTarget { + msg, + dbc, + signal: Some(signal), + node: None, + }, &mut used, )?; } @@ -658,19 +670,20 @@ impl Config<'_> { let node_ident = sanitize_name(node, "x", ToSnakeCase::to_snake_case).to_uppercase(); let name = format!( - "{}_{}_{}", + "{}_{node_ident}_{}", signal.field_name().to_uppercase(), - node_ident, spec.const_name ); Self::render_attribute_struct( w, spec, &name, - msg, - dbc, - Some(signal), - Some(node), + &AttrTarget { + msg, + dbc, + signal: Some(signal), + node: Some(node), + }, &mut used, )?; } @@ -688,10 +701,12 @@ impl Config<'_> { w, spec, &name, - msg, - dbc, - None, - Some(&node.0), + &AttrTarget { + msg, + dbc, + signal: None, + node: Some(&node.0), + }, &mut used, )?; } @@ -706,30 +721,26 @@ impl Config<'_> { w: &mut impl Write, spec: &AttributeStruct<'_>, const_name: &str, - msg: &Message, - dbc: &Dbc, - signal: Option<&Signal>, - node: Option<&str>, + target: &AttrTarget<'_>, used: &mut BTreeSet, ) -> Result<()> { ensure!( used.insert(const_name.to_string()), "attribute_structs: generated const '{const_name}' on message {:?} collides with \ another const. Use a distinct 'const_name'.", - msg.name + target.msg.name ); let mut fields = Vec::with_capacity(spec.fields.len()); for field in spec.fields { - let lit = - resolve_field_source(&field.source, msg, dbc, signal, node).ok_or_else(|| { - anyhow!( + let lit = resolve_field_source(&field.source, target).ok_or_else(|| { + anyhow!( "attribute_structs: const '{const_name}' field {:?} has no value in the DBC \ and no default (source: {:?})", field.name, field.source ) - })?; + })?; fields.push((field.name, lit)); } @@ -1715,13 +1726,13 @@ fn message_ignored(message: &Message) -> bool { } /// Resolve a [`FieldSource`] to a Rust literal. -fn resolve_field_source( - source: &FieldSource<'_>, - msg: &Message, - dbc: &Dbc, - signal: Option<&Signal>, - node: Option<&str>, -) -> Option { +fn resolve_field_source(source: &FieldSource<'_>, target: &AttrTarget<'_>) -> Option { + let AttrTarget { + msg, + dbc, + signal, + node, + } = *target; match source { FieldSource::Attr(name) => match (signal, node) { (Some(s), None) => dbc.resolved_signal_attribute(msg.id, &s.name, name), From 4556c53f4576526b8524f926c235aa36a39dc484 Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Tue, 25 Aug 2026 14:10:46 +0000 Subject: [PATCH 4/7] Deduplicate relation attribute lookups --- src/lib.rs | 107 ++++++++++++++++++++++----------------------------- src/utils.rs | 4 ++ 2 files changed, 49 insertions(+), 62 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 7a17ed9c..ef9d9159 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -38,7 +38,7 @@ use crate::signal_type::{IntSize, ValType}; use crate::utils::{ enum_name, enum_variant_name, is_screaming_snake_case, is_valid_ident, is_valid_type_path, multiplex_enum_name, multiplexed_enum_variant_name, multiplexed_enum_variant_wrapper_name, - sanitize_name, MessageExt as _, SignalExt as _, + node_field_name, MessageExt as _, SignalExt as _, }; static ALLOW_DEADCODE: &str = "#[allow(dead_code)]"; @@ -662,13 +662,18 @@ impl Config<'_> { AttributeScope::NodeSignal => { for signal in &msg.signals { for node in &signal.receivers { - if node_signal_attribute(dbc, node, msg.id, &signal.name, spec.require) - .is_none() + if node_relation_attribute( + dbc, + node, + msg.id, + Some(&signal.name), + spec.require, + ) + .is_none() { continue; } - let node_ident = - sanitize_name(node, "x", ToSnakeCase::to_snake_case).to_uppercase(); + let node_ident = node_field_name(node).to_uppercase(); let name = format!( "{}_{node_ident}_{}", signal.field_name().to_uppercase(), @@ -691,11 +696,12 @@ impl Config<'_> { } AttributeScope::NodeMessage => { for node in &dbc.nodes { - if node_message_attribute(dbc, &node.0, msg.id, spec.require).is_none() { + if node_relation_attribute(dbc, &node.0, msg.id, None, spec.require) + .is_none() + { continue; } - let node_ident = - sanitize_name(&node.0, "x", ToSnakeCase::to_snake_case).to_uppercase(); + let node_ident = node_field_name(&node.0).to_uppercase(); let name = format!("{node_ident}_{}", spec.const_name); Self::render_attribute_struct( w, @@ -1737,8 +1743,10 @@ fn resolve_field_source(source: &FieldSource<'_>, target: &AttrTarget<'_>) -> Op FieldSource::Attr(name) => match (signal, node) { (Some(s), None) => dbc.resolved_signal_attribute(msg.id, &s.name, name), (None, None) => dbc.resolved_message_attribute(msg.id, name), - (Some(s), Some(n)) => resolved_node_signal_attribute(dbc, n, msg.id, &s.name, name), - (None, Some(n)) => resolved_node_message_attribute(dbc, n, msg.id, name), + (Some(s), Some(n)) => { + resolved_node_relation_attribute(dbc, n, msg.id, Some(&s.name), name) + } + (None, Some(n)) => resolved_node_relation_attribute(dbc, n, msg.id, None, name), } .map(attr_value_literal), FieldSource::MessageAttr(name) => dbc @@ -1754,78 +1762,53 @@ fn resolve_field_source(source: &FieldSource<'_>, target: &AttrTarget<'_>) -> Op } } -/// Lookup an assigned node-to-signal relation attribute value (`BA_REL_ ... BU_SG_REL_`). -fn node_signal_attribute<'a>( - dbc: &'a Dbc, - node_name: &str, - message_id: MessageId, - signal_name: &str, - name: &str, -) -> Option<&'a AttributeValue> { - dbc.relation_attribute_values.iter().find_map(|rel| { - if rel.name != name { - return None; - } - match &rel.details { - AttributeValueForRelationType::NodeToSignal { - node_name: n, - message_id: mid, - signal_name: sig, - value, - } if n == node_name && *mid == message_id && sig == signal_name => Some(value), - _ => None, - } - }) -} - -/// Lookup a node-to-signal relation attribute value. -/// Uses the default (`BA_DEF_DEF_REL_`) if a value is not assigned. -fn resolved_node_signal_attribute<'a>( - dbc: &'a Dbc, - node_name: &str, - message_id: MessageId, - signal_name: &str, - name: &str, -) -> Option<&'a AttributeValue> { - node_signal_attribute(dbc, node_name, message_id, signal_name, name).or_else(|| { - dbc.relation_attribute_defaults - .iter() - .find(|d| d.name == name) - .map(|d| &d.value) - }) -} - -/// Lookup an assigned node-to-message relation attribute value (`BA_REL_ ... BU_BO_REL_`). -fn node_message_attribute<'a>( +/// Lookup an assigned node relation attribute value. This can be either +/// node-to-signal (`BA_REL_ ... BU_SG_REL_`, `signal_name: Some`) or +/// node-to-message (`BA_REL_ ... BU_BO_REL_`, `signal_name: None`). +fn node_relation_attribute<'a>( dbc: &'a Dbc, node_name: &str, message_id: MessageId, + signal_name: Option<&str>, name: &str, ) -> Option<&'a AttributeValue> { dbc.relation_attribute_values.iter().find_map(|rel| { if rel.name != name { return None; } - match &rel.details { - AttributeValueForRelationType::NodeToMessage { - node_name: n, - message_id: mid, - value, - } if n == node_name && *mid == message_id => Some(value), + match (&rel.details, signal_name) { + ( + AttributeValueForRelationType::NodeToSignal { + node_name: n, + message_id: mid, + signal_name: sig, + value, + }, + Some(want_sig), + ) if n == node_name && *mid == message_id && sig == want_sig => Some(value), + ( + AttributeValueForRelationType::NodeToMessage { + node_name: n, + message_id: mid, + value, + }, + None, + ) if n == node_name && *mid == message_id => Some(value), _ => None, } }) } -/// Lookup a node-to-message relation attribute value. +/// Lookup a node relation attribute value. /// Uses the default (`BA_DEF_DEF_REL_`) if a value is not assigned. -fn resolved_node_message_attribute<'a>( +fn resolved_node_relation_attribute<'a>( dbc: &'a Dbc, node_name: &str, message_id: MessageId, + signal_name: Option<&str>, name: &str, ) -> Option<&'a AttributeValue> { - node_message_attribute(dbc, node_name, message_id, name).or_else(|| { + node_relation_attribute(dbc, node_name, message_id, signal_name, name).or_else(|| { dbc.relation_attribute_defaults .iter() .find(|d| d.name == name) diff --git a/src/utils.rs b/src/utils.rs index d7b1cd11..74db5639 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -69,6 +69,10 @@ impl MessageExt for Message { } } +pub fn node_field_name(name: &str) -> String { + sanitize_name(name, "x", ToSnakeCase::to_snake_case) +} + pub fn sanitize_name(x: &str, prefix: &str, to_case: fn(&str) -> String) -> String { if keywords::is_keyword(x) || !x.starts_with(|c: char| c.is_ascii_alphabetic()) { format!("{prefix}{}", to_case(x)) From a7d891d709444a8b497d1723576256dbcb415966 Mon Sep 17 00:00:00 2001 From: Felix van Oost Date: Tue, 25 Aug 2026 14:38:18 +0000 Subject: [PATCH 5/7] Add for_node option to restrict scope to single node --- README.md | 36 ++++++++++ examples/attribute_structs.rs | 2 + src/lib.rs | 54 ++++++++++++--- tests/attribute_structs.rs | 125 ++++++++++++++++++++++++++++++++++ 4 files changed, 209 insertions(+), 8 deletions(-) diff --git a/README.md b/README.md index e4472a00..62e0305f 100644 --- a/README.md +++ b/README.md @@ -132,6 +132,7 @@ let e2e = AttributeStruct { AttributeField { name: "start_byte", source: FieldSource::StartByte }, AttributeField { name: "width_bit", source: FieldSource::Attr("E2EDataLength") }, ], + for_node: None, }; let dbc_file = ""; @@ -168,6 +169,7 @@ let sig_timeout = AttributeStruct { AttributeField { name: "node", source: FieldSource::NodeName }, AttributeField { name: "timeout_ms", source: FieldSource::Attr("GenSigTimeoutTime") }, ], + for_node: None, }; let dbc_file = ""; @@ -189,6 +191,40 @@ impl SomeMessage { } ``` +If you're generating code for a single node/ECU, you can set `for_node` to the node name to generates constants only for relation attributes that are relevant to the specific node. This also removes the node name from the constant names, making the generated code node/ECU name-agnostic. + +```rust,no_run +use dbc_codegen::{AttributeField, AttributeScope, AttributeStruct, Config, FieldSource}; + +let sig_timeout_for_ecu2 = AttributeStruct { + type_path: "relation::SigTimeoutInfo", + const_name: "SIG_TIMEOUT", + scope: AttributeScope::NodeSignal, + require: "GenSigTimeoutTime", + fields: &[ + AttributeField { name: "node", source: FieldSource::NodeName }, + AttributeField { name: "timeout_ms", source: FieldSource::Attr("GenSigTimeoutTime") }, + ], + for_node: Some("ECU2"), +}; + +let dbc_file = ""; +Config::builder() + .dbc_name("example.dbc") + .dbc_content(dbc_file) + .attribute_structs(&[sig_timeout_for_ecu2]) + .build() + .generate() + .unwrap(); +``` + +```rust,ignore +impl SomeMessage { + pub const SOME_SIGNAL_SIG_TIMEOUT: relation::SigTimeoutInfo = + relation::SigTimeoutInfo { node: "ECU2", timeout_ms: 60 }; +} +``` + ### `no_std` The generated code is `no_std` compatible. diff --git a/examples/attribute_structs.rs b/examples/attribute_structs.rs index 7f7bbbd8..a6f6db61 100644 --- a/examples/attribute_structs.rs +++ b/examples/attribute_structs.rs @@ -44,6 +44,7 @@ fn main() { field("width_bit", FieldSource::Attr("E2EDataLength")), field("profile", FieldSource::Attr("E2EProfile")), ], + for_node: None, }; let secoc = AttributeStruct { type_path: "data_protection::SecOcInfo", @@ -54,6 +55,7 @@ fn main() { "freshness_id", FieldSource::Attr("SCP_FreshnessValueId"), )], + for_node: None, }; let code = Config::builder() diff --git a/src/lib.rs b/src/lib.rs index ef9d9159..2f5d28ea 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -129,6 +129,12 @@ pub struct AttributeStruct<'a> { /// The struct fields, written in this order. pub fields: &'a [AttributeField<'a>], + + /// Restrict [`AttributeScope::NodeSignal`]/[`AttributeScope::NodeMessage`] + /// emission to this one node and remove the node name from the generated + /// const. Set to `None` to keep the default behavior (one constant per + /// matching node). + pub for_node: Option<&'a str>, } /// Whether an [`AttributeStruct`] is emitted once per message or once per signal. @@ -557,6 +563,19 @@ impl Config<'_> { spec.scope, AttributeScope::NodeSignal | AttributeScope::NodeMessage ); + if let Some(for_node) = spec.for_node { + ensure!( + !for_node.is_empty(), + "attribute_structs: 'for_node' must not be empty for {:?}", + spec.const_name + ); + ensure!( + has_node, + "attribute_structs: {:?} sets 'for_node' but its scope has no associated \ + node (scope must be NodeSignal or NodeMessage)", + spec.const_name + ); + } let mut seen = BTreeSet::new(); for field in spec.fields { ensure!( @@ -662,6 +681,9 @@ impl Config<'_> { AttributeScope::NodeSignal => { for signal in &msg.signals { for node in &signal.receivers { + if spec.for_node.is_some_and(|target| node != target) { + continue; + } if node_relation_attribute( dbc, node, @@ -673,12 +695,21 @@ impl Config<'_> { { continue; } - let node_ident = node_field_name(node).to_uppercase(); - let name = format!( - "{}_{node_ident}_{}", - signal.field_name().to_uppercase(), - spec.const_name - ); + let name = match spec.for_node { + Some(_) => format!( + "{}_{}", + signal.field_name().to_uppercase(), + spec.const_name + ), + None => { + let node_ident = node_field_name(node).to_uppercase(); + format!( + "{}_{node_ident}_{}", + signal.field_name().to_uppercase(), + spec.const_name + ) + } + }; Self::render_attribute_struct( w, spec, @@ -696,13 +727,20 @@ impl Config<'_> { } AttributeScope::NodeMessage => { for node in &dbc.nodes { + if spec.for_node.is_some_and(|target| node.0 != target) { + continue; + } if node_relation_attribute(dbc, &node.0, msg.id, None, spec.require) .is_none() { continue; } - let node_ident = node_field_name(&node.0).to_uppercase(); - let name = format!("{node_ident}_{}", spec.const_name); + let name = match spec.for_node { + Some(_) => spec.const_name.to_string(), + None => { + format!("{}_{}", node_field_name(&node.0).to_uppercase(), spec.const_name) + } + }; Self::render_attribute_struct( w, spec, diff --git a/tests/attribute_structs.rs b/tests/attribute_structs.rs index c010954f..e6d447da 100644 --- a/tests/attribute_structs.rs +++ b/tests/attribute_structs.rs @@ -84,6 +84,7 @@ const E2E: AttributeStruct = AttributeStruct { source: FieldSource::Attr("E2EProfile"), }, ], + for_node: None, }; /// Message-scoped attributes @@ -102,6 +103,7 @@ const SEC_OC: AttributeStruct = AttributeStruct { source: FieldSource::Attr("SC_Message"), }, ], + for_node: None, }; /// Non-attribute sources @@ -140,6 +142,7 @@ const LAYOUT: AttributeStruct = AttributeStruct { source: FieldSource::Str("hello"), }, ], + for_node: None, }; /// Node-signal relation attributes @@ -166,6 +169,7 @@ const SIG_TIMEOUT: AttributeStruct = AttributeStruct { source: FieldSource::StartByte, }, ], + for_node: None, }; /// Node-message relation attributes @@ -184,6 +188,19 @@ const MSG_PROJECT: AttributeStruct = AttributeStruct { source: FieldSource::Attr("MsgProject"), }, ], + for_node: None, +}; + +/// Node-signal relation attributes, filtered to one node +const SIG_TIMEOUT_FOR_ECU2: AttributeStruct = AttributeStruct { + for_node: Some("ECU2"), + ..SIG_TIMEOUT +}; + +/// Node-message relation attributes, filtered to one node +const MSG_PROJECT_FOR_ECU1: AttributeStruct = AttributeStruct { + for_node: Some("ECU1"), + ..MSG_PROJECT }; #[test] @@ -310,6 +327,7 @@ fn message_scope_with_signal_source_is_rejected() { name: "x", source: FieldSource::StartByte, }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -332,6 +350,7 @@ fn missing_field_value_is_an_error() { name: "x", source: FieldSource::Attr("NoSuchAttr"), }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -354,6 +373,7 @@ fn duplicate_const_name_is_an_error() { name: "freshness_id", source: FieldSource::Attr("SCP_FreshnessValueId"), }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -377,6 +397,7 @@ fn non_screaming_snake_case_const_name_is_rejected() { name: "x", source: FieldSource::Attr("SCP_FreshnessValueId"), }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -403,6 +424,7 @@ fn const_name_colliding_with_reserved_const_is_rejected() { name: "x", source: FieldSource::Attr("SCP_FreshnessValueId"), }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -425,6 +447,7 @@ fn invalid_type_path_is_rejected() { name: "x", source: FieldSource::Attr("SCP_FreshnessValueId"), }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -450,6 +473,7 @@ fn invalid_field_name_is_rejected() { name: "1x", // not a valid identifier source: FieldSource::Attr("SCP_FreshnessValueId"), }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -478,6 +502,7 @@ fn duplicate_field_names_are_rejected() { source: FieldSource::Int(1), }, ], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -514,6 +539,7 @@ fn fixture_message_scope_resolves_hex_float_enum_and_size() { source: FieldSource::MessageSize, }, ], + for_node: None, }; let bytes = std::fs::read(FIXTURE).unwrap(); let dbc = can_dbc::decode_cp1252(&bytes).unwrap(); @@ -556,6 +582,7 @@ fn fixture_signal_scope_resolves_string_enum_and_layout() { source: FieldSource::BitWidth, }, ], + for_node: None, }; let bytes = std::fs::read(FIXTURE).unwrap(); let dbc = can_dbc::decode_cp1252(&bytes).unwrap(); @@ -585,6 +612,7 @@ fn fixture_absent_attribute_falls_back_to_default() { name: "label", source: FieldSource::Attr("TheSignalStringAttribute"), }], + for_node: None, }; let bytes = std::fs::read(FIXTURE).unwrap(); let dbc = can_dbc::decode_cp1252(&bytes).unwrap(); @@ -611,6 +639,7 @@ fn fixture_enum_attribute_is_index_not_label() { name: "send_type", source: FieldSource::Attr("GenMsgSendType"), }], + for_node: None, }; let bytes = std::fs::read(FIXTURE).unwrap(); let dbc = can_dbc::decode_cp1252(&bytes).unwrap(); @@ -636,6 +665,7 @@ fn fixture_definition_without_default_is_an_error() { name: "x", source: FieldSource::Attr("TheUnusedAttributeDefinitionWithoutDefault"), }], + for_node: None, }; let bytes = std::fs::read(FIXTURE).unwrap(); let dbc = can_dbc::decode_cp1252(&bytes).unwrap(); @@ -692,6 +722,99 @@ fn node_message_scope_emits_const_per_related_node() { assert!(out.contains("project: 1"), "{out}"); } +#[test] +fn node_signal_scope_with_for_node_drops_node_suffix() { + let out = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[SIG_TIMEOUT_FOR_ECU2]) + .build() + .generate() + .unwrap(); + assert!( + out.contains("pub const REL_SIG_SIG_TIMEOUT: relation::SigTimeoutInfo"), + "{out}" + ); + assert!(!out.contains("ECU2_SIG_TIMEOUT"), "{out}"); + assert!(out.contains(r#"node: "ECU2""#), "{out}"); + assert!(out.contains("timeout_ms: 60"), "{out}"); + assert_eq!(out.matches("pub const REL_SIG_SIG_TIMEOUT").count(), 1, "{out}"); +} + +#[test] +fn node_message_scope_with_for_node_drops_node_suffix() { + let out = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[MSG_PROJECT_FOR_ECU1]) + .build() + .generate() + .unwrap(); + assert!( + out.contains("pub const MSG_PROJECT: relation::MsgProjectInfo"), + "{out}" + ); + assert!(!out.contains("ECU1_MSG_PROJECT"), "{out}"); + assert!(!out.contains("ECU2_MSG_PROJECT"), "{out}"); + assert!(out.contains(r#"node: "ECU1""#), "{out}"); + assert!(out.contains("project: 0"), "{out}"); + assert_eq!(out.matches("pub const MSG_PROJECT").count(), 1, "{out}"); +} + +#[test] +fn for_node_targeting_absent_node_emits_nothing() { + let spec = AttributeStruct { + for_node: Some("ECU1"), // ECU1 is not a receiver of RelSig and has no GenSigTimeoutTime + ..SIG_TIMEOUT + }; + let out = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[spec]) + .build() + .generate() + .unwrap(); + assert!(!out.contains("SIG_TIMEOUT"), "{out}"); +} + +#[test] +fn for_node_with_message_scope_is_rejected() { + let bad = AttributeStruct { + for_node: Some("ECU1"), + ..SEC_OC + }; + let err = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[bad]) + .build() + .generate() + .unwrap_err(); + assert!( + format!("{err:#}").contains("no associated node"), + "{err:#}" + ); +} + +#[test] +fn empty_for_node_is_rejected() { + let bad = AttributeStruct { + for_node: Some(""), + ..SIG_TIMEOUT + }; + let err = Config::builder() + .dbc_name("test") + .dbc_content(DBC) + .attribute_structs(&[bad]) + .build() + .generate() + .unwrap_err(); + assert!( + format!("{err:#}").contains("'for_node' must not be empty"), + "{err:#}" + ); +} + #[test] fn node_name_source_is_rejected_outside_node_scopes() { let bad = AttributeStruct { @@ -703,6 +826,7 @@ fn node_name_source_is_rejected_outside_node_scopes() { name: "node", source: FieldSource::NodeName, }], + for_node: None, }; let err = Config::builder() .dbc_name("test") @@ -725,6 +849,7 @@ fn node_message_scope_with_signal_source_is_rejected() { name: "x", source: FieldSource::StartByte, }], + for_node: None, }; let err = Config::builder() .dbc_name("test") From 43ee3cfae2b7cead250e449be4917a07b91411e9 Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 15:03:50 +0000 Subject: [PATCH 6/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lib.rs | 32 +++++++++++++++----------------- tests/attribute_structs.rs | 11 ++++++----- 2 files changed, 21 insertions(+), 22 deletions(-) diff --git a/src/lib.rs b/src/lib.rs index 2f5d28ea..f3b17346 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -695,20 +695,15 @@ impl Config<'_> { { continue; } - let name = match spec.for_node { - Some(_) => format!( - "{}_{}", + let name = if spec.for_node.is_some() { + format!("{}_{}", signal.field_name().to_uppercase(), spec.const_name) + } else { + let node_ident = node_field_name(node).to_uppercase(); + format!( + "{}_{node_ident}_{}", signal.field_name().to_uppercase(), spec.const_name - ), - None => { - let node_ident = node_field_name(node).to_uppercase(); - format!( - "{}_{node_ident}_{}", - signal.field_name().to_uppercase(), - spec.const_name - ) - } + ) }; Self::render_attribute_struct( w, @@ -735,11 +730,14 @@ impl Config<'_> { { continue; } - let name = match spec.for_node { - Some(_) => spec.const_name.to_string(), - None => { - format!("{}_{}", node_field_name(&node.0).to_uppercase(), spec.const_name) - } + let name = if spec.for_node.is_some() { + spec.const_name.to_string() + } else { + format!( + "{}_{}", + node_field_name(&node.0).to_uppercase(), + spec.const_name + ) }; Self::render_attribute_struct( w, diff --git a/tests/attribute_structs.rs b/tests/attribute_structs.rs index e6d447da..9e8774ad 100644 --- a/tests/attribute_structs.rs +++ b/tests/attribute_structs.rs @@ -738,7 +738,11 @@ fn node_signal_scope_with_for_node_drops_node_suffix() { assert!(!out.contains("ECU2_SIG_TIMEOUT"), "{out}"); assert!(out.contains(r#"node: "ECU2""#), "{out}"); assert!(out.contains("timeout_ms: 60"), "{out}"); - assert_eq!(out.matches("pub const REL_SIG_SIG_TIMEOUT").count(), 1, "{out}"); + assert_eq!( + out.matches("pub const REL_SIG_SIG_TIMEOUT").count(), + 1, + "{out}" + ); } #[test] @@ -790,10 +794,7 @@ fn for_node_with_message_scope_is_rejected() { .build() .generate() .unwrap_err(); - assert!( - format!("{err:#}").contains("no associated node"), - "{err:#}" - ); + assert!(format!("{err:#}").contains("no associated node"), "{err:#}"); } #[test] From 8af02a2d47d25cb4a66591c78966c42691115dcd Mon Sep 17 00:00:00 2001 From: "pre-commit-ci[bot]" <66853113+pre-commit-ci[bot]@users.noreply.github.com> Date: Tue, 25 Aug 2026 17:04:35 +0000 Subject: [PATCH 7/7] [pre-commit.ci] auto fixes from pre-commit.com hooks for more information, see https://pre-commit.ci --- src/lib.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/lib.rs b/src/lib.rs index f3b17346..6e2cff25 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -696,7 +696,11 @@ impl Config<'_> { continue; } let name = if spec.for_node.is_some() { - format!("{}_{}", signal.field_name().to_uppercase(), spec.const_name) + format!( + "{}_{}", + signal.field_name().to_uppercase(), + spec.const_name + ) } else { let node_ident = node_field_name(node).to_uppercase(); format!(