From 627a32022585d774a22e15a151d783b8686d2de7 Mon Sep 17 00:00:00 2001 From: Mateusz Date: Tue, 11 Aug 2026 01:24:02 +0200 Subject: [PATCH] Quote plain phrases containing reserved chars, not just whitespace MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit interpreter.ts's strFromExpr serialised a CqlStr (plain phrase) back to a string using hasWhitespace() alone to decide whether to re-quote it. Chip keys/values (strFromField) already used the more complete shouldQuoteFieldValue() (whitespace OR reserved char: `:`, `(`, `)`, `"`). The two should agree — an unquoted colon is exactly as ambiguous in a plain phrase as in a chip value, since the scanner treats a bare colon as chip syntax everywhere. Symptom: a quoted phrase like "hello:world" loses its quotes the moment queryChange reports it, then gets mis-parsed as a hello:world chip the next time that text is read (e.g. the next keystroke, or just focusing the input again after the value was set once). --- .changeset/gentle-moles-jump.md | 5 +++++ lib/cql/src/lang/interpreter.spec.ts | 27 +++++++++++++++++++++++++++ lib/cql/src/lang/interpreter.ts | 4 ++-- 3 files changed, 34 insertions(+), 2 deletions(-) create mode 100644 .changeset/gentle-moles-jump.md diff --git a/.changeset/gentle-moles-jump.md b/.changeset/gentle-moles-jump.md new file mode 100644 index 00000000..d13ff899 --- /dev/null +++ b/.changeset/gentle-moles-jump.md @@ -0,0 +1,5 @@ +--- +"@guardian/cql": patch +--- + +Fix `queryChange`'s serialised `queryStr` dropping quotes from a plain phrase containing a reserved character (`:`, `(`, `)`, or `"`) with no whitespace, e.g. `"hello:world"`. Previously only whitespace triggered re-quoting for plain phrases, while chip keys/values already correctly checked reserved characters too — the two now use the same predicate (`shouldQuoteFieldValue`). Without this fix, a quoted phrase containing a colon is silently corrupted into an unquoted string, which then gets mis-parsed as a `key:value` chip the next time it's read. diff --git a/lib/cql/src/lang/interpreter.spec.ts b/lib/cql/src/lang/interpreter.spec.ts index 93897519..bd7045e2 100644 --- a/lib/cql/src/lang/interpreter.spec.ts +++ b/lib/cql/src/lang/interpreter.spec.ts @@ -27,6 +27,33 @@ describe("interpreter", () => { expect(str).toBe(queryStr); }); + it("should quote a plain string containing a reserved char (colon)", () => { + const queryStr = `"hello:world"`; + const query = parser(queryStr).queryAst!; + + const str = cqlQueryStrFromQueryAst(query); + + expect(str).toBe(queryStr); + }); + + it("should quote a plain string containing a reserved char (parens)", () => { + const queryStr = `"(parenthetical)"`; + const query = parser(queryStr).queryAst!; + + const str = cqlQueryStrFromQueryAst(query); + + expect(str).toBe(queryStr); + }); + + it("should not quote a plain string with no reserved chars or whitespace", () => { + const queryStr = `hello`; + const query = parser(queryStr).queryAst!; + + const str = cqlQueryStrFromQueryAst(query); + + expect(str).toBe(queryStr); + }); + it("should escape reserved characters in chip keys and values", () => { const queryStr = `key:"\\"value\\""`; const query = parser(queryStr).queryAst!; diff --git a/lib/cql/src/lang/interpreter.ts b/lib/cql/src/lang/interpreter.ts index 92fa1abd..bcc6070f 100644 --- a/lib/cql/src/lang/interpreter.ts +++ b/lib/cql/src/lang/interpreter.ts @@ -1,5 +1,5 @@ import { CqlBinary, CqlExpr, CqlField, CqlQuery } from "./ast"; -import { hasWhitespace, shouldQuoteFieldValue } from "./utils"; +import { shouldQuoteFieldValue } from "./utils"; export const cqlQueryStrFromQueryAst = (query: CqlQuery): string => { const { content } = query; @@ -17,7 +17,7 @@ const strFromExpr = (queryExpr: CqlExpr): string | undefined => { const renderedContent = (() => { switch (content.type) { case "CqlStr": - return hasWhitespace(content.searchExpr) + return shouldQuoteFieldValue(content.searchExpr) ? `"${content.searchExpr}"` : content.searchExpr; case "CqlGroup":