Skip to content

Commit 6ded0a1

Browse files
author
merge-queue-bot
committed
Merge PR #702: test(lint): add dedicated unit tests for 9 lineclass_scan HTML helpers (plan 2606260614)
2 parents 3d63fdd + 475e8b2 commit 6ded0a1

7 files changed

Lines changed: 421 additions & 62 deletions

File tree

PLAN.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,4 +231,6 @@ footer: |
231231
| 2606241814 || sonnet | [Add unit tests for lsp/rename dispatch helpers and workspace adapter methods](plan/2606241814_arch-fix-lsp-rename-dispatch-tests.md) |
232232
| 2606241815 || sonnet | [Add unit tests for three remaining unexported helpers in internal/index/locate.go](plan/2606241815_arch-fix-locate-remaining-helper-tests.md) |
233233
| 2606260211 || sonnet | [Add dedicated unit tests for layer0_html.go helpers](plan/2606260211_arch-fix-layer0-html-helper-tests.md) |
234+
| 2606260614 || sonnet | [arch-fix: add dedicated unit tests for lineclass_scan.go HTML-scanning helpers](plan/2606260614_arch-fix-lineclass-scan-helper-tests.md) |
235+
| 2606260615 | 🔲 | sonnet | [arch-fix: add dedicated unit tests for cue/cuelite/engine.go unexported helpers](plan/2606260615_arch-fix-cuelite-engine-helper-tests.md) |
234236
<?/catalog?>

docs/development/architecture-audit.md

Lines changed: 54 additions & 58 deletions
Original file line numberDiff line numberDiff line change
@@ -47,24 +47,14 @@ three packages; export from `internal/mdtext` —
4747

4848
### plan/153 non-goal superseded
4949

50-
Plan 153 kept the workspace symbol
51-
index at `internal/lsp/index`. Its
52-
stated non-goal: "only link/edge
53-
extraction is in scope." Plan 174
54-
supersedes that. The package is now
55-
`internal/index`, a peer support
56-
package.
57-
58-
The move is a pure `git mv`; no logic
59-
changed. Two forces drove it.
60-
`internal/schema` already imported the
61-
index from outside `internal/lsp`. The
62-
new `mdsmith rename` and `mdsmith deps`
63-
surfaces need it too, and the layering
64-
map forbids `cmd/mdsmith`
65-
`internal/lsp`. A peer package removes
66-
the conflict. `internal/index` must
67-
never import `internal/lsp`.
50+
Plan 174 moved the workspace symbol index
51+
from `internal/lsp/index` to `internal/index`.
52+
Pure `git mv`; no logic changed.
53+
`internal/schema` already imported it from
54+
outside `internal/lsp`. `mdsmith rename` and
55+
`mdsmith deps` need it. The layering map
56+
forbids `cmd/mdsmith``internal/lsp`.
57+
`internal/index` must never import `internal/lsp`.
6858

6959
## Audit 2026-05-19 (range: 7464d273..41e61a5)
7060

@@ -135,59 +125,33 @@ Tax: [new-pkg-docs](../../plan/2606162213_arch-fix-new-pkg-docs.md),
135125

136126
## Audit 2026-06-21 (range: 7793b97..e701b94)
137127

138-
Parity + Layer-0 parse-skip series; symlink
139-
containment; engine panic recovery; VS Code
140-
`kinds` and `rule-doc` commands; security
141-
hardening batch. 270 Go/TS sources outside
142-
fixtures.
143-
144-
No blockers. New rule / convention packages
145-
follow the OCP barrel pattern correctly; no
146-
rule-to-rule imports added; no DIP violations
147-
in the new `internal/rules/listscan` helper
148-
(it follows the established `astutil` /
149-
`fencepos` pattern).
128+
Parity + Layer-0 parse-skip series.
129+
Symlink containment; engine panic recovery.
130+
VS Code `kinds` and `rule-doc` commands.
131+
270 Go/TS sources. No blockers,
132+
rule-to-rule imports, or DIP violations.
150133

151134
### tax (2026-06-21)
152135

153136
- `internal/engine/runner.go` (1 290 lines) —
154-
SRP violation. Seven concerns in one file:
155-
file dispatch, Layer-0 skip gate,
156-
config-resolution cache, source-mode lint
157-
path, front-matter parsing, config-target
158-
rules, and logging. Go arch doc
159-
§"Common violations to flag" names engine
160-
as a dumping-ground risk. Fixed this cycle:
161-
split into `runner_layer0.go`,
162-
`runner_cache.go`, `runner_log.go`
163-
[plan/2606211907][2606211907].
137+
SRP: 7 concerns. Fixed this cycle: split into
138+
`runner_layer0.go`, `runner_cache.go`,
139+
`runner_log.go`[plan/2606211907][2606211907].
164140

165141
- `internal/lint/layer0.go` (1 203 lines) —
166-
the full Layer-0 block scanner in one file:
167-
types, scanner state machine, HTML-block
168-
detection (types 1–7), fence handling, ATX
169-
heading, indented code, paragraph. Maintenance
170-
risk at this size. Fix: split along
142+
full Layer-0 block scanner. Fix: split along
171143
block-type sub-parsers —
172144
[plan/2606211908][2606211908].
173145

174146
- `internal/lsp/server.go` (1 007 lines) —
175-
plan 203 was green but the file has crept
176-
back over 1 000 lines with the new `kinds`
177-
and `rule-doc` capability wiring. Checklist
178-
names it explicitly. Fix: apply the same
179-
dispatch-group split plan 203 described —
180-
[plan/2606211909][2606211909].
147+
crept back over 1 000 lines. Dispatch-group
148+
split — [plan/2606211909][2606211909].
181149

182150
### nice-to-have (2026-06-21)
183151

184152
- `pkg/mdsmith/workspace.go` trivial methods
185-
(`memFile.Close`, `memDir.Close`,
186-
`memDirEntry.Name`, `memDirEntry.IsDir`,
187-
`memFileInfo.Name`, `memFileInfo.Size`) lack
188-
the one-line "no test by design" exemption
189-
comment the audit policy requires. Tests doc
190-
§"Exemptions" — [plan/2606211910][2606211910].
153+
lack "// no test by design" exemptions —
154+
[plan/2606211910][2606211910].
191155

192156
[2606211907]: ../../plan/2606211907_arch-fix-runner-srp-split.md
193157
[2606211908]: ../../plan/2606211908_arch-fix-layer0-split.md
@@ -298,4 +262,36 @@ line-count violations.
298262
§"every function by name" —
299263
[plan/2606260211][2606260211].
300264

301-
[2606260211]: ../../plan/2606260211_arch-fix-layer0-html-helper-tests.md
265+
# <<<<<<< .merge_file_9i0bNn[2606260211]: ../../plan/2606260211_arch-fix-layer0-html-helper-tests.md
266+
267+
=======
268+
[2606241814]: ../../plan/2606241814_arch-fix-lsp-rename-dispatch-tests.md
269+
[2606241815]: ../../plan/2606241815_arch-fix-locate-remaining-helper-tests.md
270+
271+
## Audit 2026-06-26 (range: 3d35b77..fe7141b)
272+
273+
Plans 2606241814 and 2606241815 green.
274+
No DIP, SRP, or line-count violations.
275+
No rule-to-rule imports. No file crossed
276+
1 000 lines. Perf commit e7cb8b0 and
277+
PR #690 are in range.
278+
279+
### tax (2026-06-26)
280+
281+
- `internal/lint/lineclass_scan.go` — 9
282+
unexported helpers lack tests.
283+
Sub-functions of `htmlType7Start`
284+
[plan/2606260614][2606260614].
285+
286+
- `cue/cuelite/engine.go` — 7 unexported
287+
helpers lack tests —
288+
[plan/2606260615][2606260615].
289+
290+
### nice-to-have (2026-06-26)
291+
292+
- `internal/lint/layer0_html.go` — 7
293+
unexported helpers. PR #701 in-flight.
294+
295+
[2606260614]: ../../plan/2606260614_arch-fix-lineclass-scan-helper-tests.md
296+
[2606260615]: ../../plan/2606260615_arch-fix-cuelite-engine-helper-tests.md
297+
>>>>>>> .merge_file_86ISsX

docs/development/architecture/audit-checklist.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -211,8 +211,8 @@ explicit mention here:
211211
- **A rule package importing another
212212
rule package** — always a DIP
213213
blocker. Helpers belong in
214-
`internal/mdtext` or
215-
`internal/rules/astutil`.
214+
`internal/mdtext`, `internal/rules/astutil`,
215+
or `internal/rules/listscan`.
216216
- **`cmd/mdsmith/main.go` past ~1000
217217
lines** — handler bodies have crept
218218
in; relocate to `internal/engine` or

docs/development/architecture/index.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -275,8 +275,8 @@ caught and the reasons we reject them:
275275
attracts unrelated code.
276276
- **A rule package importing another rule
277277
package.** Rules share helpers via
278-
`internal/mdtext` or
279-
`internal/rules/astutil`; reaching
278+
`internal/mdtext`, `internal/rules/astutil`,
279+
or `internal/rules/listscan`; reaching
280280
sideways into a sibling rule binds
281281
release cycles that should stay
282282
independent.

internal/lint/lineclass_scan_test.go

Lines changed: 184 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,16 @@ func TestHTMLType6AndClose(t *testing.T) {
190190
assert.False(t, containsType1Close([]byte("short")), "shorter than any closer")
191191
}
192192

193+
func TestContainsFold(t *testing.T) {
194+
needle := []byte("</pre>")
195+
assert.True(t, containsFold([]byte("</PRE>"), needle), "match at position 0")
196+
assert.True(t, containsFold([]byte("x</Pre>y"), needle), "match in middle")
197+
assert.True(t, containsFold([]byte("x</PRE>"), needle), "match at last window")
198+
assert.False(t, containsFold([]byte("x</em>y"), needle), "no match")
199+
assert.False(t, containsFold([]byte("</pr"), needle), "needle longer than line")
200+
assert.False(t, containsFold([]byte(""), needle), "empty line")
201+
}
202+
193203
func TestContainerMarkerScanners(t *testing.T) {
194204
assert.Equal(t, 2, blockquoteMarker([]byte("> x")))
195205
assert.Equal(t, 1, blockquoteMarker([]byte(">x")), "marker with no following space")
@@ -292,3 +302,177 @@ func TestHTMLType6Tags_CommonMarkComplete(t *testing.T) {
292302
}
293303
}
294304
}
305+
306+
// TestScanHTMLTag pins the dispatch between open and closing tags.
307+
func TestScanHTMLTag(t *testing.T) {
308+
n, ok := scanHTMLTag([]byte("<img>"))
309+
assert.True(t, ok)
310+
assert.Equal(t, 5, n)
311+
312+
n, ok = scanHTMLTag([]byte("</div>"))
313+
assert.True(t, ok)
314+
assert.Equal(t, 6, n)
315+
316+
// Does not start with '<'.
317+
_, ok = scanHTMLTag([]byte("img>"))
318+
assert.False(t, ok)
319+
320+
// Fewer than 3 bytes.
321+
_, ok = scanHTMLTag([]byte("<a"))
322+
assert.False(t, ok)
323+
}
324+
325+
// TestScanClosingTag pins the closing-tag scanner.
326+
func TestScanClosingTag(t *testing.T) {
327+
s := []byte("</div>")
328+
n, ok := scanClosingTag(s, 2)
329+
assert.True(t, ok)
330+
assert.Equal(t, 6, n)
331+
332+
// Optional whitespace before '>'.
333+
n, ok = scanClosingTag([]byte("</div >"), 2)
334+
assert.True(t, ok)
335+
assert.Equal(t, 7, n)
336+
337+
// Missing '>'.
338+
_, ok = scanClosingTag([]byte("</div"), 2)
339+
assert.False(t, ok)
340+
341+
// Name starts with a digit.
342+
_, ok = scanClosingTag([]byte("</1tag>"), 2)
343+
assert.False(t, ok)
344+
}
345+
346+
// TestScanOpenTag pins the open-tag scanner: tag name, attributes, self-close.
347+
func TestScanOpenTag(t *testing.T) {
348+
n, ok := scanOpenTag([]byte("<img>"), 1)
349+
assert.True(t, ok)
350+
assert.Equal(t, 5, n)
351+
352+
n, ok = scanOpenTag([]byte("<br/>"), 1)
353+
assert.True(t, ok)
354+
assert.Equal(t, 5, n)
355+
356+
n, ok = scanOpenTag([]byte("<br />"), 1)
357+
assert.True(t, ok)
358+
assert.Equal(t, 6, n)
359+
360+
n, ok = scanOpenTag([]byte(`<img src="x">`), 1)
361+
assert.True(t, ok)
362+
assert.Equal(t, 13, n)
363+
364+
// '=' with no value.
365+
_, ok = scanOpenTag([]byte("<img src=>"), 1)
366+
assert.False(t, ok)
367+
368+
// No closing '>'.
369+
_, ok = scanOpenTag([]byte("<img"), 1)
370+
assert.False(t, ok)
371+
}
372+
373+
// TestScanTagName pins the tag-name scanner.
374+
func TestScanTagName(t *testing.T) {
375+
i, ok := scanTagName([]byte("img>"), 0)
376+
assert.True(t, ok)
377+
assert.Equal(t, 3, i)
378+
379+
// Hyphens allowed inside a name.
380+
i, ok = scanTagName([]byte("a-b>"), 0)
381+
assert.True(t, ok)
382+
assert.Equal(t, 3, i)
383+
384+
// Digit at start → reject.
385+
_, ok = scanTagName([]byte("1tag"), 0)
386+
assert.False(t, ok)
387+
388+
// Empty input.
389+
_, ok = scanTagName([]byte(""), 0)
390+
assert.False(t, ok)
391+
392+
// Name runs to end of input (no terminator byte).
393+
i, ok = scanTagName([]byte("img"), 0)
394+
assert.True(t, ok)
395+
assert.Equal(t, 3, i)
396+
}
397+
398+
// TestScanAttribute pins the attribute scanner.
399+
func TestScanAttribute(t *testing.T) {
400+
// Valueless attribute.
401+
i, ok := scanAttribute([]byte(" disabled>"), 0)
402+
assert.True(t, ok)
403+
assert.Equal(t, 9, i)
404+
405+
// Unquoted value.
406+
i, ok = scanAttribute([]byte(" src=x>"), 0)
407+
assert.True(t, ok)
408+
assert.Equal(t, 6, i)
409+
410+
// Double-quoted value.
411+
i, ok = scanAttribute([]byte(` src="x">`), 0)
412+
assert.True(t, ok)
413+
assert.Equal(t, 8, i)
414+
415+
// '=' with no value.
416+
_, ok = scanAttribute([]byte(" src=>"), 0)
417+
assert.False(t, ok)
418+
419+
// No leading whitespace → reject.
420+
_, ok = scanAttribute([]byte("src=x>"), 0)
421+
assert.False(t, ok)
422+
}
423+
424+
// TestScanAttrValue pins the attribute-value scanner.
425+
func TestScanAttrValue(t *testing.T) {
426+
// Unquoted value (stops at '>').
427+
i, ok := scanAttrValue([]byte("value>"), 0)
428+
assert.True(t, ok)
429+
assert.Equal(t, 5, i)
430+
431+
// Single-quoted.
432+
i, ok = scanAttrValue([]byte("'value'"), 0)
433+
assert.True(t, ok)
434+
assert.Equal(t, 7, i)
435+
436+
// Double-quoted.
437+
i, ok = scanAttrValue([]byte(`"value"`), 0)
438+
assert.True(t, ok)
439+
assert.Equal(t, 7, i)
440+
441+
// Unclosed single quote.
442+
_, ok = scanAttrValue([]byte("'value"), 0)
443+
assert.False(t, ok)
444+
445+
// Empty unquoted value (stop byte at position 0).
446+
_, ok = scanAttrValue([]byte(">"), 0)
447+
assert.False(t, ok)
448+
}
449+
450+
// TestSkipHTMLWS pins the whitespace-skipper.
451+
func TestSkipHTMLWS(t *testing.T) {
452+
assert.Equal(t, 2, skipHTMLWS([]byte(" x"), 0))
453+
assert.Equal(t, 1, skipHTMLWS([]byte("\tx"), 0))
454+
assert.Equal(t, 2, skipHTMLWS([]byte(" \tx"), 0))
455+
assert.Equal(t, 0, skipHTMLWS([]byte("x"), 0))
456+
assert.Equal(t, 2, skipHTMLWS([]byte(" "), 2))
457+
}
458+
459+
// TestIsUnquotedStop pins which bytes end an unquoted attribute value.
460+
func TestIsUnquotedStop(t *testing.T) {
461+
for _, b := range []byte{' ', '\t', '"', '\'', '=', '<', '>', '`'} {
462+
assert.Truef(t, isUnquotedStop(b), "expected stop for 0x%02x", b)
463+
}
464+
for _, b := range []byte{'a', '0', '-', '_', '/'} {
465+
assert.Falsef(t, isUnquotedStop(b), "expected non-stop for 0x%02x", b)
466+
}
467+
}
468+
469+
// TestEqualFoldASCII pins the case-insensitive ASCII byte comparison.
470+
// b is always lowercase; a may be any case. Callers always pass same-length
471+
// slices.
472+
func TestEqualFoldASCII(t *testing.T) {
473+
assert.True(t, equalFoldASCII([]byte("pre"), []byte("pre")))
474+
assert.True(t, equalFoldASCII([]byte("PRE"), []byte("pre")))
475+
assert.True(t, equalFoldASCII([]byte("ScRiPt"), []byte("script")))
476+
assert.False(t, equalFoldASCII([]byte("pre"), []byte("div")))
477+
assert.False(t, equalFoldASCII([]byte("foo"), []byte("bar")))
478+
}

0 commit comments

Comments
 (0)