@@ -501,6 +501,113 @@ static void scan_yaml_for_infra_bindings(CBMExtractCtx *ctx, TSNode node) {
501501 }
502502}
503503
504+ /* ── HCL infrastructure binding extraction ───────────────────────────
505+ * Scan HCL block nodes (resource, dynamic) for attribute pairs
506+ * where one is a source key (topic, queue_name) and another is a
507+ * target key (uri, push_endpoint). Handles nested blocks like
508+ * push_config { push_endpoint = "..." }. */
509+ static void scan_hcl_block_for_bindings (CBMExtractCtx * ctx , TSNode block ) {
510+ const char * sources [8 ] = {NULL };
511+ const char * source_keys [8 ] = {NULL };
512+ int n_sources = 0 ;
513+ const char * targets [8 ] = {NULL };
514+ int n_targets = 0 ;
515+
516+ /* Scan attributes at this level and one level deep (nested blocks) */
517+ uint32_t nc = ts_node_named_child_count (block );
518+ for (uint32_t i = 0 ; i < nc ; i ++ ) {
519+ TSNode child = ts_node_named_child (block , i );
520+ const char * ck = ts_node_type (child );
521+
522+ if (strcmp (ck , "attribute" ) == 0 ) {
523+ /* HCL attribute: key = value */
524+ TSNode key_node = ts_node_named_child (child , 0 );
525+ TSNode val_node = ts_node_named_child (child , 1 );
526+ if (ts_node_is_null (key_node ) || ts_node_is_null (val_node )) {
527+ continue ;
528+ }
529+ char * key = cbm_node_text (ctx -> arena , key_node , ctx -> source );
530+ if (!key ) {
531+ continue ;
532+ }
533+
534+ /* Only extract literal string values (quoted_template or template_literal) */
535+ const char * vk = ts_node_type (val_node );
536+ char * val = NULL ;
537+ if (strcmp (vk , "quoted_template" ) == 0 || strcmp (vk , "template_literal" ) == 0 ||
538+ strcmp (vk , "string_lit" ) == 0 ) {
539+ val = cbm_node_text (ctx -> arena , val_node , ctx -> source );
540+ if (val ) {
541+ int vlen = (int )strlen (val );
542+ if (vlen >= 2 && (val [0 ] == '"' || val [0 ] == '\'' )) {
543+ val = cbm_arena_strndup (ctx -> arena , val + 1 , (size_t )(vlen - 2 ));
544+ }
545+ }
546+ }
547+ if (!val || !val [0 ]) {
548+ continue ;
549+ }
550+
551+ if (is_source_key (key ) && n_sources < 8 ) {
552+ sources [n_sources ] = val ;
553+ source_keys [n_sources ] = key ;
554+ n_sources ++ ;
555+ }
556+ if (is_target_key (key ) && n_targets < 8 && strstr (val , "://" )) {
557+ targets [n_targets ++ ] = val ;
558+ }
559+ } else if (strcmp (ck , "block" ) == 0 ) {
560+ /* Nested block (e.g., push_config { push_endpoint = "..." })
561+ * Scan its attributes for target keys */
562+ uint32_t bnc = ts_node_named_child_count (child );
563+ for (uint32_t bi = 0 ; bi < bnc ; bi ++ ) {
564+ TSNode bchild = ts_node_named_child (child , bi );
565+ if (strcmp (ts_node_type (bchild ), "attribute" ) != 0 ) {
566+ continue ;
567+ }
568+ TSNode bkey = ts_node_named_child (bchild , 0 );
569+ TSNode bval = ts_node_named_child (bchild , 1 );
570+ if (ts_node_is_null (bkey ) || ts_node_is_null (bval )) {
571+ continue ;
572+ }
573+ char * bk = cbm_node_text (ctx -> arena , bkey , ctx -> source );
574+ if (!bk || !is_target_key (bk )) {
575+ continue ;
576+ }
577+ const char * bvk = ts_node_type (bval );
578+ if (strcmp (bvk , "quoted_template" ) == 0 || strcmp (bvk , "template_literal" ) == 0 ||
579+ strcmp (bvk , "string_lit" ) == 0 ) {
580+ char * bv = cbm_node_text (ctx -> arena , bval , ctx -> source );
581+ if (bv ) {
582+ int bvlen = (int )strlen (bv );
583+ if (bvlen >= 2 && (bv [0 ] == '"' || bv [0 ] == '\'' )) {
584+ bv = cbm_arena_strndup (ctx -> arena , bv + 1 , (size_t )(bvlen - 2 ));
585+ }
586+ if (bv && strstr (bv , "://" ) && n_targets < 8 ) {
587+ targets [n_targets ++ ] = bv ;
588+ }
589+ }
590+ }
591+ }
592+ }
593+ }
594+
595+ /* Emit bindings for each source × target pair */
596+ for (int si = 0 ; si < n_sources ; si ++ ) {
597+ for (int ti = 0 ; ti < n_targets ; ti ++ ) {
598+ if (!sources [si ] || !targets [ti ]) {
599+ continue ;
600+ }
601+ CBMInfraBinding ib = {
602+ .source_name = sources [si ],
603+ .target_url = targets [ti ],
604+ .broker = infer_broker (ctx -> rel_path , source_keys [si ]),
605+ };
606+ cbm_infrabinding_push (& ctx -> result -> infra_bindings , ctx -> arena , ib );
607+ }
608+ }
609+ }
610+
504611/* Handle YAML files: walk top-level block_mapping recursively */
505612static void handle_yaml_nested (CBMExtractCtx * ctx , TSNode node ) {
506613 if (ctx -> language != CBM_LANG_YAML ) {
@@ -567,6 +674,14 @@ void cbm_extract_unified(CBMExtractCtx *ctx) {
567674 }
568675 }
569676
677+ /* Scan HCL for infra bindings (resource blocks with topic+endpoint) */
678+ if (ctx -> language == CBM_LANG_HCL ) {
679+ const char * nk = ts_node_type (node );
680+ if (strcmp (nk , "block" ) == 0 ) {
681+ scan_hcl_block_for_bindings (ctx , node );
682+ }
683+ }
684+
570685 // 4. Push scope markers for boundary nodes
571686 if (spec -> function_node_types && cbm_kind_in_set (node , spec -> function_node_types )) {
572687 const char * fqn = compute_func_qn (ctx , node , spec , & state );
0 commit comments