1use std::sync::Arc;
21
22use crate::{
23 Result,
24 diagnostic::{Diagnostic, Severity},
25 eval::{
26 context::EvalContext,
27 error::EvalError,
28 locals::solve_locals,
29 reduce::{Scope, reduce_expression},
30 },
31 ir::{
32 AttributeMap, Component, Expression, Local, Map, ModuleCall, Output, ProviderBlock,
33 Resource, Variable,
34 },
35};
36
37pub trait Evaluator: Send + Sync + std::fmt::Debug {
40 fn evaluate(&self, component: &Component, ctx: &EvalContext) -> Result<EvaluatedComponent>;
50}
51
52#[derive(Debug, Default)]
54#[non_exhaustive]
55pub struct HclEvaluator;
56
57impl HclEvaluator {
58 #[must_use]
61 pub const fn new() -> Self {
62 Self
63 }
64}
65
66#[derive(Clone, Debug)]
75#[non_exhaustive]
76pub struct EvaluatedComponent {
77 pub raw: Arc<Component>,
79 pub variables: Vec<Variable>,
81 pub locals: Vec<Local>,
84 pub providers: Vec<ProviderBlock>,
87 pub resources: Vec<Resource>,
90 pub modules: Vec<ModuleCall>,
92 pub outputs: Vec<Output>,
94 pub diagnostics: Vec<Diagnostic>,
97}
98
99impl EvaluatedComponent {
100 #[must_use]
107 pub fn from_component(c: Component) -> Self {
108 Self {
109 variables: c.variables.clone(),
110 locals: c.locals.clone(),
111 providers: c.providers.clone(),
112 resources: c.resources.clone(),
113 modules: c.modules.clone(),
114 outputs: c.outputs.clone(),
115 diagnostics: Vec::new(),
116 raw: Arc::new(c),
117 }
118 }
119}
120
121impl Evaluator for HclEvaluator {
122 #[tracing::instrument(
123 skip(self, component, ctx),
124 fields(
125 component_id = ?component.id,
126 component_path = %component.path.display(),
127 n_repo_vars = ctx.repo_vars.len(),
130 n_cascade_locals = ctx.cascade_locals.len(),
131 ),
132 )]
133 fn evaluate(&self, component: &Component, ctx: &EvalContext) -> Result<EvaluatedComponent> {
134 let mut diagnostics: Vec<Diagnostic> = Vec::new();
135
136 let (variables, var_bindings) = bind_variables(&component.variables, ctx);
138
139 let mut vars: Map = var_bindings;
142 for (k, v) in &ctx.repo_vars {
143 override_or_push(&mut vars, k, v);
144 }
145
146 let mut locals_namespace: Map = ctx.cascade_locals.clone();
149
150 let mut scope = Scope::new(
152 vars.clone(),
153 locals_namespace.clone(),
154 ctx.workspace_root.as_ref(),
155 &ctx.env_vars,
156 &ctx.limits,
157 &ctx.funcs,
158 ctx.environment.clone(),
159 );
160
161 let locals = match solve_locals(&component.locals, &mut scope) {
162 Ok(reduced) => reduced,
163 Err(EvalError::Cycle { participants }) => {
164 let participant_names: Vec<String> = participants
165 .iter()
166 .map(|p| p.as_str().to_string())
167 .collect();
168 let summary = participant_names.join(", ");
169 diagnostics.push(
170 Diagnostic::new(
171 Severity::Error,
172 "TF1401",
173 format!("cycle in locals: {summary}"),
174 )
175 .with_span(component_span_for_diag(component)),
176 );
177 component.locals.clone()
180 }
181 Err(other) => {
182 diagnostics.push(Diagnostic::new(
183 Severity::Warn,
184 "TF1499",
185 format!("locals reduction failed: {other}"),
186 ));
187 component.locals.clone()
188 }
189 };
190
191 for l in &locals {
193 if let Expression::Literal(v) = &l.value {
194 override_or_push(&mut locals_namespace, &l.name, v);
195 }
196 }
197 scope.locals = locals_namespace;
198
199 let providers = component
201 .providers
202 .iter()
203 .map(|p| reduce_provider(p, &scope))
204 .collect();
205 let resources = component
206 .resources
207 .iter()
208 .map(|r| reduce_resource(r, &scope))
209 .collect();
210 let modules = component
211 .modules
212 .iter()
213 .map(|m| reduce_module(m, &scope))
214 .collect();
215 let outputs = component
216 .outputs
217 .iter()
218 .map(|o| reduce_output(o, &scope))
219 .collect();
220
221 Ok(EvaluatedComponent {
222 raw: Arc::new(component.clone()),
223 variables,
224 locals,
225 providers,
226 resources,
227 modules,
228 outputs,
229 diagnostics,
230 })
231 }
232}
233
234fn bind_variables(variables: &[Variable], ctx: &EvalContext) -> (Vec<Variable>, Map) {
238 let mut out: Vec<Variable> = Vec::with_capacity(variables.len());
239 let mut bindings: Map = Vec::new();
240
241 let empty = Map::new();
245 let minimal_scope = Scope::new(
246 Map::new(),
247 empty,
248 ctx.workspace_root.as_ref(),
249 &ctx.env_vars,
250 &ctx.limits,
251 &ctx.funcs,
252 ctx.environment.clone(),
253 );
254
255 for var in variables {
256 let reduced_default = var
257 .default
258 .as_ref()
259 .map(|d| reduce_expression(d, &minimal_scope));
260
261 let bound: Option<&crate::ir::Value> = reduced_default.as_ref().and_then(|d| match d {
262 Expression::Literal(v) => Some(v),
263 _ => None,
264 });
265 if let Some(v) = bound {
266 bindings.push((Arc::clone(&var.name), v.clone()));
267 }
268
269 out.push(
270 Variable::builder()
271 .name(Arc::clone(&var.name))
272 .description(var.description.clone())
273 .type_expr(var.type_expr.clone())
274 .default(reduced_default)
275 .sensitive(var.sensitive)
276 .span(var.span.clone())
277 .build(),
278 );
279 }
280
281 (out, bindings)
282}
283
284fn override_or_push(map: &mut Map, key: &Arc<str>, value: &crate::ir::Value) {
285 if let Some(slot) = map.iter_mut().find(|(k, _)| k == key) {
286 slot.1 = value.clone();
287 } else {
288 map.push((Arc::clone(key), value.clone()));
289 }
290}
291
292fn reduce_attrs(attrs: &AttributeMap, scope: &Scope<'_>) -> AttributeMap {
293 attrs
294 .iter()
295 .map(|(k, v)| (Arc::clone(k), reduce_expression(v, scope)))
296 .collect()
297}
298
299fn reduce_provider(p: &ProviderBlock, scope: &Scope<'_>) -> ProviderBlock {
300 ProviderBlock::builder()
301 .local_name(Arc::clone(&p.local_name))
302 .alias(p.alias.clone())
303 .source_addr(p.source_addr.clone())
304 .region_expr(p.region_expr.as_ref().map(|e| reduce_expression(e, scope)))
305 .profile_expr(p.profile_expr.as_ref().map(|e| reduce_expression(e, scope)))
306 .assume_role(p.assume_role.clone())
307 .raw(reduce_attrs(&p.raw, scope))
308 .span(p.span.clone())
309 .build()
310}
311
312fn reduce_resource(r: &Resource, scope: &Scope<'_>) -> Resource {
313 Resource::builder()
314 .address(r.address.clone())
315 .kind(r.kind)
316 .type_(Arc::clone(&r.type_))
317 .name(Arc::clone(&r.name))
318 .provider_ref(r.provider_ref.clone())
319 .count_expr(r.count_expr.as_ref().map(|e| reduce_expression(e, scope)))
320 .for_each_expr(
321 r.for_each_expr
322 .as_ref()
323 .map(|e| reduce_expression(e, scope)),
324 )
325 .depends_on(r.depends_on.clone())
326 .attributes(reduce_attrs(&r.attributes, scope))
327 .span(r.span.clone())
328 .build()
329}
330
331fn reduce_module(m: &ModuleCall, scope: &Scope<'_>) -> ModuleCall {
332 ModuleCall::builder()
333 .address(m.address.clone())
334 .source_raw(Arc::clone(&m.source_raw))
335 .source(m.source.clone())
336 .resolved(m.resolved)
337 .providers(m.providers.clone())
338 .inputs(reduce_attrs(&m.inputs, scope))
339 .count_expr(m.count_expr.as_ref().map(|e| reduce_expression(e, scope)))
340 .for_each_expr(
341 m.for_each_expr
342 .as_ref()
343 .map(|e| reduce_expression(e, scope)),
344 )
345 .span(m.span.clone())
346 .build()
347}
348
349fn reduce_output(o: &Output, scope: &Scope<'_>) -> Output {
350 Output::builder()
351 .name(Arc::clone(&o.name))
352 .value(reduce_expression(&o.value, scope))
353 .description(o.description.clone())
354 .sensitive(o.sensitive)
355 .span(o.span.clone())
356 .build()
357}
358
359fn component_span_for_diag(component: &Component) -> crate::ir::Span {
360 if let Some(file) = component.files.first() {
365 return crate::ir::Span::new(Arc::clone(&file.path), 0..0, 1, 1)
366 .unwrap_or_else(|_| crate::ir::Span::synthetic());
367 }
368 crate::ir::Span::synthetic()
369}
370
371#[cfg(test)]
372#[allow(
373 clippy::unwrap_used,
374 clippy::expect_used,
375 clippy::panic,
376 clippy::indexing_slicing
377)]
378mod tests {
379 use std::path::{Path, PathBuf};
380
381 use super::*;
382 use crate::{
383 eval::{context::EvalLimits, registry::FuncRegistry},
384 ir::{
385 Address, BinaryOp, ComponentId, ComponentKind, Expression, Local, ProviderBlock,
386 Resource, ResourceKind, Span, SymbolKind, Symbolic, Value, Variable,
387 },
388 };
389
390 fn make_ctx() -> EvalContext {
391 EvalContext {
392 workspace_root: Arc::from(Path::new("/tmp/repo")),
393 environment: None,
394 env_vars: super::super::EnvVarMode::default(),
395 repo_vars: vec![(Arc::from("region"), Value::Str(Arc::from("us-east-2")))],
396 cascade_locals: Vec::new(),
397 funcs: Arc::new(FuncRegistry::default_with_stdlib()),
398 limits: EvalLimits::default(),
399 }
400 }
401
402 fn var(name: &str) -> Variable {
403 Variable::builder()
404 .name(Arc::<str>::from(name))
405 .span(Span::synthetic())
406 .build()
407 }
408
409 fn var_ref(name: &str, kind: SymbolKind) -> Expression {
410 Expression::Unresolved(
411 Symbolic::builder()
412 .kind(kind)
413 .source(Arc::<str>::from(name))
414 .span(Span::synthetic())
415 .build(),
416 )
417 }
418
419 #[test]
420 fn test_should_resolve_provider_region_from_var() {
421 let provider = ProviderBlock::builder()
422 .local_name(Arc::<str>::from("aws"))
423 .region_expr(Some(var_ref("var.region", SymbolKind::Var)))
424 .span(Span::synthetic())
425 .build();
426 let component = Component::builder()
427 .id(ComponentId::from_index(0))
428 .path(Arc::<Path>::from(PathBuf::from("c")))
429 .kind(ComponentKind::Component)
430 .variables(vec![var("region")])
431 .providers(vec![provider])
432 .build();
433 let evald = HclEvaluator.evaluate(&component, &make_ctx()).unwrap();
434 let region = evald.providers[0].region_expr.as_ref().unwrap();
435 assert_eq!(
436 region,
437 &Expression::Literal(Value::Str(Arc::from("us-east-2")))
438 );
439 }
440
441 #[test]
442 fn test_should_use_default_when_no_repo_var() {
443 let mut ctx = make_ctx();
444 ctx.repo_vars.clear();
445 let var_block = Variable::builder()
446 .name(Arc::<str>::from("region"))
447 .default(Some(Expression::Literal(Value::Str(Arc::from(
448 "us-west-1",
449 )))))
450 .span(Span::synthetic())
451 .build();
452 let provider = ProviderBlock::builder()
453 .local_name(Arc::<str>::from("aws"))
454 .region_expr(Some(var_ref("var.region", SymbolKind::Var)))
455 .span(Span::synthetic())
456 .build();
457 let component = Component::builder()
458 .id(ComponentId::from_index(0))
459 .path(Arc::<Path>::from(PathBuf::from("c")))
460 .kind(ComponentKind::Component)
461 .variables(vec![var_block])
462 .providers(vec![provider])
463 .build();
464 let evald = HclEvaluator.evaluate(&component, &ctx).unwrap();
465 let region = evald.providers[0].region_expr.as_ref().unwrap();
466 assert_eq!(
467 region,
468 &Expression::Literal(Value::Str(Arc::from("us-west-1")))
469 );
470 }
471
472 #[test]
473 fn test_should_resolve_local_from_var() {
474 let local = Local::builder()
476 .name(Arc::<str>::from("zone"))
477 .value(Expression::BinaryOp {
478 op: BinaryOp::Add,
479 lhs: Box::new(Expression::Literal(Value::Str(Arc::from("zone-")))),
480 rhs: Box::new(var_ref("var.region", SymbolKind::Var)),
481 span: Span::synthetic(),
482 })
483 .span(Span::synthetic())
484 .build();
485 let component = Component::builder()
486 .id(ComponentId::from_index(0))
487 .path(Arc::<Path>::from(PathBuf::from("c")))
488 .kind(ComponentKind::Component)
489 .variables(vec![var("region")])
490 .locals(vec![local])
491 .build();
492 let evald = HclEvaluator.evaluate(&component, &make_ctx()).unwrap();
493 assert_eq!(
494 evald.locals[0].value,
495 Expression::Literal(Value::Str(Arc::from("zone-us-east-2")))
496 );
497 }
498
499 #[test]
500 fn test_should_emit_cycle_diagnostic() {
501 let a = Local::builder()
502 .name(Arc::<str>::from("a"))
503 .value(var_ref("local.b", SymbolKind::Local))
504 .span(Span::synthetic())
505 .build();
506 let b = Local::builder()
507 .name(Arc::<str>::from("b"))
508 .value(var_ref("local.a", SymbolKind::Local))
509 .span(Span::synthetic())
510 .build();
511 let component = Component::builder()
512 .id(ComponentId::from_index(0))
513 .path(Arc::<Path>::from(PathBuf::from("c")))
514 .kind(ComponentKind::Component)
515 .locals(vec![a, b])
516 .build();
517 let evald = HclEvaluator.evaluate(&component, &make_ctx()).unwrap();
518 assert!(
519 evald
520 .diagnostics
521 .iter()
522 .any(|d| d.severity == Severity::Error && &*d.code == "TF1401"),
523 "{:?}",
524 evald.diagnostics
525 );
526 assert!(matches!(evald.locals[0].value, Expression::Unresolved(_)));
528 }
529
530 #[test]
531 fn test_should_reduce_resource_attribute_with_var() {
532 let resource = Resource::builder()
533 .address(Address::new("aws_iam_role.r").unwrap())
534 .kind(ResourceKind::Managed)
535 .type_(Arc::<str>::from("aws_iam_role"))
536 .name(Arc::<str>::from("r"))
537 .attributes(vec![(
538 Arc::from("name"),
539 Expression::TemplateConcat(vec![
540 Expression::Literal(Value::Str(Arc::from("role-"))),
541 var_ref("var.region", SymbolKind::Var),
542 ]),
543 )])
544 .span(Span::synthetic())
545 .build();
546 let component = Component::builder()
547 .id(ComponentId::from_index(0))
548 .path(Arc::<Path>::from(PathBuf::from("c")))
549 .kind(ComponentKind::Component)
550 .variables(vec![var("region")])
551 .resources(vec![resource])
552 .build();
553 let evald = HclEvaluator.evaluate(&component, &make_ctx()).unwrap();
554 let attrs = &evald.resources[0].attributes;
555 let (_, v) = attrs.iter().find(|(k, _)| k.as_ref() == "name").unwrap();
556 assert_eq!(
557 v,
558 &Expression::Literal(Value::Str(Arc::from("role-us-east-2")))
559 );
560 }
561
562 #[test]
563 fn test_should_keep_module_outputs_unresolved() {
564 let attr = var_ref("module.foo.id", SymbolKind::Module);
566 let resource = Resource::builder()
567 .address(Address::new("aws_iam_role.r").unwrap())
568 .kind(ResourceKind::Managed)
569 .type_(Arc::<str>::from("aws_iam_role"))
570 .name(Arc::<str>::from("r"))
571 .attributes(vec![(Arc::from("id"), attr.clone())])
572 .span(Span::synthetic())
573 .build();
574 let component = Component::builder()
575 .id(ComponentId::from_index(0))
576 .path(Arc::<Path>::from(PathBuf::from("c")))
577 .kind(ComponentKind::Component)
578 .resources(vec![resource])
579 .build();
580 let evald = HclEvaluator.evaluate(&component, &make_ctx()).unwrap();
581 let (_, v) = evald.resources[0]
582 .attributes
583 .iter()
584 .find(|(k, _)| k.as_ref() == "id")
585 .unwrap();
586 assert_eq!(v, &attr);
587 }
588
589 #[test]
590 fn test_should_resolve_terraform_workspace() {
591 let mut ctx = make_ctx();
592 ctx.environment = Some(Arc::from("staging"));
593 let resource = Resource::builder()
594 .address(Address::new("aws_iam_role.r").unwrap())
595 .kind(ResourceKind::Managed)
596 .type_(Arc::<str>::from("aws_iam_role"))
597 .name(Arc::<str>::from("r"))
598 .attributes(vec![(
599 Arc::from("env"),
600 var_ref("terraform.workspace", SymbolKind::Terraform),
601 )])
602 .span(Span::synthetic())
603 .build();
604 let component = Component::builder()
605 .id(ComponentId::from_index(0))
606 .path(Arc::<Path>::from(PathBuf::from("c")))
607 .kind(ComponentKind::Component)
608 .resources(vec![resource])
609 .build();
610 let evald = HclEvaluator.evaluate(&component, &ctx).unwrap();
611 let (_, v) = evald.resources[0]
612 .attributes
613 .iter()
614 .find(|(k, _)| k.as_ref() == "env")
615 .unwrap();
616 assert_eq!(v, &Expression::Literal(Value::Str(Arc::from("staging"))));
617 }
618
619 #[test]
620 fn test_evaluator_is_send_sync() {
621 const fn assert_send_sync<T: Send + Sync + 'static>() {}
622 assert_send_sync::<HclEvaluator>();
623 assert_send_sync::<Box<dyn Evaluator>>();
624 assert_send_sync::<EvalContext>();
625 assert_send_sync::<EvaluatedComponent>();
626 }
627}