@platformos/platformos-check-common 0.0.16 → 0.0.18

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/CHANGELOG.md +14 -0
  2. package/dist/checks/index.d.ts +1 -1
  3. package/dist/checks/index.js +6 -2
  4. package/dist/checks/index.js.map +1 -1
  5. package/dist/checks/json-literal-quote-style/index.d.ts +2 -0
  6. package/dist/checks/json-literal-quote-style/index.js +42 -0
  7. package/dist/checks/json-literal-quote-style/index.js.map +1 -0
  8. package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.d.ts +21 -0
  9. package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js +60 -0
  10. package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js.map +1 -0
  11. package/dist/checks/liquid-html-syntax-error/checks/InvalidOutputPush.d.ts +17 -0
  12. package/dist/checks/liquid-html-syntax-error/checks/InvalidOutputPush.js +37 -0
  13. package/dist/checks/liquid-html-syntax-error/checks/InvalidOutputPush.js.map +1 -0
  14. package/dist/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.js +1 -1
  15. package/dist/checks/liquid-html-syntax-error/index.js +17 -0
  16. package/dist/checks/liquid-html-syntax-error/index.js.map +1 -1
  17. package/dist/checks/partial-call-arguments/extract-undefined-variables.d.ts +14 -0
  18. package/dist/checks/partial-call-arguments/extract-undefined-variables.js +234 -0
  19. package/dist/checks/partial-call-arguments/extract-undefined-variables.js.map +1 -0
  20. package/dist/checks/partial-call-arguments/index.d.ts +2 -0
  21. package/dist/checks/partial-call-arguments/index.js +117 -0
  22. package/dist/checks/partial-call-arguments/index.js.map +1 -0
  23. package/dist/checks/unknown-property/index.js +22 -2
  24. package/dist/checks/unknown-property/index.js.map +1 -1
  25. package/dist/checks/valid-frontmatter/index.d.ts +2 -0
  26. package/dist/checks/valid-frontmatter/index.js +279 -0
  27. package/dist/checks/valid-frontmatter/index.js.map +1 -0
  28. package/dist/frontmatter/index.d.ts +1 -59
  29. package/dist/frontmatter/index.js +6 -298
  30. package/dist/frontmatter/index.js.map +1 -1
  31. package/dist/tsconfig.tsbuildinfo +1 -1
  32. package/package.json +2 -2
  33. package/src/checks/index.ts +6 -2
  34. package/src/checks/json-literal-quote-style/index.spec.ts +129 -0
  35. package/src/checks/json-literal-quote-style/index.ts +45 -0
  36. package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.spec.ts +422 -0
  37. package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.ts +63 -0
  38. package/src/checks/liquid-html-syntax-error/checks/InvalidOutputPush.spec.ts +104 -0
  39. package/src/checks/liquid-html-syntax-error/checks/InvalidOutputPush.ts +39 -0
  40. package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.spec.ts +86 -2
  41. package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.ts +1 -1
  42. package/src/checks/liquid-html-syntax-error/index.ts +19 -0
  43. package/src/checks/partial-call-arguments/extract-undefined-variables.spec.ts +218 -0
  44. package/src/checks/{metadata-params → partial-call-arguments}/extract-undefined-variables.ts +31 -6
  45. package/src/checks/partial-call-arguments/index.spec.ts +436 -0
  46. package/src/checks/{metadata-params → partial-call-arguments}/index.ts +18 -11
  47. package/src/checks/undefined-object/index.spec.ts +101 -0
  48. package/src/checks/unknown-property/index.spec.ts +42 -0
  49. package/src/checks/unknown-property/index.ts +24 -2
  50. package/src/checks/valid-frontmatter/index.spec.ts +666 -0
  51. package/src/checks/valid-frontmatter/index.ts +344 -0
  52. package/src/frontmatter/index.ts +9 -344
  53. package/src/checks/metadata-params/extract-undefined-variables.spec.ts +0 -115
  54. package/src/checks/metadata-params/index.spec.ts +0 -257
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@platformos/platformos-check-common",
3
- "version": "0.0.16",
3
+ "version": "0.0.18",
4
4
  "license": "MIT",
5
5
  "main": "dist/index.js",
6
6
  "types": "dist/index.d.ts",
@@ -26,7 +26,7 @@
26
26
  "type-check": "tsc --noEmit"
27
27
  },
28
28
  "dependencies": {
29
- "@platformos/liquid-html-parser": "0.0.15",
29
+ "@platformos/liquid-html-parser": "0.0.16",
30
30
  "graphql": "^16.12.0",
31
31
  "js-yaml": "^4.1.1",
32
32
  "jsonc-parser": "^3.3.1",
@@ -30,7 +30,7 @@ import { ValidJSON } from './valid-json';
30
30
  import { ValidDocParamTypes } from './valid-doc-param-types';
31
31
  import { ValidRenderPartialArgumentTypes } from './valid-render-partial-argument-types';
32
32
  import { VariableName } from './variable-name';
33
- import { MetadataParamsCheck } from './metadata-params';
33
+ import { PartialCallArguments } from './partial-call-arguments';
34
34
  import { GraphQLVariablesCheck } from './graphql-variables';
35
35
  import { GraphQLCheck } from './graphql';
36
36
  import { UnknownProperty } from './unknown-property';
@@ -39,6 +39,8 @@ import { DuplicateFunctionArguments } from './duplicate-function-arguments';
39
39
  import { MissingRenderPartialArguments } from './missing-render-partial-arguments';
40
40
  import { NestedGraphQLQuery } from './nested-graphql-query';
41
41
  import { MissingPage } from './missing-page';
42
+ import { ValidFrontmatter } from './valid-frontmatter';
43
+ import { JsonLiteralQuoteStyle } from './json-literal-quote-style';
42
44
 
43
45
  export const allChecks: (
44
46
  | LiquidCheckDefinition
@@ -71,7 +73,7 @@ export const allChecks: (
71
73
  ValidDocParamTypes,
72
74
  ValidRenderPartialArgumentTypes,
73
75
  VariableName,
74
- MetadataParamsCheck,
76
+ PartialCallArguments,
75
77
  GraphQLVariablesCheck,
76
78
  GraphQLCheck,
77
79
  UnknownProperty,
@@ -79,6 +81,8 @@ export const allChecks: (
79
81
  MissingRenderPartialArguments,
80
82
  NestedGraphQLQuery,
81
83
  MissingPage,
84
+ ValidFrontmatter,
85
+ JsonLiteralQuoteStyle,
82
86
  ];
83
87
 
84
88
  /**
@@ -0,0 +1,129 @@
1
+ import { expect, describe, it } from 'vitest';
2
+ import { JsonLiteralQuoteStyle } from './index';
3
+ import { applyFix, runLiquidCheck } from '../../test';
4
+
5
+ describe('Module: JsonLiteralQuoteStyle', () => {
6
+ it('should report single-quoted keys in an inline hash literal', async () => {
7
+ const sourceCode = `{% assign a = {'a': 5} %}`;
8
+
9
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
10
+
11
+ expect(offenses).to.have.length(1);
12
+ expect(offenses[0].message).to.equal(
13
+ 'Use double quotes for string literals inside object/array literals (e.g. \'{"key": "value"}\', not "{\'key\': \'value\'}").',
14
+ );
15
+ });
16
+
17
+ it('should report single-quoted string values in an inline hash literal', async () => {
18
+ const sourceCode = `{% assign a = {"key": 'value'} %}`;
19
+
20
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
21
+
22
+ expect(offenses).to.have.length(1);
23
+ });
24
+
25
+ it('should report both single-quoted keys and values', async () => {
26
+ const sourceCode = `{% assign a = {'a': 'b', 'c': 'd'} %}`;
27
+
28
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
29
+
30
+ expect(offenses).to.have.length(4);
31
+ });
32
+
33
+ it('should report single-quoted strings in nested hash literals', async () => {
34
+ const sourceCode = `{% assign a = {"outer": {'inner': 1}} %}`;
35
+
36
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
37
+
38
+ expect(offenses).to.have.length(1);
39
+ });
40
+
41
+ it('should report single-quoted strings inside array literals', async () => {
42
+ const sourceCode = `{% assign a = ['x', 'y'] %}`;
43
+
44
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
45
+
46
+ expect(offenses).to.have.length(2);
47
+ });
48
+
49
+ it('should not report double-quoted strings in object literals', async () => {
50
+ const sourceCode = `{% assign a = {"a": "b", "c": [1, "d"]} %}`;
51
+
52
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
53
+
54
+ expect(offenses).to.be.empty;
55
+ });
56
+
57
+ it('should not report bare keys in object literals', async () => {
58
+ const sourceCode = `{% assign a = {a: 2} %}`;
59
+
60
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
61
+
62
+ expect(offenses).to.be.empty;
63
+ });
64
+
65
+ it('should not report single-quoted strings outside of inline JSON literals', async () => {
66
+ const sourceCode = `
67
+ {% assign a = 'plain string' %}
68
+ {% assign b = 'pass' | upcase %}
69
+ {{ 'hello' }}
70
+ `;
71
+
72
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
73
+
74
+ expect(offenses).to.be.empty;
75
+ });
76
+
77
+ it('should not report parse_json style JSON-in-a-string', async () => {
78
+ const sourceCode = `{% assign a = '{"a": 5}' | parse_json %}`;
79
+
80
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
81
+
82
+ expect(offenses).to.be.empty;
83
+ });
84
+
85
+ it('should fix single-quoted keys to double-quoted keys', async () => {
86
+ const sourceCode = `{% assign a = {'a': 5} %}`;
87
+
88
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
89
+ const fixed = applyFix(sourceCode, offenses[0]);
90
+
91
+ expect(fixed).to.equal(`{% assign a = {"a": 5} %}`);
92
+ });
93
+
94
+ it('should fix single-quoted string values to double-quoted values', async () => {
95
+ const sourceCode = `{% assign a = {"key": 'value'} %}`;
96
+
97
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
98
+ const fixed = applyFix(sourceCode, offenses[0]);
99
+
100
+ expect(fixed).to.equal(`{% assign a = {"key": "value"} %}`);
101
+ });
102
+
103
+ it('should properly escape embedded double quotes when fixing', async () => {
104
+ const sourceCode = `{% assign a = {'msg': 'she said "hi"'} %}`;
105
+
106
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
107
+ expect(offenses).to.have.length(2);
108
+
109
+ const valueOffense = offenses.find((o) =>
110
+ sourceCode.slice(o.start.index, o.end.index).includes('she said'),
111
+ );
112
+ expect(valueOffense).to.not.be.undefined;
113
+
114
+ const fixedValue = applyFix(sourceCode, valueOffense!);
115
+ expect(fixedValue).to.equal(`{% assign a = {'msg': "she said \\"hi\\""} %}`);
116
+ });
117
+
118
+ it('should report single-quoted strings in function return literals', async () => {
119
+ const sourceCode = `
120
+ {% function result = my_func %}
121
+ {% return {'a': 5} %}
122
+ {% endfunction %}
123
+ `;
124
+
125
+ const offenses = await runLiquidCheck(JsonLiteralQuoteStyle, sourceCode);
126
+
127
+ expect(offenses).to.have.length(1);
128
+ });
129
+ });
@@ -0,0 +1,45 @@
1
+ import { NodeTypes } from '@platformos/liquid-html-parser';
2
+ import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
3
+
4
+ export const JsonLiteralQuoteStyle: LiquidCheckDefinition = {
5
+ meta: {
6
+ code: 'JsonLiteralQuoteStyle',
7
+ name: 'Use double quotes in JSON literals',
8
+ docs: {
9
+ description:
10
+ '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.',
11
+ recommended: true,
12
+ url: undefined,
13
+ },
14
+ type: SourceCodeType.LiquidHtml,
15
+ severity: Severity.ERROR,
16
+ schema: {},
17
+ targets: [],
18
+ },
19
+
20
+ create(context) {
21
+ return {
22
+ async String(node, ancestors) {
23
+ if (!node.single) return;
24
+
25
+ // Only flag strings that are inside an inline object/array literal.
26
+ const insideJsonLiteral = ancestors.some(
27
+ (ancestor) =>
28
+ ancestor.type === NodeTypes.JsonHashLiteral ||
29
+ ancestor.type === NodeTypes.JsonArrayLiteral,
30
+ );
31
+ if (!insideJsonLiteral) return;
32
+
33
+ context.report({
34
+ message:
35
+ 'Use double quotes for string literals inside object/array literals (e.g. \'{"key": "value"}\', not "{\'key\': \'value\'}").',
36
+ startIndex: node.position.start,
37
+ endIndex: node.position.end,
38
+ fix: (corrector) => {
39
+ corrector.replace(node.position.start, node.position.end, JSON.stringify(node.value));
40
+ },
41
+ });
42
+ },
43
+ };
44
+ },
45
+ };
@@ -0,0 +1,422 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { runLiquidCheck } from '../../../test';
3
+ import { LiquidHTMLSyntaxError } from '../index';
4
+
5
+ describe('detectInvalidAssignSyntax', () => {
6
+ describe('structurally-broken assign tags', () => {
7
+ const brokenCases: Array<[string, string]> = [
8
+ ['missing `=` with quoted value', `{% assign x "var" %}`],
9
+ ['missing `=` with bare identifier', `{% assign x abc %}`],
10
+ ['missing target', `{% assign = 'val' %}`],
11
+ ['completely empty', `{% assign %}`],
12
+ ['target only, no operator', `{% assign x %}`],
13
+ ['empty RHS after `=`', `{% assign x = %}`],
14
+ ];
15
+
16
+ for (const [label, sourceCode] of brokenCases) {
17
+ it(`should report: ${label} — ${sourceCode}`, async () => {
18
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
19
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
20
+ expect(syntaxOffenses).toHaveLength(1);
21
+ expect(syntaxOffenses[0].message).toContain("Invalid syntax for tag 'assign'");
22
+ });
23
+ }
24
+ });
25
+
26
+ describe('invalid targets', () => {
27
+ // Literal delimiters at the start of the target are never a valid assign target.
28
+ // Digit-starting names (e.g. `23_hours_ago`) are accepted by the platformOS runtime
29
+ // even though our parser's `variableSegment` grammar rule falls back on them —
30
+ // to avoid false positives, we do not flag digit-starting targets here.
31
+ const invalidTargetCases: Array<[string, string]> = [
32
+ ['target is a single-quoted string', `{% assign 'str' = 'v' %}`],
33
+ ['target is a double-quoted string', `{% assign "str" = 'v' %}`],
34
+ ['target is an array literal', `{% assign [] = 'v' %}`],
35
+ ['target is a hash literal', `{% assign {} = 'v' %}`],
36
+ ];
37
+
38
+ for (const [label, sourceCode] of invalidTargetCases) {
39
+ it(`should report: ${label} — ${sourceCode}`, async () => {
40
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
41
+ const syntaxOffenses = offenses.filter((o) =>
42
+ o.message.includes("Invalid syntax for tag 'assign'"),
43
+ );
44
+ expect(syntaxOffenses).toHaveLength(1);
45
+ });
46
+ }
47
+
48
+ it('should NOT flag a valid dotted target (parser accepts it)', async () => {
49
+ const sourceCode = `{% assign foo.bar = 'v' %}`;
50
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
51
+ expect(offenses).toHaveLength(0);
52
+ });
53
+
54
+ it('should NOT flag a valid indexed target', async () => {
55
+ const sourceCode = `{% assign foo[0] = 'v' %}`;
56
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
57
+ expect(offenses).toHaveLength(0);
58
+ });
59
+
60
+ it('should NOT flag a digit-starting target (valid per platformOS runtime)', async () => {
61
+ // Liquify accepts `23_hours_ago` as a valid name; our parser falls back to base
62
+ // case but this check must not over-report vs. runtime.
63
+ const sourceCode = `{% assign 23_hours_ago = 'some value' %}`;
64
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
65
+ const syntaxOffenses = offenses.filter((o) =>
66
+ o.message.includes("Invalid syntax for tag 'assign'"),
67
+ );
68
+ expect(syntaxOffenses).toHaveLength(0);
69
+ });
70
+ });
71
+
72
+ describe('operator variants', () => {
73
+ it('should report `:=` (not a recognized operator) as an assign syntax error', async () => {
74
+ const sourceCode = `{% assign x := 'v' %}`;
75
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
76
+ const syntaxOffenses = offenses.filter((o) =>
77
+ o.message.includes("Invalid syntax for tag 'assign'"),
78
+ );
79
+ expect(syntaxOffenses).toHaveLength(1);
80
+ });
81
+
82
+ it('should have some offense for `==` (MultipleAssignValues handles it)', async () => {
83
+ const sourceCode = `{% assign x == 'v' %}`;
84
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
85
+ expect(offenses.length).toBeGreaterThan(0);
86
+ });
87
+
88
+ it('should have some offense for `=+`', async () => {
89
+ const sourceCode = `{% assign x =+ 'v' %}`;
90
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
91
+ expect(offenses.length).toBeGreaterThan(0);
92
+ });
93
+ });
94
+
95
+ describe('valid syntax — should NOT report', () => {
96
+ const validCases: Array<[string, string]> = [
97
+ // primitives
98
+ ['single-quoted string', `{% assign x = 'str' %}`],
99
+ ['double-quoted string', `{% assign x = "str" %}`],
100
+ ['integer', `{% assign x = 42 %}`],
101
+ ['negative float', `{% assign x = -1.5 %}`],
102
+ ['true', `{% assign x = true %}`],
103
+ ['false', `{% assign x = false %}`],
104
+ ['nil', `{% assign x = nil %}`],
105
+ ['null', `{% assign x = null %}`],
106
+ ['blank', `{% assign x = blank %}`],
107
+ ['empty', `{% assign x = empty %}`],
108
+ ['range', `{% assign x = (1..10) %}`],
109
+
110
+ // lookups
111
+ ['variable', `{% assign x = other %}`],
112
+ ['dot lookup', `{% assign x = other.prop %}`],
113
+ ['string index lookup', `{% assign x = other["prop"] %}`],
114
+ ['variable index lookup', `{% assign x = other[key] %}`],
115
+ ['deep dot chain', `{% assign x = a.b.c.d %}`],
116
+
117
+ // filters
118
+ ['single filter', `{% assign x = y | upcase %}`],
119
+ ['filter with arg', `{% assign x = y | default: 'z' %}`],
120
+ ['chained filters', `{% assign x = y | default: 'z' | upcase %}`],
121
+ ['filter with positional args', `{% assign x = y | append: "a" %}`],
122
+ ['filter with JSON array arg', `{% assign x = y | concat: [1,2] %}`],
123
+ ['filter with JSON hash arg', `{% assign x = y | merge: {a:1} %}`],
124
+ ['chained filter after JSON arg', `{% assign x = y | default: [] | join: "," %}`],
125
+
126
+ // JSON hash
127
+ ['empty hash', `{% assign x = {} %}`],
128
+ ['hash with string key', `{% assign x = { "a": 1 } %}`],
129
+ ['hash with bare key', `{% assign x = { a: 1 } %}`],
130
+ ['hash with multiple string keys', `{% assign x = { "a": 1, "b": 2 } %}`],
131
+ ['hash with mixed keys', `{% assign x = { "a": 1, b: 2 } %}`],
132
+ ['hash with nested hash', `{% assign x = { "a": { "nested": true } } %}`],
133
+ ['hash with nested array', `{% assign x = { "a": [1,2,3] } %}`],
134
+ [
135
+ 'hash with string-interpolated value',
136
+ `{% assign x = { "email": "{{ email | downcase }}" } %}`,
137
+ ],
138
+
139
+ // JSON array
140
+ ['empty array', `{% assign x = [] %}`],
141
+ ['array of numbers', `{% assign x = [1, 2, 3] %}`],
142
+ ['array of strings', `{% assign x = ["a", "b"] %}`],
143
+ ['array of literals', `{% assign x = [true, false, nil] %}`],
144
+ ['array of variables', `{% assign x = [a, b, c] %}`],
145
+ ['nested arrays', `{% assign x = [[1,2],[3,4]] %}`],
146
+ ['array of hashes', `{% assign x = [{"a":1}, {"b":2}] %}`],
147
+
148
+ // push syntax — only the bare form `a << value` is valid; `a = b << c`
149
+ // is a compound operator and is NOT accepted by the platformOS runtime.
150
+ ['bare push with string value', `{% assign my_val << "item" %}`],
151
+ ['bare push with variable value', `{% assign my_val << val %}`],
152
+ ['bare push with filter', `{% assign my_val << val | upcase %}`],
153
+
154
+ // whitespace-strip
155
+ ['both trim', `{%- assign x = 'v' -%}`],
156
+ ['both trim with hash', `{%- assign x = {} -%}`],
157
+ ['left trim with array', `{%- assign x = [] %}`],
158
+ ['right trim only', `{% assign x = 'v' -%}`],
159
+
160
+ // identifiers
161
+ ['underscore prefix', `{% assign _private = 1 %}`],
162
+ ['snake_case', `{% assign snake_case = 1 %}`],
163
+ ['hyphen in identifier', `{% assign my-var = "hello" %}`],
164
+ ['multiple hyphens', `{% assign my-complex-var-name = "test" %}`],
165
+ ['digit in middle', `{% assign x1 = 1 %}`],
166
+ ];
167
+
168
+ for (const [label, sourceCode] of validCases) {
169
+ it(`should not report: ${label} — ${sourceCode}`, async () => {
170
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
171
+ expect(offenses).toHaveLength(0);
172
+ });
173
+ }
174
+ });
175
+
176
+ describe('interaction with other sub-checks', () => {
177
+ it('should not fire when MultipleAssignValues already reports trailing garbage', async () => {
178
+ const sourceCode = `{% assign foo = '123' 555 text %}`;
179
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
180
+ const syntaxOffenses = offenses.filter((o) => o.message.includes('Invalid syntax for tag'));
181
+ expect(syntaxOffenses).toHaveLength(0);
182
+ const supportOffenses = offenses.filter((o) => o.message === 'Syntax is not supported');
183
+ expect(supportOffenses).toHaveLength(1);
184
+ });
185
+
186
+ it('should not fire on assign with invalid filter name', async () => {
187
+ const sourceCode = `{% assign x = "v" | upcase@ %}`;
188
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
189
+ const syntaxOffenses = offenses.filter((o) =>
190
+ o.message.includes("Invalid syntax for tag 'assign'"),
191
+ );
192
+ expect(syntaxOffenses).toHaveLength(0);
193
+ });
194
+
195
+ it('should not fire on assign with pipe syntax issue', async () => {
196
+ const sourceCode = `{% assign x = "v" || upcase %}`;
197
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
198
+ const syntaxOffenses = offenses.filter((o) =>
199
+ o.message.includes("Invalid syntax for tag 'assign'"),
200
+ );
201
+ expect(syntaxOffenses).toHaveLength(0);
202
+ });
203
+
204
+ it('should not fire for non-assign tags', async () => {
205
+ const sourceCode = `{% echo x %}`;
206
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
207
+ const syntaxOffenses = offenses.filter((o) =>
208
+ o.message.includes("Invalid syntax for tag 'assign'"),
209
+ );
210
+ expect(syntaxOffenses).toHaveLength(0);
211
+ });
212
+ });
213
+
214
+ describe('inside {% liquid %} blocks', () => {
215
+ it('should accept a simple assign statement', async () => {
216
+ const sourceCode = `{% liquid
217
+ assign x = 1
218
+ %}`;
219
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
220
+ expect(offenses).toHaveLength(0);
221
+ });
222
+
223
+ it('should accept a multi-line hash literal', async () => {
224
+ const sourceCode = `{% liquid
225
+ assign h = {
226
+ "a": 1,
227
+ "b": 2
228
+ }
229
+ %}`;
230
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
231
+ expect(offenses).toHaveLength(0);
232
+ });
233
+
234
+ it('should accept a multi-line array literal', async () => {
235
+ const sourceCode = `{% liquid
236
+ assign a = [
237
+ "a",
238
+ "b"
239
+ ]
240
+ %}`;
241
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
242
+ expect(offenses).toHaveLength(0);
243
+ });
244
+
245
+ it('should accept nested multi-line hash', async () => {
246
+ const sourceCode = `{% liquid
247
+ assign h = {
248
+ outer: {
249
+ inner: "v"
250
+ }
251
+ }
252
+ %}`;
253
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
254
+ expect(offenses).toHaveLength(0);
255
+ });
256
+
257
+ it('should not let an empty hash swallow the next statement', async () => {
258
+ const sourceCode = `{% liquid
259
+ assign h = {}
260
+ assign a = 'x'
261
+ %}`;
262
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
263
+ expect(offenses).toHaveLength(0);
264
+ });
265
+
266
+ it('should parse subsequent tag cleanly after array', async () => {
267
+ const sourceCode = `{% liquid
268
+ assign a = [1]
269
+ render 'p'
270
+ %}`;
271
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
272
+ expect(offenses).toHaveLength(0);
273
+ });
274
+
275
+ it('should report target-only assign', async () => {
276
+ const sourceCode = `{% liquid
277
+ assign x
278
+ %}`;
279
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
280
+ const syntaxOffenses = offenses.filter((o) =>
281
+ o.message.includes("Invalid syntax for tag 'assign'"),
282
+ );
283
+ expect(syntaxOffenses).toHaveLength(1);
284
+ });
285
+
286
+ it('should report assign with empty RHS', async () => {
287
+ const sourceCode = `{% liquid
288
+ assign x =
289
+ %}`;
290
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
291
+ const syntaxOffenses = offenses.filter((o) =>
292
+ o.message.includes("Invalid syntax for tag 'assign'"),
293
+ );
294
+ expect(syntaxOffenses).toHaveLength(1);
295
+ });
296
+
297
+ it('should report assign without `=`', async () => {
298
+ const sourceCode = `{% liquid
299
+ assign x "v"
300
+ %}`;
301
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
302
+ const syntaxOffenses = offenses.filter((o) =>
303
+ o.message.includes("Invalid syntax for tag 'assign'"),
304
+ );
305
+ expect(syntaxOffenses).toHaveLength(1);
306
+ });
307
+
308
+ it('should report garbage between hash entries as a parse error', async () => {
309
+ const sourceCode = `{% liquid
310
+ assign x = {
311
+ "a": 1
312
+ extra garbage
313
+ }
314
+ %}`;
315
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
316
+ expect(offenses.length).toBeGreaterThan(0);
317
+ });
318
+
319
+ it('should report an unclosed hash before `%}` in tolerant mode', async () => {
320
+ const sourceCode = `{% liquid
321
+ assign x = { "a": 1
322
+ %}`;
323
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
324
+ expect(offenses.length).toBeGreaterThan(0);
325
+ });
326
+ });
327
+
328
+ describe('whitespace-trimming delimiters', () => {
329
+ it('should report with trim delimiters', async () => {
330
+ const sourceCode = `{%- assign x "var" -%}`;
331
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
332
+ const syntaxOffenses = offenses.filter((o) =>
333
+ o.message.includes("Invalid syntax for tag 'assign'"),
334
+ );
335
+ expect(syntaxOffenses).toHaveLength(1);
336
+ });
337
+ });
338
+
339
+ // These assertions mirror scenarios from the platformOS runtime test suite
340
+ // (desksnearme/test/lib/liquify/tags/assign_tag_test.rb) to make sure our
341
+ // lint-level acceptance aligns with runtime acceptance for common patterns.
342
+ describe('runtime-alignment scenarios', () => {
343
+ it('accepts JSON literal as default filter argument (matches Liquify behavior)', async () => {
344
+ const sourceCode = `{% assign my_val = null | default: [] %}`;
345
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
346
+ expect(offenses).toHaveLength(0);
347
+ });
348
+
349
+ it('accepts empty hash as default filter argument', async () => {
350
+ const sourceCode = `{% assign my_hash = null | default: {} %}`;
351
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
352
+ expect(offenses).toHaveLength(0);
353
+ });
354
+
355
+ it('accepts hash_merge with two hash arguments', async () => {
356
+ const sourceCode = `{% assign merged = { "a": 1 } | hash_merge: h2 | hash_merge: h3 %}`;
357
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
358
+ expect(offenses).toHaveLength(0);
359
+ });
360
+
361
+ it('accepts JSON array with mixed primitive types', async () => {
362
+ const sourceCode = `{% assign arr = ["string", 42, true, null, { "nested": "object" }] %}`;
363
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
364
+ expect(offenses).toHaveLength(0);
365
+ });
366
+
367
+ it('accepts JSON hash with escaped double-quote in string value', async () => {
368
+ const sourceCode = `{% assign data = { "quote": "He said \\"hello\\"" } %}`;
369
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
370
+ expect(offenses).toHaveLength(0);
371
+ });
372
+
373
+ it('accepts escaped backslash in double-quoted string', async () => {
374
+ const sourceCode = `{% assign x = "path\\\\to\\\\file" %}`;
375
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
376
+ expect(offenses).toHaveLength(0);
377
+ });
378
+
379
+ it('accepts escaped single-quote in single-quoted string', async () => {
380
+ const sourceCode = `{% assign x = 'can\\'t' %}`;
381
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
382
+ expect(offenses).toHaveLength(0);
383
+ });
384
+
385
+ it('accepts deeply nested JSON structures', async () => {
386
+ const sourceCode = `{% assign data = {
387
+ "level1": {
388
+ "level2": {
389
+ "level3": {
390
+ "level4": {
391
+ "value": "deep",
392
+ "array": [1, 2, 3]
393
+ }
394
+ }
395
+ }
396
+ }
397
+ } %}`;
398
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
399
+ expect(offenses).toHaveLength(0);
400
+ });
401
+
402
+ it('accepts JSON array of hashes then map/join filters', async () => {
403
+ const sourceCode = `{% assign names = users | map: "name" | join: ", " %}`;
404
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
405
+ expect(offenses).toHaveLength(0);
406
+ });
407
+
408
+ it('accepts push syntax — `{% assign v << "x" %}`', async () => {
409
+ const sourceCode = `{% assign my_val << "item" %}`;
410
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
411
+ expect(offenses).toHaveLength(0);
412
+ });
413
+
414
+ it('rejects compound operator — `{% assign a = b << c %}` is invalid', async () => {
415
+ // Per platformOS runtime: an assign uses exactly one of `=` or `<<`, never both.
416
+ // Our parser falls back to base case; check should flag via MAV or IAS.
417
+ const sourceCode = `{% assign a = b << c %}`;
418
+ const offenses = await runLiquidCheck(LiquidHTMLSyntaxError, sourceCode);
419
+ expect(offenses.length).toBeGreaterThan(0);
420
+ });
421
+ });
422
+ });