mirror of
https://github.com/larvit/adf-codec.git
synced 2026-11-12 11:48:29 +01:00
Assert outcomes in the entity, JSON and error-code tests, so none reads source text
This commit is contained in:
@@ -885,3 +885,20 @@ Read most (opens and searches):
|
||||
- `AGENTS.md` 1
|
||||
- `docker-runner.sh` 1
|
||||
- `package.json` 1
|
||||
|
||||
## Reading cost, chunk 0a04be76fd43, finished 2026-11-09T06:51:38Z
|
||||
|
||||
152169 new input tokens before the first commit.
|
||||
|
||||
Read most (opens and searches):
|
||||
|
||||
- `README.md` 14
|
||||
- `todo.md` 12
|
||||
- `src/adf/document.test.ts` 11
|
||||
- `docs/decisions.md` 6
|
||||
- `src/adf/document.ts` 6
|
||||
- `src/conformance/corpus.test.ts` 6
|
||||
- `MIGRATION.md` 4
|
||||
- `AGENTS.md` 3
|
||||
- `src/index.ts` 3
|
||||
- `CHANGELOG.md` 2
|
||||
|
||||
+35
-9
@@ -1,13 +1,17 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { dirname, join } from 'node:path'
|
||||
import fc from 'fast-check'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import type { AdfDocument, AdfNode } from './adf/document.ts'
|
||||
import type { JsonSpelling } from './canonical-json.ts'
|
||||
import { adfToHtml } from './html/emit/adf-to-html.ts'
|
||||
import { adfToLosslessMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||
import { adfToPortableMarkdown } from './markdown/portable/adf-to-portable-markdown.ts'
|
||||
import { htmlToAdf } from './html/parse/html-to-adf.ts'
|
||||
import { jsonValue, propertyRuns } from './conformance/property-harness.ts'
|
||||
import { losslessMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||
import { parseJsonText } from './json-text.ts'
|
||||
import { portableMarkdownToAdf } from './markdown/portable/portable-markdown-to-adf.ts'
|
||||
import { serializeCanonicalJson } from './canonical-json.ts'
|
||||
|
||||
test('reads every JSON value kind', () => {
|
||||
@@ -87,10 +91,32 @@ test('accepts exactly the text JSON.parse accepts', () => {
|
||||
)
|
||||
})
|
||||
|
||||
test('no shipped file reads JSON with JSON.parse, which parseJsonText stands in for', () => {
|
||||
const sourceRoot = dirname(fileURLToPath(import.meta.url))
|
||||
const readers = readdirSync(sourceRoot, { encoding: 'utf8', recursive: true }).filter(
|
||||
(name) => name.endsWith('.ts') && !name.endsWith('.test.ts') && !name.startsWith('conformance') && readFileSync(join(sourceRoot, name), 'utf8').includes('JSON.parse('),
|
||||
)
|
||||
assert.deepEqual(readers, [])
|
||||
test('no reader reads JSON text with JSON.parse, which parseJsonText stands in for', () => {
|
||||
const unknownBlock: AdfNode = { attrs: { a: [1, { b: 'c' }] }, content: [{ text: 'x', type: 'text' }], type: 'unknownBlock' }
|
||||
const unknownInline: AdfNode = { attrs: { a: { b: 2 } }, type: 'unknownInline' }
|
||||
const table: AdfNode = {
|
||||
content: [{ content: [{ attrs: { colwidth: [100, 200] }, content: [{ content: [{ text: 'x', type: 'text' }], type: 'paragraph' }], type: 'tableCell' }], type: 'tableRow' }],
|
||||
type: 'table',
|
||||
}
|
||||
const marked: AdfNode = { content: [{ marks: [{ attrs: { color: '#fff' }, type: 'textColor' }], text: 'x', type: 'text' }, unknownInline], type: 'paragraph' }
|
||||
const document: AdfDocument = { content: [unknownBlock, table, marked, { attrs: { panelType: 'info' }, content: [marked], type: 'panel' }], type: 'doc', version: 1 }
|
||||
const spellings = [adfToLosslessMarkdown(document), adfToPortableMarkdown(document), adfToHtml(document)]
|
||||
const [lossless, portable, html] = spellings.map((spelled) => (spelled.ok ? spelled.value : undefined))
|
||||
assert.ok(lossless !== undefined && portable !== undefined && html !== undefined)
|
||||
const parse = JSON.parse
|
||||
let called = 0
|
||||
JSON.parse = (...args: Parameters<typeof JSON.parse>) => {
|
||||
called += 1
|
||||
return parse(...args)
|
||||
}
|
||||
try {
|
||||
const read = [losslessMarkdownToAdf(lossless), portableMarkdownToAdf(portable), htmlToAdf(html)]
|
||||
assert.deepEqual(
|
||||
read.map((result) => result.ok),
|
||||
[true, true, true],
|
||||
)
|
||||
} finally {
|
||||
JSON.parse = parse
|
||||
}
|
||||
assert.equal(called, 0)
|
||||
})
|
||||
|
||||
@@ -1,21 +1,24 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
import { readEntityReference } from './entity-references.ts'
|
||||
|
||||
const source = readFileSync(join(dirname(fileURLToPath(import.meta.url)), 'entity-references.ts'), 'utf8')
|
||||
import { namedReferences, readEntityReference } from './entity-references.ts'
|
||||
|
||||
// A value holding a space or a tilde mis-splits into a wrong name and a lost one, silently, at load.
|
||||
test('every packed reference parts into one name and one value the table decodes', () => {
|
||||
const packed = /const packedReferences =\n {2}'([\s\S]*?)'\n/.exec(source)?.[1]
|
||||
assert.ok(packed !== undefined)
|
||||
const references = packed.split(/(?:\\\n)? +/)
|
||||
assert.equal(references.length, 2125)
|
||||
for (const reference of references) {
|
||||
assert.match(reference, /^[A-Za-z][A-Za-z0-9]*~[^ ~]+$/)
|
||||
assert.ok((readEntityReference(`&${reference.slice(0, reference.indexOf('~'))};`, 0)?.text ?? '') !== '')
|
||||
test('every named reference parts into one name and one value', () => {
|
||||
assert.equal(namedReferences.size, 2125)
|
||||
for (const [name, value] of namedReferences) {
|
||||
assert.match(name, /^[A-Za-z][A-Za-z0-9]*$/, name)
|
||||
assert.match(value, /^[^ ~]+$/u, name)
|
||||
}
|
||||
})
|
||||
|
||||
test('decodes a reference to the characters HTML5 gives its name', () => {
|
||||
assert.deepEqual(readEntityReference('⁡', 0), { length: 15, text: '' })
|
||||
assert.deepEqual(readEntityReference('
', 0), { length: 9, text: '\n' })
|
||||
assert.deepEqual(readEntityReference('	', 0), { length: 5, text: '\t' })
|
||||
assert.deepEqual(readEntityReference('  ', 0), { length: 12, text: ' ' })
|
||||
assert.deepEqual(readEntityReference('⊂⃒', 0), { length: 11, text: '⊂⃒' })
|
||||
assert.deepEqual(readEntityReference(' ', 0), { length: 6, text: ' ' })
|
||||
assert.deepEqual(readEntityReference('‌', 0), { length: 6, text: '' })
|
||||
assert.equal(readEntityReference('¬areference;', 0), undefined)
|
||||
})
|
||||
|
||||
@@ -200,7 +200,7 @@ const packedReferences =
|
||||
xsqcup~⨆ xuplus~⨄ xutri~△ xvee~⋁ xwedge~⋀ yacute~ý yacy~я ycirc~ŷ ycy~ы yen~¥ yfr~𝔶 yicy~ї yopf~𝕪 yscr~𝓎 yucy~ю\
|
||||
yuml~ÿ zacute~ź zcaron~ž zcy~з zdot~ż zeetrf~ℨ zeta~ζ zfr~𝔷 zhcy~ж zigrarr~⇝ zopf~𝕫 zscr~𝓏 zwj~ zwnj~'
|
||||
|
||||
const namedReferences = new Map(packedReferences.split(/ +/).map(namedReference))
|
||||
export const namedReferences: ReadonlyMap<string, string> = new Map(packedReferences.split(/ +/).map(namedReference))
|
||||
|
||||
export function holdsEntityReference(text: string): boolean {
|
||||
for (let index = text.indexOf('&'); index !== -1; index = text.indexOf('&', index + 1)) {
|
||||
|
||||
+37
-22
@@ -1,31 +1,46 @@
|
||||
import assert from 'node:assert/strict'
|
||||
import { dirname, join } from 'node:path'
|
||||
import { fileURLToPath } from 'node:url'
|
||||
import { readFileSync, readdirSync } from 'node:fs'
|
||||
import test from 'node:test'
|
||||
|
||||
const sourceRoot = dirname(fileURLToPath(import.meta.url))
|
||||
const union = /export type ConvertErrorCode =\n((?:\s+\| '[a-z-]+'\n)+)/
|
||||
const declared = /'([a-z-]+)'/g
|
||||
const callSite = /(?:failure\(|code: )'([a-z-]+)'/g
|
||||
import type { AdfDocument, AdfMark } from './adf/document.ts'
|
||||
import type { ConvertErrorCode, Result } from './result.ts'
|
||||
import type { JsonValue } from './json-value.ts'
|
||||
import { adfToLosslessMarkdown } from './markdown/emit/adf-to-markdown.ts'
|
||||
import { htmlToAdf } from './html/parse/html-to-adf.ts'
|
||||
import { largestNesting } from './nesting.ts'
|
||||
import { losslessMarkdownToAdf } from './markdown/parse/markdown-to-adf.ts'
|
||||
import { readAdfDocument } from './adf/document.ts'
|
||||
import { withinEngineLimits } from './length-bound.ts'
|
||||
|
||||
function declaredCodes(): string[] {
|
||||
const source = readFileSync(join(sourceRoot, 'result.ts'), 'utf8')
|
||||
const members = union.exec(source)?.[1]
|
||||
assert.notEqual(members, undefined, 'result.ts declares no ConvertErrorCode union')
|
||||
return [...(members ?? '').matchAll(declared)].map(([, name]) => name ?? '').sort()
|
||||
const em: AdfMark = { type: 'em' }
|
||||
const duplicateMarks: AdfDocument = { content: [{ content: [{ marks: [em, em], text: 'x', type: 'text' }], type: 'paragraph' }], type: 'doc', version: 1 }
|
||||
|
||||
// One refusal per code. The Record type fails the build where a code has no entry, or an entry names no code.
|
||||
const causes: Record<ConvertErrorCode, () => Result<unknown>> = {
|
||||
'engine-limit': () =>
|
||||
withinEngineLimits(() => {
|
||||
throw new RangeError('Invalid array length')
|
||||
}),
|
||||
'malformed-directive': () => losslessMarkdownToAdf('!adf:/panel\n'),
|
||||
'malformed-pipe-table': () => losslessMarkdownToAdf('| Part | Qty |\n'),
|
||||
'not-a-string': () => Reflect.apply(losslessMarkdownToAdf, undefined, [null]),
|
||||
'not-an-adf-document': () => readAdfDocument(null),
|
||||
'unknown-directive-name': () => losslessMarkdownToAdf('!adf:widget[a]\n'),
|
||||
'unmappable-html': () => htmlToAdf('<table><tr></tr></table>\n'),
|
||||
'unsupported-document-version': () => adfToLosslessMarkdown({ type: 'doc', version: 2 }),
|
||||
'unsupported-nesting-depth': () => adfToLosslessMarkdown(nestedAttribute(largestNesting + 1)),
|
||||
'unsupported-node-shape': () => adfToLosslessMarkdown(duplicateMarks),
|
||||
}
|
||||
|
||||
function calledCodes(): string[] {
|
||||
const called = new Set<string>()
|
||||
for (const name of readdirSync(sourceRoot, { encoding: 'utf8', recursive: true })) {
|
||||
if (!name.endsWith('.ts') || name.endsWith('.test.ts') || name === 'result.ts') continue
|
||||
for (const [, code] of readFileSync(join(sourceRoot, name), 'utf8').matchAll(callSite)) called.add(code ?? '')
|
||||
}
|
||||
return [...called].sort()
|
||||
function nestedAttribute(depth: number): AdfDocument {
|
||||
let value: JsonValue = 1
|
||||
for (let level = 0; level < depth; level += 1) value = { a: value }
|
||||
return { content: [{ attrs: { a: value }, type: 'paragraph' }], type: 'doc', version: 1 }
|
||||
}
|
||||
|
||||
// Removing a code is breaking (docs/decisions.md §The code list), so a code must not outlive its cause.
|
||||
test('every ConvertErrorCode is the code of a production call site, and every call site names a declared one', () => {
|
||||
assert.deepEqual(calledCodes(), declaredCodes())
|
||||
})
|
||||
for (const [code, cause] of Object.entries(causes)) {
|
||||
test(`${code} is the code of a refusal the library makes`, () => {
|
||||
const result = cause()
|
||||
assert.equal(!result.ok && result.error.code, code)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -13,7 +13,6 @@
|
||||
| ID | Release | Blocked by | Exempt | Item | L | E | I | R | S | Moves | Score |
|
||||
|---|---|---|---|---|---|---|---|---|---|---|---|
|
||||
| 153 | 0.4.0 | | question | **Decide whether `src/markdown/` root may import `emit/`, `parse/` or `portable/`, and hold the answer in `.oxlintrc.json` and §The source parts by ADF and format.** | 1 | 2 | 2 | 3 | 2 | code health by 3 | 26.2 |
|
||||
| 127 | 0.4.0 | | principle | **Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.** | 2 | 2 | 1 | 3 | 3 | code health by 2 | 13.1 |
|
||||
| 159 | 0.4.0 | | | **Delete `taskBlocks`'s branch that writes the task marker as its own paragraph, once a test shows no block task item reaches it.** | 1 | 1 | 1 | 1 | 1 | code health by 1 | 10.5 |
|
||||
| 144 | 0.4.0 | | | **Give every container `docker-runner.sh` and `publish.sh` start a memory limit and an equal swap limit, sized from the largest a gate leg measurably needs.** | 1 | 1 | 1 | 1 | 2 | code health by 1 | 8.7 |
|
||||
| 143 | 0.4.0 | | defect | **Keep, in `adfToPortableMarkdown`, the link around a note, so a linked stored image writes every target.** | 2 | 1 | 3 | 4 | 3 | 7 by 1 | 6.5 |
|
||||
@@ -68,24 +67,6 @@ a paragraph opening with a spellable `!adf:link` (§Nothing recurses unbounded).
|
||||
`src/markdown/**` bans only `html/`; banning `emit/`, `parse/` and `portable/` for the root's own
|
||||
files, with its `ci.sh` probe, extends the decision.
|
||||
|
||||
### 127. Hold what `entity-references.test.ts`, `json-text.test.ts` and `result.test.ts` check by asserting outcomes, so none reads source text.
|
||||
|
||||
Technical principle "Assert the outcome", which bans scanning the source with `ast` or a regex.
|
||||
Three tests read source text:
|
||||
|
||||
- `src/markdown/commonmark/entity-references.test.ts` regex-extracts the `packedReferences` literal
|
||||
from `entity-references.ts`, to guard that each of the 2125 packed references parts into one name
|
||||
and one value the table decodes. Reformatting the literal fails it with the table unchanged.
|
||||
- `src/json-text.test.ts` greps every shipped file for `JSON.parse(`, to guard that only
|
||||
`parseJsonText` reads JSON text. `JSON.parse (` or `JSON['parse'](` escapes it.
|
||||
- `src/result.test.ts` regex-parses the `ConvertErrorCode` union out of `result.ts`, and each
|
||||
`failure('…'` and `code: '…'` out of every shipped file, to guard that every declared code has a
|
||||
production cause and every call site names a declared code. A code held in a variable or passed
|
||||
through another helper escapes it.
|
||||
|
||||
Item 81 changes `result.test.ts` too. Found by the technical-principles audit of
|
||||
item 60, 2026-10-22.
|
||||
|
||||
### 159. Delete `taskBlocks`'s branch that writes the task marker as its own paragraph, once a test shows no block task item reaches it.
|
||||
|
||||
`taskBlocks` (`portable/block-spelling.ts`) writes the marker as a paragraph of its own where a
|
||||
@@ -172,9 +153,10 @@ Technical principle: DRY. Found by the principles audit of the release review of
|
||||
|
||||
### 81. Fail the gate unless the README's error tables list exactly the codes `ConvertErrorCode` holds.
|
||||
|
||||
`src/result.test.ts` checks the union against the call sites only, so a code added or removed
|
||||
leaves the README's tables a false contract under a green gate. Assert that the tables list exactly
|
||||
the union. Found by the technical-principles audit of item 78, 2026-10-05.
|
||||
`src/result.test.ts` holds one refusal per code in a `Record<ConvertErrorCode, …>`, so a code added
|
||||
or removed leaves the README's tables a false contract under a green gate. Assert that the tables
|
||||
list exactly the codes of that record's keys. Found by the technical-principles audit of item 78,
|
||||
2026-10-05.
|
||||
|
||||
### 147. Ship `adfToPlainText`, writing a document's content with no markup, under a README goal for plain text.
|
||||
|
||||
|
||||
Reference in New Issue
Block a user