test(mobile): pin the whole document script against the modules

Every line of the script now has a module behind it, so the whole thing can be
compared at once. This is the review of the move, as one number per class:

  qualifier            609 references + 73 declarations = 682 sites
  var rebindings       373, the document's 446 declarators less those 73
  curly braces         279, the number measured before any of this started
  unbound catches      36 of 38; two name their error and report it
  Number properties    17, also measured up front
  shorthand properties 4, two SGR flags written twice each
  unshadowed names     7

A seventh class was needed and is counted like the others: a binding that
shadowed a document variable stops being a shadow once that variable moves
onto the scope, so the printer stops disambiguating it. It has its own
acceptance case.

The module order lives in one file that both this test and the generator read,
so neither can drift from the other.

Claude-Session: https://claude.ai/code/session_01JNnE9qzUZMMnqpZWCqM3nb
This commit is contained in:
Jinwoo-H
2026-09-20 10:04:26 -04:00
parent 8cdeafbc78
commit 4376f50f71
30 changed files with 195 additions and 30 deletions
@@ -0,0 +1,45 @@
/**
* The order the document's modules are spliced back into the script, which is the order the
* hand-written document had. It is data, not a dependency graph: the document is one function
* scope, so declarations must land where they landed before.
*
* Both the generator and the equivalence test read this, so neither can drift from the other.
*/
export const TERMINAL_DOCUMENT_MODULE_ORDER = [
'runtime-constants',
'terminal-handle',
'query-reply',
'surface-swap',
'text-scaling',
'viewport-transform',
'terminal-theme',
'fit-scale',
'mouse-mode-decset-scan',
'write-queue',
'webgl-recovery',
'terminal-init',
'reflow',
'host-notify',
'host-message-router',
'selection-state-and-eviction',
'mode-mirroring',
'keyboard-avoidance-metrics',
'term-observers',
'viewport-cell',
'mouse-report-cell',
'mouse-input-encoding',
'normal-buffer-smooth-scroll',
'cell-geometry',
'path-tap',
'url-tap',
'osc-link-tap',
'surface-tap',
'selection-range',
'selection-overlay',
'tap-dispatch',
'wheel-scroll',
'mouse-click-drag',
'selection-menu-buttons',
'surface-touch-gestures',
'message-bridge'
]
@@ -21,7 +21,8 @@ describe('the terminal fit-scale slice', () => {
bracedBodies: 20,
unboundCatches: 0,
numberProperties: 9,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -23,7 +23,8 @@ describe('the host-message-router slice', () => {
bracedBodies: 12,
unboundCatches: 2,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -23,7 +23,8 @@ describe('the keyboard-avoidance metrics module', () => {
// The row scan and the alternate-screen probe.
unboundCatches: 2,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -20,7 +20,8 @@ describe('the message-bridge module', () => {
// The parse guard. The second catch names its error and reports it, so it keeps its binding.
unboundCatches: 1,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -22,7 +22,8 @@ describe('the mouse-click-drag module', () => {
// The pointer-capture call, which throws when capture is unavailable.
unboundCatches: 1,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -21,7 +21,8 @@ describe('the mouse-mode DECSET scan module', () => {
bracedBodies: 9,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -30,7 +30,8 @@ describe('the mouse-report and scroll-routing slice', () => {
// Three reads of xterm's mode state, each of which may not exist.
unboundCatches: 3,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -29,7 +29,8 @@ describe('the mouse-report cell module', () => {
bracedBodies: 13,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
}
)
@@ -21,7 +21,8 @@ describe('the path-tap module', () => {
unboundCatches: 0,
// Both `parseInt` calls take a digit run a capture group already matched.
numberProperties: 2,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -23,7 +23,8 @@ describe('the query-reply module', () => {
// Both `catch (e) {}` clauses, whose binding was never read.
unboundCatches: 2,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
+2 -1
View File
@@ -22,7 +22,8 @@ describe('the reflow module', () => {
bracedBodies: 2,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -21,7 +21,8 @@ describe('the runtime-constants module', () => {
bracedBodies: 0,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -35,7 +35,8 @@ describe('the runtime-state and text-scaling slice', () => {
bracedBodies: 0,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -57,7 +58,8 @@ describe('the runtime-state and text-scaling slice', () => {
bracedBodies: 13,
unboundCatches: 1,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -28,7 +28,8 @@ describe('the selection-overlay slice', () => {
bracedBodies: 63,
unboundCatches: 9,
numberProperties: 6,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -23,7 +23,8 @@ describe('the selection-state-and-eviction module', () => {
bracedBodies: 3,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -28,7 +28,8 @@ describe('the smooth-scroll and cell-geometry slice', () => {
bracedBodies: 16,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -22,7 +22,8 @@ describe('the surface-swap module', () => {
bracedBodies: 2,
unboundCatches: 2,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -27,7 +27,8 @@ describe('the surface-touch-gestures slice', () => {
bracedBodies: 57,
unboundCatches: 2,
numberProperties: 2,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -20,7 +20,8 @@ describe('the tap-dispatch module', () => {
bracedBodies: 11,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -31,7 +31,8 @@ describe('the term-observers slice', () => {
numberProperties: 0,
// The two SGR mode flags, written twice each: the document's shorthand cannot survive a
// qualified value.
shorthandProperties: 4
shorthandProperties: 4,
unshadowedNames: 0
}
})
})
@@ -50,6 +50,11 @@ export type TerminalDocumentNormalisations = {
readonly numberProperties: number
/** `{ name: name }` was shorthand; qualifying the value spells the property out again. */
readonly shorthandProperties: number
/**
* An inner binding that shadowed a document variable stopped being a shadow once that variable
* moved onto the scope, so the printer stopped renaming it.
*/
readonly unshadowedNames: number
}
/**
@@ -60,6 +65,17 @@ export type TerminalDocumentNormalisations = {
* behind a `typeof … === 'number'` check or is parsing a string, which is what the `Number` form
* does with no coercion of its own.
*/
/**
* Whether `printed` is the printer's disambiguated form of `original`: the same name with a decimal
* suffix it appends when two bindings of that name are visible at once.
*/
function isPrinterDisambiguation(printed: string, original: string): boolean {
if (!printed.startsWith(original) || printed.length === original.length) {
return false
}
return /^[2-9][0-9]*$/.test(printed.slice(original.length))
}
const NUMBER_GLOBALS = new Set(['isFinite', 'isNaN', 'parseInt', 'parseFloat'])
export type TerminalDocumentEquivalence =
@@ -179,6 +195,7 @@ export function compareTerminalDocumentScripts(
let unboundCatches = 0
let numberProperties = 0
let shorthandProperties = 0
let unshadowedNames = 0
// Braces arrive in pairs around one statement, so a counter is enough: a close is only ever
// absorbed while an inserted open is outstanding, which bounds how far this can mask a real one.
let openInsertedBraces = 0
@@ -194,6 +211,19 @@ export function compareTerminalDocumentScripts(
right += 1
continue
}
// `term2` -> `term`: the printer disambiguated a shadowed binding on the baseline side, and
// qualifying the outer name removed the shadow, so the inner one keeps its own name.
if (
expected.label === 'name' &&
actual.label === 'name' &&
isPrinterDisambiguation(expected.text, actual.text)
) {
unshadowedNames += 1
lastMatched = actual
left += 1
right += 1
continue
}
// `{ name }` -> `{ name: <qualifier>.name }`: the printer writes the baseline's shorthand back
// as one token, and qualifying the value makes the property name unavoidable again.
if (
@@ -303,7 +333,8 @@ export function compareTerminalDocumentScripts(
bracedBodies,
unboundCatches,
numberProperties,
shorthandProperties
shorthandProperties,
unshadowedNames
}
}
}
@@ -24,7 +24,8 @@ const NONE: TerminalDocumentNormalisations = {
bracedBodies: 0,
unboundCatches: 0,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
function normalisationsOf(before: string, after: string): TerminalDocumentNormalisations | string {
@@ -92,7 +93,8 @@ describe('terminal document script equivalence', () => {
...NONE,
unboundCatches: 1,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
})
})
@@ -133,6 +135,15 @@ describe('terminal document script equivalence', () => {
expect(normalisationsOf(script, script)).toEqual(NONE)
})
it('counts a name the printer no longer has to disambiguate', () => {
expect(
normalisationsOf(
'var term = null; function f(term) { return term; }',
'scope.term = null; function f(term) { return term; }'
)
).toEqual({ ...NONE, scopeFieldDeclarations: 1, unshadowedNames: 2 })
})
it('refuses a changed literal', () => {
expect(normalisationsOf('var a = 1;', 'var a = 2')).toBe(
'token 3: expected num 1, generated num 2'
@@ -0,0 +1,51 @@
import { fileURLToPath } from 'node:url'
import { describe, expect, it } from 'vitest'
import { emitTerminalDocumentModule } from '../../../scripts/build-terminal-document-script.mjs'
import { TERMINAL_DOCUMENT_MODULE_ORDER } from '../../../scripts/terminal-document-module-order.mjs'
import { XTERM_ENGINE_JS } from '../terminal-webview-engine.generated'
import { XTERM_HTML } from '../terminal-webview-html'
import {
compareTerminalDocumentScripts,
readTerminalDocumentScript
} from './terminal-document-equivalence.test-support'
/**
* The review of the move, as one number per difference class.
*
* Every line of the document's script is now a module, and this says the two are the same program
* modulo the qualifier and the repository's own rules rewriting an ES5 document the moment its
* source is a linted module. Anything outside those classes refuses with the token index and both
* sides, so a reordered statement, a changed literal or a renamed local cannot pass here.
*/
describe('the whole terminal document script', () => {
it('is what the modules emit, modulo the seven normalisations', async () => {
const emitted = await Promise.all(
TERMINAL_DOCUMENT_MODULE_ORDER.map((name) =>
emitTerminalDocumentModule(fileURLToPath(new URL(`./${name}.ts`, import.meta.url)))
)
)
const candidate = `(function() {\n${emitted.join('\n')}\n})();`
const baseline = readTerminalDocumentScript(XTERM_HTML, XTERM_ENGINE_JS)
expect(compareTerminalDocumentScripts(baseline, candidate, 'scope')).toEqual({
equivalent: true,
normalisations: {
// The qualifier, partitioned: 609 reads and writes of a name whose declaration stayed put,
// and 73 declarations that moved onto the scope object. 682 sites in all.
qualifiedReferences: 609,
scopeFieldDeclarations: 73,
// The document's 446 `var` declarators, less the 73 that became scope fields.
rebindings: 373,
// `curly`, measured over the whole script before any of this started.
bracedBodies: 279,
// Of the document's 38 catch clauses, two name their error and report it, so they keep it.
unboundCatches: 36,
// `unicorn/prefer-number-properties`, also measured up front.
numberProperties: 17,
// Two SGR mode flags written twice each: shorthand cannot survive a qualified value.
shorthandProperties: 4,
// Names the printer had to disambiguate while an outer binding of the same name existed.
unshadowedNames: 7
}
})
})
})
@@ -23,7 +23,8 @@ describe('the terminal init-and-write slice', () => {
bracedBodies: 18,
unboundCatches: 7,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -22,7 +22,8 @@ describe('the terminal-theme module', () => {
// Every `parseInt`, `parseFloat` and `isFinite` here is applied to a value already proved
// numeric, or to a string the two forms agree on.
numberProperties: 9,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
+2 -1
View File
@@ -29,7 +29,8 @@ describe('the url-tap group', () => {
unboundCatches: 6,
// All four take a digit run a capture group already matched.
numberProperties: 4,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -19,7 +19,8 @@ describe('the WebGL recovery module', () => {
// Five of the six catch clauses; the attach failure reads its error and keeps its binding.
unboundCatches: 5,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -23,7 +23,8 @@ describe('the wheel-scroll module', () => {
// The one `isFinite`, behind a `typeof delta !== 'number'` check that makes the two forms
// the same test.
numberProperties: 1,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})
@@ -21,7 +21,8 @@ describe('the write-queue module', () => {
// Disposing an observer that is already gone.
unboundCatches: 1,
numberProperties: 0,
shorthandProperties: 0
shorthandProperties: 0,
unshadowedNames: 0
}
})
})