Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# Tree Walking & Editing — Investigation Notes

## Context

Investigating "power-user" features for walking/editing the parsed LarkElement tree.

## Two Uncommitted Approaches Found

### 1. `hcl2/processor.py` — `RulesProcessor` (Selenium-like wrapper)

Already fleshed out. Wraps a `LarkRule` node, returns new `RulesProcessor` instances (chainable).

**Capabilities:**

- **Search:** `find_blocks()`, `find_attributes()`, `find_rules()`, `find_by_predicate()`
- **Walk:** `walk()` generator yielding `(processor, children)` tuples
- **Navigate:** `next()`, `previous()`, `siblings`, `next_siblings`, `previous_siblings`
- **Mutate:** `replace(new_node)`, `append_child(new_node)`
- Commented out: `insert_before()`

### 2. `hcl2/editor.py` — `Editor` with `TreePath` (XPath-like navigation)

Embryonic. `TreePath` = list of `(rule_name, index)` steps. Has debug `print()` statements. `visit()` method sketched but commented out.

## Recommendation: Selenium-style (`RulesProcessor`) wins

**Why it fits:**

- Tree is heterogeneous — chainable typed wrappers that "find, then act" match well
- Users want semantic searches (find blocks by label, attributes by name) — Selenium's `find_element_by_*` pattern
- Fluent chaining: `proc.find_block(["resource", "aws_instance"]).attribute("ami").replace(new_value)`
- Already handles skipping whitespace/comment nodes during navigation

**Why XPath-style (`Editor`) doesn't fit:**

- Paths are fragile — depend on exact tree structure and child indices
- Users shouldn't need to know `attribute → expr_term → expression → literal` path structure
- Doesn't compose with searches — purely positional

## Gaps in `RulesProcessor` Before It's a Complete Power-User API

1. **Missing mutations:** `insert_before()` / `insert_after()` / `remove()` / `delete()`
1. **Not exposed in `api.py`** — no public entry point
1. **No `__repr__`** — hard to inspect interactively
1. **No Builder integration** — no easy way to construct replacement nodes inline
1. **`walk()` quirk** — wraps all children including `None` and tokens, can be awkward
1. **Naming** — could become `HCLNavigator` or `TreeCursor` for public surface

## Key Architecture Context

- `LarkElement` (ABC) → `LarkToken` (terminals) and `LarkRule` (non-terminals)
- Bidirectional parent-child refs auto-set in `LarkRule.__init__`
- ~41 rule classes across 7 domain files
- Primitive mutation: `LarkToken.set_value()`, direct `_children` list mutation
4 changes: 2 additions & 2 deletions .coveragerc
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@ omit =
hcl2/lark_parser.py
hcl2/version.py
hcl2/__init__.py
hcl2/rules/__init__.py
hcl2/rules/__init__.py

[report]
show_missing = true
fail_under = 80
fail_under = 95
77 changes: 77 additions & 0 deletions hcl2/editor.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,77 @@
import dataclasses
from copy import copy, deepcopy
from typing import List, Optional, Set, Tuple

from hcl2.rule_transformer.rules.abstract import LarkRule
from hcl2.rule_transformer.rules.base import BlockRule, StartRule


@dataclasses.dataclass
class TreePathElement:

name: str
index: int = 0


@dataclasses.dataclass
class TreePath:

elements: List[TreePathElement] = dataclasses.field(default_factory=list)

@classmethod
def build(cls, elements: List[Tuple[str, Optional[int]] | str]):
results = []
for element in elements:
if isinstance(element, tuple):
if len(element) == 1:
result = TreePathElement(element[0], 0)
else:
result = TreePathElement(*element)
else:
result = TreePathElement(element, 0)

results.append(result)

return cls(results)

def __iter__(self):
return self.elements.__iter__()

def __len__(self):
return self.elements.__len__()


class Editor:
def __init__(self, rules_tree: LarkRule):
self.rules_tree = rules_tree

@classmethod
def _find_one(cls, rules_tree: LarkRule, path_element: TreePathElement) -> LarkRule:
return cls._find_all(rules_tree, path_element.name)[path_element.index]

@classmethod
def _find_all(cls, rules_tree: LarkRule, rule_name: str) -> List[LarkRule]:
children = []
print("rule", rules_tree)
print("rule children", rules_tree.children)
for child in rules_tree.children:
if isinstance(child, LarkRule) and child.lark_name() == rule_name:
children.append(child)

return children

def find_by_path(self, path: TreePath, rule_name: str) -> List[LarkRule]:
path = deepcopy(path.elements)

current_rule = self.rules_tree
while len(path) > 0:
current_path, *path = path
print(current_path, path)
current_rule = self._find_one(current_rule, current_path)

return self._find_all(current_rule, rule_name)

# def visit(self, path: TreePath) -> "Editor":
#
# while len(path) > 1:
# current =
Loading
Loading