Skip to content

Commit a951228

Browse files
committed
fix(MDS071): read front matter from f.Source before the f.FS re-read
Address Copilot review: a File parsed without front-matter stripping (e.g. lint.NewFile) keeps the prefix inline in f.Source, so prefer that in-memory copy over a workspace-FS re-read. This makes required-frontmatter correct for callers that don't wire f.FS (previously such a file's fields read as missing) and avoids an unnecessary disk read. Split the fallback test to cover both the f.Source and f.FS paths; coverage stays at 100%. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_012fET55utKej7P3VWhiNnLW
1 parent 687d6a0 commit a951228

2 files changed

Lines changed: 39 additions & 19 deletions

File tree

internal/rules/requiredfrontmatter/rule.go

Lines changed: 15 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -114,17 +114,23 @@ func (r *Rule) diag(f *lint.File, line int, msg string) lint.Diagnostic {
114114
}
115115

116116
// docFrontMatter returns the file's parsed top-level front matter, or
117-
// nil when none is available. It prefers f.FrontMatter, which the
118-
// engine populates in production. When that is empty — files built via
119-
// lint.NewFile in unit and fixture tests, or a real file with no front
120-
// matter — it falls back to reading the file from the workspace FS so a
121-
// file's own front matter is still visible. An unreadable path yields
122-
// nil, which Check treats as "every field missing".
117+
// nil when none is available. It reads, in order: f.FrontMatter (the
118+
// engine populates it when it strips front matter); f.Source (a File
119+
// parsed without stripping — e.g. lint.NewFile — keeps the prefix
120+
// inline, so use that in-memory copy rather than a disk re-read); and
121+
// finally the file re-read from f.FS (the fixture harness builds a File
122+
// from an already-stripped body, so the original on disk still carries
123+
// the front matter). An unreadable path yields nil, which Check treats
124+
// as "every field missing".
123125
func (r *Rule) docFrontMatter(f *lint.File) map[string]any {
124126
fmBytes := f.FrontMatter
125-
if len(fmBytes) == 0 && f.FS != nil && f.Path != "" {
126-
if data, err := fs.ReadFile(f.FS, filepath.ToSlash(f.Path)); err == nil {
127-
fmBytes, _ = lint.StripFrontMatter(data)
127+
if len(fmBytes) == 0 {
128+
if prefix, _ := lint.StripFrontMatter(f.Source); len(prefix) > 0 {
129+
fmBytes = prefix
130+
} else if f.FS != nil && f.Path != "" {
131+
if data, err := fs.ReadFile(f.FS, filepath.ToSlash(f.Path)); err == nil {
132+
fmBytes, _ = lint.StripFrontMatter(data)
133+
}
128134
}
129135
}
130136
body := extractYAMLBody(fmBytes)

internal/rules/requiredfrontmatter/rule_test.go

Lines changed: 24 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -119,21 +119,35 @@ func TestCheck_ExcludeReservedFiles(t *testing.T) {
119119
assert.Len(t, r.Check(f), 1)
120120
}
121121

122-
func TestCheck_FSFallbackReadsOwnFrontMatter(t *testing.T) {
123-
// When f.FrontMatter is empty (files built via lint.NewFile), the
124-
// rule reads the file from f.FS so its own front matter is visible.
125-
src := "---\ntype: Playbook\n---\n# Steps\n"
126-
f, err := lint.NewFile("play.md", []byte(src))
122+
func TestCheck_ReadsFrontMatterFromSource(t *testing.T) {
123+
// A File parsed without stripping (lint.NewFile) keeps its front
124+
// matter in f.Source; the rule reads it with no f.FS wired.
125+
r := &Rule{Fields: []string{"type"}}
126+
f, err := lint.NewFile("play.md", []byte("---\ntype: Playbook\n---\n# Steps\n"))
127+
require.NoError(t, err)
128+
assert.Nil(t, r.Check(f))
129+
130+
f2, err := lint.NewFile("play.md", []byte("---\ntitle: x\n---\n# Steps\n"))
127131
require.NoError(t, err)
128-
f.FS = fstest.MapFS{"play.md": &fstest.MapFile{Data: []byte(src)}}
132+
assert.Len(t, r.Check(f2), 1)
133+
}
134+
135+
func TestCheck_FSFallbackWhenSourceStripped(t *testing.T) {
136+
// The fixture harness builds a File from an already-stripped body, so
137+
// f.Source carries no front matter; the rule re-reads the original
138+
// from f.FS to see the file's own front matter.
129139
r := &Rule{Fields: []string{"type"}}
140+
f, err := lint.NewFile("play.md", []byte("# Steps\n"))
141+
require.NoError(t, err)
142+
f.FS = fstest.MapFS{"play.md": &fstest.MapFile{
143+
Data: []byte("---\ntype: Playbook\n---\n# Steps\n")}}
130144
assert.Nil(t, r.Check(f))
131145

132-
// Same file path but the on-disk copy lacks type → flagged.
133-
bad := "---\ntitle: x\n---\n# Steps\n"
134-
f2, err := lint.NewFile("play.md", []byte(bad))
146+
// Same path, but the on-disk copy lacks type → flagged.
147+
f2, err := lint.NewFile("play.md", []byte("# Steps\n"))
135148
require.NoError(t, err)
136-
f2.FS = fstest.MapFS{"play.md": &fstest.MapFile{Data: []byte(bad)}}
149+
f2.FS = fstest.MapFS{"play.md": &fstest.MapFile{
150+
Data: []byte("---\ntitle: x\n---\n# Steps\n")}}
137151
assert.Len(t, r.Check(f2), 1)
138152
}
139153

0 commit comments

Comments
 (0)