1use std::sync::Arc;
24
25use crate::{
26 Diagnostic, Severity,
27 diagnostic::Diagnostic as Diag,
28 ir::{
29 Address, AttributeMap, BlockKind, Component, ComponentId, Expression, Local, ModuleCall,
30 ModuleSource, Output, ProviderBlock, ProviderRef, Resource, ResourceKind, SymbolKind,
31 Value, Variable,
32 },
33 loader::{RawBlock, RawComponent},
34};
35
36#[must_use]
47pub fn project_component(
48 raw: &RawComponent,
49 component_id: ComponentId,
50 out_diagnostics: &mut Vec<Diagnostic>,
51) -> Component {
52 let mut variables: Vec<Variable> = Vec::new();
53 let mut locals: Vec<Local> = Vec::new();
54 let mut providers: Vec<ProviderBlock> = Vec::new();
55 let mut resources: Vec<Resource> = Vec::new();
56 let mut module_calls: Vec<ModuleCall> = Vec::new();
57 let mut outputs: Vec<Output> = Vec::new();
58
59 for block in &raw.raw_blocks {
60 match block.kind {
61 BlockKind::Resource | BlockKind::Data => {
62 match project_resource(block, out_diagnostics) {
63 Some(r) => resources.push(r),
64 None => out_diagnostics.push(Diag::new(
65 Severity::Warn,
66 "TF1301",
67 format!(
68 "resource/data block missing labels in {}",
69 block.source.display()
70 ),
71 )),
72 }
73 }
74 BlockKind::Provider => {
75 if let Some(p) = project_provider(block, out_diagnostics) {
76 providers.push(p);
77 }
78 }
79 BlockKind::Module => {
80 if let Some(m) = project_module_call(block, out_diagnostics) {
81 module_calls.push(m);
82 }
83 }
84 BlockKind::Variable => {
85 if let Some(v) = project_variable(block) {
86 variables.push(v);
87 }
88 }
89 BlockKind::Locals => project_locals_into(block, &mut locals),
90 BlockKind::Output => {
91 if let Some(o) = project_output(block) {
92 outputs.push(o);
93 }
94 }
95 BlockKind::Terraform
96 | BlockKind::Include
97 | BlockKind::Generate
98 | BlockKind::Dependency
99 | BlockKind::Inputs
100 | BlockKind::Unknown => {
101 }
106 }
107 }
108
109 Component::builder()
110 .id(component_id)
111 .path(Arc::clone(&raw.path))
112 .kind(raw.kind)
113 .variables(variables)
114 .locals(locals)
115 .providers(providers)
116 .resources(resources)
117 .modules(module_calls)
118 .outputs(outputs)
119 .build()
120}
121
122fn project_resource(block: &RawBlock, diags: &mut Vec<Diagnostic>) -> Option<Resource> {
123 let (type_label, name_label) = match (block.labels.first(), block.labels.get(1)) {
124 (Some(t), Some(n)) => (Arc::clone(t), Arc::clone(n)),
125 _ => return None,
126 };
127 let (kind, addr_str) = match block.kind {
128 BlockKind::Resource => (ResourceKind::Managed, format!("{type_label}.{name_label}")),
129 BlockKind::Data => (
130 ResourceKind::Data,
131 format!("data.{type_label}.{name_label}"),
132 ),
133 _ => return None,
134 };
135 let address = match Address::new(&addr_str) {
136 Ok(a) => a,
137 Err(err) => {
138 diags.push(Diag::new(
139 Severity::Warn,
140 "TF1302",
141 format!(
142 "invalid resource address `{addr_str}` in {}: {err}",
143 block.source.display()
144 ),
145 ));
146 return None;
147 }
148 };
149
150 let mut attributes: AttributeMap = Vec::with_capacity(block.body.len());
151 let mut count_expr: Option<Expression> = None;
152 let mut for_each_expr: Option<Expression> = None;
153 let mut provider_ref: Option<ProviderRef> = None;
154 let mut depends_on: Vec<Address> = Vec::new();
155
156 for (key, value) in &block.body {
157 match key.as_ref() {
158 "count" => count_expr = Some(value.clone()),
159 "for_each" => for_each_expr = Some(value.clone()),
160 "provider" => {
161 provider_ref = extract_provider_ref(value);
162 if provider_ref.is_none() {
163 diags.push(Diag::new(
164 Severity::Trace,
165 "TF1303",
166 format!(
167 "ignored non-static provider expression on {addr_str} in {}",
168 block.source.display()
169 ),
170 ));
171 }
172 }
173 "depends_on" => depends_on.extend(extract_depends_on(value, diags)),
174 _ => attributes.push((Arc::clone(key), value.clone())),
175 }
176 }
177
178 Some(
179 Resource::builder()
180 .address(address)
181 .kind(kind)
182 .type_(Arc::clone(&type_label))
183 .name(Arc::clone(&name_label))
184 .provider_ref(provider_ref)
185 .count_expr(count_expr)
186 .for_each_expr(for_each_expr)
187 .depends_on(depends_on)
188 .attributes(attributes)
189 .span(block.span.clone())
190 .build(),
191 )
192}
193
194fn project_provider(block: &RawBlock, diags: &mut Vec<Diagnostic>) -> Option<ProviderBlock> {
195 let local_name = block.labels.first().cloned().or_else(|| {
196 diags.push(Diag::new(
197 Severity::Warn,
198 "TF1304",
199 format!("provider block missing label in {}", block.source.display()),
200 ));
201 None
202 })?;
203
204 let mut alias: Option<Arc<str>> = None;
205 let mut region_expr: Option<Expression> = None;
206 let mut profile_expr: Option<Expression> = None;
207 let mut raw: AttributeMap = Vec::with_capacity(block.body.len());
208
209 for (key, value) in &block.body {
210 match key.as_ref() {
211 "alias" => {
212 if let Expression::Literal(Value::Str(s)) = value {
213 alias = Some(Arc::clone(s));
214 } else {
215 raw.push((Arc::clone(key), value.clone()));
216 }
217 }
218 "region" => {
219 region_expr = Some(value.clone());
220 raw.push((Arc::clone(key), value.clone()));
221 }
222 "profile" => {
223 profile_expr = Some(value.clone());
224 raw.push((Arc::clone(key), value.clone()));
225 }
226 _ => raw.push((Arc::clone(key), value.clone())),
227 }
228 }
229
230 Some(
231 ProviderBlock::builder()
232 .local_name(local_name)
233 .alias(alias)
234 .region_expr(region_expr)
235 .profile_expr(profile_expr)
236 .raw(raw)
237 .span(block.span.clone())
238 .build(),
239 )
240}
241
242fn project_module_call(block: &RawBlock, diags: &mut Vec<Diagnostic>) -> Option<ModuleCall> {
243 let name = block.labels.first()?;
244 let addr_str = format!("module.{name}");
245 let address = match Address::new(&addr_str) {
246 Ok(a) => a,
247 Err(err) => {
248 diags.push(Diag::new(
249 Severity::Warn,
250 "TF1305",
251 format!(
252 "invalid module address `{addr_str}` in {}: {err}",
253 block.source.display()
254 ),
255 ));
256 return None;
257 }
258 };
259 let mut source_raw: Arc<str> = Arc::from("");
260 let mut inputs: AttributeMap = Vec::new();
261 let mut count_expr: Option<Expression> = None;
262 let mut for_each_expr: Option<Expression> = None;
263 let mut providers: Vec<(Arc<str>, ProviderRef)> = Vec::new();
264
265 for (key, value) in &block.body {
266 match key.as_ref() {
267 "source" => {
268 if let Expression::Literal(Value::Str(s)) = value {
269 source_raw = Arc::clone(s);
270 }
271 }
272 "count" => count_expr = Some(value.clone()),
273 "for_each" => for_each_expr = Some(value.clone()),
274 "providers" => providers.extend(extract_providers_map(value)),
275 _ => inputs.push((Arc::clone(key), value.clone())),
276 }
277 }
278
279 let source = ModuleSource::classify(source_raw.as_ref());
280 Some(
281 ModuleCall::builder()
282 .address(address)
283 .source_raw(source_raw)
284 .source(source)
285 .providers(providers)
286 .inputs(inputs)
287 .count_expr(count_expr)
288 .for_each_expr(for_each_expr)
289 .span(block.span.clone())
290 .build(),
291 )
292}
293
294fn project_variable(block: &RawBlock) -> Option<Variable> {
295 let name = block.labels.first()?;
296 let mut description: Option<Arc<str>> = None;
297 let mut type_expr: Option<Expression> = None;
298 let mut default: Option<Expression> = None;
299 let mut sensitive = false;
300 for (key, value) in &block.body {
301 match key.as_ref() {
302 "description" => {
303 if let Expression::Literal(Value::Str(s)) = value {
304 description = Some(Arc::clone(s));
305 }
306 }
307 "type" => type_expr = Some(normalize_variable_type(value)),
308 "default" => default = Some(value.clone()),
309 "sensitive" => {
310 if let Expression::Literal(Value::Bool(b)) = value {
311 sensitive = *b;
312 }
313 }
314 _ => {}
315 }
316 }
317 Some(
318 Variable::builder()
319 .name(Arc::clone(name))
320 .description(description)
321 .type_expr(type_expr)
322 .default(default)
323 .sensitive(sensitive)
324 .span(block.span.clone())
325 .build(),
326 )
327}
328
329fn normalize_variable_type(value: &Expression) -> Expression {
345 match value {
346 Expression::Unresolved(sym)
347 if sym.kind == SymbolKind::Other && is_terraform_type_keyword(&sym.source) =>
348 {
349 Expression::Literal(Value::Str(Arc::clone(&sym.source)))
350 }
351 Expression::FuncCall(call) => {
352 let mut new_call = (**call).clone();
353 new_call.args = call.args.iter().map(normalize_variable_type).collect();
354 Expression::FuncCall(Box::new(new_call))
355 }
356 Expression::Object(entries) => Expression::Object(
357 entries
358 .iter()
359 .map(|(k, v)| (k.clone(), normalize_variable_type(v)))
360 .collect(),
361 ),
362 Expression::Array(items) => {
363 Expression::Array(items.iter().map(normalize_variable_type).collect())
364 }
365 other => other.clone(),
366 }
367}
368
369fn is_terraform_type_keyword(s: &str) -> bool {
370 matches!(
371 s,
372 "string" | "number" | "bool" | "any" | "null" | "list" | "map" | "set" | "tuple" | "object"
373 )
374}
375
376fn project_locals_into(block: &RawBlock, out: &mut Vec<Local>) {
377 for (key, value) in &block.body {
378 out.push(
379 Local::builder()
380 .name(Arc::clone(key))
381 .value(value.clone())
382 .span(block.span.clone())
383 .build(),
384 );
385 }
386}
387
388fn project_output(block: &RawBlock) -> Option<Output> {
389 let name = block.labels.first()?;
390 let mut value_expr: Option<Expression> = None;
391 let mut description: Option<Arc<str>> = None;
392 let mut sensitive = false;
393 for (key, expr) in &block.body {
394 match key.as_ref() {
395 "value" => value_expr = Some(expr.clone()),
396 "description" => {
397 if let Expression::Literal(Value::Str(s)) = expr {
398 description = Some(Arc::clone(s));
399 }
400 }
401 "sensitive" => {
402 if let Expression::Literal(Value::Bool(b)) = expr {
403 sensitive = *b;
404 }
405 }
406 _ => {}
407 }
408 }
409 let value = value_expr?;
410 Some(
411 Output::builder()
412 .name(Arc::clone(name))
413 .value(value)
414 .description(description)
415 .sensitive(sensitive)
416 .span(block.span.clone())
417 .build(),
418 )
419}
420
421fn extract_provider_ref(expr: &Expression) -> Option<ProviderRef> {
431 let Expression::Unresolved(s) = expr else {
432 return None;
433 };
434 if !matches!(s.kind, SymbolKind::Other | SymbolKind::Resource) {
435 return None;
436 }
437 let source: &str = s.source.as_ref();
438 if !is_provider_identifier(source) {
439 return None;
440 }
441 let mut parts = source.splitn(2, '.');
442 let local_name: Arc<str> = Arc::from(parts.next()?);
443 let alias: Option<Arc<str>> = parts.next().map(Arc::from);
444 Some(
445 ProviderRef::builder()
446 .local_name(local_name)
447 .alias(alias)
448 .span(s.span.clone())
449 .build(),
450 )
451}
452
453fn is_provider_identifier(s: &str) -> bool {
457 fn is_segment(seg: &str) -> bool {
458 let mut bytes = seg.bytes();
459 match bytes.next() {
460 Some(b) if b.is_ascii_lowercase() || b == b'_' => {}
461 _ => return false,
462 }
463 bytes.all(|b| b.is_ascii_lowercase() || b.is_ascii_digit() || b == b'_')
464 }
465 let mut parts = s.split('.');
466 let Some(first) = parts.next() else {
467 return false;
468 };
469 if !is_segment(first) {
470 return false;
471 }
472 match parts.next() {
473 None => true,
474 Some(second) => is_segment(second) && parts.next().is_none(),
475 }
476}
477
478fn extract_depends_on(expr: &Expression, diags: &mut Vec<Diagnostic>) -> Vec<Address> {
485 let mut out: Vec<Address> = Vec::new();
486 let elements: &[Expression] = match expr {
487 Expression::Literal(Value::List(items)) => {
488 for item in items {
489 if let Value::Str(s) = item {
490 match Address::new(s.as_ref()) {
491 Ok(a) => out.push(a),
492 Err(err) => diags.push(Diag::new(
493 Severity::Trace,
494 "TF1306",
495 format!("depends_on entry rejected (`{s}`): {err}"),
496 )),
497 }
498 }
499 }
500 return out;
501 }
502 Expression::Array(items) => items.as_slice(),
503 _ => return out,
504 };
505 for item in elements {
506 match item {
507 Expression::Unresolved(s) => {
508 if let Some(hint) = &s.address_hint {
509 out.push(hint.clone());
510 } else if let Ok(addr) = Address::new(s.source.as_ref()) {
511 out.push(addr);
512 } else {
513 diags.push(Diag::new(
514 Severity::Trace,
515 "TF1306",
516 format!("depends_on entry not a static reference: {}", s.source),
517 ));
518 }
519 }
520 Expression::Literal(Value::Str(s)) => match Address::new(s.as_ref()) {
521 Ok(a) => out.push(a),
522 Err(err) => diags.push(Diag::new(
523 Severity::Trace,
524 "TF1306",
525 format!("depends_on entry rejected (`{s}`): {err}"),
526 )),
527 },
528 _ => diags.push(Diag::new(
529 Severity::Trace,
530 "TF1306",
531 "depends_on entry not a static reference",
532 )),
533 }
534 }
535 out
536}
537
538fn extract_providers_map(expr: &Expression) -> Vec<(Arc<str>, ProviderRef)> {
540 let mut out: Vec<(Arc<str>, ProviderRef)> = Vec::new();
541 let Expression::Object(entries) = expr else {
542 return out;
543 };
544 for (key, value) in entries {
545 let key_str: Arc<str> = match key {
546 Expression::Literal(Value::Str(s)) => Arc::clone(s),
547 _ => continue,
548 };
549 if let Some(reference) = extract_provider_ref(value) {
550 out.push((key_str, reference));
551 }
552 }
553 out
554}
555
556#[cfg(test)]
557#[allow(
558 clippy::unwrap_used,
559 clippy::expect_used,
560 clippy::panic,
561 clippy::indexing_slicing
562)]
563mod tests {
564 use std::path::Path;
565
566 use super::*;
567 use crate::{
568 ir::{ComponentKind, Span, Symbolic},
569 loader::{HclEditLoader, LoaderLimits},
570 };
571
572 fn raw_from_source(src: &str) -> RawComponent {
573 let path: Arc<Path> = Arc::from(Path::new("/tmp/x.tf"));
574 let limits = LoaderLimits::default();
575 let parsed = HclEditLoader.parse_bytes(src.as_bytes(), &path, &limits);
576 let mut raw = RawComponent::new(Arc::from(Path::new("")), ComponentKind::Component);
577 raw.raw_blocks.extend(parsed.blocks);
578 raw.diagnostics.extend(parsed.diagnostics);
579 raw
580 }
581
582 #[test]
583 fn test_should_project_resource_with_count_and_provider() {
584 let src = r#"
585resource "aws_iam_role" "service" {
586 provider = aws.main
587 count = 3
588 name = "x"
589}
590"#;
591 let raw = raw_from_source(src);
592 let mut diags = Vec::new();
593 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
594 assert_eq!(comp.resources.len(), 1);
595 let r = &comp.resources[0];
596 assert_eq!(r.address.as_str(), "aws_iam_role.service");
597 assert!(r.count_expr.is_some());
598 let provider_ref = r.provider_ref.as_ref().expect("provider_ref");
599 assert_eq!(provider_ref.local_name.as_ref(), "aws");
600 assert_eq!(provider_ref.alias.as_deref(), Some("main"));
601 assert!(r.attributes.iter().any(|(k, _)| k.as_ref() == "name"));
602 assert!(r.attributes.iter().all(|(k, _)| k.as_ref() != "count"));
603 assert!(r.attributes.iter().all(|(k, _)| k.as_ref() != "provider"));
604 assert!(diags.is_empty(), "{diags:?}");
605 }
606
607 #[test]
608 fn test_should_project_data_block_with_data_prefix() {
609 let src = r#"data "aws_caller_identity" "self" {}"#;
610 let raw = raw_from_source(src);
611 let mut diags = Vec::new();
612 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
613 assert_eq!(comp.resources.len(), 1);
614 let r = &comp.resources[0];
615 assert_eq!(r.kind, ResourceKind::Data);
616 assert_eq!(r.address.as_str(), "data.aws_caller_identity.self");
617 }
618
619 #[test]
620 fn test_should_project_provider_with_alias() {
621 let src = r#"
622provider "aws" {
623 alias = "main"
624 region = "us-east-1"
625}
626"#;
627 let raw = raw_from_source(src);
628 let mut diags = Vec::new();
629 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
630 assert_eq!(comp.providers.len(), 1);
631 let p = &comp.providers[0];
632 assert_eq!(p.local_name.as_ref(), "aws");
633 assert_eq!(p.alias.as_deref(), Some("main"));
634 assert!(p.region_expr.is_some());
635 assert!(diags.is_empty(), "{diags:?}");
636 }
637
638 #[test]
639 fn test_should_project_module_call_with_source_and_inputs() {
640 let src = r#"
641module "vpc" {
642 source = "../../modules/vpc"
643 cidr = "10.0.0.0/16"
644 providers = {
645 aws = aws.main
646 }
647}
648"#;
649 let raw = raw_from_source(src);
650 let mut diags = Vec::new();
651 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
652 assert_eq!(comp.modules.len(), 1);
653 let m = &comp.modules[0];
654 assert_eq!(m.address.as_str(), "module.vpc");
655 assert_eq!(m.source_raw.as_ref(), "../../modules/vpc");
656 assert!(matches!(m.source, ModuleSource::Local(_)));
657 assert!(m.inputs.iter().any(|(k, _)| k.as_ref() == "cidr"));
658 assert_eq!(m.providers.len(), 1);
659 assert_eq!(m.providers[0].0.as_ref(), "aws");
660 assert_eq!(m.providers[0].1.alias.as_deref(), Some("main"));
661 }
662
663 #[test]
664 fn test_should_project_locals_block() {
665 let src = r#"
666locals {
667 name = "northwind"
668 region = "us-west-2"
669}
670"#;
671 let raw = raw_from_source(src);
672 let mut diags = Vec::new();
673 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
674 assert_eq!(comp.locals.len(), 2);
675 let names: Vec<&str> = comp.locals.iter().map(|l| l.name.as_ref()).collect();
676 assert_eq!(names, vec!["name", "region"]);
677 }
678
679 #[test]
680 fn test_should_project_variable_with_default_and_sensitive() {
681 let src = r#"
682variable "db_password" {
683 type = string
684 default = "ignored-in-debug"
685 sensitive = true
686}
687"#;
688 let raw = raw_from_source(src);
689 let mut diags = Vec::new();
690 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
691 assert_eq!(comp.variables.len(), 1);
692 let v = &comp.variables[0];
693 assert!(v.sensitive);
694 assert!(v.default.is_some());
695 assert!(v.type_expr.is_some());
696 }
697
698 #[test]
706 fn test_should_normalize_bare_type_keyword_to_literal_string() {
707 let src = r#"
708variable "name" { type = string }
709variable "age" { type = number }
710variable "ok" { type = bool }
711variable "any_" { type = any }
712"#;
713 let raw = raw_from_source(src);
714 let mut diags = Vec::new();
715 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
716 let by_name = |n: &str| -> Expression {
717 comp.variables
718 .iter()
719 .find(|v| v.name.as_ref() == n)
720 .and_then(|v| v.type_expr.clone())
721 .unwrap_or_else(|| panic!("variable {n} missing"))
722 };
723 for (var, expected) in [
724 ("name", "string"),
725 ("age", "number"),
726 ("ok", "bool"),
727 ("any_", "any"),
728 ] {
729 match by_name(var) {
730 Expression::Literal(crate::ir::Value::Str(s)) => assert_eq!(s.as_ref(), expected),
731 other => panic!("variable {var}: expected literal `{expected}`, got {other:?}"),
732 }
733 }
734 }
735
736 #[test]
741 fn test_should_normalize_constructor_type_keyword_args() {
742 let src = r#"
743variable "regions" { type = list(string) }
744variable "tags" { type = map(string) }
745variable "shape" { type = object({ enabled = bool, count = number }) }
746"#;
747 let raw = raw_from_source(src);
748 let mut diags = Vec::new();
749 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
750
751 let regions = comp
752 .variables
753 .iter()
754 .find(|v| v.name.as_ref() == "regions")
755 .and_then(|v| v.type_expr.clone())
756 .expect("regions type_expr");
757 match regions {
758 Expression::FuncCall(call) => {
759 assert_eq!(call.name.as_ref(), "list");
760 assert!(
761 matches!(
762 &call.args[..],
763 [Expression::Literal(crate::ir::Value::Str(s))] if s.as_ref() == "string"
764 ),
765 "args={:?}",
766 call.args
767 );
768 }
769 other => panic!("regions: expected list(...) FuncCall, got {other:?}"),
770 }
771
772 let shape = comp
773 .variables
774 .iter()
775 .find(|v| v.name.as_ref() == "shape")
776 .and_then(|v| v.type_expr.clone())
777 .expect("shape type_expr");
778 let shape_call = match shape {
779 Expression::FuncCall(call) => call,
780 other => panic!("shape: expected object(...) FuncCall, got {other:?}"),
781 };
782 assert_eq!(shape_call.name.as_ref(), "object");
783 let entries = match &shape_call.args[..] {
785 [Expression::Object(entries)] => entries,
786 other => panic!("shape: expected object literal arg, got {other:?}"),
787 };
788 let entry = |key: &str| -> Expression {
789 entries
790 .iter()
791 .find(|(k, _)| {
792 matches!(k, Expression::Literal(crate::ir::Value::Str(s)) if s.as_ref() == key)
793 })
794 .map_or_else(
795 || panic!("entry {key} missing"),
796 |(_, v)| v.clone(),
797 )
798 };
799 match entry("enabled") {
800 Expression::Literal(crate::ir::Value::Str(s)) => assert_eq!(s.as_ref(), "bool"),
801 other => panic!("enabled: expected literal `bool`, got {other:?}"),
802 }
803 match entry("count") {
804 Expression::Literal(crate::ir::Value::Str(s)) => assert_eq!(s.as_ref(), "number"),
805 other => panic!("count: expected literal `number`, got {other:?}"),
806 }
807 }
808
809 #[test]
810 fn test_should_project_output_with_value() {
811 let src = r#"
812output "role_arn" {
813 value = aws_iam_role.r.arn
814}
815"#;
816 let raw = raw_from_source(src);
817 let mut diags = Vec::new();
818 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
819 assert_eq!(comp.outputs.len(), 1);
820 assert_eq!(comp.outputs[0].name.as_ref(), "role_arn");
821 }
822
823 #[test]
824 fn test_should_project_depends_on_static_addresses() {
825 let src = r#"
826resource "aws_iam_role" "r" {
827 depends_on = [aws_iam_role.parent]
828}
829"#;
830 let raw = raw_from_source(src);
831 let mut diags = Vec::new();
832 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
833 let r = &comp.resources[0];
834 assert_eq!(r.depends_on.len(), 1);
835 assert_eq!(r.depends_on[0].as_str(), "aws_iam_role.parent");
836 }
837
838 #[test]
839 fn test_should_drop_non_static_provider_to_none() {
840 let src = r#"
841resource "aws_iam_role" "r" {
842 provider = var.x
843}
844"#;
845 let raw = raw_from_source(src);
846 let mut diags = Vec::new();
847 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
848 let r = &comp.resources[0];
849 assert!(r.provider_ref.is_none());
850 assert!(diags.iter().any(|d| d.severity == Severity::Trace));
851 }
852
853 #[test]
854 fn test_should_extract_provider_ref_without_alias() {
855 let s = Symbolic::builder()
856 .kind(SymbolKind::Other)
857 .source(Arc::<str>::from("aws"))
858 .span(Span::synthetic())
859 .build();
860 let expr = Expression::Unresolved(s);
861 let pr = extract_provider_ref(&expr).expect("provider ref");
862 assert_eq!(pr.local_name.as_ref(), "aws");
863 assert!(pr.alias.is_none());
864 }
865
866 #[test]
867 fn test_should_reject_var_x_as_provider_ref() {
868 let s = Symbolic::builder()
869 .kind(SymbolKind::Var)
870 .source(Arc::<str>::from("var.x"))
871 .span(Span::synthetic())
872 .build();
873 let expr = Expression::Unresolved(s);
874 assert!(extract_provider_ref(&expr).is_none());
875 }
876
877 #[test]
878 fn test_should_reject_path_module_as_provider_ref() {
879 for (kind, source) in [
883 (SymbolKind::Path, "path.module"),
884 (SymbolKind::Terraform, "terraform.workspace"),
885 (SymbolKind::Iteration, "each.value"),
886 (SymbolKind::TerragruntDependency, "dependency.vpc"),
887 ] {
888 let s = Symbolic::builder()
889 .kind(kind)
890 .source(Arc::<str>::from(source))
891 .span(Span::synthetic())
892 .build();
893 assert!(
894 extract_provider_ref(&Expression::Unresolved(s)).is_none(),
895 "expected None for {source:?}"
896 );
897 }
898 }
899
900 #[test]
901 fn test_should_reject_too_many_dot_segments_as_provider_ref() {
902 let s = Symbolic::builder()
905 .kind(SymbolKind::Resource)
906 .source(Arc::<str>::from("aws_iam_role.r.arn"))
907 .span(Span::synthetic())
908 .build();
909 assert!(extract_provider_ref(&Expression::Unresolved(s)).is_none());
910 }
911
912 #[test]
913 fn test_provider_identifier_charset() {
914 assert!(is_provider_identifier("aws"));
915 assert!(is_provider_identifier("aws.main"));
916 assert!(is_provider_identifier("_foo"));
917 assert!(!is_provider_identifier("Aws.main"));
918 assert!(!is_provider_identifier("aws main"));
919 assert!(!is_provider_identifier("aws."));
920 assert!(!is_provider_identifier(".aws"));
921 assert!(!is_provider_identifier("aws.main.extra"));
922 assert!(!is_provider_identifier("3aws"));
923 }
924
925 #[test]
926 fn test_should_project_resource_with_for_each_unresolved() {
927 let src = r#"
928resource "aws_iam_role" "r" {
929 for_each = var.roles
930}
931"#;
932 let raw = raw_from_source(src);
933 let mut diags = Vec::new();
934 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
935 let r = &comp.resources[0];
936 assert!(matches!(r.for_each_expr, Some(Expression::Unresolved(_))));
937 }
938
939 #[test]
940 fn test_should_skip_unknown_blocks_silently() {
941 let src = r#"
942terraform {
943 required_version = ">= 1.5.0"
944}
945"#;
946 let raw = raw_from_source(src);
947 let mut diags = Vec::new();
948 let comp = project_component(&raw, ComponentId::from_index(0), &mut diags);
949 assert!(comp.resources.is_empty());
950 assert!(comp.providers.is_empty());
951 assert!(diags.is_empty(), "{diags:?}");
952 }
953}