From f3f9590b59b58e2078ca39205cec6fb961adf28c Mon Sep 17 00:00:00 2001 From: Seong Yong-ju Date: Thu, 29 Jan 2026 13:22:21 +0900 Subject: [PATCH 1/2] feat: add `node_ext` module with `full_name` methods for constant nodes Port Ruby's `node_ext.rb` functionality to Rust bindings, providing `full_name` and `full_name_parts` methods for `ConstantReadNode`, `ConstantWriteNode`, `ConstantTargetNode`, `ConstantPathNode`, and `ConstantPathTargetNode`. Co-Authored-By: Claude Opus 4.5 --- rust/ruby-prism/src/lib.rs | 2 + rust/ruby-prism/src/node_ext.rs | 429 ++++++++++++++++++++++++++++++++ 2 files changed, 431 insertions(+) create mode 100644 rust/ruby-prism/src/node_ext.rs diff --git a/rust/ruby-prism/src/lib.rs b/rust/ruby-prism/src/lib.rs index 6824768193..9a178b6d3d 100644 --- a/rust/ruby-prism/src/lib.rs +++ b/rust/ruby-prism/src/lib.rs @@ -14,6 +14,7 @@ mod bindings { } mod node; +mod node_ext; mod parse_result; use std::mem::MaybeUninit; @@ -21,6 +22,7 @@ use std::ptr::NonNull; pub use self::bindings::*; pub use self::node::{ConstantId, ConstantList, ConstantListIter, Integer, NodeList, NodeListIter}; +pub use self::node_ext::ConstantPathError; pub use self::parse_result::{Comment, CommentType, Comments, Diagnostic, Diagnostics, Location, MagicComment, MagicComments, ParseResult}; use ruby_prism_sys::{pm_parse, pm_parser_init, pm_parser_t}; diff --git a/rust/ruby-prism/src/node_ext.rs b/rust/ruby-prism/src/node_ext.rs new file mode 100644 index 0000000000..66f3074043 --- /dev/null +++ b/rust/ruby-prism/src/node_ext.rs @@ -0,0 +1,429 @@ +//! Node extension methods for the prism parser. +//! +//! This module provides convenience methods on AST nodes that aren't generated +//! from the config, mirroring Ruby's `node_ext.rb`. + +use std::borrow::Cow; +use std::fmt; + +use crate::{ConstantPathNode, ConstantPathTargetNode, ConstantReadNode, ConstantTargetNode, ConstantWriteNode, Node}; + +/// Errors for constant path name computation. +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ConstantPathError { + /// An error returned when dynamic parts are found while computing a + /// constant path's full name. For example: + /// `Foo::Bar::Baz` -> succeeds because all parts of the constant + /// path are simple constants. + /// `var::Bar::Baz` -> fails because the first part of the constant path + /// is a local variable. + DynamicParts, + /// An error returned when missing nodes are found while computing a + /// constant path's full name. For example: + /// `Foo::` -> fails because the constant path is missing the last part. + MissingNodes, +} + +impl fmt::Display for ConstantPathError { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + match self { + Self::DynamicParts => { + write!(f, "Constant path contains dynamic parts. Cannot compute full name") + }, + Self::MissingNodes => { + write!(f, "Constant path contains missing nodes. Cannot compute full name") + }, + } + } +} + +impl std::error::Error for ConstantPathError {} + +impl<'pr> ConstantReadNode<'pr> { + /// Returns the list of parts for the full name of this constant. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let constant = stmt.as_constant_read_node().unwrap(); + /// assert_eq!(constant.full_name_parts(), vec!["Foo"]); + /// ``` + #[must_use] + pub fn full_name_parts(&self) -> Vec> { + vec![String::from_utf8_lossy(self.name().as_slice())] + } + + /// Returns the full name of this constant. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let constant = stmt.as_constant_read_node().unwrap(); + /// assert_eq!(constant.full_name(), "Foo"); + /// ``` + #[must_use] + pub fn full_name(&self) -> Cow<'pr, str> { + String::from_utf8_lossy(self.name().as_slice()) + } +} + +impl<'pr> ConstantWriteNode<'pr> { + /// Returns the list of parts for the full name of this constant. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo = 1"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let constant = stmt.as_constant_write_node().unwrap(); + /// assert_eq!(constant.full_name_parts(), vec!["Foo"]); + /// ``` + #[must_use] + pub fn full_name_parts(&self) -> Vec> { + vec![String::from_utf8_lossy(self.name().as_slice())] + } + + /// Returns the full name of this constant. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo = 1"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let constant = stmt.as_constant_write_node().unwrap(); + /// assert_eq!(constant.full_name(), "Foo"); + /// ``` + #[must_use] + pub fn full_name(&self) -> Cow<'pr, str> { + String::from_utf8_lossy(self.name().as_slice()) + } +} + +impl<'pr> ConstantTargetNode<'pr> { + /// Returns the list of parts for the full name of this constant. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo, Bar = [1, 2]"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let target = stmt.as_multi_write_node().unwrap() + /// .lefts().iter().next().unwrap(); + /// let constant = target.as_constant_target_node().unwrap(); + /// assert_eq!(constant.full_name_parts(), vec!["Foo"]); + /// ``` + #[must_use] + pub fn full_name_parts(&self) -> Vec> { + vec![String::from_utf8_lossy(self.name().as_slice())] + } + + /// Returns the full name of this constant. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo, Bar = [1, 2]"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let target = stmt.as_multi_write_node().unwrap() + /// .lefts().iter().next().unwrap(); + /// let constant = target.as_constant_target_node().unwrap(); + /// assert_eq!(constant.full_name(), "Foo"); + /// ``` + #[must_use] + pub fn full_name(&self) -> Cow<'pr, str> { + String::from_utf8_lossy(self.name().as_slice()) + } +} + +impl<'pr> ConstantPathNode<'pr> { + /// Returns the list of parts for the full name of this constant path. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo::Bar"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let constant_path = stmt.as_constant_path_node().unwrap(); + /// assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); + /// ``` + /// + /// # Errors + /// + /// Returns [`ConstantPathError::DynamicParts`] if the path contains + /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path + /// contains missing nodes. + pub fn full_name_parts(&self) -> Result>, ConstantPathError> { + let mut parts = Vec::new(); + let mut current: Option> = Some(self.as_node()); + + while let Some(ref node) = current { + if let Some(path_node) = node.as_constant_path_node() { + let name = path_node.name().ok_or(ConstantPathError::MissingNodes)?; + parts.push(String::from_utf8_lossy(name.as_slice())); + current = path_node.parent(); + } else if let Some(read_node) = node.as_constant_read_node() { + parts.push(String::from_utf8_lossy(read_node.name().as_slice())); + current = None; + } else { + return Err(ConstantPathError::DynamicParts); + } + } + + parts.reverse(); + + if self.is_stovetop() { + parts.insert(0, Cow::Borrowed("")); + } + + Ok(parts) + } + + /// Returns the full name of this constant path. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo::Bar"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let constant_path = stmt.as_constant_path_node().unwrap(); + /// assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + /// ``` + /// + /// # Errors + /// + /// Returns [`ConstantPathError::DynamicParts`] if the path contains + /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path + /// contains missing nodes. + pub fn full_name(&self) -> Result { + Ok(self.full_name_parts()?.join("::")) + } + + fn is_stovetop(&self) -> bool { + let mut current: Option> = Some(self.as_node()); + + while let Some(ref node) = current { + if let Some(path_node) = node.as_constant_path_node() { + current = path_node.parent(); + } else { + return false; + } + } + + true + } +} + +impl<'pr> ConstantPathTargetNode<'pr> { + /// Returns the list of parts for the full name of this constant path. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo::Bar, Baz = [1, 2]"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let target = stmt.as_multi_write_node().unwrap() + /// .lefts().iter().next().unwrap(); + /// let constant_path = target.as_constant_path_target_node().unwrap(); + /// assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); + /// ``` + /// + /// # Errors + /// + /// Returns [`ConstantPathError::DynamicParts`] if the path contains + /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path + /// contains missing nodes. + pub fn full_name_parts(&self) -> Result>, ConstantPathError> { + let name = self.name().ok_or(ConstantPathError::MissingNodes)?; + + let mut parts = if let Some(parent) = self.parent() { + if let Some(path_node) = parent.as_constant_path_node() { + path_node.full_name_parts()? + } else if let Some(read_node) = parent.as_constant_read_node() { + read_node.full_name_parts() + } else { + return Err(ConstantPathError::DynamicParts); + } + } else { + vec![Cow::Borrowed("")] + }; + + parts.push(String::from_utf8_lossy(name.as_slice())); + Ok(parts) + } + + /// Returns the full name of this constant path. + /// + /// # Examples + /// + /// ``` + /// # use ruby_prism::parse; + /// let result = parse(b"Foo::Bar, Baz = [1, 2]"); + /// let stmt = result.node().as_program_node().unwrap() + /// .statements().body().iter().next().unwrap(); + /// let target = stmt.as_multi_write_node().unwrap() + /// .lefts().iter().next().unwrap(); + /// let constant_path = target.as_constant_path_target_node().unwrap(); + /// assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + /// ``` + /// + /// # Errors + /// + /// Returns [`ConstantPathError::DynamicParts`] if the path contains + /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path + /// contains missing nodes. + pub fn full_name(&self) -> Result { + Ok(self.full_name_parts()?.join("::")) + } +} + +#[cfg(test)] +mod tests { + use super::ConstantPathError; + use crate::parse; + + #[test] + fn test_full_name_for_constant_read_node() { + let result = parse(b"Foo"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let constant = node.as_constant_read_node().unwrap(); + + assert_eq!(constant.full_name_parts(), vec!["Foo"]); + assert_eq!(constant.full_name(), "Foo"); + } + + #[test] + fn test_full_name_for_constant_write_node() { + let result = parse(b"Foo = 1"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let constant = node.as_constant_write_node().unwrap(); + + assert_eq!(constant.full_name_parts(), vec!["Foo"]); + assert_eq!(constant.full_name(), "Foo"); + } + + #[test] + fn test_full_name_for_constant_target_node() { + let result = parse(b"Foo, Bar = [1, 2]"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let multi_write = node.as_multi_write_node().unwrap(); + let target = multi_write.lefts().iter().next().unwrap(); + let constant = target.as_constant_target_node().unwrap(); + + assert_eq!(constant.full_name_parts(), vec!["Foo"]); + assert_eq!(constant.full_name(), "Foo"); + } + + #[test] + fn test_full_name_for_constant_path() { + let result = parse(b"Foo::Bar"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let constant_path = node.as_constant_path_node().unwrap(); + + assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); + assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + } + + #[test] + fn test_full_name_for_constant_path_with_stovetop() { + let result = parse(b"::Foo::Bar"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let constant_path = node.as_constant_path_node().unwrap(); + + assert_eq!(constant_path.full_name_parts().unwrap(), vec!["", "Foo", "Bar"]); + assert_eq!(constant_path.full_name().unwrap(), "::Foo::Bar"); + } + + #[test] + fn test_full_name_for_constant_path_with_self() { + let source = r" +self:: + Bar::Baz:: + Qux +"; + let result = parse(source.as_bytes()); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let constant_path = node.as_constant_path_node().unwrap(); + + assert_eq!(constant_path.full_name().unwrap_err(), ConstantPathError::DynamicParts); + } + + #[test] + fn test_full_name_for_constant_path_with_variable() { + let source = r" +foo:: + Bar::Baz:: + Qux +"; + let result = parse(source.as_bytes()); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let constant_path = node.as_constant_path_node().unwrap(); + + assert_eq!(constant_path.full_name().unwrap_err(), ConstantPathError::DynamicParts); + } + + #[test] + fn test_full_name_for_constant_path_with_missing_name() { + let result = parse(b"Foo::"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let constant_path = node.as_constant_path_node().unwrap(); + + assert_eq!(constant_path.full_name().unwrap_err(), ConstantPathError::MissingNodes); + } + + #[test] + fn test_full_name_for_constant_path_target() { + let result = parse(b"Foo::Bar, Baz = [1, 2]"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let multi_write = node.as_multi_write_node().unwrap(); + let target = multi_write.lefts().iter().next().unwrap(); + let constant_path = target.as_constant_path_target_node().unwrap(); + + assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); + assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + } + + #[test] + fn test_full_name_for_constant_path_target_with_stovetop() { + let result = parse(b"::Foo, Bar = [1, 2]"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let multi_write = node.as_multi_write_node().unwrap(); + let target = multi_write.lefts().iter().next().unwrap(); + let constant_path = target.as_constant_path_target_node().unwrap(); + + assert_eq!(constant_path.full_name_parts().unwrap(), vec!["", "Foo"]); + assert_eq!(constant_path.full_name().unwrap(), "::Foo"); + } + + #[test] + fn test_full_name_for_constant_path_target_with_self() { + let result = parse(b"self::Foo, Bar = [1, 2]"); + let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); + let multi_write = node.as_multi_write_node().unwrap(); + let target = multi_write.lefts().iter().next().unwrap(); + let constant_path = target.as_constant_path_target_node().unwrap(); + + assert_eq!(constant_path.full_name().unwrap_err(), ConstantPathError::DynamicParts); + } +} From ec991aec372e52be20f52c4b501c3d26fcf347e1 Mon Sep 17 00:00:00 2001 From: Seong Yong-ju Date: Thu, 19 Feb 2026 01:17:56 +0900 Subject: [PATCH 2/2] fix: return raw bytes instead of Cow in node_ext methods Address review feedback: stop assuming UTF-8 encoding since the parser supports ~90 encodings. Return &[u8] / Vec instead of Cow / String to avoid silently corrupting non-UTF-8 data via from_utf8_lossy. Co-Authored-By: Claude Opus 4.6 --- rust/ruby-prism/src/node_ext.rs | 111 ++++++++++++++++++-------------- 1 file changed, 63 insertions(+), 48 deletions(-) diff --git a/rust/ruby-prism/src/node_ext.rs b/rust/ruby-prism/src/node_ext.rs index 66f3074043..3088176cd9 100644 --- a/rust/ruby-prism/src/node_ext.rs +++ b/rust/ruby-prism/src/node_ext.rs @@ -3,7 +3,6 @@ //! This module provides convenience methods on AST nodes that aren't generated //! from the config, mirroring Ruby's `node_ext.rb`. -use std::borrow::Cow; use std::fmt; use crate::{ConstantPathNode, ConstantPathTargetNode, ConstantReadNode, ConstantTargetNode, ConstantWriteNode, Node}; @@ -50,11 +49,11 @@ impl<'pr> ConstantReadNode<'pr> { /// let stmt = result.node().as_program_node().unwrap() /// .statements().body().iter().next().unwrap(); /// let constant = stmt.as_constant_read_node().unwrap(); - /// assert_eq!(constant.full_name_parts(), vec!["Foo"]); + /// assert_eq!(constant.full_name_parts(), vec![b"Foo".as_slice()]); /// ``` #[must_use] - pub fn full_name_parts(&self) -> Vec> { - vec![String::from_utf8_lossy(self.name().as_slice())] + pub fn full_name_parts(&self) -> Vec<&'pr [u8]> { + vec![self.name().as_slice()] } /// Returns the full name of this constant. @@ -67,11 +66,11 @@ impl<'pr> ConstantReadNode<'pr> { /// let stmt = result.node().as_program_node().unwrap() /// .statements().body().iter().next().unwrap(); /// let constant = stmt.as_constant_read_node().unwrap(); - /// assert_eq!(constant.full_name(), "Foo"); + /// assert_eq!(constant.full_name(), b"Foo"); /// ``` #[must_use] - pub fn full_name(&self) -> Cow<'pr, str> { - String::from_utf8_lossy(self.name().as_slice()) + pub fn full_name(&self) -> &'pr [u8] { + self.name().as_slice() } } @@ -86,11 +85,11 @@ impl<'pr> ConstantWriteNode<'pr> { /// let stmt = result.node().as_program_node().unwrap() /// .statements().body().iter().next().unwrap(); /// let constant = stmt.as_constant_write_node().unwrap(); - /// assert_eq!(constant.full_name_parts(), vec!["Foo"]); + /// assert_eq!(constant.full_name_parts(), vec![b"Foo".as_slice()]); /// ``` #[must_use] - pub fn full_name_parts(&self) -> Vec> { - vec![String::from_utf8_lossy(self.name().as_slice())] + pub fn full_name_parts(&self) -> Vec<&'pr [u8]> { + vec![self.name().as_slice()] } /// Returns the full name of this constant. @@ -103,11 +102,11 @@ impl<'pr> ConstantWriteNode<'pr> { /// let stmt = result.node().as_program_node().unwrap() /// .statements().body().iter().next().unwrap(); /// let constant = stmt.as_constant_write_node().unwrap(); - /// assert_eq!(constant.full_name(), "Foo"); + /// assert_eq!(constant.full_name(), b"Foo"); /// ``` #[must_use] - pub fn full_name(&self) -> Cow<'pr, str> { - String::from_utf8_lossy(self.name().as_slice()) + pub fn full_name(&self) -> &'pr [u8] { + self.name().as_slice() } } @@ -124,11 +123,11 @@ impl<'pr> ConstantTargetNode<'pr> { /// let target = stmt.as_multi_write_node().unwrap() /// .lefts().iter().next().unwrap(); /// let constant = target.as_constant_target_node().unwrap(); - /// assert_eq!(constant.full_name_parts(), vec!["Foo"]); + /// assert_eq!(constant.full_name_parts(), vec![b"Foo".as_slice()]); /// ``` #[must_use] - pub fn full_name_parts(&self) -> Vec> { - vec![String::from_utf8_lossy(self.name().as_slice())] + pub fn full_name_parts(&self) -> Vec<&'pr [u8]> { + vec![self.name().as_slice()] } /// Returns the full name of this constant. @@ -143,11 +142,11 @@ impl<'pr> ConstantTargetNode<'pr> { /// let target = stmt.as_multi_write_node().unwrap() /// .lefts().iter().next().unwrap(); /// let constant = target.as_constant_target_node().unwrap(); - /// assert_eq!(constant.full_name(), "Foo"); + /// assert_eq!(constant.full_name(), b"Foo"); /// ``` #[must_use] - pub fn full_name(&self) -> Cow<'pr, str> { - String::from_utf8_lossy(self.name().as_slice()) + pub fn full_name(&self) -> &'pr [u8] { + self.name().as_slice() } } @@ -162,7 +161,7 @@ impl<'pr> ConstantPathNode<'pr> { /// let stmt = result.node().as_program_node().unwrap() /// .statements().body().iter().next().unwrap(); /// let constant_path = stmt.as_constant_path_node().unwrap(); - /// assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); + /// assert_eq!(constant_path.full_name_parts().unwrap(), vec![b"Foo".as_slice(), b"Bar".as_slice()]); /// ``` /// /// # Errors @@ -170,17 +169,17 @@ impl<'pr> ConstantPathNode<'pr> { /// Returns [`ConstantPathError::DynamicParts`] if the path contains /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path /// contains missing nodes. - pub fn full_name_parts(&self) -> Result>, ConstantPathError> { + pub fn full_name_parts(&self) -> Result, ConstantPathError> { let mut parts = Vec::new(); let mut current: Option> = Some(self.as_node()); while let Some(ref node) = current { if let Some(path_node) = node.as_constant_path_node() { let name = path_node.name().ok_or(ConstantPathError::MissingNodes)?; - parts.push(String::from_utf8_lossy(name.as_slice())); + parts.push(name.as_slice()); current = path_node.parent(); } else if let Some(read_node) = node.as_constant_read_node() { - parts.push(String::from_utf8_lossy(read_node.name().as_slice())); + parts.push(read_node.name().as_slice()); current = None; } else { return Err(ConstantPathError::DynamicParts); @@ -190,7 +189,7 @@ impl<'pr> ConstantPathNode<'pr> { parts.reverse(); if self.is_stovetop() { - parts.insert(0, Cow::Borrowed("")); + parts.insert(0, b"".as_slice()); } Ok(parts) @@ -206,7 +205,7 @@ impl<'pr> ConstantPathNode<'pr> { /// let stmt = result.node().as_program_node().unwrap() /// .statements().body().iter().next().unwrap(); /// let constant_path = stmt.as_constant_path_node().unwrap(); - /// assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + /// assert_eq!(constant_path.full_name().unwrap(), b"Foo::Bar"); /// ``` /// /// # Errors @@ -214,8 +213,16 @@ impl<'pr> ConstantPathNode<'pr> { /// Returns [`ConstantPathError::DynamicParts`] if the path contains /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path /// contains missing nodes. - pub fn full_name(&self) -> Result { - Ok(self.full_name_parts()?.join("::")) + pub fn full_name(&self) -> Result, ConstantPathError> { + let parts = self.full_name_parts()?; + let mut result = Vec::new(); + for (i, part) in parts.iter().enumerate() { + if i > 0 { + result.extend_from_slice(b"::"); + } + result.extend_from_slice(part); + } + Ok(result) } fn is_stovetop(&self) -> bool { @@ -246,7 +253,7 @@ impl<'pr> ConstantPathTargetNode<'pr> { /// let target = stmt.as_multi_write_node().unwrap() /// .lefts().iter().next().unwrap(); /// let constant_path = target.as_constant_path_target_node().unwrap(); - /// assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); + /// assert_eq!(constant_path.full_name_parts().unwrap(), vec![b"Foo".as_slice(), b"Bar".as_slice()]); /// ``` /// /// # Errors @@ -254,7 +261,7 @@ impl<'pr> ConstantPathTargetNode<'pr> { /// Returns [`ConstantPathError::DynamicParts`] if the path contains /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path /// contains missing nodes. - pub fn full_name_parts(&self) -> Result>, ConstantPathError> { + pub fn full_name_parts(&self) -> Result, ConstantPathError> { let name = self.name().ok_or(ConstantPathError::MissingNodes)?; let mut parts = if let Some(parent) = self.parent() { @@ -266,10 +273,10 @@ impl<'pr> ConstantPathTargetNode<'pr> { return Err(ConstantPathError::DynamicParts); } } else { - vec![Cow::Borrowed("")] + vec![b"".as_slice()] }; - parts.push(String::from_utf8_lossy(name.as_slice())); + parts.push(name.as_slice()); Ok(parts) } @@ -285,7 +292,7 @@ impl<'pr> ConstantPathTargetNode<'pr> { /// let target = stmt.as_multi_write_node().unwrap() /// .lefts().iter().next().unwrap(); /// let constant_path = target.as_constant_path_target_node().unwrap(); - /// assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + /// assert_eq!(constant_path.full_name().unwrap(), b"Foo::Bar"); /// ``` /// /// # Errors @@ -293,8 +300,16 @@ impl<'pr> ConstantPathTargetNode<'pr> { /// Returns [`ConstantPathError::DynamicParts`] if the path contains /// dynamic parts, or [`ConstantPathError::MissingNodes`] if the path /// contains missing nodes. - pub fn full_name(&self) -> Result { - Ok(self.full_name_parts()?.join("::")) + pub fn full_name(&self) -> Result, ConstantPathError> { + let parts = self.full_name_parts()?; + let mut result = Vec::new(); + for (i, part) in parts.iter().enumerate() { + if i > 0 { + result.extend_from_slice(b"::"); + } + result.extend_from_slice(part); + } + Ok(result) } } @@ -309,8 +324,8 @@ mod tests { let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); let constant = node.as_constant_read_node().unwrap(); - assert_eq!(constant.full_name_parts(), vec!["Foo"]); - assert_eq!(constant.full_name(), "Foo"); + assert_eq!(constant.full_name_parts(), vec![b"Foo".as_slice()]); + assert_eq!(constant.full_name(), b"Foo"); } #[test] @@ -319,8 +334,8 @@ mod tests { let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); let constant = node.as_constant_write_node().unwrap(); - assert_eq!(constant.full_name_parts(), vec!["Foo"]); - assert_eq!(constant.full_name(), "Foo"); + assert_eq!(constant.full_name_parts(), vec![b"Foo".as_slice()]); + assert_eq!(constant.full_name(), b"Foo"); } #[test] @@ -331,8 +346,8 @@ mod tests { let target = multi_write.lefts().iter().next().unwrap(); let constant = target.as_constant_target_node().unwrap(); - assert_eq!(constant.full_name_parts(), vec!["Foo"]); - assert_eq!(constant.full_name(), "Foo"); + assert_eq!(constant.full_name_parts(), vec![b"Foo".as_slice()]); + assert_eq!(constant.full_name(), b"Foo"); } #[test] @@ -341,8 +356,8 @@ mod tests { let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); let constant_path = node.as_constant_path_node().unwrap(); - assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); - assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + assert_eq!(constant_path.full_name_parts().unwrap(), vec![b"Foo".as_slice(), b"Bar".as_slice()]); + assert_eq!(constant_path.full_name().unwrap(), b"Foo::Bar"); } #[test] @@ -351,8 +366,8 @@ mod tests { let node = result.node().as_program_node().unwrap().statements().body().iter().next().unwrap(); let constant_path = node.as_constant_path_node().unwrap(); - assert_eq!(constant_path.full_name_parts().unwrap(), vec!["", "Foo", "Bar"]); - assert_eq!(constant_path.full_name().unwrap(), "::Foo::Bar"); + assert_eq!(constant_path.full_name_parts().unwrap(), vec![b"".as_slice(), b"Foo".as_slice(), b"Bar".as_slice()]); + assert_eq!(constant_path.full_name().unwrap(), b"::Foo::Bar"); } #[test] @@ -400,8 +415,8 @@ foo:: let target = multi_write.lefts().iter().next().unwrap(); let constant_path = target.as_constant_path_target_node().unwrap(); - assert_eq!(constant_path.full_name_parts().unwrap(), vec!["Foo", "Bar"]); - assert_eq!(constant_path.full_name().unwrap(), "Foo::Bar"); + assert_eq!(constant_path.full_name_parts().unwrap(), vec![b"Foo".as_slice(), b"Bar".as_slice()]); + assert_eq!(constant_path.full_name().unwrap(), b"Foo::Bar"); } #[test] @@ -412,8 +427,8 @@ foo:: let target = multi_write.lefts().iter().next().unwrap(); let constant_path = target.as_constant_path_target_node().unwrap(); - assert_eq!(constant_path.full_name_parts().unwrap(), vec!["", "Foo"]); - assert_eq!(constant_path.full_name().unwrap(), "::Foo"); + assert_eq!(constant_path.full_name_parts().unwrap(), vec![b"".as_slice(), b"Foo".as_slice()]); + assert_eq!(constant_path.full_name().unwrap(), b"::Foo"); } #[test]