test(scripts): answer scope questions by parsing, and guard 71 slice bounds - #440
Merged
Merged
Conversation
…bounds
Two holes in the test tooling, both demonstrated before they were fixed.
1. `enclosingFunctionName` matched `\n\t…function NAME(` and nothing else, so a
call inside `const f = async () => {}` was attributed to whichever classic
`function` preceded it. Planting
const renderRawBypass = async (raw: string) => {
return (await invoke('render_markdown', { content: raw })) as string;
};
in MarkdownViewer.svelte left all 524 tests green. It is now answered from
the real AST via `svelte/compiler`, which the suite already depends on, so
every declaration form in `src/` — 451 classic, 74 arrow, class methods,
object shorthand — and every form nobody has written yet is covered by
construction. Same for `$effect` bodies: the regex needed a tab-indented
`});` to terminate, so a one-line effect merged into its successor and its
ungated `editor` read was masked by the next effect's `editorReady`.
2. 71 `indexOf`-derived slice and ordering bounds had no `-1` guard, the defect
#432 fixed in two places. `sliceFrom`/`sliceBetween` cover the slices;
`offsetOf` is added for the ordering comparisons, where `a < b` is also
satisfied by `a === -1`. 110 helper call sites replace 155 raw `indexOf`
bounds; 194 anchors were each corrupted in turn and 193 produced a failure
naming the missing anchor.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
PathGao
force-pushed
the
fix/convention-tests-see-arrow-functions
branch
from
August 3, 2026 11:03
ae7667a to
252b29b
Compare
This was referenced Aug 3, 2026
PathGao
added a commit
that referenced
this pull request
Aug 3, 2026
A survey instrumented all 82 test files and applied 35 source mutations, one full suite run each. Nine of the assertions it flagged as unable to fail sit on real guards; those nine are fixed here. Sixteen of the eighteen affected test files did not exist at the last release — this is debt the recent PR series produced, not debt it inherited. The failure mode is not the one #440 fixed. #440 fixed anchors that were *missing*, so a slice degenerated to "". These are anchors that are too far apart: the slice swallows a neighbouring function and something else in it satisfies the match. Six had a mutation that left the suite green, reproduced here before being fixed and re-applied afterwards: * canTransfer's "&& !tab.isTruncated" and handleDetach's whole ensureFullContent guard, each deletable — the transfer-a-truncated- buffer-then-auto-save-truncates-the-file path truncatedBufferGuard is named for. The two slices ran through canDetach and moveTabToWindow, whose character-identical guards satisfied the match. * appExit's unsaved-tab review, movable verbatim into a helper nobody calls: the slice spanned 969 lines for a 16-line function. * previewSanitize's "all {@html} sinks" list, which captured bare identifiers only, so a second raw sink spelled as a member expression was invisible. * the Destroyed handler's window_registry removal, deletable because the two tokens matched ~460 lines apart. * the recent-files storage key and cap, each compared with itself through an export that existed for that comparison alone. * every context-menu label in 26 languages, restated 26 times: t() falls back to English, so deleting the German entry was green. Two more could not fail by construction and are deleted rather than repaired: an assertion on a template literal declared in the same file, and a count of a pattern the line above had already proved absent. A third of that kind, the config's URI regexp compared with the constant it is assigned from, turned out to be reachable — rebuilding the regexp from the same source and flags fails it, which is the hand-copied-pattern regression the file exists for — so it stays. New: sourceTree.ts gains functionSource(text, name), which extracts a function from the AST by its own name instead of by naming whatever text follows it, and fails loudly on a rename or a duplicate. Four guards that were only ever stood in front of — canDetach, moveTabToWindow, and each of the two independent recent-list caps — are now asserted about directly, and appExit's confirmation must be acted on rather than merely present. Source changes are limited to dropping "export" from RECENT_FILES_KEY and RECENT_FILES_LIMIT, whose only importer was the test that compared them with themselves. No behaviour changes. Co-authored-by: Claude <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Two holes in the test tooling. They are independent; they share a PR because
scripts/sourceTree.tsis where both are fixed. Both were reproduced beforethey were touched.
1. A convention test was bypassed by writing an arrow function
enclosingFunctionNamedecides which function an offset sits inside. It matchedone spelling:
So a call site inside
const f = async () => {}was attributed to whicheverclassic
functionhappened to precede it — and inMarkdownViewer.sveltethefunction preceding the obvious place to add one is
renderMarkdownPreview, theexact wrapper the test demands.
Measured, both ways
Planted immediately after
renderMarkdownPreview:npm testtests 524 pass 524 fail 0fail 1—actual: [ 'renderMarkdownPreview', 'renderRawBypass' ]renderPipelineConventionandsingleImplementationConventionwere both greenwith the bypass in place. Nothing else in the suite saw it either.
The same weakness in monacoStartupGraph, confirmed
"every effect that drives the editor waits for
editorReady" sliced effectbodies with
/\$effect\(\(\) => \{([\s\S]*?)\n\t\}\);/. That needs atab-indented
});to end a body, so an effect written on one line has noterminator of its own and the lazy match runs on to the next effect's:
editorReadyan effect touching the editor is not gated on editorReady: { if (editor) editor.layout(); }The
effects.length >= 5vacuity guard did not notice, because merging twoeffects into one body kept the count at six.
Parsing, not a longer pattern
svelte/compileris already a dependency andhomeTabRender.test.tsalreadyparses a component with it.
enclosingFunctionNameand the newcallbackBodiesnow walk the real AST:
parse(source, { modern: true }), collect everyFunctionDeclaration/FunctionExpression/ArrowFunctionExpressionspanwith the name it is bound to, and answer containment against that. Parses are
cached per source string; the largest component costs ~170ms once.
A survey of
src/first, because the alternative was to widen the regex towhatever is there today:
src/function f()/export async function f()const f = () => {}/const f = async () => {}stores/)TabManageracceptNode(node) {)A regex covering those four fails the next time someone writes a fifth. A
function node is a function node whatever the spelling, so there is no longer a
pattern to write around. The cost is one dependency the suite already had and
~170ms; the alternative saves that and keeps a hole whose size is "whatever
nobody thought of".
Two decisions inside it, both deliberate:
.then(() => …)insiderenderMarkdownPreviewis still lexically insiderenderMarkdownPreview,which is the question the callers ask. An offset with no named function above
it at all — an inline
onclick={() => …}in the markup — isnull, and acaller comparing against a wrapper name fails loudly on it.
.tsfiles are supported by wrapping them in a<script>before parsingand subtracting the wrapper length, so a caller that passes a store instead of
a component gets an answer rather than a silent
nulleverywhere.scripts/sourceTree.test.ts(new) pins all twelve declaration forms, thenesting rules, and the one-line-effect case against string literals rather than
against
src/— refactoring the app cannot make it fail, and changing thehelper is the only thing that can.
No violation was hiding behind the old regex
Both convention tests are green on the real tree after the tightening. The
bypass had to be planted to make either fail.
2. 71 unguarded slice and ordering bounds
slice(x.indexOf(m))with an absentmslices from -1, which yields the lastcharacter of the file — so
assert.doesNotMatchagainst it can never fail.#432 fixed two instances and added
sliceFrom/sliceBetween; the rest wereleft.
Re-derived, because #433 deleted 14 files since
At
e196547(merge base), countingindexOfcalls on source text whose resultbecomes a slice bound or an ordering operand, with nothing asserting they are
not -1:
indexOf4251f0a(#432 landed)e196547(after #433)The remaining one is
assert.equal('abc'.slice('abc'.indexOf('zzz')), 'c')inthe new unit test, which exists to demonstrate the defect.
155 raw
indexOfbounds are replaced by 110 helper call sites across 31 files.Seven files carried a private copy of
sliceBetweenunder another name(
sliceBlock,slice,pluck, an IIFE) — those are deleted and the shared oneused, which is also how three of them acquired a guard on their end anchor.
One new shape,
offsetOfThe migration exposed a second form the two existing helpers do not cover:
a < breads as an ordering claim but is also satisfied bya === -1, so ananchor that stopped existing turns it into a claim about nothing.
offsetOfasserts and returns; 56 of the migrated sites are this shape, which is what
justifies a third helper rather than a fourth copy of
assert.notEqual(…, -1).Two sites had bespoke bounds that no helper covers and were rewritten in place:
truncatedBufferGuard.test.tsended a handler atindexOf('\n\tfunction ') + 1 || indexOf('\n\tasync function ') + 1. Thatreads as "or else" but is really "or else, if the first is absent or at
offset 0", and when the first form appeared later in the file than the second
it won anyway — widening the body past the handler, which for an
assert.matchis a false green. It now takes the earlier of the two thatexist, and asserts at least one does.
renderProtocol.test.tssliced betweenindexOf('$$')andlastIndexOf('$$'); those coincide for a fixture with one$$, which is nowa failure.
Two anchors replaced by a parse instead of a guard
scrollSyncInput.test.ts— the file #432 named as the prior fix, and flaggedas still half-guarded — was sliced between
'&& onscrollsync)'and'\n\t$effect(() => {'. Its start anchor had already drifted once, silently,when the effect gained an
editorReady &&term; its end anchor never got aguard and depended on the next effect's indentation. It now selects the one
$effectwhose body readsonscrollsyncand asserts there is exactly one.settingsPersistence.test.ts's open-effect anchor(
'$effect(() => {\n\t\tif (show) {') is replaced the same way.Falsifiability, checked by breaking every anchor
Each string anchor passed to
sliceFrom/sliceBetween/offsetOfwascorrupted in turn —
ZZNOPEinserted before the closing quote — and its testfile run:
The one that did not fail is the second argument of
assert.throws(() => sliceBetween('abc', 'zzz', 'b'), /expected to find "zzz"/)in the new unit test — unreachable by construction, since the first anchor
throws first.
Not covered
indexOfcalls remain inscripts/, deliberately. CSS and commenttokenizers that already branch on
-1(editorPdfExport,exportFoldParity,i18nCoverage);Array#indexOfon DOM shims and event logs, where-1means"not a child" rather than "anchor missing"; and
exportSanitize.test.ts,already guarded with messages more specific than
offsetOf's.foldStatePerDocument,viewModeWithoutSaving) doi = source.indexOf('\n', i)with no guard. A-1there hangs the loop rather than passing a degenerate assertion, so it isa different failure mode; it cannot trigger while every source file ends with
a newline. Left as is.
from
renderMarkdownPreview" is still lexical containment. That the valuereaching it had its front matter stripped is not established here, and was not
established by the regex either.
(
sliceBetween(viewer, 'async function a', 'async function b')). They areguarded now, so a rename fails loudly instead of silently, but a
functionSource(text, name)built on the same parse would remove the secondanchor entirely. Not done here — it changes what those slices contain.
src/andsrc-tauri/are untouched;npm test(530),npm run check(631 files, 0 errors) andnpm run buildare clean.
🤖 Generated with Claude Code