refactor(spec): add selectable legacy and structural editors - #333
Open
Thien Trung Vuong (trungams) wants to merge 2 commits into
Open
Conversation
Thien Trung Vuong (trungams)
force-pushed
the
tvuong/structural-spec-editor-implementation
branch
from
September 2, 2026 23:50
d8cf4b5 to
8457ab8
Compare
Thien Trung Vuong (trungams)
force-pushed
the
tvuong/structural-spec-editor-implementation
branch
from
September 10, 2026 03:54
8457ab8 to
bbe0fcf
Compare
Copilot started reviewing on behalf of
Thien Trung Vuong (trungams)
September 10, 2026 03:55
View session
Contributor
There was a problem hiding this comment.
🟡 Changes recommended
Unresolved moderate findings affect release handling, structural edits, tag matching, and legacy mutation coverage.
Once you've addressed the issues Copilot identified, you can request another Copilot review.
Pull request overview
This PR unifies legacy and structural RPM spec editing behind the existing Spec API while preserving legacy behavior by default.
Changes:
- Adds selectable editor implementations and facade delegation.
- Implements structural section, tag, search, patch, and changelog operations.
- Propagates editor selection through source preparation, overlays, release, and provenance handling.
- Adds fixtures and regression tests for macro- and conditional-heavy specs.
File summaries
| File | Summary / review note |
|---|---|
internal/rpm/spec/tree_fixture_internal_test.go |
Structural fixture stress tests. |
internal/rpm/spec/testdata/specs/subpackage-define-unreferenced.spec |
Fixture for unreferenced subpackage definitions. |
internal/rpm/spec/testdata/specs/straddling-wrapper.spec |
Fixture for sections straddling wrappers. |
internal/rpm/spec/testdata/specs/script-section-tag-shaped.spec |
Fixture for script and tag-shaped sections. |
internal/rpm/spec/testdata/specs/nested-wrappers.spec |
Fixture for nested conditional wrappers. |
internal/rpm/spec/testdata/specs/multi-package-mixed.spec |
Fixture for mixed multi-package content. |
internal/rpm/spec/testdata/specs/macro-with-parameters.spec |
Fixture for parameterized macros. |
internal/rpm/spec/testdata/specs/macro-continuation.spec |
Fixture for macro continuations. |
internal/rpm/spec/testdata/specs/macro-conditional.spec |
Fixture for macro conditionals. |
internal/rpm/spec/testdata/specs/if-with-continuation.spec |
Fixture for conditional continuations. |
internal/rpm/spec/testdata/specs/elif-with-sections.spec |
Fixture for sections within elif branches. |
internal/rpm/spec/testdata/specs/elif-chain.spec |
Fixture for elif chains. |
internal/rpm/spec/testdata/specs/comment-only-conditional.spec |
Fixture for comment-only conditionals. |
internal/rpm/spec/testdata_test.go |
Structural fixture edit tests. |
internal/rpm/spec/structural_tree_api.go |
Tree accessors and mutations; section-name matching needs to remain case-insensitive (moderate, 2 votes). |
internal/rpm/spec/structural_tree_api_internal_test.go |
Structural tree API tests. |
internal/rpm/spec/structural_spec.go |
Structural spec representation. |
internal/rpm/spec/structural_edit.go |
Structural editing operations; replacement-driven continuation changes can leave classification stale (moderate, 1 vote). |
internal/rpm/spec/spec_test.go |
Spec behavior tests. |
internal/rpm/spec/legacy_spec.go |
Preserved legacy spec implementation. |
internal/rpm/spec/legacy_edit.go |
Preserved legacy editing operations. |
internal/rpm/spec/editor.go |
Public facade and editor selection; exported facade methods need documentation (nit, 1 vote). |
internal/rpm/spec/edit_test.go |
Editor behavior tests; retain default-mode mutation coverage (moderate, 1 vote). |
internal/app/azldev/core/sources/upstream_provenance.go |
Editor propagation for provenance handling. |
internal/app/azldev/core/sources/upstream_provenance_internal_test.go |
Provenance parser tests. |
internal/app/azldev/core/sources/sourceprep.go |
Spec editor preparer option; option documentation is misplaced (nit, 2 votes). |
internal/app/azldev/core/sources/release.go |
Release tag editor integration; preserve empty-value handling (moderate, 2 votes). |
internal/app/azldev/core/sources/release_test.go |
Release parsing tests. |
internal/app/azldev/core/sources/release_internal_test.go |
Release bump tests. |
internal/app/azldev/core/sources/overlays.go |
Overlay editor propagation. |
internal/app/azldev/core/sources/overlays_test.go |
Sequential overlay tests. |
Review details
Suppressed comments (3)
internal/rpm/spec/edit_test.go:283
- These cases now explicitly open
EditorStructural, so the edit tables that previously exercised the default editor no longer verify the preserved legacy implementation. Production still defaults to legacy, and the remaining default-mode tests cover visitors/read-only behavior rather than these mutations; retain default-mode coverage or run the mutation tables against both editor modes.
specFile, err := spec.OpenSpec(strings.NewReader(test.input), spec.WithEditor(spec.EditorStructural))
internal/rpm/spec/editor.go:116
- The new public
Specfacade methods (starting withReplaceLine/RemoveLine) have no name-prefixed documentation; the comments on the old legacy implementations no longer document these declarations. Because the repository enables revive'sexportedrule (.golangci.yml:147-152), this file will fail the configured lint and the public API loses its method contracts. Add concise doc comments to each exported facade method.
func (s *Spec) ReplaceLine(lineNumber int, replacement string) {
s.editor.ReplaceLine(lineNumber, replacement)
}
func (s *Spec) RemoveLine(lineNumber int) { s.editor.RemoveLine(lineNumber) }
func (s *Spec) RemoveLines(start, end int) { s.editor.RemoveLines(start, end) }
func (s *Spec) InsertLinesAt(lines []string, lineNumber int) {
s.editor.InsertLinesAt(lines, lineNumber)
}
internal/rpm/spec/structural_edit.go:604
headerAtand the macro/conditional state are derived from the original lines, even though replacements are written back tolinesbefore the next iteration. If a replacement adds or removes the trailing\\on a%define/%globalline, subsequent%packageor%iflines change between macro text and structural content, but this walk still uses the old classification; section-scoped replacements can then target the wrong section or be skipped. Recompute classification from the updated lines (or explicitly reject replacements that change continuation structure).
headers := findSectionHeaderLines(lines)
headerAt := make(map[int]bool, len(headers))
- Files reviewed: 31/31 changed files
- Comments generated: 3
- Review effort level: Lite
💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.
Comment on lines
+48
to
53
| releaseValue, err := openedSpec.GetLastTag("", "Release") | ||
| if err != nil { | ||
| return "", fmt.Errorf("failed to visit tags in spec %#q:\n%w", specPath, err) | ||
| } | ||
|
|
||
| if releaseValue == "" { | ||
| return "", fmt.Errorf("release tag not found in spec %#q:\n%w", specPath, spec.ErrNoSuchTag) | ||
| return "", fmt.Errorf("failed to get Release tag from spec %#q:\n%w", specPath, err) | ||
| } | ||
|
|
||
| return releaseValue, nil |
| // contain shell that happens to match the "word: word" pattern; we must avoid | ||
| // treating those as tags. | ||
| func isTagBearingSection(secName string) bool { | ||
| return secName == "" || secName == packageSectionName |
| // Git-tracked files (spec, patches, scripts, configs) are still fetched from | ||
| // the upstream clone. This is useful for rendering, where only the spec and | ||
| // sidecar files are needed and downloading large source tarballs is unnecessary. | ||
| func WithSpecEditor(mode spec.EditorMode) PreparerOption { |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
This PR puts the existing line-oriented editor and the new structural editor behind the same public
SpecAPI. The legacy implementation remains intact, while the structural implementation uses the parser and tree API introduced in the previous PR.Editor selection happens once when a spec is opened. There are no mode checks scattered through individual operations and no automatic fallback from one editor to the other. This PR also adds a curated set of real-world spec fixtures to exercise the structural implementation. Production continues to use the legacy editor until the rollout configuration is introduced at the top of the stack.
Motivation
Issue #214 calls for structural awareness when editing sections and conditionals. At the same time, azldev is stabilizing and we want an escape hatch during adoption. Keeping both implementations behind one facade gives projects a controlled migration path without changing the public editing API.
Changes
Specas the unchanged public facade.Validation
mage buildmage unitThe cumulative stack was mechanically rebased onto
eb9fb3fand revalidated. The complete rollout was also exercised against the full Azure Linux structural render corpus and the default-legacy E2E suite.Known limitations
Macro preservation during section and subpackage removal is added by the next PR. The structural editor does not evaluate conditional expressions, so content after a wrapper's
%endifcannot always be attributed to a section declared inside that wrapper. There is intentionally no automatic fallback between editors.