Skip to content

Commit

Permalink
feat!: add support for namespaced functions (#343)
Browse files Browse the repository at this point in the history
Closes #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.
  • Loading branch information
martinohmann authored May 16, 2024
1 parent 559c196 commit 9a46a4c
Show file tree
Hide file tree
Showing 16 changed files with 379 additions and 59 deletions.
18 changes: 14 additions & 4 deletions crates/hcl-edit/src/encode/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};

Expand Down Expand Up @@ -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('(')?;
Expand Down
64 changes: 58 additions & 6 deletions crates/hcl-edit/src/expr/func_call.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,

Expand All @@ -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,
Expand All @@ -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
}
}

Expand Down
2 changes: 1 addition & 1 deletion crates/hcl-edit/src/expr/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
97 changes: 74 additions & 23 deletions crates/hcl-edit/src/parser/expr.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
};
Expand Down Expand Up @@ -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)
}
}

Expand All @@ -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))) => {
Expand All @@ -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)
}
2 changes: 2 additions & 0 deletions crates/hcl-edit/src/parser/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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( )",
Expand Down
15 changes: 13 additions & 2 deletions crates/hcl-edit/src/visit.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
15 changes: 13 additions & 2 deletions crates/hcl-edit/src/visit_mut.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand Down Expand Up @@ -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,
Expand Down
33 changes: 33 additions & 0 deletions crates/hcl-edit/tests/parse.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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"#}
);
}
Loading

0 comments on commit 9a46a4c

Please sign in to comment.