Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 25 additions & 2 deletions src/view/compiler/expression.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,7 +115,12 @@ class Bail extends Error {}
/**
* Scans a single/double-quoted string literal starting at `start` (the opening
* quote), respecting backslash escapes. Returns the index past the closing
* quote (or end of input).
* quote.
*
* Bails (falls back to the runtime evaluator) on an unterminated literal so a
* malformed expression is never copied verbatim into the emitted module — one
* broken entry would otherwise throw at import and take down every precompiled
* expression in the file.
*/
const scanStringLiteral = (src: string, start: number): number => {
const quote = src[start];
Expand All @@ -124,9 +129,22 @@ const scanStringLiteral = (src: string, start: number): number => {
if (src[i] === '\\') i += 1;
i += 1;
}
// Reaching end-of-input without the matching quote means it was never closed.
if (i >= src.length || src[i] !== quote) {
throw new Bail('unterminated string literal');
}
return i + 1;
};

/**
* A valid JS numeric literal in the supported subset: decimal (with optional
* fraction/exponent), hex, octal, or binary, each allowing `_` separators
* between digits. Used to reject runs like `1ex` that {@link scanNumericLiteral}
* would otherwise consume and emit as invalid code.
*/
const NUMERIC_LITERAL_RE =
/^(?:0[xX][0-9a-fA-F](?:_?[0-9a-fA-F])*|0[oO][0-7](?:_?[0-7])*|0[bB][01](?:_?[01])*|(?:\d(?:_?\d)*)(?:\.(?:\d(?:_?\d)*)?)?(?:[eE][+-]?\d(?:_?\d)*)?|\.\d(?:_?\d)*(?:[eE][+-]?\d(?:_?\d)*)?)$/;

/**
* Scans a numeric literal (hex/float/exponent/separators) starting at `start`,
* returning the end index. A `.` is consumed only as a single decimal point
Expand Down Expand Up @@ -237,7 +255,12 @@ const rewrite = (src: string, param: string, globals: ReadonlySet<string>): stri
if (isDigit(c) || (c === '.' && isDigit(src[i + 1]))) {
const start = i;
i = scanNumericLiteral(src, i);
out += src.slice(start, i);
const literal = src.slice(start, i);
// Reject invalid runs (e.g. `1ex`) rather than emitting unparsable code.
if (!NUMERIC_LITERAL_RE.test(literal)) {
throw new Bail('invalid numeric literal');
}
out += literal;
prev = '0';
continue;
}
Expand Down
20 changes: 20 additions & 0 deletions tests/view-compiler.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,26 @@ describe('compileExpression — conservative bail-outs (#138)', () => {
bail('a /* c */ + b');
bail('');
});

it('bails on unterminated string literals instead of emitting broken code (#170)', () => {
bail("'oops");
bail('"unclosed');
bail("greeting + 'tail");
// A valid closing quote after escapes still compiles.
const r = compileExpression("'a\\'b'");
expect(r.ok).toBe(true);
});

it('bails on invalid numeric literals (#170)', () => {
bail('1ex');
bail('1e');
bail('0xG1');
// Valid numerics still compile.
for (const n of ['1', '1.5', '0xFF', '1e3', '1_000', '.5', '0b1010']) {
const r = compileExpression(n);
expect(r.ok).toBe(true);
}
});
});

describe('compileViews — template walking (#138)', () => {
Expand Down