From f52406018485849757269ba281a65c2508d0fa19 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:41:58 +0000 Subject: [PATCH 1/3] =?UTF-8?q?Bring=20djs/parser=20to=20100%=20coverage:?= =?UTF-8?q?=2094.05%=20=E2=86=92=20100%=20branch?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - tokenToValue: narrow its parameter to a new _ValueToken type (the union isValueToken already establishes as a type guard) so the switch is exhaustive without a defensive default arm, matching the fix already applied to media/json/parser (#1544). - Add proof cases for every parser state's unhandled eof/unexpected- token branch that had no test: bare 'export'/'const'/'const x'/ 'import'/'import a'/'import a from', an unterminated array/object, an object key with no ':', a ':' with no value, a ',' with no next member, and a stray token after an object value. - Add a direct parseFromTokens(null) case: the tokenizer never produces an empty token list (it always emits at least eof), but the exported function's own contract still needs to handle one — the initial state falls through to the same "unexpected end". fjs/djs/parser/module.f.mjs reaches 100% line/branch/function coverage. --- fjs/djs/parser/module.f.mjs | 15 +++-- fjs/djs/parser/proof.f.mjs | 109 ++++++++++++++++++++++++++++++++++++ fjs/djs/parser/types.ts | 6 ++ 3 files changed, 126 insertions(+), 4 deletions(-) diff --git a/fjs/djs/parser/module.f.mjs b/fjs/djs/parser/module.f.mjs index 6b164cef0..fb30a1080 100644 --- a/fjs/djs/parser/module.f.mjs +++ b/fjs/djs/parser/module.f.mjs @@ -10,7 +10,7 @@ * @import { OrderedMap } from '../../types/ordered_map/types.ts' * @import { AstArray, AstConst, AstModule, AstModuleRef } from '../ast/types.ts' * @import { TokenMetadata } from '../../js/tokenizer/types.ts' - * @import { ParseError } from './types.ts' + * @import { ParseError, _ValueToken } from './types.ts' */ import { error, ok } from '../../types/result/module.f.mjs' @@ -302,7 +302,12 @@ const endObject = state => { return pushValue(newState)(obj) } -/** @type {(token: DjsToken) => AstConst} */ +/** + * Only ever called on a token `isValueToken` has already confirmed carries a + * value, so the switch covers every `_ValueToken` case with no fallback arm. + * + * @type {(token: _ValueToken) => AstConst} + */ const tokenToValue = token => { switch (token.kind) { case 'null': return null @@ -312,11 +317,13 @@ const tokenToValue = token => { case 'string': return token.value case 'bigint': return token.value case 'undefined': return undefined - default: return null } } -/** @type {(token: DjsToken) => boolean} */ +/** + * @param {DjsToken} token + * @returns {token is _ValueToken} + */ const isValueToken = token => { switch (token.kind) { case 'null': diff --git a/fjs/djs/parser/proof.f.mjs b/fjs/djs/parser/proof.f.mjs index 3dbcff2eb..041799a82 100644 --- a/fjs/djs/parser/proof.f.mjs +++ b/fjs/djs/parser/proof.f.mjs @@ -300,6 +300,115 @@ export const proof = { assert(obj[0] === 'error', obj) assertEq(obj[1].message, 'unexpected token') }, + () => { + // 'export' with no 'default' before eof. + const tokenList = tokenizeString('export') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // 'const' with no name before eof. + const tokenList = tokenizeString('const') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // 'const ' with no '=' before eof. + const tokenList = tokenizeString('const x') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // 'const ' followed by a token that isn't '='. + const tokenList = tokenizeString('const x 5') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected token') + }, + () => { + // 'import' with no name before eof. + const tokenList = tokenizeString('import') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // 'import' followed by a token that isn't an id. + const tokenList = tokenizeString('import 5') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected token') + }, + () => { + // 'import ' with no 'from' before eof. + const tokenList = tokenizeString('import a') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // 'import from' with no module string before eof. + const tokenList = tokenizeString('import a from') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // Array opened but eof arrives before any value/']'. + const tokenList = tokenizeString('export default [') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // Object opened but eof arrives before any key/'}'. + const tokenList = tokenizeString('export default {') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // Object key given but eof arrives before ':'. + const tokenList = tokenizeString('export default {"a"') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // Object ':' given but eof arrives before the value. + const tokenList = tokenizeString('export default {"a":') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // Object value given, followed by a token that's neither ',' nor '}'. + const tokenList = tokenizeString('export default {"a":1 2}') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected token') + }, + () => { + // Object ',' given but eof arrives before the next key/'}'. + const tokenList = tokenizeString('export default {"a":1,') + const obj = parseFromTokens(tokenList) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + }, + () => { + // `parseFromTokens` itself, called with no tokens at all (the + // tokenizer never produces this — it always emits at least an + // `eof` token — but the exported function's own contract must + // still handle it: the initial state is neither 'result' nor + // 'error', so it falls through to the same "unexpected end". + const obj = parseFromTokens(null) + assert(obj[0] === 'error', obj) + assertEq(obj[1].message, 'unexpected end') + assertEq(obj[1].metadata, null) + }, ], errorMetadata: [ () => { diff --git a/fjs/djs/parser/types.ts b/fjs/djs/parser/types.ts index dc456b72c..0cf03144d 100644 --- a/fjs/djs/parser/types.ts +++ b/fjs/djs/parser/types.ts @@ -6,8 +6,14 @@ */ import type { TokenMetadata } from '../../js/tokenizer/types.ts' +import type { DjsToken } from '../tokenizer/types.ts' export type ParseError = { readonly message: string, readonly metadata: TokenMetadata | null } + +/** DJS tokens that carry a directly-usable value. */ +export type _ValueToken = Extract From d353d4cfca44933895981402b8f3aa163d7b8644 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 17:42:19 +0000 Subject: [PATCH 2/3] Add changelog entry for #1556 --- changelog/unreleased/1556.md | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 changelog/unreleased/1556.md diff --git a/changelog/unreleased/1556.md b/changelog/unreleased/1556.md new file mode 100644 index 000000000..d442e1d93 --- /dev/null +++ b/changelog/unreleased/1556.md @@ -0,0 +1,4 @@ +- `djs/parser`: `tokenToValue` drops its defensive `default` arm — narrowed + to a new `_ValueToken` type derived from `isValueToken`'s type guard, so + the switch is exhaustive + [#1556](https://github.com/functionalscript/functionalscript/pull/1556) From 8580f0dbc120c7f2cadd9508e5fe6c80a44e4983 Mon Sep 17 00:00:00 2001 From: Claude Date: Fri, 14 Aug 2026 18:08:18 +0000 Subject: [PATCH 3/3] Update types.ts module doc to mention _ValueToken Non-blocking review note: the @module comment still described the file as holding only the ParseError shape after this PR added _ValueToken alongside it. --- fjs/djs/parser/types.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/fjs/djs/parser/types.ts b/fjs/djs/parser/types.ts index 0cf03144d..f9c9a6c06 100644 --- a/fjs/djs/parser/types.ts +++ b/fjs/djs/parser/types.ts @@ -1,6 +1,7 @@ /** * Type-level API for `fjs/djs/parser/module.f.mjs`: the `ParseError` shape - * `parseFromTokens` reports. + * `parseFromTokens` reports, and the `_ValueToken` subset `tokenToValue` + * accepts. * * @module */