Skip to content

Commit 3ed02af

Browse files
committed
Validate nested end markers are alone on their line
Extract isEndMarkerAloneOnLine helper and apply it to nested end markers so trailing content like <?/mock?> extra emits a diagnostic instead of silently decrementing depth. Add subdirectory recursive expansion test proving no double-pathing occurs with RootFS=readFS. https://claude.ai/code/session_01J3g8NnEwGYyJWXD1bqtTii
1 parent 5877c7e commit 3ed02af

3 files changed

Lines changed: 55 additions & 6 deletions

File tree

internal/archetype/gensection/engine_test.go

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -209,6 +209,24 @@ func TestEngine_Check_MalformedNestedEndMarker(t *testing.T) {
209209
"expected malformed nested end marker diagnostic, got %v", diags)
210210
}
211211

212+
func TestEngine_Check_NestedEndMarkerTrailingContent(t *testing.T) {
213+
// A nested end marker with trailing content on its line should
214+
// emit a diagnostic and not decrement depth.
215+
src := "<?mock\nkey: a\n?>\n<?mock\nkey: b\n?>\ninner\n<?/mock?> extra\n<?/mock?>\n<?/mock?>\n"
216+
f := newTestFile(t, "test.md", src)
217+
d := &mockDirective{content: ""}
218+
e := NewEngine(d)
219+
diags := e.Check(f)
220+
found := false
221+
for _, d := range diags {
222+
if strings.Contains(d.Message, "only content on its line") {
223+
found = true
224+
}
225+
}
226+
assert.True(t, found,
227+
"expected 'only content on its line' diagnostic, got %v", diags)
228+
}
229+
212230
func TestEngine_Check_InvalidYAML(t *testing.T) {
213231
src := "<?mock\n: invalid : yaml ::: [\n?>\n<?/mock?>\n"
214232
f := newTestFile(t, "test.md", src)

internal/archetype/gensection/parse.go

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,10 @@ func FindMarkerPairs(
7777
piLine := f.LineOfOffset(pi.Lines().At(0).Start)
7878
diags = append(diags, MakeDiag(ruleID, ruleName, f.Path, piLine,
7979
fmt.Sprintf("generated section end marker <?%s is missing closing ?>", pi.Name)))
80+
} else if !isEndMarkerAloneOnLine(pi, f) {
81+
piLine := f.LineOfOffset(pi.Lines().At(0).Start)
82+
diags = append(diags, MakeDiag(ruleID, ruleName, f.Path, piLine,
83+
"generated section end marker must be the only content on its line"))
8084
} else {
8185
// Well-formed nested end marker — reduce depth.
8286
depth--
@@ -138,12 +142,7 @@ func handleEndMarker(
138142
fmt.Sprintf("generated section end marker <?%s is missing closing ?>", pi.Name)))
139143
}
140144

141-
// End marker must be the only content on its line.
142-
seg := pi.Lines().At(0)
143-
raw := string(seg.Value(f.Source))
144-
trimmed := strings.TrimSpace(raw)
145-
expected := fmt.Sprintf("<?%s?>", pi.Name)
146-
if trimmed != expected {
145+
if !isEndMarkerAloneOnLine(pi, f) {
147146
return current, pairs, append(diags,
148147
MakeDiag(ruleID, ruleName, f.Path, piLine,
149148
"generated section end marker must be the only content on its line"))
@@ -154,6 +153,16 @@ func handleEndMarker(
154153
return nil, append(pairs, *current), diags
155154
}
156155

156+
// isEndMarkerAloneOnLine checks that the end marker PI is the only
157+
// content on its source line.
158+
func isEndMarkerAloneOnLine(pi *lint.ProcessingInstruction, f *lint.File) bool {
159+
seg := pi.Lines().At(0)
160+
raw := string(seg.Value(f.Source))
161+
trimmed := strings.TrimSpace(raw)
162+
expected := fmt.Sprintf("<?%s?>", pi.Name)
163+
return trimmed == expected
164+
}
165+
157166
// extractYAMLBody returns the YAML content from a PI's Lines(),
158167
// skipping the first line (the <?name line).
159168
func extractYAMLBody(pi *lint.ProcessingInstruction, source []byte) string {

internal/rules/include/rule_test.go

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -656,6 +656,28 @@ func TestFix_RecursiveExpansionWithFrontmatter(t *testing.T) {
656656
"Fix output mismatch\ngot:\n%s\nwant:\n%s", got, want)
657657
}
658658

659+
func TestFix_RecursiveExpansionSubdir(t *testing.T) {
660+
// B is in a subdirectory and includes C relative to itself.
661+
// Recursive expansion must resolve C correctly via readFS,
662+
// not double the subdirectory path.
663+
fsys := fstest.MapFS{
664+
"sub/b.md": {Data: []byte(
665+
"# B\n\n<?include\nfile: c.md\n?>\n" +
666+
"stale\n<?/include?>\n")},
667+
"sub/c.md": {Data: []byte("Fresh from C\n")},
668+
}
669+
src := "# A\n\n<?include\nfile: sub/b.md\n?>\n" +
670+
"old\n<?/include?>\n"
671+
f := newTestFile(t, "a.md", src, fsys)
672+
r := &Rule{}
673+
got := string(r.Fix(f))
674+
want := "# A\n\n<?include\nfile: sub/b.md\n?>\n" +
675+
"# B\n\n<?include\nfile: c.md\n?>\n" +
676+
"Fresh from C\n<?/include?>\n<?/include?>\n"
677+
assert.Equal(t, want, got,
678+
"Fix output mismatch\ngot:\n%s\nwant:\n%s", got, want)
679+
}
680+
659681
// =====================================================================
660682
// No FS
661683
// =====================================================================

0 commit comments

Comments
 (0)