diff --git a/crates/hcl-edit/src/encode/expr.rs b/crates/hcl-edit/src/encode/expr.rs index 6a4e47f2..7226bc75 100644 --- a/crates/hcl-edit/src/encode/expr.rs +++ b/crates/hcl-edit/src/encode/expr.rs @@ -3,9 +3,9 @@ use super::{ LEADING_SPACE_DECOR, NO_DECOR, TRAILING_SPACE_DECOR, }; use crate::expr::{ - Array, BinaryOp, Conditional, Expression, ForCond, ForExpr, ForIntro, FuncArgs, FuncCall, Null, - Object, ObjectKey, ObjectValue, ObjectValueAssignment, ObjectValueTerminator, Parenthesis, - Splat, Traversal, TraversalOperator, UnaryOp, + Array, BinaryOp, Conditional, Expression, ForCond, ForExpr, ForIntro, FuncArgs, FuncCall, + FuncName, Null, Object, ObjectKey, ObjectValue, ObjectValueAssignment, ObjectValueTerminator, + Parenthesis, Splat, Traversal, TraversalOperator, UnaryOp, }; use std::fmt::{self, Write}; @@ -184,11 +184,21 @@ impl Encode for Conditional { impl Encode for FuncCall { fn encode(&self, buf: &mut EncodeState) -> fmt::Result { - self.ident.encode_decorated(buf, NO_DECOR)?; + self.name.encode(buf)?; self.args.encode_decorated(buf, NO_DECOR) } } +impl Encode for FuncName { + fn encode(&self, buf: &mut EncodeState) -> fmt::Result { + for component in &self.namespace { + component.encode_decorated(buf, NO_DECOR)?; + buf.write_str("::")?; + } + self.name.encode_decorated(buf, NO_DECOR) + } +} + impl Encode for FuncArgs { fn encode(&self, buf: &mut EncodeState) -> fmt::Result { buf.write_char('(')?; diff --git a/crates/hcl-edit/src/expr/func_call.rs b/crates/hcl-edit/src/expr/func_call.rs index be52ee7f..c4905df5 100644 --- a/crates/hcl-edit/src/expr/func_call.rs +++ b/crates/hcl-edit/src/expr/func_call.rs @@ -2,11 +2,63 @@ use crate::expr::{Expression, IntoIter, Iter, IterMut}; use crate::{Decor, Decorate, Decorated, Ident, RawString}; use std::ops::Range; +/// Type representing a (potentially namespaced) function name. +#[derive(Debug, Clone, PartialEq, Eq)] +pub struct FuncName { + /// The function's namespace components, if any. + pub namespace: Vec>, + /// The function name. + pub name: Decorated, +} + +impl FuncName { + /// Create a new `FuncName` from a name identifier. + pub fn new(name: impl Into>) -> FuncName { + FuncName { + namespace: Vec::new(), + name: name.into(), + } + } + + /// Sets the function namespace from an iterator of namespace parts. + pub fn set_namespace(&mut self, namespace: I) + where + I: IntoIterator, + I::Item: Into>, + { + self.namespace = namespace.into_iter().map(Into::into).collect(); + } + + /// Returns `true` if the function name is namespaced. + pub fn is_namespaced(&self) -> bool { + !self.namespace.is_empty() + } + + pub(crate) fn despan(&mut self, input: &str) { + for scope in &mut self.namespace { + scope.decor_mut().despan(input); + } + self.name.decor_mut().despan(input); + } +} + +impl From for FuncName +where + T: Into>, +{ + fn from(name: T) -> Self { + FuncName { + namespace: Vec::new(), + name: name.into(), + } + } +} + /// Type representing a function call. #[derive(Debug, Clone, Eq)] pub struct FuncCall { - /// The function identifier (or name). - pub ident: Decorated, + /// The function name. + pub name: FuncName, /// The arguments between the function call's `(` and `)` argument delimiters. pub args: FuncArgs, @@ -16,9 +68,9 @@ pub struct FuncCall { impl FuncCall { /// Create a new `FuncCall` from an identifier and arguments. - pub fn new(ident: impl Into>, args: FuncArgs) -> FuncCall { + pub fn new(name: impl Into, args: FuncArgs) -> FuncCall { FuncCall { - ident: ident.into(), + name: name.into(), args, decor: Decor::default(), span: None, @@ -27,14 +79,14 @@ impl FuncCall { pub(crate) fn despan(&mut self, input: &str) { self.decor.despan(input); - self.ident.decor_mut().despan(input); + self.name.despan(input); self.args.despan(input); } } impl PartialEq for FuncCall { fn eq(&self, other: &Self) -> bool { - self.ident == other.ident && self.args == other.args + self.name == other.name && self.args == other.args } } diff --git a/crates/hcl-edit/src/expr/mod.rs b/crates/hcl-edit/src/expr/mod.rs index 836a95f8..e3afe902 100644 --- a/crates/hcl-edit/src/expr/mod.rs +++ b/crates/hcl-edit/src/expr/mod.rs @@ -11,7 +11,7 @@ mod traversal; pub use self::array::{Array, IntoIter, Iter, IterMut}; pub use self::conditional::Conditional; pub use self::for_expr::{ForCond, ForExpr, ForIntro}; -pub use self::func_call::{FuncArgs, FuncCall}; +pub use self::func_call::{FuncArgs, FuncCall, FuncName}; pub use self::object::{ Object, ObjectIntoIter, ObjectIter, ObjectIterMut, ObjectKey, ObjectKeyMut, ObjectValue, ObjectValueAssignment, ObjectValueTerminator, diff --git a/crates/hcl-edit/src/parser/expr.rs b/crates/hcl-edit/src/parser/expr.rs index f825d768..918b6bbd 100644 --- a/crates/hcl-edit/src/parser/expr.rs +++ b/crates/hcl-edit/src/parser/expr.rs @@ -11,7 +11,7 @@ use super::template::{heredoc_template, string_template}; use super::trivia::{line_comment, sp, ws}; use crate::expr::{ - Array, BinaryOperator, Expression, ForCond, ForExpr, ForIntro, FuncArgs, FuncCall, Null, + Array, BinaryOperator, Expression, ForCond, ForExpr, ForIntro, FuncArgs, FuncCall, FuncName, Object, ObjectKey, ObjectValue, ObjectValueAssignment, ObjectValueTerminator, Parenthesis, Splat, TraversalOperator, UnaryOperator, }; @@ -642,26 +642,75 @@ fn identlike<'i, 's>( state: &'s RefCell, ) -> impl Parser, (), ContextError> + 's { move |input: &mut Input<'i>| { - (str_ident.with_span(), opt(prefix_decorated(ws, func_args))) - .map(|((ident, span), func_args)| { - let expr = match func_args { - Some(func_args) => { - let mut ident = Decorated::new(Ident::new_unchecked(ident)); - ident.set_span(span); - let func_call = FuncCall::new(ident, func_args); - Expression::FuncCall(Box::new(func_call)) - } - None => match ident { - "null" => Expression::Null(Null.into()), - "true" => Expression::Bool(true.into()), - "false" => Expression::Bool(false.into()), - var => Expression::Variable(Ident::new_unchecked(var).into()), - }, - }; - - state.borrow_mut().on_expr_term(expr); - }) - .parse_next(input) + let (ident, span) = str_ident.with_span().parse_next(input)?; + + let checkpoint = input.checkpoint(); + + // Parse the next whitespace sequence and only add it as decor suffix to the identifier if + // we actually encounter a function call. + let suffix = ws_or_sp(state).span().parse_next(input)?; + + let expr = if let Ok(peeked @ (b"::" | [b'(', _])) = + peek(take::<_, _, ContextError>(2usize)).parse_next(input) + { + // This is a function call: parsed identifier starts a function namespace, or function + // arguments follow. + let mut ident = Decorated::new(Ident::new_unchecked(ident)); + ident.decor_mut().set_suffix(RawString::from_span(suffix)); + ident.set_span(span); + + let func_name = if peeked == b"::" { + // Consume the remaining namespace components and function name. + let mut namespace = func_namespace_components(state).parse_next(input)?; + + // We already parsed the first namespace element before and the function name is + // now part of the remaining namspace components, so we have to correct this. + let name = namespace.pop().unwrap(); + namespace.insert(0, ident); + + FuncName { namespace, name } + } else { + FuncName::from(ident) + }; + + let func_args = func_args.parse_next(input)?; + let func_call = FuncCall::new(func_name, func_args); + Expression::FuncCall(Box::new(func_call)) + } else { + // This is not a function call: identifier is either keyword or variable name. + input.reset(&checkpoint); + + match ident { + "null" => Expression::null(), + "true" => Expression::from(true), + "false" => Expression::from(false), + var => Expression::from(Ident::new_unchecked(var)), + } + }; + + state.borrow_mut().on_expr_term(expr); + Ok(()) + } +} + +fn func_namespace_components<'i, 's>( + state: &'s RefCell, +) -> impl Parser, Vec>, ContextError> + 's { + move |input: &mut Input<'i>| { + repeat( + 1.., + preceded( + b"::", + decorated( + ws_or_sp(state), + cut_err(ident).context(StrContext::Expected(StrContextValue::Description( + "identifier", + ))), + ws_or_sp(state), + ), + ), + ) + .parse_next(input) } } @@ -685,7 +734,7 @@ fn func_args(input: &mut Input) -> PResult { }; delimited( - b'(', + cut_char('('), (opt((args, opt(trailer))), raw_string(ws)).map(|(args, trailing)| { let mut args = match args { Some((args, Some(trailer))) => { @@ -705,7 +754,9 @@ fn func_args(input: &mut Input) -> PResult { args.set_trailing(trailing); args }), - cut_char(')'), + cut_char(')').context(StrContext::Expected(StrContextValue::Description( + "expression", + ))), ) .parse_next(input) } diff --git a/crates/hcl-edit/src/parser/tests.rs b/crates/hcl-edit/src/parser/tests.rs index 42c466df..78886131 100644 --- a/crates/hcl-edit/src/parser/tests.rs +++ b/crates/hcl-edit/src/parser/tests.rs @@ -57,6 +57,8 @@ fn roundtrip_expr() { r#""foo ${bar} $${baz}, %{if cond ~} qux %{~ endif}""#, r#""${var.l ? "us-east-1." : ""}""#, "element(concat(aws_kms_key.key-one.*.arn, aws_kms_key.key-two.*.arn), 0)", + "foo::bar(baz...)", + "foo :: bar ()", "foo(bar...)", "foo(bar,)", "foo( )", diff --git a/crates/hcl-edit/src/visit.rs b/crates/hcl-edit/src/visit.rs index 70398e09..84579b04 100644 --- a/crates/hcl-edit/src/visit.rs +++ b/crates/hcl-edit/src/visit.rs @@ -62,7 +62,7 @@ use crate::expr::{ Array, BinaryOp, BinaryOperator, Conditional, Expression, ForCond, ForExpr, ForIntro, FuncArgs, - FuncCall, Null, Object, ObjectKey, ObjectValue, Parenthesis, Splat, Traversal, + FuncCall, FuncName, Null, Object, ObjectKey, ObjectValue, Parenthesis, Splat, Traversal, TraversalOperator, UnaryOp, UnaryOperator, }; use crate::structure::{Attribute, Block, BlockLabel, Body, Structure}; @@ -130,6 +130,7 @@ pub trait Visit { visit_traversal => Traversal, visit_traversal_operator => TraversalOperator, visit_func_call => FuncCall, + visit_func_name => FuncName, visit_func_args => FuncArgs, visit_for_expr => ForExpr, visit_for_intro => ForIntro, @@ -328,10 +329,20 @@ pub fn visit_func_call(v: &mut V, node: &FuncCall) where V: Visit + ?Sized, { - v.visit_ident(&node.ident); + v.visit_func_name(&node.name); v.visit_func_args(&node.args); } +pub fn visit_func_name(v: &mut V, node: &FuncName) +where + V: Visit + ?Sized, +{ + for component in &node.namespace { + v.visit_ident(component); + } + v.visit_ident(&node.name); +} + pub fn visit_func_args(v: &mut V, node: &FuncArgs) where V: Visit + ?Sized, diff --git a/crates/hcl-edit/src/visit_mut.rs b/crates/hcl-edit/src/visit_mut.rs index 314e460f..ab61427e 100644 --- a/crates/hcl-edit/src/visit_mut.rs +++ b/crates/hcl-edit/src/visit_mut.rs @@ -79,7 +79,7 @@ use crate::expr::{ Array, BinaryOp, BinaryOperator, Conditional, Expression, ForCond, ForExpr, ForIntro, FuncArgs, - FuncCall, Null, Object, ObjectKeyMut, ObjectValue, Parenthesis, Splat, Traversal, + FuncCall, FuncName, Null, Object, ObjectKeyMut, ObjectValue, Parenthesis, Splat, Traversal, TraversalOperator, UnaryOp, UnaryOperator, }; use crate::structure::{AttributeMut, Block, BlockLabel, Body, StructureMut}; @@ -144,6 +144,7 @@ pub trait VisitMut { visit_traversal_mut => Traversal, visit_traversal_operator_mut => TraversalOperator, visit_func_call_mut => FuncCall, + visit_func_name_mut => FuncName, visit_func_args_mut => FuncArgs, visit_for_expr_mut => ForExpr, visit_for_intro_mut => ForIntro, @@ -344,10 +345,20 @@ pub fn visit_func_call_mut(v: &mut V, node: &mut FuncCall) where V: VisitMut + ?Sized, { - v.visit_ident_mut(&mut node.ident); + v.visit_func_name_mut(&mut node.name); v.visit_func_args_mut(&mut node.args); } +pub fn visit_func_name_mut(v: &mut V, node: &mut FuncName) +where + V: VisitMut + ?Sized, +{ + for component in &mut node.namespace { + v.visit_ident_mut(component); + } + v.visit_ident_mut(&mut node.name); +} + pub fn visit_func_args_mut(v: &mut V, node: &mut FuncArgs) where V: VisitMut + ?Sized, diff --git a/crates/hcl-edit/tests/parse.rs b/crates/hcl-edit/tests/parse.rs index ff2fc47f..fe4f3005 100644 --- a/crates/hcl-edit/tests/parse.rs +++ b/crates/hcl-edit/tests/parse.rs @@ -117,4 +117,37 @@ fn invalid_exprs() { | = invalid object item; expected `}`, `,` or newline"#} ); + + assert_error!( + "ident = foo::", + indoc! {r#" + --> HCL parse error in line 1, column 14 + | + 1 | ident = foo:: + | ^--- + | + = expected identifier"#} + ); + + assert_error!( + "ident = foo::bar", + indoc! {r#" + --> HCL parse error in line 1, column 17 + | + 1 | ident = foo::bar + | ^--- + | + = expected `(`"#} + ); + + assert_error!( + "ident = foo( ", + indoc! {r#" + --> HCL parse error in line 1, column 14 + | + 1 | ident = foo( + | ^--- + | + = expected `)` or expression"#} + ); } diff --git a/crates/hcl-rs/src/eval/error.rs b/crates/hcl-rs/src/eval/error.rs index d0219ea5..ed39d262 100644 --- a/crates/hcl-rs/src/eval/error.rs +++ b/crates/hcl-rs/src/eval/error.rs @@ -196,7 +196,7 @@ pub enum ErrorKind { /// An expression contained an undefined variable. UndefinedVar(Identifier), /// An expression contained a call to an undefined function. - UndefinedFunc(Identifier), + UndefinedFunc(FuncName), /// A different type of value was expected. Unexpected(Value, &'static str), /// An expression tried to access a non-existing array index. @@ -210,7 +210,7 @@ pub enum ErrorKind { /// A `for` expression attempted to set the same object key twice. KeyExists(String), /// A function call in an expression returned an error. - FuncCall(Identifier, String), + FuncCall(FuncName, String), /// It was attempted to evaluate a raw expression. #[deprecated( since = "0.16.3", @@ -244,8 +244,8 @@ impl fmt::Display for ErrorKind { ErrorKind::UndefinedVar(ident) => { write!(f, "undefined variable `{ident}`") } - ErrorKind::UndefinedFunc(ident) => { - write!(f, "undefined function `{ident}`") + ErrorKind::UndefinedFunc(func_name) => { + write!(f, "undefined function `{func_name}`") } ErrorKind::Unexpected(value, expected) => { write!(f, "unexpected value `{value}`, expected {expected}") diff --git a/crates/hcl-rs/src/eval/mod.rs b/crates/hcl-rs/src/eval/mod.rs index f5b12fca..e0126441 100644 --- a/crates/hcl-rs/src/eval/mod.rs +++ b/crates/hcl-rs/src/eval/mod.rs @@ -229,8 +229,8 @@ pub use self::func::{ Func, FuncArgs, FuncDef, FuncDefBuilder, ParamType, PositionalArgs, VariadicArgs, }; use crate::expr::{ - BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, Object, ObjectKey, - Operation, TemplateExpr, Traversal, TraversalOperator, UnaryOp, UnaryOperator, + BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, FuncName, Object, + ObjectKey, Operation, TemplateExpr, Traversal, TraversalOperator, UnaryOp, UnaryOperator, }; use crate::parser; use crate::structure::{Attribute, Block, Body, Structure}; @@ -239,6 +239,7 @@ use crate::template::{ }; use crate::{Identifier, Map, Result, Value}; use serde::{de, ser}; +use vecmap::VecMap; mod private { pub trait Sealed {} @@ -296,7 +297,7 @@ pub trait Evaluate: private::Sealed { #[derive(Debug, Clone)] pub struct Context<'a> { vars: Map, - funcs: Map, + funcs: VecMap, parent: Option<&'a Context<'a>>, expr: Option<&'a Expression>, } @@ -305,7 +306,7 @@ impl Default for Context<'_> { fn default() -> Self { Context { vars: Map::new(), - funcs: Map::new(), + funcs: VecMap::new(), parent: None, expr: None, } @@ -378,7 +379,7 @@ impl<'a> Context<'a> { /// ``` pub fn declare_func(&mut self, name: I, func: FuncDef) where - I: Into, + I: Into, { self.funcs.insert(name.into(), func); } @@ -396,7 +397,7 @@ impl<'a> Context<'a> { /// /// When the function is declared in multiple parent scopes, the innermost definition is /// returned. - fn lookup_func(&self, name: &Identifier) -> EvalResult<&FuncDef> { + fn lookup_func(&self, name: &FuncName) -> EvalResult<&FuncDef> { self.func(name) .ok_or_else(|| self.error(ErrorKind::UndefinedFunc(name.clone()))) } @@ -420,7 +421,7 @@ impl<'a> Context<'a> { .or_else(|| self.parent.and_then(|parent| parent.var(name))) } - fn func(&self, name: &Identifier) -> Option<&FuncDef> { + fn func(&self, name: &FuncName) -> Option<&FuncDef> { self.funcs .get(name) .or_else(|| self.parent.and_then(|parent| parent.func(name))) diff --git a/crates/hcl-rs/src/expr/de.rs b/crates/hcl-rs/src/expr/de.rs index 1309009c..9d6423a0 100644 --- a/crates/hcl-rs/src/expr/de.rs +++ b/crates/hcl-rs/src/expr/de.rs @@ -729,7 +729,7 @@ impl<'de> de::VariantAccess<'de> for TraversalOperator { } pub struct FuncCallAccess { - name: Option, + name: Option, args: Option>, expand_final: Option, } @@ -779,6 +779,50 @@ impl<'de> de::MapAccess<'de> for FuncCallAccess { } } +pub struct FuncNameAccess { + namespace: Option>, + name: Option, +} + +impl FuncNameAccess { + fn new(func_name: FuncName) -> Self { + FuncNameAccess { + namespace: Some(func_name.namespace), + name: Some(func_name.name), + } + } +} + +impl<'de> de::MapAccess<'de> for FuncNameAccess { + type Error = Error; + + fn next_key_seed(&mut self, seed: K) -> Result, Self::Error> + where + K: de::DeserializeSeed<'de>, + { + if self.namespace.is_some() { + seed.deserialize("namespace".into_deserializer()).map(Some) + } else if self.name.is_some() { + seed.deserialize("name".into_deserializer()).map(Some) + } else { + Ok(None) + } + } + + fn next_value_seed(&mut self, seed: V) -> Result + where + V: de::DeserializeSeed<'de>, + { + if let Some(namespace) = self.namespace.take() { + seed.deserialize(namespace.into_deserializer()) + } else if let Some(name) = self.name.take() { + seed.deserialize(name.into_deserializer()) + } else { + Err(de::Error::custom("invalid HCL function name")) + } + } +} + #[allow(clippy::struct_field_names)] pub struct ConditionalAccess { cond_expr: Option, @@ -1251,6 +1295,7 @@ impl_into_map_access_deserializer! { Conditional => ConditionalAccess, ForExpr => ForExprAccess, FuncCall => FuncCallAccess, + FuncName => FuncNameAccess, Heredoc => HeredocAccess, Traversal => TraversalAccess, UnaryOp => UnaryOpAccess diff --git a/crates/hcl-rs/src/expr/edit.rs b/crates/hcl-rs/src/expr/edit.rs index b6f487cb..01603246 100644 --- a/crates/hcl-rs/src/expr/edit.rs +++ b/crates/hcl-rs/src/expr/edit.rs @@ -130,11 +130,29 @@ impl From for expr::ForExpr { } } +impl From for FuncName { + fn from(value: expr::FuncName) -> Self { + FuncName { + namespace: value.namespace.into_iter().map(Into::into).collect(), + name: value.name.into(), + } + } +} + +impl From for expr::FuncName { + fn from(value: FuncName) -> Self { + expr::FuncName { + namespace: value.namespace.into_iter().map(Into::into).collect(), + name: value.name.into(), + } + } +} + impl From for FuncCall { fn from(value: expr::FuncCall) -> Self { let expand_final = value.args.expand_final(); FuncCall { - name: value.ident.into(), + name: value.name.into(), args: value.args.into_iter().map(Into::into).collect(), expand_final, } diff --git a/crates/hcl-rs/src/expr/func_call.rs b/crates/hcl-rs/src/expr/func_call.rs index 345fc48a..3704a473 100644 --- a/crates/hcl-rs/src/expr/func_call.rs +++ b/crates/hcl-rs/src/expr/func_call.rs @@ -1,12 +1,68 @@ use super::Expression; +use crate::format; use crate::Identifier; use serde::Deserialize; +use std::fmt; + +/// Type representing a (potentially namespaced) function name. +#[derive(Deserialize, Debug, Clone, PartialEq, Eq)] +pub struct FuncName { + /// The function's namespace components, if any. + pub namespace: Vec, + /// The function name. + pub name: Identifier, +} + +impl FuncName { + /// Create a new `FuncName` from a name identifier. + pub fn new(name: impl Into) -> FuncName { + FuncName { + namespace: Vec::new(), + name: name.into(), + } + } + + /// Adds a namespace to the function name. + pub fn with_namespace(mut self, namespace: I) -> FuncName + where + I: IntoIterator, + I::Item: Into, + { + self.namespace = namespace.into_iter().map(Into::into).collect(); + self + } + + /// Returns `true` if the function name is namespaced. + pub fn is_namespaced(&self) -> bool { + !self.namespace.is_empty() + } +} + +impl From for FuncName +where + T: Into, +{ + fn from(name: T) -> Self { + FuncName { + namespace: Vec::new(), + name: name.into(), + } + } +} + +impl fmt::Display for FuncName { + fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { + // Formatting a `FuncName` as string cannot fail. + let formatted = format::to_string(self).expect("a FuncName failed to format unexpectedly"); + f.write_str(&formatted) + } +} /// Represents a function call expression with zero or more arguments. #[derive(Deserialize, Debug, Clone, PartialEq, Eq)] pub struct FuncCall { - /// The name of the function. - pub name: Identifier, + /// The function name. + pub name: FuncName, /// The function arguments. pub args: Vec, /// If `true`, the final argument should be an array which will expand to be one argument per @@ -18,7 +74,7 @@ impl FuncCall { /// Creates a new `FuncCall` for the function with given name. pub fn new(name: T) -> FuncCall where - T: Into, + T: Into, { FuncCall { name: name.into(), @@ -30,7 +86,7 @@ impl FuncCall { /// Creates a new `FuncCallBuilder` for the function with given name. pub fn builder(name: T) -> FuncCallBuilder where - T: Into, + T: Into, { FuncCallBuilder { f: FuncCall::new(name), diff --git a/crates/hcl-rs/src/expr/mod.rs b/crates/hcl-rs/src/expr/mod.rs index 52f3f3b9..2f80ab05 100644 --- a/crates/hcl-rs/src/expr/mod.rs +++ b/crates/hcl-rs/src/expr/mod.rs @@ -20,7 +20,7 @@ use self::ser::ExpressionSerializer; pub use self::{ conditional::Conditional, for_expr::ForExpr, - func_call::{FuncCall, FuncCallBuilder}, + func_call::{FuncCall, FuncCallBuilder, FuncName}, operation::{BinaryOp, BinaryOperator, Operation, UnaryOp, UnaryOperator}, template_expr::{Heredoc, HeredocStripMode, TemplateExpr}, traversal::{Traversal, TraversalBuilder, TraversalOperator}, diff --git a/crates/hcl-rs/src/format/impls.rs b/crates/hcl-rs/src/format/impls.rs index 50246c9b..ca7d76c0 100644 --- a/crates/hcl-rs/src/format/impls.rs +++ b/crates/hcl-rs/src/format/impls.rs @@ -2,8 +2,8 @@ use super::{private, Format, Formatter}; #[allow(deprecated)] use crate::expr::RawExpression; use crate::expr::{ - BinaryOp, Conditional, Expression, ForExpr, FuncCall, Heredoc, HeredocStripMode, ObjectKey, - Operation, TemplateExpr, Traversal, TraversalOperator, UnaryOp, Variable, + BinaryOp, Conditional, Expression, ForExpr, FuncCall, FuncName, Heredoc, HeredocStripMode, + ObjectKey, Operation, TemplateExpr, Traversal, TraversalOperator, UnaryOp, Variable, }; use crate::structure::{Attribute, Block, BlockLabel, Body, Structure}; use crate::template::{ @@ -351,6 +351,22 @@ impl Format for FuncCall { } } +impl private::Sealed for FuncName {} + +impl Format for FuncName { + fn format(&self, fmt: &mut Formatter) -> Result<()> + where + W: io::Write, + { + for component in &self.namespace { + component.format(fmt)?; + fmt.write_bytes(b"::")?; + } + + self.name.format(fmt) + } +} + impl private::Sealed for Conditional {} impl Format for Conditional { diff --git a/crates/hcl-rs/tests/format.rs b/crates/hcl-rs/tests/format.rs index 998e5013..9269e83d 100644 --- a/crates/hcl-rs/tests/format.rs +++ b/crates/hcl-rs/tests/format.rs @@ -2,7 +2,7 @@ mod common; use common::{assert_format, assert_format_builder}; use hcl::expr::{ - BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, Heredoc, + BinaryOp, BinaryOperator, Conditional, Expression, ForExpr, FuncCall, FuncName, Heredoc, HeredocStripMode, Traversal, TraversalOperator, Variable, }; use hcl::format::Formatter; @@ -106,6 +106,20 @@ fn func_call_expand_final() { ); } +#[test] +fn namespaced_func_call() { + assert_format( + FuncCall::builder(FuncName::new("func").with_namespace(["some", "namespaced"])) + .arg(1) + .arg(2) + .build(), + indoc! {r#" + some::namespaced::func(1, 2) + "#} + .trim_end(), + ); +} + #[test] fn for_list_expr() { assert_format(