1use std::collections::VecDeque;
2
3use hcl_edit::{expr::Object, structure::Body};
4
5use crate::{
6 hcl::{
7 expr::{Expression, ObjectKey},
8 structure::{Block, BlockLabel},
9 template::{Element, StringTemplate},
10 },
11 types::EvaluatableInput,
12};
13
14use crate::{helpers::fs::FileLocation, types::diagnostics::Diagnostic};
15
16#[derive(Debug, Clone)]
17pub enum StringExpression {
18 Literal(String),
19 Template(StringTemplate),
20}
21
22#[derive(Debug)]
23pub enum VisitorError {
24 MissingField(String),
25 MissingAttribute(String),
26 TypeMismatch(String, String),
27 TypeExpected(String),
28}
29
30pub fn visit_label(index: usize, name: &str, block: &Block) -> Result<String, VisitorError> {
31 let label = block.labels.get(index).ok_or(VisitorError::MissingField(name.to_string()))?;
32 match label {
33 BlockLabel::String(literal) => Ok(literal.to_string()),
34 BlockLabel::Ident(_e) => Err(VisitorError::TypeMismatch("string".into(), name.to_string())),
35 }
36}
37
38pub fn visit_optional_string_attribute(
39 field_name: &str,
40 block: &Block,
41) -> Result<Option<StringExpression>, VisitorError> {
42 let Some(attribute) = block.body.get_attribute(field_name) else {
43 return Ok(None);
44 };
45
46 match attribute.value.clone() {
47 Expression::String(value) => Ok(Some(StringExpression::Literal(value.to_string()))),
48 Expression::StringTemplate(template) => Ok(Some(StringExpression::Template(template))),
49 _ => Err(VisitorError::TypeExpected("string".into())),
50 }
51}
52
53pub fn visit_required_string_literal_attribute(
54 field_name: &str,
55 block: &Block,
56) -> Result<String, VisitorError> {
57 let Some(attribute) = block.body.get_attribute(field_name) else {
58 return Err(VisitorError::MissingAttribute(field_name.to_string()));
59 };
60
61 match attribute.value.clone() {
62 Expression::String(value) => Ok(value.to_string()),
63 _ => Err(VisitorError::TypeExpected("string".into())),
64 }
65}
66
67pub fn visit_optional_untyped_attribute(field_name: &str, block: &Block) -> Option<Expression> {
68 let Some(attribute) = block.body.get_attribute(field_name) else {
69 return None;
70 };
71 Some(attribute.value.clone())
72}
73
74pub fn get_object_expression_key(obj: &Object, key: &str) -> Option<hcl_edit::expr::ObjectValue> {
75 obj.into_iter()
76 .find(|(k, _)| k.as_ident().and_then(|i| Some(i.as_str().eq(key))).unwrap_or(false))
77 .map(|(_, v)| v)
78 .cloned()
79}
80
81pub fn build_diagnostics_for_unused_fields(
82 fields_names: Vec<&str>,
83 block: &Block,
84 location: &FileLocation,
85) -> Vec<Diagnostic> {
86 let mut diagnostics = vec![];
87 for attr in block.body.attributes().into_iter() {
88 if fields_names.contains(&attr.key.as_str()) {
89 continue;
90 }
91 diagnostics.push(
92 Diagnostic::error_from_string(format!("'{}' field is unused", attr.key.as_str()))
93 .location(&location),
94 )
95 }
96 diagnostics
97}
98
99pub fn collect_constructs_references_from_block<'a>(
102 block: &Block,
103 input: Option<Box<dyn EvaluatableInput>>,
104 dependencies: &mut Vec<(Option<Box<dyn EvaluatableInput>>, Expression)>,
105) {
106 for attribute in block.body.attributes() {
107 let expr = attribute.value.clone();
108 let mut references = vec![];
109 collect_constructs_references_from_expression(&expr, input.clone(), &mut references);
110 dependencies.append(&mut references);
111 }
112 for block in block.body.blocks() {
113 collect_constructs_references_from_block(block, input.clone(), dependencies);
114 }
115}
116
117pub fn collect_constructs_references_from_expression<'a>(
124 expr: &Expression,
125 input: Option<Box<dyn EvaluatableInput>>,
126 dependencies: &mut Vec<(Option<Box<dyn EvaluatableInput>>, Expression)>,
127) {
128 match expr {
129 Expression::Variable(_) => {
130 dependencies.push((input.clone(), expr.clone()));
131 }
132 Expression::Array(elements) => {
133 for element in elements.iter() {
134 collect_constructs_references_from_expression(element, input.clone(), dependencies);
135 }
136 }
137 Expression::BinaryOp(op) => {
138 collect_constructs_references_from_expression(
139 &op.lhs_expr,
140 input.clone(),
141 dependencies,
142 );
143 collect_constructs_references_from_expression(
144 &op.rhs_expr,
145 input.clone(),
146 dependencies,
147 );
148 }
149 Expression::Bool(_)
150 | Expression::Null(_)
151 | Expression::Number(_)
152 | Expression::String(_) => return,
153 Expression::Conditional(cond) => {
154 collect_constructs_references_from_expression(
155 &cond.cond_expr,
156 input.clone(),
157 dependencies,
158 );
159 collect_constructs_references_from_expression(
160 &cond.false_expr,
161 input.clone(),
162 dependencies,
163 );
164 collect_constructs_references_from_expression(
165 &cond.true_expr,
166 input.clone(),
167 dependencies,
168 );
169 }
170 Expression::ForExpr(for_expr) => {
171 collect_constructs_references_from_expression(
172 &for_expr.value_expr,
173 input.clone(),
174 dependencies,
175 );
176 if let Some(ref key_expr) = for_expr.key_expr {
177 collect_constructs_references_from_expression(
178 &key_expr,
179 input.clone(),
180 dependencies,
181 );
182 }
183 if let Some(ref cond) = for_expr.cond {
184 collect_constructs_references_from_expression(
185 &cond.expr,
186 input.clone(),
187 dependencies,
188 );
189 }
190 }
191 Expression::FuncCall(expr) => {
192 for arg in expr.args.iter() {
193 collect_constructs_references_from_expression(arg, input.clone(), dependencies);
194 }
195 }
196 Expression::HeredocTemplate(expr) => {
197 for element in expr.template.iter() {
198 match element {
199 Element::Directive(_) | Element::Literal(_) => {}
200 Element::Interpolation(interpolation) => {
201 collect_constructs_references_from_expression(
202 &interpolation.expr,
203 input.clone(),
204 dependencies,
205 );
206 }
207 }
208 }
209 }
210 Expression::Object(obj) => {
211 for (k, v) in obj.iter() {
212 match k {
213 ObjectKey::Expression(expr) => {
214 collect_constructs_references_from_expression(
215 &expr,
216 input.clone(),
217 dependencies,
218 );
219 }
220 ObjectKey::Ident(_) => {}
221 }
222 collect_constructs_references_from_expression(
223 &v.expr(),
224 input.clone(),
225 dependencies,
226 );
227 }
228 }
229 Expression::Parenthesis(expr) => {
230 collect_constructs_references_from_expression(
231 &expr.inner(),
232 input.clone(),
233 dependencies,
234 );
235 }
236 Expression::StringTemplate(template) => {
237 for element in template.iter() {
238 match element {
239 Element::Directive(_) | Element::Literal(_) => {}
240 Element::Interpolation(interpolation) => {
241 collect_constructs_references_from_expression(
242 &interpolation.expr,
243 input.clone(),
244 dependencies,
245 );
246 }
247 }
248 }
249 }
250 Expression::Traversal(traversal) => {
251 let Expression::Variable(_) = traversal.expr else {
252 return;
253 };
254 dependencies.push((input.clone(), expr.clone()));
255 }
256 Expression::UnaryOp(op) => {
257 collect_constructs_references_from_expression(&op.expr, input, dependencies);
258 }
259 }
260}
261
262#[derive(Debug, Clone, Serialize, Deserialize)]
263pub struct RawHclContent(String);
264impl RawHclContent {
265 pub fn from_string(s: String) -> Self {
266 RawHclContent(s)
267 }
268 pub fn from_file_location(file_location: &FileLocation) -> Result<Self, Diagnostic> {
269 file_location
270 .read_content_as_utf8()
271 .map_err(|e| {
272 Diagnostic::error_from_string(format!("{}", e.to_string())).location(&file_location)
273 })
274 .map(|s| RawHclContent(s))
275 }
276
277 pub fn into_blocks(&self) -> Result<VecDeque<Block>, Diagnostic> {
278 let content = crate::hcl::parser::parse_body(&self.0).map_err(|e| {
279 Diagnostic::error_from_string(format!("parsing error: {}", e.to_string()))
280 })?;
281 Ok(content.into_blocks().into_iter().collect::<VecDeque<Block>>())
282 }
283
284 pub fn into_typed_blocks(&self) -> Result<VecDeque<crate::types::typed_block::OwnedTypedBlock>, Diagnostic> {
291 Ok(self.into_blocks()?
292 .into_iter()
293 .map(crate::types::typed_block::OwnedTypedBlock::new)
294 .collect())
295 }
296
297 pub fn into_block_instance(&self) -> Result<Block, Diagnostic> {
298 let mut blocks = self.into_blocks()?;
299 if blocks.len() != 1 {
300 return Err(Diagnostic::error_from_string(
301 "expected exactly one block instance".into(),
302 ));
303 }
304 Ok(blocks.pop_front().unwrap())
305 }
306
307 pub fn to_bytes(&self) -> Result<Vec<u8>, Diagnostic> {
308 let mut bytes = vec![0u8; 2 * self.0.len()];
309 crate::hex::encode_to_slice(self.0.clone(), &mut bytes).map_err(|e| {
310 Diagnostic::error_from_string(format!("failed to encode raw content: {e}"))
311 })?;
312 Ok(bytes)
313 }
314 pub fn to_string(&self) -> String {
315 self.0.clone()
316 }
317 pub fn from_block(block: &Block) -> Self {
318 RawHclContent::from_string(
319 Body::builder().block(block.clone()).build().to_string().trim().to_string(),
320 )
321 }
322}
323
324#[cfg(test)]
325mod tests {
326
327 use super::*;
328
329 #[test]
330 fn test_block_to_raw_hcl() {
331 let addon_block_str = r#"
332 addon "evm" {
333 test = "hi"
334 chain_id = input.chain_id
335 rpc_api_url = input.rpc_api_url
336 }
337 "#
338 .trim();
339
340 let signer_block_str = r#"
341 signer "deployer" "evm::web_wallet" {
342 expected_address = "0xCe246168E59dd8e28e367BB49b38Dc621768F425"
343 }
344 "#
345 .trim();
346
347 let runbook_block_str = r#"
348 runbook "test" {
349 location = "./embedded-runbook.json"
350 chain_id = input.chain_id
351 rpc_api_url = input.rpc_api_url
352 deployer = signer.deployer
353 }
354 "#
355 .trim();
356
357 let output_block_str = r#"
358 output "contract_address1" {
359 value = runbook.test.action.deploy1.contract_address
360 }
361 "#
362 .trim();
363
364 let input = format!(
365 r#"
366 {addon_block_str}
367
368 {signer_block_str}
369
370 {runbook_block_str}
371
372 {output_block_str}
373 "#
374 );
375
376 let raw_hcl = RawHclContent::from_string(input.trim().to_string());
377 let blocks = raw_hcl.into_blocks().unwrap();
378 assert_eq!(blocks.len(), 4);
379 let addon_block = RawHclContent::from_block(&blocks[0]).to_string();
380 assert_eq!(addon_block, addon_block_str);
381 let signer_block = RawHclContent::from_block(&blocks[1]).to_string();
382 assert_eq!(signer_block, signer_block_str);
383 let runbook_block = RawHclContent::from_block(&blocks[2]).to_string();
384 assert_eq!(runbook_block, runbook_block_str);
385 let output_block = RawHclContent::from_block(&blocks[3]).to_string();
386 assert_eq!(output_block, output_block_str);
387 }
388
389 #[test]
390 fn test_collect_constructs_references_from_block() {
391 let input = r#"
392 runbook "test" {
393 location = "./embedded-runbook.json"
394 chain_id = input.chain_id
395 rpc_api_url = input.rpc_api_url
396 deployer = signer.deployer
397 arr = [variable.a, variable.b]
398 my_map {
399 key1 = variable.a
400 my_inner_map {
401 key2 = variable.b
402 }
403 }
404 }
405 "#;
406
407 let raw_hcl = RawHclContent::from_string(input.trim().to_string());
408 let block = raw_hcl.into_block_instance().unwrap();
409 let mut dependencies = vec![];
410 collect_constructs_references_from_block(
411 &block,
412 None::<Box<dyn EvaluatableInput>>,
413 &mut dependencies,
414 );
415
416 assert_eq!(dependencies.len(), 7);
417 }
418
419 #[test]
420 fn test_collect_constructs_references_expression() {
421 let input = r#"
422 runbook "test" {
423 location = "./embedded-runbook.json"
424 chain_id = input.chain_id
425 rpc_api_url = input.rpc_api_url
426 deployer = signer.deployer
427 arr = [variable.a, variable.b]
428 my_map {
429 key1 = variable.a
430 my_inner_map {
431 key2 = variable.b
432 }
433 }
434 }
435 "#;
436
437 let raw_hcl = RawHclContent::from_string(input.trim().to_string());
438 let block = raw_hcl.into_block_instance().unwrap();
439 let attribute = block.body.get_attribute("chain_id").unwrap();
440
441 let mut dependencies = vec![];
442 collect_constructs_references_from_expression(
443 &attribute.value,
444 None::<Box<dyn EvaluatableInput>>,
445 &mut dependencies,
446 );
447
448 assert_eq!(dependencies.len(), 1);
449 }
450}