From d7e3feade19a94aaf18e1c7ce0c4b77c901e6eee Mon Sep 17 00:00:00 2001 From: Wojciech Grzeszczak Date: Tue, 21 Apr 2026 07:07:43 +0000 Subject: [PATCH 1/2] json double quote requirement check --- .../src/checks/index.ts | 2 + .../json-literal-quote-style/index.spec.ts | 129 ++++++++++++++++++ .../checks/json-literal-quote-style/index.ts | 45 ++++++ .../platformos-check-node/configs/all.yml | 3 + .../configs/recommended.yml | 3 + 5 files changed, 182 insertions(+) create mode 100644 packages/platformos-check-common/src/checks/json-literal-quote-style/index.spec.ts create mode 100644 packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts diff --git a/packages/platformos-check-common/src/checks/index.ts b/packages/platformos-check-common/src/checks/index.ts index 17e2343..f00748b 100644 --- a/packages/platformos-check-common/src/checks/index.ts +++ b/packages/platformos-check-common/src/checks/index.ts @@ -40,6 +40,7 @@ import { MissingRenderPartialArguments } from './missing-render-partial-argument import { NestedGraphQLQuery } from './nested-graphql-query'; import { MissingPage } from './missing-page'; import { ValidFrontmatter } from './valid-frontmatter'; +import { JsonLiteralQuoteStyle } from './json-literal-quote-style'; export const allChecks: ( | LiquidCheckDefinition @@ -81,6 +82,7 @@ export const allChecks: ( NestedGraphQLQuery, MissingPage, ValidFrontmatter, + JsonLiteralQuoteStyle, ]; /** diff --git a/packages/platformos-check-common/src/checks/json-literal-quote-style/index.spec.ts b/packages/platformos-check-common/src/checks/json-literal-quote-style/index.spec.ts new file mode 100644 index 0000000..cb8973e --- /dev/null +++ b/packages/platformos-check-common/src/checks/json-literal-quote-style/index.spec.ts @@ -0,0 +1,129 @@ +import { expect, describe, it } from 'vitest'; +import { JsonLiteralQuoteStyle } from './index'; +import { applyFix, runLiquidCheck } from '../../test'; + +describe('Module: JsonLiteralQuoteStyle', () => { + it('should report single-quoted keys in an inline hash literal', async () => { + const sourceCode = `{% assign a = {'a': 5} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.have.length(1); + expect(offenses[0].message).to.equal( + 'Use double quotes for string literals inside object/array literals (e.g. \'{"key": "value"}\', not "{\'key\': \'value\'}").', + ); + }); + + it('should report single-quoted string values in an inline hash literal', async () => { + const sourceCode = `{% assign a = {"key": 'value'} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.have.length(1); + }); + + it('should report both single-quoted keys and values', async () => { + const sourceCode = `{% assign a = {'a': 'b', 'c': 'd'} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.have.length(4); + }); + + it('should report single-quoted strings in nested hash literals', async () => { + const sourceCode = `{% assign a = {"outer": {'inner': 1}} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.have.length(1); + }); + + it('should report single-quoted strings inside array literals', async () => { + const sourceCode = `{% assign a = ['x', 'y'] %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.have.length(2); + }); + + it('should not report double-quoted strings in object literals', async () => { + const sourceCode = `{% assign a = {"a": "b", "c": [1, "d"]} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.be.empty; + }); + + it('should not report bare keys in object literals', async () => { + const sourceCode = `{% assign a = {a: 2} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.be.empty; + }); + + it('should not report single-quoted strings outside of inline JSON literals', async () => { + const sourceCode = ` + {% assign a = 'plain string' %} + {% assign b = 'pass' | upcase %} + {{ 'hello' }} + `; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.be.empty; + }); + + it('should not report parse_json style JSON-in-a-string', async () => { + const sourceCode = `{% assign a = '{"a": 5}' | parse_json %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.be.empty; + }); + + it('should fix single-quoted keys to double-quoted keys', async () => { + const sourceCode = `{% assign a = {'a': 5} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + const fixed = applyFix(sourceCode, offenses[0]); + + expect(fixed).to.equal(`{% assign a = {"a": 5} %}`); + }); + + it('should fix single-quoted string values to double-quoted values', async () => { + const sourceCode = `{% assign a = {"key": 'value'} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + const fixed = applyFix(sourceCode, offenses[0]); + + expect(fixed).to.equal(`{% assign a = {"key": "value"} %}`); + }); + + it('should properly escape embedded double quotes when fixing', async () => { + const sourceCode = `{% assign a = {'msg': 'she said "hi"'} %}`; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + expect(offenses).to.have.length(2); + + const valueOffense = offenses.find((o) => + sourceCode.slice(o.start.index, o.end.index).includes('she said'), + ); + expect(valueOffense).to.not.be.undefined; + + const fixedValue = applyFix(sourceCode, valueOffense!); + expect(fixedValue).to.equal(`{% assign a = {'msg': "she said \\"hi\\""} %}`); + }); + + it('should report single-quoted strings in function return literals', async () => { + const sourceCode = ` + {% function result = my_func %} + {% return {'a': 5} %} + {% endfunction %} + `; + + const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode); + + expect(offenses).to.have.length(1); + }); +}); diff --git a/packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts b/packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts new file mode 100644 index 0000000..b16415a --- /dev/null +++ b/packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts @@ -0,0 +1,45 @@ +import { NodeTypes } from '@platformos/liquid-html-parser'; +import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types'; + +export const JsonLiteralQuoteStyle: LiquidCheckDefinition = { + meta: { + code: 'JsonLiteralQuoteStyle', + name: 'Use double quotes in JSON literals', + docs: { + description: + 'Enforces double-quoted string literals inside inline object/array literals (e.g. {% assign a = {"a": 5} %}). Single-quoted strings inside these literals are not valid JSON.', + recommended: true, + url: undefined, + }, + type: SourceCodeType.LiquidHtml, + severity: Severity.ERROR, + schema: {}, + targets: [], + }, + + create(context) { + return { + async String(node, ancestors) { + if (!node.single) return; + + // Only flag strings that are inside an inline object/array literal. + const insideJsonLiteral = ancestors.some( + (ancestor) => + ancestor.type === NodeTypes.JsonHashLiteral || + ancestor.type === NodeTypes.JsonArrayLiteral, + ); + if (!insideJsonLiteral) return; + + context.report({ + message: + "Use double quotes for string literals inside object/array literals (e.g. '{\"key\": \"value\"}', not \"{'key': 'value'}\").", + startIndex: node.position.start, + endIndex: node.position.end, + fix: (corrector) => { + corrector.replace(node.position.start, node.position.end, JSON.stringify(node.value)); + }, + }); + }, + }; + }, +}; diff --git a/packages/platformos-check-node/configs/all.yml b/packages/platformos-check-node/configs/all.yml index 9d284c6..771b7c8 100644 --- a/packages/platformos-check-node/configs/all.yml +++ b/packages/platformos-check-node/configs/all.yml @@ -30,6 +30,9 @@ InvalidHashAssignTarget: JSONSyntaxError: enabled: true severity: 0 +JsonLiteralQuoteStyle: + enabled: true + severity: 0 LiquidHTMLSyntaxError: enabled: true severity: 0 diff --git a/packages/platformos-check-node/configs/recommended.yml b/packages/platformos-check-node/configs/recommended.yml index 9d284c6..771b7c8 100644 --- a/packages/platformos-check-node/configs/recommended.yml +++ b/packages/platformos-check-node/configs/recommended.yml @@ -30,6 +30,9 @@ InvalidHashAssignTarget: JSONSyntaxError: enabled: true severity: 0 +JsonLiteralQuoteStyle: + enabled: true + severity: 0 LiquidHTMLSyntaxError: enabled: true severity: 0 From 2f4a9e313bf88fb1bf63b5e68d90b41a03926fe3 Mon Sep 17 00:00:00 2001 From: Wojciech Grzeszczak Date: Tue, 21 Apr 2026 07:55:44 +0000 Subject: [PATCH 2/2] Formatting --- .../src/checks/json-literal-quote-style/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts b/packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts index b16415a..926c0e1 100644 --- a/packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts +++ b/packages/platformos-check-common/src/checks/json-literal-quote-style/index.ts @@ -32,7 +32,7 @@ export const JsonLiteralQuoteStyle: LiquidCheckDefinition = { context.report({ message: - "Use double quotes for string literals inside object/array literals (e.g. '{\"key\": \"value\"}', not \"{'key': 'value'}\").", + 'Use double quotes for string literals inside object/array literals (e.g. \'{"key": "value"}\', not "{\'key\': \'value\'}").', startIndex: node.position.start, endIndex: node.position.end, fix: (corrector) => {