refactor(spec): add lossless structural parser and tree API - #332
refactor(spec): add lossless structural parser and tree API#332Thien Trung Vuong (trungams) wants to merge 2 commits into
Conversation
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
There was a problem hiding this comment.
🔵 Needs a closer look
The PR adds a substantial new parsing subsystem with complex lexical/state handling, so it warrants final human review despite strong test coverage and only minor review findings.
Pull request overview
Introduces a private, lossless structural RPM spec parser that builds a block tree (sections, conditionals, text, macro definitions) and provides a transactional tree API for querying and performing safe structural edits while preserving byte-for-byte serialization.
Changes:
- Added a two-pass structural parser and serializer to round-trip spec input without altering whitespace/comments.
- Added internal
specTree/sectionHandleAPIs for section queries and transactional mutation with post-mutation validation. - Added unit tests covering conditional nesting/branches, macro continuation opacity (including Lua/expand bodies), and removal validation invariants.
File summaries
| File | Description |
|---|---|
| internal/rpm/spec/tree.go | Implements the structural parse/serialize logic and macro/conditional/section scanning helpers. |
| internal/rpm/spec/tree_test.go | Adds round-trip and malformed-input tests for the structural parser, plus macro edge cases. |
| internal/rpm/spec/tree_raw_braces_test.go | Adds regression coverage for raw-brace shell fragments inside %{expand: ...} macro bodies. |
| internal/rpm/spec/structural_tree_api.go | Adds internal tree query/mutation primitives (sections, append/prepend lines, remove sections). |
| internal/rpm/spec/structural_tree_api_internal_test.go | Adds transactional semantics tests and removal-safety validation tests for the tree API. |
| internal/rpm/spec/structural_spec.go | Introduces the private structuralSpec wrapper holding raw spec lines. |
Review details
- Files reviewed: 6/6 changed files
- Comments generated: 2
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
| func (t *specTree) HasSection(name string) bool { | ||
| found := false | ||
|
|
||
| walkBlocks(t.root, func(blk *block) bool { | ||
| if blk.Kind == sectionBlock && blk.Name == name { | ||
| found = true | ||
| } | ||
|
|
||
| return !found | ||
| }) | ||
|
|
||
| return found | ||
| } |
| // findSectionHeaderLines returns the 0-indexed line numbers of all section headers, | ||
| // respecting line continuations (backslash-terminated lines suppress the next line). |
92f2d71 to
811e2f8
Compare
There was a problem hiding this comment.
🔵 Needs a closer look
The change introduces a substantial new parsing subsystem whose correctness depends on many subtle edge cases, warranting careful human review despite good test coverage.
Review details
Suppressed comments (2)
internal/rpm/spec/structural_tree_api.go:232
- Same as above: this can refer to a '%elif…' header as an “%if block”. Using a neutral term like “conditional block” would make the error accurate for both %if and %elif nodes.
if wouldEmptySectionWrapper(child, removeSet) && index+1 < len(children) {
next := children[index+1]
if next.Kind == conditionalBlock && !containsSectionBlocks(next) && conditionalHasTextOrMacroContent(next) {
return fmt.Errorf("content in %%if block at %#q would be orphaned after removing the preceding section:\n%w",
next.Header, ErrConditionalSpansSections)
}
internal/rpm/spec/tree.go:232
- The comment says this function respects general backslash continuations, but the implementation only skips directive-shaped lines inside multi-line '%define'/'%global' bodies (and intentionally does not treat ordinary '\' continuations as structural suppression per parseTree's doc comment). This is misleading for future maintainers.
// findSectionHeaderLines returns the 0-indexed line numbers of all section headers,
// respecting line continuations (backslash-terminated lines suppress the next line).
- Files reviewed: 6/6 changed files
- Comments generated: 1
- Review effort level: Lite
| if conditionalHasTextOrMacroContent(child) && containsSectionBlocks(child) { | ||
| if preceding != nil && removeSet[preceding] { | ||
| return fmt.Errorf("%%if block at %#q contains content belonging to the preceding section:\n%w", | ||
| child.Header, ErrConditionalSpansSections) | ||
| } | ||
| } |
Summary
RPM spec files are only loosely structured, and the existing line-oriented editor has to infer section boundaries after the fact. That becomes fragile around nested conditionals, multiline macros, and sections that begin or end inside
%ifblocks.This PR adds the private foundation for a structural editor. It parses a spec into a lossless tree of sections, conditionals, text, and macro definitions, and provides transactional query and mutation primitives over that tree. Parsing and serializing a valid spec preserves its contents byte for byte.
Nothing selects this parser in production yet, and the public overlay configuration is unchanged.
Motivation
Issue #214 collects several cases where line ranges are not enough to preserve section and conditional boundaries safely. A structural representation lets later changes reason about those boundaries directly without trying to evaluate RPM conditions or expand macros.
Changes
%if/%elif/%else/%endif, including nested and empty branches.%defineand%globalbodies opaque.${...}expressions, comments, blank lines, and original ordering.structuralSpec,specTree, and section handles under their final filenames and types.Validation
mage buildmage unitThe cumulative stack was mechanically rebased onto
eb9fb3fand revalidated after the rebase.Known limitations
The parser deliberately does not evaluate RPM macros or conditional expressions. Sections created dynamically through macro expansion are therefore not visible to it. Public editor selection and actual overlay integration arrive in later PRs in the stack.