Skip to content

Commit 162105b

Browse files
committed
review(round1): single-pass foreign-region restore + coverage
Code-review round 1 fixes for the foreign-regions feature (PR #749). Correctness: no bugs found. Scan (line ranges) and matchedRegionSpans (byte spans) agree on the matched-pair state machine, GeneratedRanges are produced and consumed in consistent post-front-matter coordinates, and the parse-time AppendRanges / read-only Diagnostics split avoids mutating the shared cached *File. Efficiency/simplification: - foreignregion.Restore now rebuilds the buffer in a single forward pass (spliceSpans) instead of a full-buffer rebuild per span (was O(k*n) allocations for k regions). - checkForeignRegionList trims each marker once instead of up to five times in the equality branch. Coverage (CI diff gate): add unit tests covering the previously uncovered paths — Apply/AppendRanges/Diagnostics/resolve, the Scan nil-file guard, EffectiveForeignRegions nil-config and empty-override branches, the override validation path, empty-end rejection, and copyForeignRegions. All three flagged files now at 100%. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01EwKxDxhjkTQPPkBrNhsrNG
1 parent 3d55f02 commit 162105b

4 files changed

Lines changed: 163 additions & 15 deletions

File tree

internal/config/foreignregion.go

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -57,16 +57,18 @@ func validateForeignRegions(cfg *Config) error {
5757

5858
func checkForeignRegionList(regions []ForeignRegion) error {
5959
for i, r := range regions {
60-
if strings.TrimSpace(r.Start) == "" {
60+
start := strings.TrimSpace(r.Start)
61+
end := strings.TrimSpace(r.End)
62+
if start == "" {
6163
return fmt.Errorf("foreign-regions[%d]: start marker must not be empty", i)
6264
}
63-
if strings.TrimSpace(r.End) == "" {
65+
if end == "" {
6466
return fmt.Errorf("foreign-regions[%d]: end marker must not be empty", i)
6567
}
66-
if strings.TrimSpace(r.Start) == strings.TrimSpace(r.End) {
68+
if start == end {
6769
return fmt.Errorf(
6870
"foreign-regions[%d]: start and end markers must differ (both %q)",
69-
i, strings.TrimSpace(r.Start))
71+
i, start)
7072
}
7173
}
7274
return nil
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
package config
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/assert"
7+
"github.com/stretchr/testify/require"
8+
)
9+
10+
// TestEffectiveForeignRegionsNilConfig returns nil for a nil config.
11+
func TestEffectiveForeignRegionsNilConfig(t *testing.T) {
12+
assert.Nil(t, EffectiveForeignRegions(nil, "README.md"))
13+
}
14+
15+
// TestEffectiveForeignRegionsSkipsEmptyOverride ignores an override that
16+
// declares no foreign regions, even when its glob matches.
17+
func TestEffectiveForeignRegionsSkipsEmptyOverride(t *testing.T) {
18+
cfg := &Config{
19+
ForeignRegions: []ForeignRegion{{Start: "<!-- a -->", End: "<!-- b -->"}},
20+
Overrides: []Override{
21+
{Glob: []string{"README.md"}}, // matches, but has no ForeignRegions
22+
},
23+
}
24+
got := EffectiveForeignRegions(cfg, "README.md")
25+
require.Len(t, got, 1)
26+
assert.Equal(t, "<!-- a -->", got[0].Start)
27+
}
28+
29+
// TestParseForeignRegionsEmptyEndRejected rejects a marker pair with a
30+
// blank end marker.
31+
func TestParseForeignRegionsEmptyEndRejected(t *testing.T) {
32+
yml := `foreign-regions:
33+
- start: "<!-- apm:start -->"
34+
end: ""
35+
`
36+
_, err := ParseBytes([]byte(yml))
37+
require.Error(t, err)
38+
assert.Contains(t, err.Error(), "end marker must not be empty")
39+
}
40+
41+
// TestParseForeignRegionsOverrideInvalidRejected surfaces a malformed
42+
// marker pair declared on an override, not just the top-level list.
43+
func TestParseForeignRegionsOverrideInvalidRejected(t *testing.T) {
44+
yml := `overrides:
45+
- glob: ["AGENTS.md"]
46+
foreign-regions:
47+
- start: ""
48+
end: "<!-- gen:end -->"
49+
`
50+
_, err := ParseBytes([]byte(yml))
51+
require.Error(t, err)
52+
assert.Contains(t, err.Error(), "start marker must not be empty")
53+
}
54+
55+
// TestCopyForeignRegionsNil returns nil for a nil input.
56+
func TestCopyForeignRegionsNil(t *testing.T) {
57+
assert.Nil(t, copyForeignRegions(nil))
58+
}
59+
60+
// TestCopyForeignRegionsIsolatesBackingArray returns an independent copy
61+
// whose mutation does not touch the source slice.
62+
func TestCopyForeignRegionsIsolatesBackingArray(t *testing.T) {
63+
src := []ForeignRegion{{Start: "<!-- a -->", End: "<!-- b -->"}}
64+
out := copyForeignRegions(src)
65+
require.Len(t, out, 1)
66+
assert.Equal(t, src[0], out[0])
67+
out[0].Start = "mutated"
68+
assert.Equal(t, "<!-- a -->", src[0].Start, "copy must not alias the source")
69+
}
Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,72 @@
1+
package foreignregion
2+
3+
import (
4+
"testing"
5+
6+
"github.com/jeduden/mdsmith/internal/config"
7+
"github.com/jeduden/mdsmith/internal/lint"
8+
"github.com/stretchr/testify/assert"
9+
"github.com/stretchr/testify/require"
10+
)
11+
12+
func apmCfg() *config.Config {
13+
return &config.Config{ForeignRegions: []config.ForeignRegion{apm}}
14+
}
15+
16+
// TestScanNilFile returns nothing for a nil *File rather than panicking.
17+
func TestScanNilFile(t *testing.T) {
18+
ranges, diags := Scan(nil, []config.ForeignRegion{apm})
19+
assert.Nil(t, ranges)
20+
assert.Nil(t, diags)
21+
}
22+
23+
// TestApplyExtendsRangesAndReturnsDiags appends the matched-pair spans to
24+
// f.GeneratedRanges and returns the malformed-region diagnostics.
25+
func TestApplyExtendsRangesAndReturnsDiags(t *testing.T) {
26+
// A matched pair plus a trailing unmatched start (malformed).
27+
src := "<!-- apm:start -->\nbody\n<!-- apm:end -->\n<!-- apm:start -->\ndangling\n"
28+
f := newFile(t, src)
29+
diags := Apply(f, apmCfg(), "test.md")
30+
require.Len(t, f.GeneratedRanges, 1)
31+
assert.Equal(t, lint.LineRange{From: 1, To: 3}, f.GeneratedRanges[0])
32+
require.Len(t, diags, 1)
33+
assert.Equal(t, "test.md", diags[0].File)
34+
assert.Contains(t, diags[0].Message, "no matching end")
35+
}
36+
37+
// TestApplyNoRegions leaves GeneratedRanges untouched and returns no
38+
// diagnostics when the config declares no marker pairs.
39+
func TestApplyNoRegions(t *testing.T) {
40+
f := newFile(t, "# Title\n\nbody\n")
41+
diags := Apply(f, &config.Config{}, "test.md")
42+
assert.Empty(t, f.GeneratedRanges)
43+
assert.Nil(t, diags)
44+
}
45+
46+
// TestAppendRangesPopulatesWithoutDiags extends the exclusion set but
47+
// discards the malformed diagnostics (the read-only RunSource path).
48+
func TestAppendRangesPopulatesWithoutDiags(t *testing.T) {
49+
src := "<!-- apm:start -->\nbody\n<!-- apm:end -->\n"
50+
f := newFile(t, src)
51+
AppendRanges(f, apmCfg(), "test.md")
52+
require.Len(t, f.GeneratedRanges, 1)
53+
assert.Equal(t, lint.LineRange{From: 1, To: 3}, f.GeneratedRanges[0])
54+
}
55+
56+
// TestDiagnosticsReturnsWithoutMutating rebuilds the malformed diagnostics
57+
// without touching f.GeneratedRanges.
58+
func TestDiagnosticsReturnsWithoutMutating(t *testing.T) {
59+
src := "<!-- apm:end -->\n"
60+
f := newFile(t, src)
61+
diags := Diagnostics(f, apmCfg(), "test.md")
62+
assert.Empty(t, f.GeneratedRanges)
63+
require.Len(t, diags, 1)
64+
assert.Contains(t, diags[0].Message, "without a matching start")
65+
assert.Equal(t, "test.md", diags[0].File)
66+
}
67+
68+
// TestDiagnosticsNoRegions returns nil when no marker pairs apply.
69+
func TestDiagnosticsNoRegions(t *testing.T) {
70+
f := newFile(t, "# Title\n")
71+
assert.Nil(t, Diagnostics(f, &config.Config{}, "test.md"))
72+
}

internal/foreignregion/restore.go

Lines changed: 16 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,21 +27,26 @@ func Restore(original, fixed []byte, regions []config.ForeignRegion) []byte {
2727
if len(origSpans) == 0 || len(origSpans) != len(fixedSpans) {
2828
continue
2929
}
30-
// Replace from the last span to the first so each splice leaves
31-
// the earlier spans' byte offsets valid.
32-
for i := len(fixedSpans) - 1; i >= 0; i-- {
33-
origText := original[origSpans[i].start:origSpans[i].end]
34-
fs := fixedSpans[i]
35-
out := make([]byte, 0, len(fixed)-(fs.end-fs.start)+len(origText))
36-
out = append(out, fixed[:fs.start]...)
37-
out = append(out, origText...)
38-
out = append(out, fixed[fs.end:]...)
39-
fixed = out
40-
}
30+
fixed = spliceSpans(original, fixed, origSpans, fixedSpans)
4131
}
4232
return fixed
4333
}
4434

35+
// spliceSpans rebuilds fixed in one pass, replacing each fixed span with
36+
// the corresponding original span's bytes. Spans are in document order,
37+
// so a single forward walk suffices — one allocation instead of the
38+
// per-span full-buffer rebuild a reverse splice would cost.
39+
func spliceSpans(original, fixed []byte, origSpans, fixedSpans []byteSpan) []byte {
40+
out := make([]byte, 0, len(fixed))
41+
prev := 0
42+
for i, fs := range fixedSpans {
43+
out = append(out, fixed[prev:fs.start]...)
44+
out = append(out, original[origSpans[i].start:origSpans[i].end]...)
45+
prev = fs.end
46+
}
47+
return append(out, fixed[prev:]...)
48+
}
49+
4550
// byteSpan is a half-open [start, end) byte range within a buffer.
4651
type byteSpan struct {
4752
start int

0 commit comments

Comments
 (0)