Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat!: add support for namespaced functions #343

Merged
merged 8 commits into from
May 16, 2024
Merged
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
Loading