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
2 changes: 2 additions & 0 deletions packages/platformos-check-common/src/checks/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -81,6 +82,7 @@ export const allChecks: (
NestedGraphQLQuery,
MissingPage,
ValidFrontmatter,
JsonLiteralQuoteStyle,
];

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -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);
});
});
Original file line number Diff line number Diff line change
@@ -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));
},
});
},
};
},
};
3 changes: 3 additions & 0 deletions packages/platformos-check-node/configs/all.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ InvalidHashAssignTarget:
JSONSyntaxError:
enabled: true
severity: 0
JsonLiteralQuoteStyle:
enabled: true
severity: 0
LiquidHTMLSyntaxError:
enabled: true
severity: 0
Expand Down
3 changes: 3 additions & 0 deletions packages/platformos-check-node/configs/recommended.yml
Original file line number Diff line number Diff line change
Expand Up @@ -30,6 +30,9 @@ InvalidHashAssignTarget:
JSONSyntaxError:
enabled: true
severity: 0
JsonLiteralQuoteStyle:
enabled: true
severity: 0
LiquidHTMLSyntaxError:
enabled: true
severity: 0
Expand Down