Skip to content

feat: Add ast/config, ast/stack, stackutils support packages for stack/values features - #128

Open
diofeher wants to merge 4 commits into
mainfrom
feat/stack-values-ast-foundation
Open

feat: Add ast/config, ast/stack, stackutils support packages for stack/values features#128
diofeher wants to merge 4 commits into
mainfrom
feat/stack-values-ast-foundation

Conversation

@diofeher

@diofeher diofeher commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Summary

Ports foundation packages from #55 onto current main (post #89 merge), adapting to main's unified Store + FileType architecture.

  • internal/ast/config: ConfigAST interface with GetIncludeLabel, GetDependencyLabel, GetLocals, GetIncludes.
  • internal/ast/stack: StackAST interface with FindUnitAt, FindStackAt, and unit/stack source/path getters.
  • internal/stackutils: LookupUnitPath and LookupStackPath against a parsed *config.StackConfig.
  • internal/ast/ast.go: include/dependency helpers and the Includes scope move out — they're config-file concerns and don't belong on the generic IndexedAST. Stack files get their own AST package.
  • internal/tg/definition/definition.go: now wraps store.AST in astconfig.NewConfigAST and calls through the new interface.

No user-facing behavior changes; definition/hover on terragrunt.hcl files is identical. Follow-up PRs build stack/values hover + definition + completion on top of these packages.

Depends on: none (based on main at df11fce).
Blocks: follow-up PRs for stack hover/definition, values hover/definition, and completion refactor.

Test plan

  • go build ./...
  • go test ./internal/... — all green
  • golangci-lint run — 0 issues
  • No manual LSP testing needed; no behavior change on terragrunt.hcl. Stack/values handlers are added in follow-ups.

Summary by CodeRabbit

  • Refactor

    • Reworked internal AST handling and removed include-block tracking from the core AST index.
    • Call sites now use the new config-specific AST layer for include/dependency lookups.
  • New Features

    • Added config and stack AST wrappers that expose block/label resolution and attribute extraction.
    • Added utilities to resolve unit and stack paths from stack configurations.
  • Tests

    • Added comprehensive unit tests for the new AST wrappers and utilities.

@diofeher
diofeher requested a review from yhakbar as a code owner April 17, 2026 10:17
@coderabbitai

coderabbitai Bot commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Warning

Rate limit exceeded

@diofeher has exceeded the limit for the number of commits that can be reviewed per hour. Please wait 15 minutes and 18 seconds before requesting another review.

To keep reviews running without waiting, you can enable usage-based add-on for your organization. This allows additional reviews beyond the hourly cap. Account admins can enable it under billing.

⌛ How to resolve this issue?

After the wait time has elapsed, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

We recommend that you space out your commits to avoid hitting the rate limit.

🚦 How do rate limits work?

CodeRabbit enforces hourly rate limits for each developer per organization.

Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout.

Please see our FAQ for further information.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: a314fa5d-239b-4115-918b-7bbd4b6881d3

📥 Commits

Reviewing files that changed from the base of the PR and between b79cda0 and b1b5455.

📒 Files selected for processing (9)
  • internal/ast/ast.go
  • internal/ast/ast_test.go
  • internal/ast/config/config.go
  • internal/ast/config/config_test.go
  • internal/ast/stack/stack.go
  • internal/ast/stack/stack_test.go
  • internal/stackutils/stackutils.go
  • internal/stackutils/stackutils_test.go
  • internal/tg/definition/definition.go

Walkthrough

Core AST no longer tracks include blocks; include/dependency helpers were removed. New packages internal/ast/config (ConfigAST) and internal/ast/stack (StackAST) were added to encapsulate include/dependency and stack/unit logic respectively. stackutils helpers and tests were added, and definition code now uses ConfigAST.

Changes

Cohort / File(s) Summary
Core AST Refactor
internal/ast/ast.go, internal/ast/ast_test.go
Removed Includes field from IndexedAST and deleted exported helpers: IsIncludeBlock, IsDependencyBlock, GetNodeIncludeLabel, GetNodeDependencyLabel. Tests updated to drop include-related assertions and adjust dependency-matching cases.
Config AST Abstraction
internal/ast/config/config.go, internal/ast/config/config_test.go
Added ConfigAST interface and NewConfigAST implementation that wraps *ast.IndexedAST, builds an includes scope, and exposes FindNodeAt, GetIncludeLabel, GetDependencyLabel, GetLocals, and GetIncludes. Comprehensive tests added.
Stack AST Abstraction
internal/ast/stack/stack.go, internal/ast/stack/stack_test.go
Added StackAST interface and implementation for terragrunt.stack.hcl supporting FindUnitAt/FindStackAt, label extraction, and source/path attribute retrieval; tests added.
Stack Utilities
internal/stackutils/stackutils.go, internal/stackutils/stackutils_test.go
Added LookupUnitPath and LookupStackPath helpers to query config.StackConfig for unit/stack paths with tests.
Definition Integration
internal/tg/definition/definition.go
GetDefinitionTargetWithContext updated to construct ConfigAST and call its FindNodeAt, GetIncludeLabel, and GetDependencyLabel instead of removed AST helpers.

Sequence Diagram(s)

sequenceDiagram
  autonumber
  participant Def as Definition code
  participant C as ConfigAST (wrapper)
  participant I as IndexedAST
  participant S as Includes Scope

  Def->>C: NewConfigAST(store.AST)
  C->>I: wrap IndexedAST
  C->>I: FindNodeAt(hcl.Pos)
  I-->>C: *IndexedNode / nil
  C->>I: (when building) iterate nodes
  I-->>C: nodes...
  C->>S: add include blocks to Includes scope
  C-->>Def: GetIncludeLabel / GetDependencyLabel result
Loading

Estimated code review effort

🎯 3 (Moderate) | ⏱️ ~25 minutes

Poem

🌲 A core trimmed, new branches grown,
Config and Stack now hold their own,
Small helpers hum, tests sing along—
Cleaner AST, a sturdier song. 🎶

🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 32.00% which is insufficient. The required threshold is 80.00%. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (2 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title accurately describes the main change: adding three new support packages (ast/config, ast/stack, stackutils) for stack/values features.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch feat/stack-values-ast-foundation

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🧹 Nitpick comments (7)
internal/ast/ast_test.go (1)

287-287: Consider a more meaningful "no match" predicate.

Using func(*ast.IndexedNode) bool { return false } works, but it's a bit of a tautology — it'll always return nil regardless of the AST, so the test no longer really exercises FindFirstParentMatch's traversal. Now that isIncludeBlock/isDependencyBlock live in internal/ast/config, a nice replacement would be a predicate that's genuinely unmatched by this fixture, e.g. looking for an inputs block or a non-existent block type, so the traversal is actually walked end-to-end.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/ast_test.go` at line 287, The test currently uses a tautological
predicate func(*ast.IndexedNode) bool { return false } which prevents exercising
FindFirstParentMatch traversal; change the predicate to a realistic but
unmatched condition (for example, test for a block type not present in the
fixture such as "inputs" or any non-existent block name) so the traversal
actually walks the tree; reference the FindFirstParentMatch call and replace the
predicate with one that inspects the node (e.g., node.Node.Type or a helper like
isIncludeBlock/isDependencyBlock style check) and returns true only for that
absent block type to ensure full traversal.
internal/ast/config/config_test.go (1)

41-45: Method-value non-nil checks are always true.

Same note as in stack_test.go: assert.NotNil(t, configAST.FindNodeAt) and friends will never fail because method values on a non-nil interface are always non-nil. require.NotNil(t, configAST) is sufficient; for real behavior coverage, call the methods. Not blocking, just noise to prune.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/config/config_test.go` around lines 41 - 45, The test currently
asserts method-value non-nil on configAST (assert.NotNil(t,
configAST.FindNodeAt), GetIncludeLabel, GetDependencyLabel, GetLocals,
GetIncludes) which is meaningless; replace these with a single require.NotNil(t,
configAST) and then call the methods (e.g., configAST.FindNodeAt(...),
configAST.GetIncludeLabel(), configAST.GetDependencyLabel(),
configAST.GetLocals(), configAST.GetIncludes()) with minimal inputs to verify
real behavior or return values, asserting expected outputs or non-nil results
instead of checking the method values themselves.
internal/ast/config/config.go (2)

45-53: Scope construction walks the entire index — fine today, worth noting.

buildIncludesScope iterates every indexed node across every line. For typical terragrunt.hcl files this is trivially cheap, but since the index already preserves structure, you could alternatively walk top-level blocks from HCLFile.Body.(*hclsyntax.Body).Blocks — O(top-level blocks) instead of O(all nodes). Not a bug, just a heads-up if stack files ever grow large.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/config/config.go` around lines 45 - 53, buildIncludesScope
currently walks every node in c.Index which is O(all nodes); change it to
iterate only top-level blocks instead. In function buildIncludesScope, obtain
the body via the file AST (e.g. assert c.HCLFile.Body to *hclsyntax.Body and
iterate its Blocks), call isIncludeBlock(block) for each top-level block and
c.includes.Add(block) when true; if the type assertion to *hclsyntax.Body fails,
fall back to the existing c.Index iteration to preserve behavior.

72-114: Same dead branch pattern as in stack.go.

isIncludeBlock and isDependencyBlock both guarantee len(Labels) > 0, so the name := ""; if labels := ...; len(labels) > 0 guard here (and in GetDependencyLabel) is dead code — labels[0] is always safe after the matcher succeeds. Feel free to simplify or, better, share a helper with the equivalent methods in internal/ast/stack/stack.go (they're all the same walk-attr-then-block-and-grab-first-label shape).

Nothing blocking; just a readability win.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/config/config.go` around lines 72 - 114, Both GetIncludeLabel
and GetDependencyLabel contain a dead len(labels) > 0 guard because their
matchers (isIncludeBlock / isDependencyBlock) guarantee a block with at least
one label; remove the redundant check and directly read labels[0], or better yet
factor the common walk-attr-then-block-and-grab-first-label pattern into a small
helper (e.g., GetFirstLabelFromParentAttribute) and use it from GetIncludeLabel,
GetDependencyLabel and the similar methods in internal/ast/stack/stack.go to
eliminate duplication and clarify intent.
internal/ast/stack/stack.go (2)

40-78: Dead branch + duplication in GetUnitLabel/GetStackLabel.

Two small things worth cleaning up:

  1. isUnitBlock/isStackBlock already guarantee len(Labels) > 0 (lines 127-136), so the name := "" + if labels := ...; len(labels) > 0 dance is unreachable-guard code. labels[0] is always safe here.
  2. GetUnitLabel and GetStackLabel are structurally identical to each other (and to GetIncludeLabel in internal/ast/config/config.go). A tiny shared helper would DRY this up nicely.
♻️ Suggested tidy-up
-// GetUnitLabel returns the label of the given node, if it is a unit block
-func (s *stackAST) GetUnitLabel(node *ast.IndexedNode) (string, bool) {
-	attr := ast.FindFirstParentMatch(node, ast.IsAttribute)
-	if attr == nil {
-		return "", false
-	}
-
-	unitBlock := ast.FindFirstParentMatch(attr, isUnitBlock)
-	if unitBlock == nil {
-		return "", false
-	}
-
-	name := ""
-	if labels := unitBlock.Node.(*hclsyntax.Block).Labels; len(labels) > 0 {
-		name = labels[0]
-	}
-
-	return name, true
-}
-
-// GetStackLabel returns the label of the given node, if it is a stack block
-func (s *stackAST) GetStackLabel(node *ast.IndexedNode) (string, bool) {
-	attr := ast.FindFirstParentMatch(node, ast.IsAttribute)
-	if attr == nil {
-		return "", false
-	}
-
-	stackBlock := ast.FindFirstParentMatch(attr, isStackBlock)
-	if stackBlock == nil {
-		return "", false
-	}
-
-	name := ""
-	if labels := stackBlock.Node.(*hclsyntax.Block).Labels; len(labels) > 0 {
-		name = labels[0]
-	}
-
-	return name, true
-}
+// GetUnitLabel returns the label of the given node, if it is a unit block
+func (s *stackAST) GetUnitLabel(node *ast.IndexedNode) (string, bool) {
+	return firstLabelFromContainingBlock(node, isUnitBlock)
+}
+
+// GetStackLabel returns the label of the given node, if it is a stack block
+func (s *stackAST) GetStackLabel(node *ast.IndexedNode) (string, bool) {
+	return firstLabelFromContainingBlock(node, isStackBlock)
+}
+
+// firstLabelFromContainingBlock walks up to the containing attribute and then
+// the nearest matching block, returning the block's first label.
+func firstLabelFromContainingBlock(node *ast.IndexedNode, blockMatcher func(*ast.IndexedNode) bool) (string, bool) {
+	attr := ast.FindFirstParentMatch(node, ast.IsAttribute)
+	if attr == nil {
+		return "", false
+	}
+	block := ast.FindFirstParentMatch(attr, blockMatcher)
+	if block == nil {
+		return "", false
+	}
+	return block.Node.(*hclsyntax.Block).Labels[0], true
+}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/stack/stack.go` around lines 40 - 78, Both GetUnitLabel and
GetStackLabel duplicate logic and contain an unreachable safety check: because
isUnitBlock/isStackBlock already ensure the block has at least one label, remove
the unnecessary name := "" and the len(labels) > 0 guard and read labels[0]
directly; then extract the shared logic into a small helper (e.g.,
getLabelFromParent(node *ast.IndexedNode, parentMatch func(*ast.IndexedNode)
bool) (string, bool) or a method on stackAST) that finds the closest attribute
parent (ast.IsAttribute), finds the desired parent via the provided matcher (use
isUnitBlock or isStackBlock), and returns the first label, and rewrite
GetUnitLabel and GetStackLabel to call that helper (also consider reusing the
same helper for GetIncludeLabel).

162-180: Use cty.String type check instead of FriendlyName() comparison.

Comparing e.Val.Type().FriendlyName() == "string" works, but it relies on a human-facing string that could theoretically change. The better approach is e.Val.Type() == cty.String, which is a direct type comparison and won't break if the friendly-name formatting ever shifts. This is the idiomatic pattern used across HashiCorp codebases.

Diff
-	case *hclsyntax.LiteralValueExpr:
-		if e.Val.Type().FriendlyName() == "string" {
-			return e.Val.AsString(), true
-		}
+	case *hclsyntax.LiteralValueExpr:
+		if e.Val.Type() == cty.String {
+			return e.Val.AsString(), true
+		}
 	case *hclsyntax.TemplateExpr:
 		// Handle quoted strings which are parsed as TemplateExpr
 		if len(e.Parts) == 1 {
 			if literal, ok := e.Parts[0].(*hclsyntax.LiteralValueExpr); ok {
-				if literal.Val.Type().FriendlyName() == "string" {
+				if literal.Val.Type() == cty.String {
 					return literal.Val.AsString(), true
 				}
 			}
 		}

You'll need to add "github.com/zclconf/go-cty/cty" to the imports—it's already a dependency and already imported elsewhere in the codebase.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/stack/stack.go` around lines 162 - 180, Replace FriendlyName()
string comparisons with direct cty type checks in stackAST.extractStringValue:
change checks like e.Val.Type().FriendlyName() == "string" to e.Val.Type() ==
cty.String in both the LiteralValueExpr and TemplateExpr branches, and add the
import "github.com/zclconf/go-cty/cty" if not already present so the cty.String
identifier is available.
internal/ast/stack/stack_test.go (1)

44-50: These assertions don't actually assert much.

A couple of test-hygiene nits:

  • assert.NotNil(t, stackAST.FindNodeAt) (and siblings): method values on a non-nil interface are always non-nil in Go, so these checks can never fail. If the intent is "the interface is wired up", then require.NotNil(t, stackAST) already covers it. If the intent is "the method works", call it with a real hcl.Pos and assert on the result.
  • var _ = stackAST in the "interface compliance" case is a no-op — it doesn't verify interface compliance (Go does that at compile time through the return type of NewStackAST). A true compile-time interface check would look like var _ stack.StackAST = (*someConcreteType)(nil), but since NewStackAST already returns StackAST, it's redundant.

Also, TestStackAST_Interface covers GetUnitLabel/GetStackLabel/GetUnitSource/GetUnitPath but skips GetStackSource/GetStackPath — worth adding for symmetry.

Also applies to: 86-87

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/stack/stack_test.go` around lines 44 - 50, The test currently
asserts method values (stackAST.FindNodeAt, GetUnitLabel, etc.) with
assert.NotNil which is meaningless in Go; replace those with a single
require.NotNil(t, stackAST) for interface wiring, remove the no-op var _ =
stackAST, and instead add real behavioral assertions: call
FindNodeAt/FindUnitAt/FindStackAt with a concrete hcl.Pos and assert expected
results (or nil) to verify functionality; also add symmetry tests for
GetStackSource and GetStackPath similar to GetUnitSource/GetUnitPath; if you
want an explicit compile-time interface check add a line like var _
stack.StackAST = (*<concreteType>)(nil) referencing the concrete implementation
type.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@internal/ast/ast_test.go`:
- Line 287: The test currently uses a tautological predicate
func(*ast.IndexedNode) bool { return false } which prevents exercising
FindFirstParentMatch traversal; change the predicate to a realistic but
unmatched condition (for example, test for a block type not present in the
fixture such as "inputs" or any non-existent block name) so the traversal
actually walks the tree; reference the FindFirstParentMatch call and replace the
predicate with one that inspects the node (e.g., node.Node.Type or a helper like
isIncludeBlock/isDependencyBlock style check) and returns true only for that
absent block type to ensure full traversal.

In `@internal/ast/config/config_test.go`:
- Around line 41-45: The test currently asserts method-value non-nil on
configAST (assert.NotNil(t, configAST.FindNodeAt), GetIncludeLabel,
GetDependencyLabel, GetLocals, GetIncludes) which is meaningless; replace these
with a single require.NotNil(t, configAST) and then call the methods (e.g.,
configAST.FindNodeAt(...), configAST.GetIncludeLabel(),
configAST.GetDependencyLabel(), configAST.GetLocals(), configAST.GetIncludes())
with minimal inputs to verify real behavior or return values, asserting expected
outputs or non-nil results instead of checking the method values themselves.

In `@internal/ast/config/config.go`:
- Around line 45-53: buildIncludesScope currently walks every node in c.Index
which is O(all nodes); change it to iterate only top-level blocks instead. In
function buildIncludesScope, obtain the body via the file AST (e.g. assert
c.HCLFile.Body to *hclsyntax.Body and iterate its Blocks), call
isIncludeBlock(block) for each top-level block and c.includes.Add(block) when
true; if the type assertion to *hclsyntax.Body fails, fall back to the existing
c.Index iteration to preserve behavior.
- Around line 72-114: Both GetIncludeLabel and GetDependencyLabel contain a dead
len(labels) > 0 guard because their matchers (isIncludeBlock /
isDependencyBlock) guarantee a block with at least one label; remove the
redundant check and directly read labels[0], or better yet factor the common
walk-attr-then-block-and-grab-first-label pattern into a small helper (e.g.,
GetFirstLabelFromParentAttribute) and use it from GetIncludeLabel,
GetDependencyLabel and the similar methods in internal/ast/stack/stack.go to
eliminate duplication and clarify intent.

In `@internal/ast/stack/stack_test.go`:
- Around line 44-50: The test currently asserts method values
(stackAST.FindNodeAt, GetUnitLabel, etc.) with assert.NotNil which is
meaningless in Go; replace those with a single require.NotNil(t, stackAST) for
interface wiring, remove the no-op var _ = stackAST, and instead add real
behavioral assertions: call FindNodeAt/FindUnitAt/FindStackAt with a concrete
hcl.Pos and assert expected results (or nil) to verify functionality; also add
symmetry tests for GetStackSource and GetStackPath similar to
GetUnitSource/GetUnitPath; if you want an explicit compile-time interface check
add a line like var _ stack.StackAST = (*<concreteType>)(nil) referencing the
concrete implementation type.

In `@internal/ast/stack/stack.go`:
- Around line 40-78: Both GetUnitLabel and GetStackLabel duplicate logic and
contain an unreachable safety check: because isUnitBlock/isStackBlock already
ensure the block has at least one label, remove the unnecessary name := "" and
the len(labels) > 0 guard and read labels[0] directly; then extract the shared
logic into a small helper (e.g., getLabelFromParent(node *ast.IndexedNode,
parentMatch func(*ast.IndexedNode) bool) (string, bool) or a method on stackAST)
that finds the closest attribute parent (ast.IsAttribute), finds the desired
parent via the provided matcher (use isUnitBlock or isStackBlock), and returns
the first label, and rewrite GetUnitLabel and GetStackLabel to call that helper
(also consider reusing the same helper for GetIncludeLabel).
- Around line 162-180: Replace FriendlyName() string comparisons with direct cty
type checks in stackAST.extractStringValue: change checks like
e.Val.Type().FriendlyName() == "string" to e.Val.Type() == cty.String in both
the LiteralValueExpr and TemplateExpr branches, and add the import
"github.com/zclconf/go-cty/cty" if not already present so the cty.String
identifier is available.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 9314d93d-bf70-4325-9285-afa5b66b2588

📥 Commits

Reviewing files that changed from the base of the PR and between df11fce and 04db3be.

📒 Files selected for processing (9)
  • internal/ast/ast.go
  • internal/ast/ast_test.go
  • internal/ast/config/config.go
  • internal/ast/config/config_test.go
  • internal/ast/stack/stack.go
  • internal/ast/stack/stack_test.go
  • internal/stackutils/stackutils.go
  • internal/stackutils/stackutils_test.go
  • internal/tg/definition/definition.go

@diofeher
diofeher marked this pull request as draft April 17, 2026 10:49
diofeher added a commit that referenced this pull request Apr 18, 2026
- Remove dead len(labels) > 0 guards in include/dependency/unit/stack label
  getters (matchers already guarantee a label)
- Extract firstLabelFromContainingBlock helper in ast/stack to DRY
  GetUnitLabel and GetStackLabel
- Use cty.String comparison instead of FriendlyName() == "string" in
  extractStringValue for idiomatic HCL type checking
- Replace tautological predicate in FindFirstParentMatch test with one
  that actually walks the tree but doesn't match
- Remove meaningless assert.NotNil checks on method values in config
  and stack tests
@diofeher
diofeher marked this pull request as ready for review April 18, 2026 11:45

@coderabbitai coderabbitai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@internal/ast/stack/stack.go`:
- Around line 41-65: The helpers require an attribute parent before locating a
block, so passing a block node itself to GetUnitLabel/GetStackLabel fails;
change firstLabelFromContainingBlock to search for the nearest matching block
starting at the node (not via ast.IsAttribute). Replace the attr lookup
(ast.FindFirstParentMatch(node, ast.IsAttribute) ->
ast.FindFirstParentMatch(attr, blockMatcher)) with a direct call that finds the
nearest block matching blockMatcher from node (e.g., block :=
ast.FindFirstParentMatch(node, blockMatcher)), keep the existing nil checks and
return block.Node.(*hclsyntax.Block).Labels[0], true when found so these
functions work when given a block node or any descendant.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 2d0d1279-ca85-4f45-b942-29c0221efe74

📥 Commits

Reviewing files that changed from the base of the PR and between 04db3be and b79cda0.

📒 Files selected for processing (5)
  • internal/ast/ast_test.go
  • internal/ast/config/config.go
  • internal/ast/config/config_test.go
  • internal/ast/stack/stack.go
  • internal/ast/stack/stack_test.go
✅ Files skipped from review due to trivial changes (1)
  • internal/ast/stack/stack_test.go
🚧 Files skipped from review as they are similar to previous changes (3)
  • internal/ast/config/config_test.go
  • internal/ast/config/config.go
  • internal/ast/ast_test.go

Comment on lines +41 to +65
// GetUnitLabel returns the label of the given node, if it is a unit block
func (s *stackAST) GetUnitLabel(node *ast.IndexedNode) (string, bool) {
return firstLabelFromContainingBlock(node, isUnitBlock)
}

// GetStackLabel returns the label of the given node, if it is a stack block
func (s *stackAST) GetStackLabel(node *ast.IndexedNode) (string, bool) {
return firstLabelFromContainingBlock(node, isStackBlock)
}

// firstLabelFromContainingBlock walks up to the containing attribute and then the
// nearest block matching blockMatcher, returning that block's first label.
func firstLabelFromContainingBlock(node *ast.IndexedNode, blockMatcher func(*ast.IndexedNode) bool) (string, bool) {
attr := ast.FindFirstParentMatch(node, ast.IsAttribute)
if attr == nil {
return "", false
}

block := ast.FindFirstParentMatch(attr, blockMatcher)
if block == nil {
return "", false
}

return block.Node.(*hclsyntax.Block).Labels[0], true
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🟡 Minor

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Description: Inspect current StackAST getter usage and confirm whether callers/tests pass block nodes or attribute nodes.

rg -nP -C3 '\b(GetUnitLabel|GetStackLabel|GetUnitSource|GetUnitPath|GetStackSource|GetStackPath|FindUnitAt|FindStackAt)\s*\('

Repository: gruntwork-io/terragrunt-ls

Length of output: 5919


🏁 Script executed:

cat -n internal/ast/stack/stack.go | head -150 | tail -100

Repository: gruntwork-io/terragrunt-ls

Length of output: 4155


🏁 Script executed:

cat -n internal/ast/stack/stack_test.go | sed -n '140,160p'

Repository: gruntwork-io/terragrunt-ls

Length of output: 759


🏁 Script executed:

cat -n internal/ast/stack/stack_test.go | sed -n '270,290p'

Repository: gruntwork-io/terragrunt-ls

Length of output: 579


Make the block getters work from the block itself, not just from attributes inside it.

Here's the thing: FindUnitAt and FindStackAt return block nodes, but if you pass those block nodes to GetUnitLabel or GetUnitSource, they return false. Why? Because both helpers currently require an attribute parent first—but blocks don't have attributes as parents; they're the parent of attributes.

The fix is straightforward: walk directly to the matching block instead of requiring an attribute step. This makes these getters work from anywhere—block labels, headers, or any node inside the block—not just from within attributes.

🐛 Proposed fix
 func firstLabelFromContainingBlock(node *ast.IndexedNode, blockMatcher func(*ast.IndexedNode) bool) (string, bool) {
-	attr := ast.FindFirstParentMatch(node, ast.IsAttribute)
-	if attr == nil {
-		return "", false
-	}
-
-	block := ast.FindFirstParentMatch(attr, blockMatcher)
+	block := ast.FindFirstParentMatch(node, blockMatcher)
 	if block == nil {
 		return "", false
 	}
 
 	return block.Node.(*hclsyntax.Block).Labels[0], true
 }
@@
 func (s *stackAST) getBlockAttribute(node *ast.IndexedNode, blockMatcher func(*ast.IndexedNode) bool, attrName string) (string, bool) {
-	// First, try to find the attribute that contains the current node
-	attr := ast.FindFirstParentMatch(node, ast.IsAttribute)
-	if attr == nil {
-		return "", false
-	}
-
-	// Check if the found attribute has the name we're looking for
-	if attrNode, ok := attr.Node.(*hclsyntax.Attribute); ok {
-		if attrNode.Name == attrName {
-			// Verify we're within the correct block type
-			block := ast.FindFirstParentMatch(attr, blockMatcher)
-			if block != nil {
-				// Extract the string value from the attribute expression
-				return s.extractStringValue(attrNode.Expr)
-			}
-		}
+	block := ast.FindFirstParentMatch(node, blockMatcher)
+	if block == nil {
+		return "", false
 	}
 
-	return "", false
+	blockNode := block.Node.(*hclsyntax.Block)
+	attrNode, ok := blockNode.Body.Attributes[attrName]
+	if !ok || attrNode == nil {
+		return "", false
+	}
+
+	return s.extractStringValue(attrNode.Expr)
 }
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
// GetUnitLabel returns the label of the given node, if it is a unit block
func (s *stackAST) GetUnitLabel(node *ast.IndexedNode) (string, bool) {
return firstLabelFromContainingBlock(node, isUnitBlock)
}
// GetStackLabel returns the label of the given node, if it is a stack block
func (s *stackAST) GetStackLabel(node *ast.IndexedNode) (string, bool) {
return firstLabelFromContainingBlock(node, isStackBlock)
}
// firstLabelFromContainingBlock walks up to the containing attribute and then the
// nearest block matching blockMatcher, returning that block's first label.
func firstLabelFromContainingBlock(node *ast.IndexedNode, blockMatcher func(*ast.IndexedNode) bool) (string, bool) {
attr := ast.FindFirstParentMatch(node, ast.IsAttribute)
if attr == nil {
return "", false
}
block := ast.FindFirstParentMatch(attr, blockMatcher)
if block == nil {
return "", false
}
return block.Node.(*hclsyntax.Block).Labels[0], true
}
// GetUnitLabel returns the label of the given node, if it is a unit block
func (s *stackAST) GetUnitLabel(node *ast.IndexedNode) (string, bool) {
return firstLabelFromContainingBlock(node, isUnitBlock)
}
// GetStackLabel returns the label of the given node, if it is a stack block
func (s *stackAST) GetStackLabel(node *ast.IndexedNode) (string, bool) {
return firstLabelFromContainingBlock(node, isStackBlock)
}
// firstLabelFromContainingBlock walks up to the containing attribute and then the
// nearest block matching blockMatcher, returning that block's first label.
func firstLabelFromContainingBlock(node *ast.IndexedNode, blockMatcher func(*ast.IndexedNode) bool) (string, bool) {
block := ast.FindFirstParentMatch(node, blockMatcher)
if block == nil {
return "", false
}
return block.Node.(*hclsyntax.Block).Labels[0], true
}
🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@internal/ast/stack/stack.go` around lines 41 - 65, The helpers require an
attribute parent before locating a block, so passing a block node itself to
GetUnitLabel/GetStackLabel fails; change firstLabelFromContainingBlock to search
for the nearest matching block starting at the node (not via ast.IsAttribute).
Replace the attr lookup (ast.FindFirstParentMatch(node, ast.IsAttribute) ->
ast.FindFirstParentMatch(attr, blockMatcher)) with a direct call that finds the
nearest block matching blockMatcher from node (e.g., block :=
ast.FindFirstParentMatch(node, blockMatcher)), keep the existing nil checks and
return block.Node.(*hclsyntax.Block).Labels[0], true when found so these
functions work when given a block node or any descendant.

@diofeher diofeher self-assigned this Apr 27, 2026
yhakbar and others added 3 commits April 27, 2026 10:02
Introduce three focused packages ported from PR #55 to support
stack/values-file features in subsequent PRs:
- internal/ast/config: ConfigAST interface with include/dependency
  label extraction and include-scope building.
- internal/ast/stack: StackAST interface with unit/stack block
  helpers (FindUnitAt, FindStackAt, source/path getters).
- internal/stackutils: LookupUnitPath and LookupStackPath against
  a parsed *config.StackConfig.

Co-authored-by: Diogenes Fernandes <diofeher@gmail.com>
Relocate IsIncludeBlock, IsDependencyBlock, GetNodeIncludeLabel,
GetNodeDependencyLabel and the Includes scope out of internal/ast
and into internal/ast/config. Include/dependency are config-file
concerns and do not belong on the generic IndexedAST. Stack and
values files get their own AST packages in follow-ups.

- definition.GetDefinitionTargetWithContext now wraps store.AST in
  astconfig.NewConfigAST and calls GetIncludeLabel/GetDependencyLabel.
- Drop corresponding tests from internal/ast/ast_test.go; equivalent
  coverage lives in internal/ast/config/config_test.go.

Co-authored-by: Diogenes Fernandes <diofeher@gmail.com>
- Remove dead len(labels) > 0 guards in include/dependency/unit/stack label
  getters (matchers already guarantee a label)
- Extract firstLabelFromContainingBlock helper in ast/stack to DRY
  GetUnitLabel and GetStackLabel
- Use cty.String comparison instead of FriendlyName() == "string" in
  extractStringValue for idiomatic HCL type checking
- Replace tautological predicate in FindFirstParentMatch test with one
  that actually walks the tree but doesn't match
- Remove meaningless assert.NotNil checks on method values in config
  and stack tests

Co-authored-by: Diogenes Fernandes <diofeher@gmail.com>
@diofeher
diofeher force-pushed the feat/stack-values-ast-foundation branch from b79cda0 to 893b027 Compare April 27, 2026 13:02
firstLabelFromContainingBlock required an attribute ancestor before
walking up to the matching block, so passing a block node (e.g. one
returned by FindUnitAt/FindStackAt) returned ("", false). Drop the
attribute step and walk directly to the nearest matching block so
the helpers work from the block itself, the label, or any descendant.

Addresses CodeRabbit review on PR #128.
)

// ConfigAST provides methods for working with standard terragrunt.hcl files.
type ConfigAST interface {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why are we abstracting this away to an interface? Can't we just use the concrete type?


// ConfigAST provides methods for working with standard terragrunt.hcl files.
type ConfigAST interface {
// Core AST methods

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Avoid leaving comments like this right above the method signatures they document. Hovering over FindNodeAt is going to return Core AST methods. The comment directly above the method signature should document it.

@@ -0,0 +1,114 @@
// Package config provides AST functionality specific to standard terragrunt.hcl files.
package config

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These are called "units", so I think calling this package "unit" instead of config, which applies to units and stacks is better.

// isIncludeBlock returns TRUE if the node is an HCL block of type "include".
func isIncludeBlock(inode *ast.IndexedNode) bool {
block, ok := inode.Node.(*hclsyntax.Block)
return ok && block.Type == "include" && len(block.Labels) > 0

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Includes can actually have zero labels (for now):
https://docs.terragrunt.com/migrate/bare-include/

return "", false
}

return includeBlock.Node.(*hclsyntax.Block).Labels[0], true

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As mentioned above, includes can have zero labels, so make sure to handle that here.

// Test HCL content with include and dependency blocks
content := `
include "root" {
path = find_in_parent_folders()

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is using deprecated functionality. Make sure that's on purpose:
https://docs.terragrunt.com/migrate/migrating-from-root-terragrunt-hcl/


// Test that locals and includes are captured
locals := configAST.GetLocals()
assert.NotNil(t, locals)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: These assertions could be stronger, right? We can also assert on the length and values.

)

// StackAST provides methods for working with terragrunt.stack.hcl files.
type StackAST interface {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question here about why this is an interface.

}

// buildIncludesScope scans the AST to build the includes scope
func (c *configAST) buildIncludesScope() {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NewConfigAST is called on every GetDefinitionTargetWithContext call, so we're going to re-run this every time. Can we not build this during the initial parse and reuse it?

}

// FindNodeAt returns the node at the given position in the file
func (c *configAST) FindNodeAt(pos hcl.Pos) *ast.IndexedNode {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This doesn't do anything. *ast.IndexedAST is already embedded in *configAST, so the method is already accessible in the parent type.

}

// extractStringValue extracts a string value from various HCL expression types
func (s *stackAST) extractStringValue(expr hclsyntax.Expression) (string, bool) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a method? It doesn't use the state from s *stackAST.

}

// getBlockAttribute is a helper to get attribute values from blocks
func (s *stackAST) getBlockAttribute(node *ast.IndexedNode, blockMatcher func(*ast.IndexedNode) bool, attrName string) (string, bool) {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Why is this a method? It doesn't use the state from s *stackAST (assuming extractStringValue is no longer a method).


indexedAST, err := ast.ParseHCLFile("test.hcl", []byte(tt.content))
require.NoError(t, err)
require.NotNil(t, indexedAST)

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We do this here, then again in the testFuncs, which doesn't actually increase coverage. If this shouldn't ever be nil in any test case, it doesn't make sense to have testFuncs also assert nil-ness.

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This also happens in TestStackAST_Interface, TestConfigAST_Methods and TestStackAST_Methods.

return "", false
}

for _, unit := range stackCfg.Units {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

NIT: Prefer slices.IndexFunc.

@yhakbar yhakbar left a comment

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would just like the question answered about the seemingly unnecessary interface addressed before we proceed.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants