From d34069a7ffb9a646e76e149cef581df009b59fc8 Mon Sep 17 00:00:00 2001 From: Daan Verstraten Date: Wed, 22 Jul 2026 12:55:55 +0200 Subject: [PATCH 1/2] fix(molang): report clear diagnostic for unterminated strings (#1) The tokenizer threw a plain Error for unterminated string literals and unexpected characters. These bypassed the MolangSyntaxError handling in the diagnoser and fell through to the "unknown error was thrown during parsing of molang" branch, which surfaced a raw stack trace to the user. Throw MolangSyntaxError instead so the diagnoser emits an actionable message pointing at the missing quotation mark, with a proper error code and source position. Fixes #621 Claude-Session: https://claude.ai/code/session_01R7TyhiGbYgxAizqPxp53sq Co-authored-by: Claude --- packages/molang/src/molang/syntax/tokens.ts | 10 ++++- packages/molang/test/syntax/tokens.test.ts | 43 +++++++++++++++++++++ 2 files changed, 51 insertions(+), 2 deletions(-) diff --git a/packages/molang/src/molang/syntax/tokens.ts b/packages/molang/src/molang/syntax/tokens.ts index e4a176ce..48fb47bc 100644 --- a/packages/molang/src/molang/syntax/tokens.ts +++ b/packages/molang/src/molang/syntax/tokens.ts @@ -1,3 +1,5 @@ +import { MolangSyntaxError } from './errors'; + /** Represents a token in the Molang code */ export interface Token { type: TokenType; @@ -280,7 +282,11 @@ export function tokenize(input: string): Token[] { pos++; } if (pos >= input.length) { - throw new Error(`Unterminated string literal starting at position ${start}`); + throw new MolangSyntaxError( + `Missing closing ${quote === '"' ? 'double' : 'single'} quotation mark (${quote}) for the string starting at position ${start}`, + start, + 'error.string.unterminated', + ); } pos++; // Skip closing quote tokens.push({ @@ -290,7 +296,7 @@ export function tokenize(input: string): Token[] { }); continue; // Use continue instead of break to avoid the pos++ at the end of switch default: - throw new Error(`Unexpected character at position ${pos}: ${char}`); + throw new MolangSyntaxError(`Unexpected character '${char}' at position ${pos}`, pos, 'error.character.unexpected'); } pos++; continue; diff --git a/packages/molang/test/syntax/tokens.test.ts b/packages/molang/test/syntax/tokens.test.ts index d20ea8b0..35e88ada 100644 --- a/packages/molang/test/syntax/tokens.test.ts +++ b/packages/molang/test/syntax/tokens.test.ts @@ -1,3 +1,4 @@ +import { MolangSyntaxError } from '../../src/molang/syntax/errors'; import { tokenize, TokenType } from '../../src/molang/syntax/tokens'; import { valid_syntaxes } from '../data/dataset-valid'; @@ -8,4 +9,46 @@ describe('molang - syntax', () => { expect(n.map((item) => `${item.value} ${TokenType[item.type]}`)).toMatchSnapshot(); }); }); + + describe('should throw a MolangSyntaxError for malformed input', () => { + test('unterminated string literal (missing closing quote)', () => { + const input = "q.all_tags('minecraft:is_tool', 'minecraft:is_pickaxe)"; + expect(() => tokenize(input)).toThrow(MolangSyntaxError); + try { + tokenize(input); + } catch (err) { + expect(err).toBeInstanceOf(MolangSyntaxError); + const syntaxError = err as MolangSyntaxError; + expect(syntaxError.code).toBe('error.string.unterminated'); + expect(syntaxError.message).toContain('Missing closing'); + expect(syntaxError.message).toContain("single quotation mark (')"); + expect(syntaxError.position).toBe(input.indexOf("'minecraft:is_pickaxe")); + } + }); + + test('unterminated double-quoted string literal', () => { + const input = 'q.all_tags("minecraft:is_tool)'; + expect(() => tokenize(input)).toThrow(MolangSyntaxError); + try { + tokenize(input); + } catch (err) { + const syntaxError = err as MolangSyntaxError; + expect(syntaxError.code).toBe('error.string.unterminated'); + expect(syntaxError.message).toContain('double quotation mark (")'); + } + }); + + test('unexpected character', () => { + const input = 'q.foo @ q.bar'; + expect(() => tokenize(input)).toThrow(MolangSyntaxError); + try { + tokenize(input); + } catch (err) { + const syntaxError = err as MolangSyntaxError; + expect(syntaxError).toBeInstanceOf(MolangSyntaxError); + expect(syntaxError.code).toBe('error.character.unexpected'); + expect(syntaxError.message).toContain("Unexpected character '@'"); + } + }); + }); }); From c3e447feaae16438207850e85073f4e5b0ab1600 Mon Sep 17 00:00:00 2001 From: Claude Date: Wed, 22 Jul 2026 10:59:21 +0000 Subject: [PATCH 2/2] fix(molang): give friendly diagnostics for malformed strings The tokenizer threw a plain Error for unterminated string literals and unexpected characters. These bypassed the MolangSyntaxError handling in the diagnoser and fell through to the "unknown error was thrown during parsing of molang" branch, which surfaced a raw stack trace to the user. Throw MolangSyntaxError with plain-language, human-friendly messages instead. The unterminated-string error names the quote style, shows a short preview of the offending text, and tells the user exactly what to add; the unexpected-character error hints at the likely cause. The editor already highlights the location, so raw character offsets are dropped from the wording. Fixes #621 Co-Authored-By: Claude Opus 4.8 Claude-Session: https://claude.ai/code/session_01R7TyhiGbYgxAizqPxp53sq --- packages/molang/src/molang/syntax/tokens.ts | 18 ++++++++++++++++-- packages/molang/test/syntax/tokens.test.ts | 9 +++++---- 2 files changed, 21 insertions(+), 6 deletions(-) diff --git a/packages/molang/src/molang/syntax/tokens.ts b/packages/molang/src/molang/syntax/tokens.ts index 48fb47bc..2a600db2 100644 --- a/packages/molang/src/molang/syntax/tokens.ts +++ b/packages/molang/src/molang/syntax/tokens.ts @@ -1,5 +1,12 @@ import { MolangSyntaxError } from './errors'; +/** Builds a short, readable preview of a snippet of source for use in error messages. */ +function previewSnippet(text: string, maxLength = 20): string { + const singleLine = text.replace(/\s+/g, ' ').trim(); + if (singleLine.length <= maxLength) return singleLine; + return `${singleLine.slice(0, maxLength)}…`; +} + /** Represents a token in the Molang code */ export interface Token { type: TokenType; @@ -282,8 +289,11 @@ export function tokenize(input: string): Token[] { pos++; } if (pos >= input.length) { + const quoteName = quote === '"' ? 'double' : 'single'; + const preview = previewSnippet(value); + const preamble = preview.length > 0 ? `The text ${quote}${preview}${quote}` : 'This string'; throw new MolangSyntaxError( - `Missing closing ${quote === '"' ? 'double' : 'single'} quotation mark (${quote}) for the string starting at position ${start}`, + `${preamble} is missing its closing ${quoteName} quote (${quote}). Add a ${quote} where the text should end.`, start, 'error.string.unterminated', ); @@ -296,7 +306,11 @@ export function tokenize(input: string): Token[] { }); continue; // Use continue instead of break to avoid the pos++ at the end of switch default: - throw new MolangSyntaxError(`Unexpected character '${char}' at position ${pos}`, pos, 'error.character.unexpected'); + throw new MolangSyntaxError( + `Molang doesn't recognize the character '${char}' here. Check for a typo or a missing quote around text values.`, + pos, + 'error.character.unexpected', + ); } pos++; continue; diff --git a/packages/molang/test/syntax/tokens.test.ts b/packages/molang/test/syntax/tokens.test.ts index 35e88ada..06dd01d5 100644 --- a/packages/molang/test/syntax/tokens.test.ts +++ b/packages/molang/test/syntax/tokens.test.ts @@ -20,8 +20,9 @@ describe('molang - syntax', () => { expect(err).toBeInstanceOf(MolangSyntaxError); const syntaxError = err as MolangSyntaxError; expect(syntaxError.code).toBe('error.string.unterminated'); - expect(syntaxError.message).toContain('Missing closing'); - expect(syntaxError.message).toContain("single quotation mark (')"); + expect(syntaxError.message).toContain('missing its closing single quote'); + expect(syntaxError.message).toContain("Add a ' where the text should end"); + // Points at the start of the unterminated string, not the end of input. expect(syntaxError.position).toBe(input.indexOf("'minecraft:is_pickaxe")); } }); @@ -34,7 +35,7 @@ describe('molang - syntax', () => { } catch (err) { const syntaxError = err as MolangSyntaxError; expect(syntaxError.code).toBe('error.string.unterminated'); - expect(syntaxError.message).toContain('double quotation mark (")'); + expect(syntaxError.message).toContain('missing its closing double quote'); } }); @@ -47,7 +48,7 @@ describe('molang - syntax', () => { const syntaxError = err as MolangSyntaxError; expect(syntaxError).toBeInstanceOf(MolangSyntaxError); expect(syntaxError.code).toBe('error.character.unexpected'); - expect(syntaxError.message).toContain("Unexpected character '@'"); + expect(syntaxError.message).toContain("doesn't recognize the character '@'"); } }); });