Skip to content

Commit 5df1f3f

Browse files
committed
astutil: walk inline descendants in HeadingLine; add missing tests
HeadingLine previously only checked direct children for *ast.Text, so ATX headings with an emphasis or link as first child (e.g. ## *foo* or ## [t](u)) on a later line fell back to 1 instead of the correct line. Switch to ast.Walk over all inline descendants, stopping at the first *ast.Text, matching the headingstyle approach. Add TestHeadingLine_ATXEmphasisOnLaterLine and TestHeadingLine_ATXLinkOnLaterLine to cover this path. https://claude.ai/code/session_01YbFAKES95Szi7i251Yv1XE
1 parent a65d715 commit 5df1f3f

2 files changed

Lines changed: 57 additions & 6 deletions

File tree

internal/rules/astutil/astutil.go

Lines changed: 16 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -9,18 +9,28 @@ import (
99

1010
// HeadingLine returns the 1-based source line of a heading node.
1111
// Setext headings expose their line via Lines(); ATX headings are found
12-
// by walking child text nodes. Returns 1 as a safe fallback.
12+
// by walking inline descendants until the first text segment. Returns 1
13+
// as a safe fallback.
1314
func HeadingLine(heading *ast.Heading, f *lint.File) int {
1415
lines := heading.Lines()
1516
if lines.Len() > 0 {
1617
return f.LineOfOffset(lines.At(0).Start)
1718
}
18-
for c := heading.FirstChild(); c != nil; c = c.NextSibling() {
19-
if t, ok := c.(*ast.Text); ok {
20-
return f.LineOfOffset(t.Segment.Start)
19+
20+
line := 1
21+
_ = ast.Walk(heading, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
22+
if !entering || n == heading {
23+
return ast.WalkContinue, nil
2124
}
22-
}
23-
return 1
25+
t, ok := n.(*ast.Text)
26+
if !ok {
27+
return ast.WalkContinue, nil
28+
}
29+
line = f.LineOfOffset(t.Segment.Start)
30+
return ast.WalkStop, nil
31+
})
32+
33+
return line
2434
}
2535

2636
// ParagraphLine returns the 1-based source line of a paragraph node.

internal/rules/astutil/astutil_test.go

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -69,6 +69,47 @@ func TestHeadingLine_ATXOnLaterLine(t *testing.T) {
6969
assert.Equal(t, 3, line)
7070
}
7171

72+
func TestHeadingLine_ATXEmphasisOnLaterLine(t *testing.T) {
73+
// ATX heading on line 3 whose only child is emphasis (not a direct *ast.Text).
74+
// HeadingLine must descend into inline children to find the text segment.
75+
src := []byte("Text\n\n## *emph*\n")
76+
f, err := lint.NewFile("test.md", src)
77+
require.NoError(t, err)
78+
79+
var line int
80+
_ = ast.Walk(f.AST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
81+
if !entering {
82+
return ast.WalkContinue, nil
83+
}
84+
if h, ok := n.(*ast.Heading); ok {
85+
line = HeadingLine(h, f)
86+
return ast.WalkStop, nil
87+
}
88+
return ast.WalkContinue, nil
89+
})
90+
assert.Equal(t, 3, line)
91+
}
92+
93+
func TestHeadingLine_ATXLinkOnLaterLine(t *testing.T) {
94+
// ATX heading on line 3 whose only child is a link node.
95+
src := []byte("Text\n\n## [link](url)\n")
96+
f, err := lint.NewFile("test.md", src)
97+
require.NoError(t, err)
98+
99+
var line int
100+
_ = ast.Walk(f.AST, func(n ast.Node, entering bool) (ast.WalkStatus, error) {
101+
if !entering {
102+
return ast.WalkContinue, nil
103+
}
104+
if h, ok := n.(*ast.Heading); ok {
105+
line = HeadingLine(h, f)
106+
return ast.WalkStop, nil
107+
}
108+
return ast.WalkContinue, nil
109+
})
110+
assert.Equal(t, 3, line)
111+
}
112+
72113
func TestHeadingLine_Fallback_Returns1(t *testing.T) {
73114
heading := ast.NewHeading(1)
74115
f, err := lint.NewFile("test.md", []byte("# X\n"))

0 commit comments

Comments
 (0)