use std::sync::Arc;
use serde::{Deserialize, Serialize};
use typed_builder::TypedBuilder;
use crate::ir::{Address, Span, Value};
pub type AttributeMap = Vec<(Arc<str>, Expression)>;
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum BinaryOp {
Add,
Sub,
Mul,
Div,
Mod,
Eq,
Ne,
Lt,
Le,
Gt,
Ge,
And,
Or,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "kebab-case")]
#[non_exhaustive]
pub enum UnaryOp {
Neg,
Not,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(rename_all = "PascalCase")]
#[non_exhaustive]
pub enum SymbolKind {
Var,
Local,
Resource,
Data,
Module,
Path,
Iteration,
Terraform,
TerragruntDependency,
Other,
}
#[derive(Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct Symbolic {
pub kind: SymbolKind,
pub source: Arc<str>,
#[builder(default)]
pub address_hint: Option<Address>,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct FuncCall {
pub name: Arc<str>,
pub args: Vec<Expression>,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
pub struct Conditional {
pub cond: Box<Expression>,
pub then_branch: Box<Expression>,
pub else_branch: Box<Expression>,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize, TypedBuilder)]
#[non_exhaustive]
#[serde(rename_all = "camelCase")]
#[builder(field_defaults(setter(into)))]
pub struct ForExpr {
pub binders: Vec<Arc<str>>,
pub collection: Box<Expression>,
#[builder(default)]
pub key: Option<Box<Expression>>,
pub value: Box<Expression>,
#[builder(default)]
pub cond: Option<Box<Expression>>,
#[builder(default = false)]
pub object_form: bool,
pub span: Span,
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
#[non_exhaustive]
#[serde(rename_all = "camelCase", tag = "kind", content = "node")]
pub enum Expression {
Literal(Value),
Unresolved(Symbolic),
BinaryOp {
op: BinaryOp,
lhs: Box<Expression>,
rhs: Box<Expression>,
span: Span,
},
UnaryOp {
op: UnaryOp,
operand: Box<Expression>,
span: Span,
},
TemplateConcat(Vec<Expression>),
Array(Vec<Expression>),
Object(Vec<(Expression, Expression)>),
FuncCall(Box<FuncCall>),
Conditional(Box<Conditional>),
For(Box<ForExpr>),
}
impl Expression {
#[must_use]
pub fn as_literal(&self) -> Option<&Value> {
match self {
Self::Literal(v) => Some(v),
_ => None,
}
}
#[must_use]
pub fn is_fully_resolved(&self) -> bool {
match self {
Self::Literal(_) => true,
Self::Unresolved(_) => false,
Self::BinaryOp { lhs, rhs, .. } => lhs.is_fully_resolved() && rhs.is_fully_resolved(),
Self::UnaryOp { operand, .. } => operand.is_fully_resolved(),
Self::TemplateConcat(parts) | Self::Array(parts) => {
parts.iter().all(Self::is_fully_resolved)
}
Self::Object(entries) => entries
.iter()
.all(|(k, v)| k.is_fully_resolved() && v.is_fully_resolved()),
Self::FuncCall(call) => call.args.iter().all(Self::is_fully_resolved),
Self::Conditional(c) => {
c.cond.is_fully_resolved()
&& c.then_branch.is_fully_resolved()
&& c.else_branch.is_fully_resolved()
}
Self::For(f) => {
f.collection.is_fully_resolved()
&& f.value.is_fully_resolved()
&& f.key.as_ref().is_none_or(|k| k.is_fully_resolved())
&& f.cond.as_ref().is_none_or(|c| c.is_fully_resolved())
}
}
}
}
#[cfg(test)]
#[allow(
clippy::unwrap_used,
clippy::expect_used,
clippy::panic,
clippy::indexing_slicing
)]
mod tests {
use std::{path::Path, sync::Arc};
use super::*;
fn fake_span() -> Span {
Span::synthetic()
}
#[test]
fn test_should_classify_literal_as_resolved() {
let e = Expression::Literal(Value::Int(42));
assert!(e.is_fully_resolved());
assert_eq!(e.as_literal(), Some(&Value::Int(42)));
}
#[test]
fn test_should_classify_unresolved_as_not_resolved() {
let e = Expression::Unresolved(Symbolic {
kind: SymbolKind::Var,
source: Arc::from("var.environment"),
address_hint: Some(Address::new("var.environment").unwrap()),
span: fake_span(),
});
assert!(!e.is_fully_resolved());
}
#[test]
fn test_should_recurse_into_binary_op() {
let e = Expression::BinaryOp {
op: BinaryOp::Add,
lhs: Box::new(Expression::Literal(Value::Int(1))),
rhs: Box::new(Expression::Unresolved(Symbolic {
kind: SymbolKind::Local,
source: Arc::from("local.x"),
address_hint: None,
span: fake_span(),
})),
span: fake_span(),
};
assert!(!e.is_fully_resolved());
}
#[test]
fn test_should_serde_round_trip_expression_tree() {
let span = Span::new(Arc::from(Path::new("/tmp/x.tf")), 0..1, 1, 1).unwrap();
let e = Expression::TemplateConcat(vec![
Expression::Literal(Value::Str(Arc::from("prefix-"))),
Expression::Unresolved(Symbolic {
kind: SymbolKind::Var,
source: Arc::from("var.environment"),
address_hint: None,
span: span.clone(),
}),
]);
let json = serde_json::to_string(&e).unwrap();
let back: Expression = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
#[test]
fn test_should_serde_round_trip_func_call() {
let call = FuncCall {
name: Arc::from("jsonencode"),
args: vec![Expression::Literal(Value::Str(Arc::from("hi")))],
span: fake_span(),
};
let e = Expression::FuncCall(Box::new(call));
let json = serde_json::to_string(&e).unwrap();
let back: Expression = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
#[test]
fn test_func_call_with_unresolved_argument_is_unresolved() {
let call = FuncCall {
name: Arc::from("jsonencode"),
args: vec![Expression::Unresolved(Symbolic {
kind: SymbolKind::Var,
source: Arc::from("var.x"),
address_hint: None,
span: fake_span(),
})],
span: fake_span(),
};
let e = Expression::FuncCall(Box::new(call));
assert!(!e.is_fully_resolved());
}
#[test]
fn test_should_serde_round_trip_conditional() {
let cond = Conditional {
cond: Box::new(Expression::Literal(Value::Bool(true))),
then_branch: Box::new(Expression::Literal(Value::Int(1))),
else_branch: Box::new(Expression::Literal(Value::Int(2))),
span: fake_span(),
};
let e = Expression::Conditional(Box::new(cond));
assert!(e.is_fully_resolved());
let json = serde_json::to_string(&e).unwrap();
let back: Expression = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
}
#[test]
fn test_conditional_with_unresolved_branch_is_unresolved() {
let cond = Conditional {
cond: Box::new(Expression::Literal(Value::Bool(true))),
then_branch: Box::new(Expression::Literal(Value::Int(1))),
else_branch: Box::new(Expression::Unresolved(Symbolic {
kind: SymbolKind::Var,
source: Arc::from("var.x"),
address_hint: None,
span: fake_span(),
})),
span: fake_span(),
};
let e = Expression::Conditional(Box::new(cond));
assert!(!e.is_fully_resolved());
}
#[test]
fn test_should_serde_round_trip_for_expr() {
let f = ForExpr {
binders: vec![Arc::from("k"), Arc::from("v")],
collection: Box::new(Expression::Literal(Value::List(vec![Value::Int(1)]))),
key: Some(Box::new(Expression::Unresolved(Symbolic {
kind: SymbolKind::Iteration,
source: Arc::from("k"),
address_hint: None,
span: fake_span(),
}))),
value: Box::new(Expression::Literal(Value::Bool(true))),
cond: None,
object_form: true,
span: fake_span(),
};
let e = Expression::For(Box::new(f));
let json = serde_json::to_string(&e).unwrap();
let back: Expression = serde_json::from_str(&json).unwrap();
assert_eq!(e, back);
assert!(!e.is_fully_resolved(), "Iteration ref keeps it unresolved");
}
}