@@ -106,13 +106,25 @@ func (r *Rule) Check(f *lint.File) []lint.Diagnostic {
106106 }
107107
108108 var diags []lint.Diagnostic
109+ // paragraphPos is a forward-only cursor into paragraphs, shared
110+ // across every heading's window. Heading windows are contiguous,
111+ // non-overlapping, and ascending (headings and paragraphs are both
112+ // in source-line order), so a paragraph consumed — or skipped as
113+ // belonging to an earlier window — by one heading can never be
114+ // needed by a later one. Threading the cursor through turns the
115+ // whole loop into a single O(headings + paragraphs) pass instead of
116+ // re-scanning all of paragraphs from index 0 per heading. See
117+ // docs/development/high-performance-go.md's "Skip work you don't
118+ // need" — the same forward-cursor pattern astutil.SectionBodies
119+ // already uses for MDS057/MDS058.
120+ paragraphPos := 0
109121 for i , h := range headings {
110122 end := totalLines
111123 if i + 1 < len (headings ) {
112124 end = headings [i + 1 ].line - 1
113125 }
114126 diags = append (diags , r .checkLineLimit (f , h , end )... )
115- diags = append (diags , r .checkWordAndParagraphLimits (f , h , end , paragraphs )... )
127+ diags = append (diags , r .checkWordAndParagraphLimits (f , h , end , paragraphs , & paragraphPos )... )
116128 }
117129 return diags
118130}
@@ -138,12 +150,12 @@ func (r *Rule) checkLineLimit(f *lint.File, h heading, end int) []lint.Diagnosti
138150}
139151
140152func (r * Rule ) checkWordAndParagraphLimits (
141- f * lint.File , h heading , end int , paragraphs []paragraph ,
153+ f * lint.File , h heading , end int , paragraphs []paragraph , paragraphPos * int ,
142154) []lint.Diagnostic {
143155 if r .MaxWords <= 0 && r .MinWords <= 0 && r .MaxParagraphs <= 0 {
144156 return nil
145157 }
146- words , paraCount := countSection (paragraphs , h .line , end )
158+ words , paraCount := countSection (paragraphs , paragraphPos , h .line , end )
147159 var diags []lint.Diagnostic
148160 if r .MaxWords > 0 && words > r .MaxWords {
149161 diags = append (diags , lint.Diagnostic {
@@ -347,16 +359,27 @@ func collectParagraphs(f *lint.File) []paragraph {
347359 return out
348360}
349361
350- // countSection sums words and paragraphs for paragraphs whose start
351- // line falls within [start, end].
352- func countSection (paragraphs []paragraph , start , end int ) (words , count int ) {
353- for _ , p := range paragraphs {
354- if p .line < start || p .line > end {
355- continue
356- }
357- words += p .words
362+ // countSection sums the words and paragraph count of every entry in
363+ // paragraphs whose line falls in [start, end]. paragraphs must be in
364+ // ascending line order (astutil.CollectSectionParagraphs guarantees
365+ // this). *pos is a forward-only cursor: callers processing a sequence
366+ // of non-overlapping, ascending [start, end] windows over the same
367+ // paragraphs slice (as Check's per-heading loop does) should thread
368+ // the same pos through every call, since a paragraph this call skips
369+ // or consumes can never belong to a later, higher-numbered window —
370+ // letting the whole sequence of calls run in O(len(paragraphs)) total
371+ // instead of O(len(headings) * len(paragraphs)).
372+ func countSection (paragraphs []paragraph , pos * int , start , end int ) (words , count int ) {
373+ for * pos < len (paragraphs ) && paragraphs [* pos ].line < start {
374+ * pos ++
375+ }
376+ i := * pos
377+ for i < len (paragraphs ) && paragraphs [i ].line <= end {
378+ words += paragraphs [i ].words
358379 count ++
380+ i ++
359381 }
382+ * pos = i
360383 return words , count
361384}
362385
0 commit comments