99package foreignregion
1010
1111import (
12+ "bytes"
1213 "fmt"
1314 "strings"
1415
@@ -35,16 +36,80 @@ func Scan(f *lint.File, regions []config.ForeignRegion) ([]lint.LineRange, []lin
3536 if len (regions ) == 0 || f == nil {
3637 return nil , nil
3738 }
39+ states := make ([]regionScanState , len (regions ))
40+ for i , reg := range regions {
41+ states [i ] = regionScanState {
42+ start : []byte (strings .TrimSpace (reg .Start )),
43+ end : []byte (strings .TrimSpace (reg .End )),
44+ }
45+ }
46+
3847 var ranges []lint.LineRange
3948 var diags []lint.Diagnostic
40- for _ , reg := range regions {
41- rs , ds := scanOne (f , reg )
42- ranges = append (ranges , rs ... )
43- diags = append (diags , ds ... )
49+ for i , line := range f .Lines {
50+ lineNum := i + 1
51+ trimmed := bytes .TrimSpace (line )
52+ for s := range states {
53+ rs , ds := states [s ].step (trimmed , lineNum )
54+ ranges = append (ranges , rs ... )
55+ diags = append (diags , ds ... )
56+ }
57+ }
58+ for s := range states {
59+ if ds := states [s ].unclosed (); ds != nil {
60+ diags = append (diags , ds ... )
61+ }
4462 }
4563 return ranges , diags
4664}
4765
66+ // regionScanState tracks one declared region's marker pair across a
67+ // single walk of f.Lines. A single pass evaluates every region's
68+ // state against the line's shared trim, instead of Scan walking
69+ // f.Lines once per region and re-converting every line to a string
70+ // each time — the redundant re-scanning docs/development/
71+ // high-performance-go.md calls out.
72+ type regionScanState struct {
73+ start , end []byte
74+ openLine int // 1-based line of the current unclosed start; 0 when none
75+ }
76+
77+ // step evaluates one already-trimmed line against this region's
78+ // markers and returns any newly closed range or malformed-region
79+ // diagnostic.
80+ func (st * regionScanState ) step (trimmed []byte , lineNum int ) ([]lint.LineRange , []lint.Diagnostic ) {
81+ switch {
82+ case bytes .Equal (trimmed , st .start ):
83+ if st .openLine != 0 {
84+ return nil , []lint.Diagnostic {diag (lineNum , fmt .Sprintf (
85+ "duplicate foreign-region start marker %q before %q closed the open region" ,
86+ st .start , st .end ))}
87+ }
88+ st .openLine = lineNum
89+ case bytes .Equal (trimmed , st .end ):
90+ if st .openLine == 0 {
91+ return nil , []lint.Diagnostic {diag (lineNum , fmt .Sprintf (
92+ "foreign-region end marker %q without a matching start marker %q" ,
93+ st .end , st .start ))}
94+ }
95+ r := lint.LineRange {From : st .openLine , To : lineNum }
96+ st .openLine = 0
97+ return []lint.LineRange {r }, nil
98+ }
99+ return nil , nil
100+ }
101+
102+ // unclosed reports the missing-end diagnostic for a region whose
103+ // start marker was never closed, once the whole file has been walked.
104+ func (st * regionScanState ) unclosed () []lint.Diagnostic {
105+ if st .openLine == 0 {
106+ return nil
107+ }
108+ return []lint.Diagnostic {diag (st .openLine , fmt .Sprintf (
109+ "foreign-region start marker %q has no matching end marker %q" ,
110+ st .start , st .end ))}
111+ }
112+
48113// Apply appends the foreign-region spans for path to f.GeneratedRanges —
49114// the same exclusion set that keeps style rules and fixers out of
50115// `<?include?>` / `<?catalog?>` bodies — and returns the malformed-region
@@ -94,46 +159,6 @@ func resolve(f *lint.File, cfg *config.Config, path string) ([]lint.LineRange, [
94159 return ranges , diags
95160}
96161
97- // scanOne walks f.Lines once for a single marker pair, matching a line
98- // against a marker by trimmed-line equality so leading indentation does
99- // not defeat the match while incidental in-prose mentions do not.
100- func scanOne (f * lint.File , reg config.ForeignRegion ) ([]lint.LineRange , []lint.Diagnostic ) {
101- start := strings .TrimSpace (reg .Start )
102- end := strings .TrimSpace (reg .End )
103- var ranges []lint.LineRange
104- var diags []lint.Diagnostic
105- openLine := 0 // 1-based line of the current unclosed start; 0 when none
106- for i , line := range f .Lines {
107- lineNum := i + 1
108- trimmed := strings .TrimSpace (string (line ))
109- switch trimmed {
110- case start :
111- if openLine != 0 {
112- diags = append (diags , diag (lineNum , fmt .Sprintf (
113- "duplicate foreign-region start marker %q before %q closed the open region" ,
114- start , end )))
115- continue
116- }
117- openLine = lineNum
118- case end :
119- if openLine == 0 {
120- diags = append (diags , diag (lineNum , fmt .Sprintf (
121- "foreign-region end marker %q without a matching start marker %q" ,
122- end , start )))
123- continue
124- }
125- ranges = append (ranges , lint.LineRange {From : openLine , To : lineNum })
126- openLine = 0
127- }
128- }
129- if openLine != 0 {
130- diags = append (diags , diag (openLine , fmt .Sprintf (
131- "foreign-region start marker %q has no matching end marker %q" ,
132- start , end )))
133- }
134- return ranges , diags
135- }
136-
137162// diag builds one malformed-region diagnostic at the given 1-based line
138163// (in f.Lines coordinates; the caller adds any front-matter offset).
139164func diag (line int , msg string ) lint.Diagnostic {
0 commit comments