Repository: martinohmann/hcl-rs
Pull request #343 (2024-05-16 14:40:59)

feat!: add support for namespaced functions
Closes https://github.com/martinohmann/hcl-rs/issues/335

This adds support for namespaced HCL function calls. The PR includes updates to the HCL parser (`hcl-edit`), formatter (`hcl-rs`), evaluation context (`hcl-rs`), encoder (`hcl-edit`) and visitors (`hcl-edit`).

BREAKING CHANGE: The `ident` field of `hcl_edit::expr::FuncCall` was renamed to `name`. Its type changed from `Decorated<Ident>` to `FuncName`. The type of `hcl::expr::FuncCall`'s `name` field changed from `Identifier` to `FuncName`. Various other places in the codebase that previously used bare identifiers as type now use `FuncName` to accommodate the change.


Patch:
diff --git a/crates/hcl-edit/src/encode/expr.rs b/crates/hcl-edit/src/encode/expr.rs
index 6a4e47f..7226bc7 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 be52ee7..c4905df 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<Decorated<Ident>>,
+    /// The function name.
+    pub name: Decorated<Ident>,
+}
+
+impl FuncName {
+    /// Create a new `FuncName` from a name identifier.
+    pub fn new(name: impl Into<Decorated<Ident>>) -> FuncName {
+        FuncName {
+            namespace: Vec::new(),
+            name: name.into(),
+        }
+    }
+
+    /// Sets the function namespace from an iterator of namespace parts.
+    pub fn set_namespace<I>(&mut self, namespace: I)
+    where
+        I: IntoIterator,
+        I::Item: Into<Decorated<Ident>>,
+    {
+        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<T> From<T> for FuncName
+where
+    T: Into<Decorated<Ident>>,
+{
+    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<Ident>,
+    /// 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<Decorated<Ident>>, args: FuncArgs) -> FuncCall {
+    pub fn new(name: impl Into<FuncName>, 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 836a95f..e3afe90 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 f825d76..918b6bb 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<ExprParseState>,
 ) -> impl Parser<Input<'i>, (), 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<ExprParseState>,
+) -> impl Parser<Input<'i>, Vec<Decorated<Ident>>, 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<FuncArgs> {
     };
 
     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<FuncArgs> {
             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/visit.rs b/crates/hcl-edit/src/visit.rs
index 70398e0..84579b0 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>(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>(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>(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 314e460..ab61427 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>(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>(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>(v: &mut V, node: &mut FuncArgs)
 where
     V: VisitMut + ?Sized,
diff --git a/crates/hcl-rs/src/eval/error.rs b/crates/hcl-rs/src/eval/error.rs
index d0219ea..ed39d26 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 f5b12fc..e012644 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<Identifier, Value>,
-    funcs: Map<Identifier, FuncDef>,
+    funcs: VecMap<FuncName, FuncDef>,
     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<I>(&mut self, name: I, func: FuncDef)
     where
-        I: Into<Identifier>,
+        I: Into<FuncName>,
     {
         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 1309009..9d6423a 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<Identifier>,
+    name: Option<FuncName>,
     args: Option<Vec<Expression>>,
     expand_final: Option<bool>,
 }
@@ -779,6 +779,50 @@ impl<'de> de::MapAccess<'de> for FuncCallAccess {
     }
 }
 
+pub struct FuncNameAccess {
+    namespace: Option<Vec<Identifier>>,
+    name: Option<Identifier>,
+}
+
+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<K>(&mut self, seed: K) -> Result<Option<K::Value>, 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<V>(&mut self, seed: V) -> Result<V::Value, Self::Error>
+    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<Expression>,
@@ -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 b6f487c..0160324 100644
--- a/crates/hcl-rs/src/expr/edit.rs
+++ b/crates/hcl-rs/src/expr/edit.rs
@@ -130,11 +130,29 @@ impl From<ForExpr> for expr::ForExpr {
     }
 }
 
+impl From<expr::FuncName> for FuncName {
+    fn from(value: expr::FuncName) -> Self {
+        FuncName {
+            namespace: value.namespace.into_iter().map(Into::into).collect(),
+            name: value.name.into(),
+        }
+    }
+}
+
+impl From<FuncName> 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<expr::FuncCall> 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 345fc48..3704a47 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<Identifier>,
+    /// The function name.
+    pub name: Identifier,
+}
+
+impl FuncName {
+    /// Create a new `FuncName` from a name identifier.
+    pub fn new(name: impl Into<Identifier>) -> FuncName {
+        FuncName {
+            namespace: Vec::new(),
+            name: name.into(),
+        }
+    }
+
+    /// Adds a namespace to the function name.
+    pub fn with_namespace<I>(mut self, namespace: I) -> FuncName
+    where
+        I: IntoIterator,
+        I::Item: Into<Identifier>,
+    {
+        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<T> From<T> for FuncName
+where
+    T: Into<Identifier>,
+{
+    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<Expression>,
     /// 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<T>(name: T) -> FuncCall
     where
-        T: Into<Identifier>,
+        T: Into<FuncName>,
     {
         FuncCall {
             name: name.into(),
@@ -30,7 +86,7 @@ impl FuncCall {
     /// Creates a new `FuncCallBuilder` for the function with given name.
     pub fn builder<T>(name: T) -> FuncCallBuilder
     where
-        T: Into<Identifier>,
+        T: Into<FuncName>,
     {
         FuncCallBuilder {
             f: FuncCall::new(name),
diff --git a/crates/hcl-rs/src/expr/mod.rs b/crates/hcl-rs/src/expr/mod.rs
index 52f3f3b..2f80ab0 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 50246c9..ca7d76c 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<W>(&self, fmt: &mut Formatter<W>) -> 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 {


Test patch:
diff --git a/crates/hcl-edit/src/parser/tests.rs b/crates/hcl-edit/src/parser/tests.rs
index 42c466d..7888613 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/tests/parse.rs b/crates/hcl-edit/tests/parse.rs
index ff2fc47..fe4f300 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/tests/format.rs b/crates/hcl-rs/tests/format.rs
index 998e501..9269e83 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(