@platformos/platformos-check-common 0.0.11 → 0.0.13
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.
- package/CHANGELOG.md +39 -0
- package/CLAUDE.md +150 -0
- package/dist/AugmentedPlatformOSDocset.js +1 -0
- package/dist/AugmentedPlatformOSDocset.js.map +1 -1
- package/dist/checks/circular-render/index.d.ts +2 -0
- package/dist/checks/circular-render/index.js +164 -0
- package/dist/checks/circular-render/index.js.map +1 -0
- package/dist/checks/deprecated-filter/index.js +15 -0
- package/dist/checks/deprecated-filter/index.js.map +1 -1
- package/dist/checks/duplicate-content-for-arguments/index.js +1 -1
- package/dist/checks/duplicate-content-for-arguments/index.js.map +1 -1
- package/dist/checks/graphql/index.d.ts +1 -0
- package/dist/checks/graphql/index.js +20 -7
- package/dist/checks/graphql/index.js.map +1 -1
- package/dist/checks/index.d.ts +1 -1
- package/dist/checks/index.js +6 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/invalid-hash-assign-target/index.js +4 -3
- package/dist/checks/invalid-hash-assign-target/index.js.map +1 -1
- package/dist/checks/missing-content-for-arguments/index.js +1 -1
- package/dist/checks/missing-content-for-arguments/index.js.map +1 -1
- package/dist/checks/missing-page/index.d.ts +2 -0
- package/dist/checks/missing-page/index.js +73 -0
- package/dist/checks/missing-page/index.js.map +1 -0
- package/dist/checks/missing-partial/index.js +31 -31
- package/dist/checks/missing-partial/index.js.map +1 -1
- package/dist/checks/missing-render-partial-arguments/index.d.ts +2 -0
- package/dist/checks/missing-render-partial-arguments/index.js +37 -0
- package/dist/checks/missing-render-partial-arguments/index.js.map +1 -0
- package/dist/checks/nested-graphql-query/index.d.ts +2 -0
- package/dist/checks/nested-graphql-query/index.js +146 -0
- package/dist/checks/nested-graphql-query/index.js.map +1 -0
- package/dist/checks/pagination-size/index.js +1 -1
- package/dist/checks/pagination-size/index.js.map +1 -1
- package/dist/checks/translation-key-exists/index.js +16 -19
- package/dist/checks/translation-key-exists/index.js.map +1 -1
- package/dist/checks/translation-utils.d.ts +20 -0
- package/dist/checks/translation-utils.js +51 -0
- package/dist/checks/translation-utils.js.map +1 -0
- package/dist/checks/undefined-object/index.js +35 -13
- package/dist/checks/undefined-object/index.js.map +1 -1
- package/dist/checks/unknown-property/index.js +75 -10
- package/dist/checks/unknown-property/index.js.map +1 -1
- package/dist/checks/unknown-property/property-shape.js +14 -1
- package/dist/checks/unknown-property/property-shape.js.map +1 -1
- package/dist/checks/unrecognized-content-for-arguments/index.js +1 -1
- package/dist/checks/unrecognized-content-for-arguments/index.js.map +1 -1
- package/dist/checks/unused-assign/index.js +23 -1
- package/dist/checks/unused-assign/index.js.map +1 -1
- package/dist/checks/unused-translation-key/index.d.ts +4 -0
- package/dist/checks/unused-translation-key/index.js +85 -0
- package/dist/checks/unused-translation-key/index.js.map +1 -0
- package/dist/checks/valid-content-for-argument-types/index.js +1 -1
- package/dist/checks/valid-content-for-argument-types/index.js.map +1 -1
- package/dist/checks/valid-render-partial-argument-types/index.js +2 -1
- package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
- package/dist/checks/variable-name/index.js +4 -0
- package/dist/checks/variable-name/index.js.map +1 -1
- package/dist/context-utils.d.ts +2 -1
- package/dist/context-utils.js +31 -1
- package/dist/context-utils.js.map +1 -1
- package/dist/doc-generator/DocBlockGenerator.d.ts +16 -0
- package/dist/doc-generator/DocBlockGenerator.js +464 -0
- package/dist/doc-generator/DocBlockGenerator.js.map +1 -0
- package/dist/doc-generator/index.d.ts +1 -0
- package/dist/doc-generator/index.js +6 -0
- package/dist/doc-generator/index.js.map +1 -0
- package/dist/frontmatter/index.d.ts +59 -0
- package/dist/frontmatter/index.js +301 -0
- package/dist/frontmatter/index.js.map +1 -0
- package/dist/index.d.ts +3 -1
- package/dist/index.js +6 -1
- package/dist/index.js.map +1 -1
- package/dist/liquid-doc/arguments.js +9 -0
- package/dist/liquid-doc/arguments.js.map +1 -1
- package/dist/liquid-doc/utils.d.ts +10 -2
- package/dist/liquid-doc/utils.js +26 -1
- package/dist/liquid-doc/utils.js.map +1 -1
- package/dist/path.d.ts +1 -1
- package/dist/path.js +3 -1
- package/dist/path.js.map +1 -1
- package/dist/to-schema.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.js.map +1 -1
- package/dist/url-helpers.d.ts +55 -0
- package/dist/url-helpers.js +334 -0
- package/dist/url-helpers.js.map +1 -0
- package/dist/utils/block.js.map +1 -1
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/levenshtein.d.ts +3 -0
- package/dist/utils/levenshtein.js +39 -0
- package/dist/utils/levenshtein.js.map +1 -0
- package/package.json +2 -2
- package/src/AugmentedPlatformOSDocset.ts +1 -0
- package/src/checks/deprecated-filter/index.spec.ts +41 -1
- package/src/checks/deprecated-filter/index.ts +17 -0
- package/src/checks/graphql/index.spec.ts +173 -0
- package/src/checks/graphql/index.ts +21 -10
- package/src/checks/index.ts +6 -0
- package/src/checks/invalid-hash-assign-target/index.spec.ts +26 -0
- package/src/checks/invalid-hash-assign-target/index.ts +6 -4
- package/src/checks/missing-page/index.spec.ts +755 -0
- package/src/checks/missing-page/index.ts +89 -0
- package/src/checks/missing-partial/index.spec.ts +361 -0
- package/src/checks/missing-partial/index.ts +39 -47
- package/src/checks/missing-render-partial-arguments/index.spec.ts +74 -0
- package/src/checks/missing-render-partial-arguments/index.ts +44 -0
- package/src/checks/nested-graphql-query/index.spec.ts +175 -0
- package/src/checks/nested-graphql-query/index.ts +203 -0
- package/src/checks/parser-blocking-script/index.spec.ts +7 -3
- package/src/checks/translation-key-exists/index.spec.ts +79 -2
- package/src/checks/translation-key-exists/index.ts +18 -27
- package/src/checks/translation-utils.ts +63 -0
- package/src/checks/undefined-object/index.spec.ts +153 -19
- package/src/checks/undefined-object/index.ts +43 -19
- package/src/checks/unknown-property/index.spec.ts +133 -0
- package/src/checks/unknown-property/index.ts +84 -10
- package/src/checks/unknown-property/property-shape.ts +15 -1
- package/src/checks/unused-assign/index.spec.ts +75 -1
- package/src/checks/unused-assign/index.ts +26 -1
- package/src/checks/unused-doc-param/index.spec.ts +4 -2
- package/src/checks/valid-doc-param-types/index.spec.ts +1 -1
- package/src/checks/valid-render-partial-argument-types/index.spec.ts +24 -1
- package/src/checks/valid-render-partial-argument-types/index.ts +3 -2
- package/src/checks/variable-name/index.spec.ts +10 -1
- package/src/checks/variable-name/index.ts +5 -0
- package/src/context-utils.ts +33 -1
- package/src/frontmatter/index.ts +344 -0
- package/src/index.ts +6 -0
- package/src/liquid-doc/arguments.ts +9 -0
- package/src/liquid-doc/utils.ts +26 -2
- package/src/path.ts +2 -0
- package/src/types.ts +9 -1
- package/src/url-helpers.spec.ts +241 -0
- package/src/url-helpers.ts +363 -0
- package/src/utils/index.ts +1 -0
- package/src/utils/levenshtein.ts +41 -0
|
@@ -145,6 +145,116 @@ describe('Module: UndefinedObject', () => {
|
|
|
145
145
|
expect(offenses).toHaveLength(0);
|
|
146
146
|
});
|
|
147
147
|
|
|
148
|
+
it('should report an offense when function result variable is used before its definition', async () => {
|
|
149
|
+
const sourceCode = `
|
|
150
|
+
{{ a }}
|
|
151
|
+
{% function a = 'test' %}
|
|
152
|
+
`;
|
|
153
|
+
|
|
154
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
155
|
+
|
|
156
|
+
expect(offenses).toHaveLength(1);
|
|
157
|
+
expect(offenses[0].message).toBe("Unknown object 'a' used.");
|
|
158
|
+
});
|
|
159
|
+
|
|
160
|
+
it('should not report an offense for multiple function result variables', async () => {
|
|
161
|
+
const sourceCode = `
|
|
162
|
+
{% function result1 = 'partial_one' %}
|
|
163
|
+
{% function result2 = 'partial_two' %}
|
|
164
|
+
{{ result1 }}
|
|
165
|
+
{{ result2 }}
|
|
166
|
+
`;
|
|
167
|
+
|
|
168
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
169
|
+
|
|
170
|
+
expect(offenses).toHaveLength(0);
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
it('should not register a scope variable when function target is a hash/array access', async () => {
|
|
174
|
+
const sourceCode = `
|
|
175
|
+
{% parse_json my_hash %}{"key": "value"}{% endparse_json %}
|
|
176
|
+
{% function my_hash['result'] = 'test' %}
|
|
177
|
+
{{ my_hash }}
|
|
178
|
+
`;
|
|
179
|
+
|
|
180
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
181
|
+
|
|
182
|
+
// my_hash is defined via parse_json; function hash-access target does not shadow it
|
|
183
|
+
expect(offenses).toHaveLength(0);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it('should report an offense when a variable partial in include is undefined', async () => {
|
|
187
|
+
const sourceCode = `
|
|
188
|
+
{% include undefined_partial %}
|
|
189
|
+
`;
|
|
190
|
+
|
|
191
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
192
|
+
|
|
193
|
+
expect(offenses).toHaveLength(1);
|
|
194
|
+
expect(offenses[0].message).toBe("Unknown object 'undefined_partial' used.");
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
it('should not report an offense when a variable partial in include is defined', async () => {
|
|
198
|
+
const sourceCode = `
|
|
199
|
+
{% assign partial_name = 'some/partial' %}
|
|
200
|
+
{% include partial_name %}
|
|
201
|
+
`;
|
|
202
|
+
|
|
203
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
204
|
+
|
|
205
|
+
expect(offenses).toHaveLength(0);
|
|
206
|
+
});
|
|
207
|
+
|
|
208
|
+
it('should report an offense when a variable partial in function is undefined', async () => {
|
|
209
|
+
const sourceCode = `
|
|
210
|
+
{% function result = undefined_partial %}
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
214
|
+
|
|
215
|
+
expect(offenses).toHaveLength(1);
|
|
216
|
+
expect(offenses[0].message).toBe("Unknown object 'undefined_partial' used.");
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
it('should not report an offense for the result variable itself in function tag', async () => {
|
|
220
|
+
const sourceCode = `
|
|
221
|
+
{% function result = undefined_partial %}
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
225
|
+
|
|
226
|
+
// only 'undefined_partial' should be reported, not 'result'
|
|
227
|
+
expect(offenses.every((o) => o.message !== "Unknown object 'result' used.")).toBe(true);
|
|
228
|
+
});
|
|
229
|
+
|
|
230
|
+
it('should report offenses for lookup key variables in function result target and partial', async () => {
|
|
231
|
+
const sourceCode = `
|
|
232
|
+
{% parse_json my_hash %}{}{% endparse_json %}
|
|
233
|
+
{% function my_hash[lookup_key] = my_hash[path_var] %}
|
|
234
|
+
`;
|
|
235
|
+
|
|
236
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
237
|
+
|
|
238
|
+
const messages = offenses.map((o) => o.message);
|
|
239
|
+
// lookup_key and path_var are undefined; my_hash is defined
|
|
240
|
+
expect(messages).toContain("Unknown object 'lookup_key' used.");
|
|
241
|
+
expect(messages).toContain("Unknown object 'path_var' used.");
|
|
242
|
+
expect(messages).not.toContain("Unknown object 'my_hash' used.");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it('should check the partial variable in function but not the hash-access result target base', async () => {
|
|
246
|
+
const sourceCode = `
|
|
247
|
+
{% parse_json my_hash %}{}{% endparse_json %}
|
|
248
|
+
{% function my_hash['key'] = undefined_partial %}
|
|
249
|
+
`;
|
|
250
|
+
|
|
251
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
252
|
+
|
|
253
|
+
const messages = offenses.map((o) => o.message);
|
|
254
|
+
expect(messages).toContain("Unknown object 'undefined_partial' used.");
|
|
255
|
+
expect(messages).not.toContain("Unknown object 'my_hash' used.");
|
|
256
|
+
});
|
|
257
|
+
|
|
148
258
|
it('should report an offense when object is defined in a for loop but used outside of the scope (in scenarios where the same variable has multiple scopes in the file)', async () => {
|
|
149
259
|
const sourceCode = `
|
|
150
260
|
{% for c in collections %}
|
|
@@ -359,10 +469,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
359
469
|
expect(offenses[0].message).toBe("Unknown object 'my_var' used.");
|
|
360
470
|
});
|
|
361
471
|
|
|
362
|
-
it('should report an offense when
|
|
472
|
+
it('should report an offense when undefined variable is used inside background block', async () => {
|
|
363
473
|
const sourceCode = `
|
|
364
|
-
{% background
|
|
365
|
-
{{
|
|
474
|
+
{% background source_type: 'some form' %}
|
|
475
|
+
{{ undefined_var }}
|
|
366
476
|
{% endbackground %}
|
|
367
477
|
`;
|
|
368
478
|
|
|
@@ -371,12 +481,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
371
481
|
expect(offenses).toHaveLength(1);
|
|
372
482
|
});
|
|
373
483
|
|
|
374
|
-
it('should not report an offense when job_id is used after background
|
|
484
|
+
it('should not report an offense when job_id is used after background file-based tag', async () => {
|
|
375
485
|
const sourceCode = `
|
|
376
|
-
{% background
|
|
377
|
-
|
|
378
|
-
{% endbackground %}
|
|
379
|
-
{{ job_id }}
|
|
486
|
+
{% background my_job = 'some_partial' %}
|
|
487
|
+
{{ my_job }}
|
|
380
488
|
`;
|
|
381
489
|
|
|
382
490
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
@@ -384,12 +492,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
384
492
|
expect(offenses).toHaveLength(0);
|
|
385
493
|
});
|
|
386
494
|
|
|
387
|
-
it('should not report an offense when job_id is used after background
|
|
495
|
+
it('should not report an offense when job_id is used after background file-based tag with named args', async () => {
|
|
388
496
|
const sourceCode = `
|
|
389
|
-
{% background
|
|
390
|
-
|
|
391
|
-
{% endbackground %}
|
|
392
|
-
{{ job_id }}
|
|
497
|
+
{% background my_job = 'some_partial', source_type: 'some form' %}
|
|
498
|
+
{{ my_job }}
|
|
393
499
|
`;
|
|
394
500
|
|
|
395
501
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
@@ -397,18 +503,16 @@ describe('Module: UndefinedObject', () => {
|
|
|
397
503
|
expect(offenses).toHaveLength(0);
|
|
398
504
|
});
|
|
399
505
|
|
|
400
|
-
it('should report an offense when job_id is used before background
|
|
506
|
+
it('should report an offense when job_id is used before background file-based tag', async () => {
|
|
401
507
|
const sourceCode = `
|
|
402
|
-
{{
|
|
403
|
-
{% background
|
|
404
|
-
{% assign a = 5 %}
|
|
405
|
-
{% endbackground %}
|
|
508
|
+
{{ my_job }}
|
|
509
|
+
{% background my_job = 'some_partial' %}
|
|
406
510
|
`;
|
|
407
511
|
|
|
408
512
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
409
513
|
|
|
410
514
|
expect(offenses).toHaveLength(1);
|
|
411
|
-
expect(offenses.map((e) => e.message)).toEqual(["Unknown object '
|
|
515
|
+
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_job' used."]);
|
|
412
516
|
});
|
|
413
517
|
|
|
414
518
|
it('should not report an offense when object is defined with a parse_json tag', async () => {
|
|
@@ -437,4 +541,34 @@ describe('Module: UndefinedObject', () => {
|
|
|
437
541
|
expect(offenses).toHaveLength(1);
|
|
438
542
|
expect(offenses[0].message).toBe("Unknown object 'groups_data' used.");
|
|
439
543
|
});
|
|
544
|
+
|
|
545
|
+
it('should not report an offense for catch variable inside catch block', async () => {
|
|
546
|
+
const sourceCode = `
|
|
547
|
+
{% try %}
|
|
548
|
+
{{ "something" }}
|
|
549
|
+
{% catch error %}
|
|
550
|
+
{{ error }}
|
|
551
|
+
{% endtry %}
|
|
552
|
+
`;
|
|
553
|
+
|
|
554
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
555
|
+
|
|
556
|
+
expect(offenses).toHaveLength(0);
|
|
557
|
+
});
|
|
558
|
+
|
|
559
|
+
it('should report an offense for catch variable used outside catch block', async () => {
|
|
560
|
+
const sourceCode = `
|
|
561
|
+
{% try %}
|
|
562
|
+
{{ "something" }}
|
|
563
|
+
{% catch error %}
|
|
564
|
+
{{ error }}
|
|
565
|
+
{% endtry %}
|
|
566
|
+
{{ error }}
|
|
567
|
+
`;
|
|
568
|
+
|
|
569
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
570
|
+
|
|
571
|
+
expect(offenses).toHaveLength(1);
|
|
572
|
+
expect(offenses[0].message).toBe("Unknown object 'error' used.");
|
|
573
|
+
});
|
|
440
574
|
});
|
|
@@ -9,7 +9,6 @@ import {
|
|
|
9
9
|
LiquidTagIncrement,
|
|
10
10
|
LiquidTagTablerow,
|
|
11
11
|
LiquidVariableLookup,
|
|
12
|
-
LiquidTagFunction,
|
|
13
12
|
NamedTags,
|
|
14
13
|
NodeTypes,
|
|
15
14
|
Position,
|
|
@@ -18,11 +17,12 @@ import {
|
|
|
18
17
|
LiquidTagGraphQL,
|
|
19
18
|
LiquidTagParseJson,
|
|
20
19
|
LiquidTagBackground,
|
|
21
|
-
|
|
20
|
+
BackgroundMarkup,
|
|
22
21
|
} from '@platformos/liquid-html-parser';
|
|
23
22
|
import { LiquidCheckDefinition, Severity, SourceCodeType, PlatformOSDocset } from '../../types';
|
|
24
23
|
import { isError, last } from '../../utils';
|
|
25
24
|
import { isWithinRawTagThatDoesNotParseItsContents } from '../utils';
|
|
25
|
+
import yaml from 'js-yaml';
|
|
26
26
|
|
|
27
27
|
type Scope = { start?: number; end?: number };
|
|
28
28
|
|
|
@@ -108,9 +108,13 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
108
108
|
}
|
|
109
109
|
|
|
110
110
|
if (node.name === 'function') {
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
111
|
+
const fnName = (node.markup as FunctionMarkup).name;
|
|
112
|
+
// Only register simple variable names (not hash/array mutations like hash['key'])
|
|
113
|
+
if (fnName.lookups.length === 0 && fnName.name !== null) {
|
|
114
|
+
indexVariableScope(fnName.name, {
|
|
115
|
+
start: node.position.end,
|
|
116
|
+
});
|
|
117
|
+
}
|
|
114
118
|
}
|
|
115
119
|
|
|
116
120
|
if (node.name === 'layout') {
|
|
@@ -148,8 +152,26 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
148
152
|
}
|
|
149
153
|
|
|
150
154
|
if (isLiquidTagBackground(node)) {
|
|
151
|
-
indexVariableScope(node.markup.jobId
|
|
152
|
-
start: node.
|
|
155
|
+
indexVariableScope(node.markup.jobId, {
|
|
156
|
+
start: node.position.end,
|
|
157
|
+
});
|
|
158
|
+
}
|
|
159
|
+
},
|
|
160
|
+
|
|
161
|
+
async LiquidBranch(node, ancestors) {
|
|
162
|
+
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
|
|
163
|
+
|
|
164
|
+
// {% try %} ... {% catch error %} registers the error variable
|
|
165
|
+
if (
|
|
166
|
+
node.name === NamedTags.catch &&
|
|
167
|
+
node.markup &&
|
|
168
|
+
typeof node.markup !== 'string' &&
|
|
169
|
+
'name' in node.markup &&
|
|
170
|
+
node.markup.name
|
|
171
|
+
) {
|
|
172
|
+
indexVariableScope(node.markup.name, {
|
|
173
|
+
start: node.blockStartPosition.end,
|
|
174
|
+
end: node.blockEndPosition?.start,
|
|
153
175
|
});
|
|
154
176
|
}
|
|
155
177
|
},
|
|
@@ -160,9 +182,10 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
160
182
|
const parent = last(ancestors);
|
|
161
183
|
if (isLiquidTag(parent) && isLiquidTagCapture(parent)) return;
|
|
162
184
|
if (isLiquidTag(parent) && isLiquidTagParseJson(parent)) return;
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
185
|
+
// Skip the result variable of function tags (it's a definition, not a usage)
|
|
186
|
+
if (isFunctionMarkup(parent) && parent.name === node) return;
|
|
187
|
+
// Skip the error variable definition in catch branches
|
|
188
|
+
if (isLiquidBranchCatch(parent) && parent.markup === node) return;
|
|
166
189
|
|
|
167
190
|
variables.push(node);
|
|
168
191
|
},
|
|
@@ -299,19 +322,20 @@ function isLiquidTagDecrement(node: LiquidTag): node is LiquidTagDecrement {
|
|
|
299
322
|
|
|
300
323
|
function isLiquidTagBackground(
|
|
301
324
|
node: LiquidTag,
|
|
302
|
-
): node is LiquidTagBackground & { markup:
|
|
325
|
+
): node is LiquidTagBackground & { markup: BackgroundMarkup } {
|
|
303
326
|
return (
|
|
304
327
|
node.name === NamedTags.background &&
|
|
305
328
|
typeof node.markup !== 'string' &&
|
|
306
|
-
node.markup.type === NodeTypes.
|
|
329
|
+
node.markup.type === NodeTypes.BackgroundMarkup
|
|
307
330
|
);
|
|
308
331
|
}
|
|
309
332
|
|
|
310
|
-
function
|
|
311
|
-
return
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
333
|
+
function isFunctionMarkup(node?: LiquidHtmlNode): node is FunctionMarkup {
|
|
334
|
+
return node?.type === NodeTypes.FunctionMarkup;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
function isLiquidBranchCatch(
|
|
338
|
+
node?: LiquidHtmlNode,
|
|
339
|
+
): node is LiquidHtmlNode & { type: typeof NodeTypes.LiquidBranch; name: 'catch'; markup: any } {
|
|
340
|
+
return node?.type === NodeTypes.LiquidBranch && (node as any).name === NamedTags.catch;
|
|
317
341
|
}
|
|
@@ -212,6 +212,139 @@ query {
|
|
|
212
212
|
// This is expected behavior without schema - we can't know users is an array
|
|
213
213
|
expect(offenses).toHaveLength(1);
|
|
214
214
|
});
|
|
215
|
+
|
|
216
|
+
it('should not report an offense for result.errors (GraphQL responses always have errors)', async () => {
|
|
217
|
+
const sourceCode = `{% graphql r %}
|
|
218
|
+
query {
|
|
219
|
+
user {
|
|
220
|
+
id
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
{% endgraphql %}
|
|
224
|
+
{% if r.errors %}
|
|
225
|
+
{{ r.errors }}
|
|
226
|
+
{% endif %}`;
|
|
227
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
228
|
+
expect(offenses).toHaveLength(0);
|
|
229
|
+
});
|
|
230
|
+
|
|
231
|
+
it('should not report file-based graphql result.errors', async () => {
|
|
232
|
+
const sourceCode = `{% graphql r = 'my_query' %}
|
|
233
|
+
{% if r.errors %}{{ r.errors }}{% endif %}`;
|
|
234
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
235
|
+
expect(offenses).toHaveLength(0);
|
|
236
|
+
});
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
describe('graphql errors field', () => {
|
|
240
|
+
it('should not report r.errors on graphql results (protocol-level field)', async () => {
|
|
241
|
+
const sourceCode = `{% graphql r %}
|
|
242
|
+
query {
|
|
243
|
+
user {
|
|
244
|
+
id
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
{% endgraphql %}
|
|
248
|
+
{% if r.errors %}error{% endif %}`;
|
|
249
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
250
|
+
expect(offenses).toHaveLength(0);
|
|
251
|
+
});
|
|
252
|
+
|
|
253
|
+
it('should not report errors on mutation results without errors in selection set', async () => {
|
|
254
|
+
const sourceCode = `{% graphql r %}
|
|
255
|
+
mutation ($id: ID!) {
|
|
256
|
+
user: user_delete(id: $id) {
|
|
257
|
+
id
|
|
258
|
+
email
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
{% endgraphql %}
|
|
262
|
+
{% unless r.errors %}ok{% endunless %}`;
|
|
263
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
264
|
+
expect(offenses).toHaveLength(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('should still report genuinely unknown properties on graphql results', async () => {
|
|
268
|
+
const sourceCode = `{% graphql r %}
|
|
269
|
+
query {
|
|
270
|
+
user {
|
|
271
|
+
id
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
{% endgraphql %}
|
|
275
|
+
{{ r.user.bogus }}`;
|
|
276
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
277
|
+
expect(offenses).toHaveLength(1);
|
|
278
|
+
expect(offenses[0].message).toContain("Unknown property 'bogus'");
|
|
279
|
+
});
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
describe('dig filter shape tracking', () => {
|
|
283
|
+
it('should infer shape after dig on a parse_json variable', async () => {
|
|
284
|
+
const sourceCode = `{% assign data = '{"user": {"name": "John", "age": 30}}' | parse_json %}
|
|
285
|
+
{% assign user = data | dig: "user" %}
|
|
286
|
+
{{ user.name }}
|
|
287
|
+
{{ user.missing }}`;
|
|
288
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
289
|
+
expect(offenses).toHaveLength(1);
|
|
290
|
+
expect(offenses[0].message).toContain("Unknown property 'missing'");
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
it('should infer array shape after dig and allow .size', async () => {
|
|
294
|
+
const sourceCode = `{% assign data = '{"results": [{"id": 1}, {"id": 2}]}' | parse_json %}
|
|
295
|
+
{% assign items = data | dig: "results" %}
|
|
296
|
+
{{ items.size }}
|
|
297
|
+
{{ items.first.id }}`;
|
|
298
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
299
|
+
expect(offenses).toHaveLength(0);
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it('should infer shape after multiple dig filters', async () => {
|
|
303
|
+
const sourceCode = `{% assign data = '{"a": {"b": {"c": 1}}}' | parse_json %}
|
|
304
|
+
{% assign val = data | dig: "a" | dig: "b" %}
|
|
305
|
+
{{ val.c }}
|
|
306
|
+
{{ val.missing }}`;
|
|
307
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
308
|
+
expect(offenses).toHaveLength(1);
|
|
309
|
+
expect(offenses[0].message).toContain("Unknown property 'missing'");
|
|
310
|
+
});
|
|
311
|
+
|
|
312
|
+
it('should not track dig when source has no known shape', async () => {
|
|
313
|
+
const sourceCode = `{% assign val = dynamic_var | dig: "key" %}
|
|
314
|
+
{{ val.anything }}`;
|
|
315
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
316
|
+
expect(offenses).toHaveLength(0);
|
|
317
|
+
});
|
|
318
|
+
|
|
319
|
+
it('should infer shape after dig on inline graphql result', async () => {
|
|
320
|
+
const sourceCode = `{% graphql result | dig: 'user' %}
|
|
321
|
+
query {
|
|
322
|
+
user {
|
|
323
|
+
id
|
|
324
|
+
email
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
{% endgraphql %}
|
|
328
|
+
{{ result.id }}
|
|
329
|
+
{{ result.email }}
|
|
330
|
+
{{ result.missing }}`;
|
|
331
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
332
|
+
expect(offenses).toHaveLength(1);
|
|
333
|
+
expect(offenses[0].message).toContain("Unknown property 'missing'");
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
it('should allow full shape when no dig filter on inline graphql', async () => {
|
|
337
|
+
const sourceCode = `{% graphql result %}
|
|
338
|
+
query {
|
|
339
|
+
user {
|
|
340
|
+
id
|
|
341
|
+
}
|
|
342
|
+
}
|
|
343
|
+
{% endgraphql %}
|
|
344
|
+
{{ result.user.id }}`;
|
|
345
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
346
|
+
expect(offenses).toHaveLength(0);
|
|
347
|
+
});
|
|
215
348
|
});
|
|
216
349
|
|
|
217
350
|
describe('error message formatting', () => {
|
|
@@ -4,6 +4,7 @@ import {
|
|
|
4
4
|
LiquidVariableLookup,
|
|
5
5
|
LiquidVariable,
|
|
6
6
|
LiquidExpression,
|
|
7
|
+
LiquidFilter,
|
|
7
8
|
LiquidString,
|
|
8
9
|
NodeTypes,
|
|
9
10
|
NamedTags,
|
|
@@ -125,6 +126,50 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
125
126
|
}
|
|
126
127
|
}
|
|
127
128
|
}
|
|
129
|
+
|
|
130
|
+
// {% assign x = y | dig: "key1" | dig: "key2" %}
|
|
131
|
+
// Follow the dig path through the source variable's known shape.
|
|
132
|
+
const digFilters =
|
|
133
|
+
markup.value.filters?.filter((f: { name: string }) => f.name === 'dig') ?? [];
|
|
134
|
+
if (digFilters.length > 0 && markup.value.expression.type === NodeTypes.VariableLookup) {
|
|
135
|
+
const expr = markup.value.expression as LiquidVariableLookup;
|
|
136
|
+
if (expr.name) {
|
|
137
|
+
const digPath: string[] = [];
|
|
138
|
+
let validDigPath = true;
|
|
139
|
+
for (const digFilter of digFilters) {
|
|
140
|
+
const arg = digFilter.args?.[0];
|
|
141
|
+
if (arg?.type === NodeTypes.String) {
|
|
142
|
+
digPath.push(arg.value);
|
|
143
|
+
} else {
|
|
144
|
+
validDigPath = false;
|
|
145
|
+
break;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (validDigPath && digPath.length > 0) {
|
|
150
|
+
// Combine any lookups on the source expression (e.g. y.a | dig: "b")
|
|
151
|
+
// with the dig path to get the full navigation path.
|
|
152
|
+
const sourceLookupPath = buildLookupPath(expr.lookups);
|
|
153
|
+
const fullPath = sourceLookupPath ? [...sourceLookupPath, ...digPath] : digPath;
|
|
154
|
+
|
|
155
|
+
const sourceIdx = findLastApplicableShapeIndex(
|
|
156
|
+
expr.name,
|
|
157
|
+
node.position.start,
|
|
158
|
+
variableShapes,
|
|
159
|
+
);
|
|
160
|
+
if (sourceIdx !== -1) {
|
|
161
|
+
const result = lookupPropertyPath(variableShapes[sourceIdx].shape, fullPath);
|
|
162
|
+
if (result.shape && !result.error) {
|
|
163
|
+
variableShapes.push({
|
|
164
|
+
name: markup.name,
|
|
165
|
+
shape: result.shape,
|
|
166
|
+
range: [node.position.end],
|
|
167
|
+
});
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
}
|
|
172
|
+
}
|
|
128
173
|
}
|
|
129
174
|
|
|
130
175
|
// {% parse_json x %}{"a": 5}{% endparse_json %}
|
|
@@ -162,11 +207,14 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
162
207
|
const schema = await getGraphQLSchema();
|
|
163
208
|
const shape = inferShapeFromGraphQL(content, schema);
|
|
164
209
|
if (shape) {
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
210
|
+
const resultShape = applyDigFilters(shape, markup.filters);
|
|
211
|
+
if (resultShape) {
|
|
212
|
+
variableShapes.push({
|
|
213
|
+
name: markup.name,
|
|
214
|
+
shape: resultShape,
|
|
215
|
+
range: [node.position.end],
|
|
216
|
+
});
|
|
217
|
+
}
|
|
170
218
|
}
|
|
171
219
|
} catch {
|
|
172
220
|
// File read error - skip
|
|
@@ -186,11 +234,14 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
186
234
|
const schema = await getGraphQLSchema();
|
|
187
235
|
const shape = inferShapeFromGraphQL(textContent, schema);
|
|
188
236
|
if (shape) {
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
237
|
+
const resultShape = applyDigFilters(shape, markup.filters);
|
|
238
|
+
if (resultShape) {
|
|
239
|
+
variableShapes.push({
|
|
240
|
+
name: markup.name,
|
|
241
|
+
shape: resultShape,
|
|
242
|
+
range: [node.blockEndPosition?.end ?? node.position.end],
|
|
243
|
+
});
|
|
244
|
+
}
|
|
194
245
|
}
|
|
195
246
|
}
|
|
196
247
|
}
|
|
@@ -361,6 +412,29 @@ function buildLookupPath(lookups: LiquidExpression[]): string[] | undefined {
|
|
|
361
412
|
return path;
|
|
362
413
|
}
|
|
363
414
|
|
|
415
|
+
/**
|
|
416
|
+
* Navigate a shape using the `dig` filters from a tag's result filters.
|
|
417
|
+
* Returns the navigated shape, or the original shape if no dig filters are present.
|
|
418
|
+
* Returns null if the dig path is dynamic or navigates to an unknown property.
|
|
419
|
+
*/
|
|
420
|
+
function applyDigFilters(shape: PropertyShape, filters: LiquidFilter[]): PropertyShape | null {
|
|
421
|
+
const digFilters = filters.filter((f) => f.name === 'dig');
|
|
422
|
+
if (digFilters.length === 0) return shape;
|
|
423
|
+
|
|
424
|
+
const digPath: string[] = [];
|
|
425
|
+
for (const filter of digFilters) {
|
|
426
|
+
const arg = filter.args?.[0];
|
|
427
|
+
if (arg?.type === NodeTypes.String) {
|
|
428
|
+
digPath.push(arg.value);
|
|
429
|
+
} else {
|
|
430
|
+
return null;
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
const result = lookupPropertyPath(shape, digPath);
|
|
435
|
+
return result.error || !result.shape ? null : result.shape;
|
|
436
|
+
}
|
|
437
|
+
|
|
364
438
|
/**
|
|
365
439
|
* Extract the lookup path from a hash_assign target.
|
|
366
440
|
* For {% hash_assign a['key1']['key2'] = value %}, returns ['key1', 'key2']
|
|
@@ -216,7 +216,21 @@ export function inferShapeFromGraphQL(
|
|
|
216
216
|
}
|
|
217
217
|
}
|
|
218
218
|
|
|
219
|
-
|
|
219
|
+
const shape = selectionSetToShape(definition.selectionSet, rootType);
|
|
220
|
+
|
|
221
|
+
// platformOS always exposes a top-level 'errors' array on graphql results
|
|
222
|
+
// (GraphQL protocol-level errors), regardless of what's in the selection set.
|
|
223
|
+
const properties = new Map(shape.properties);
|
|
224
|
+
if (!properties.has('errors')) {
|
|
225
|
+
properties.set('errors', {
|
|
226
|
+
kind: 'array',
|
|
227
|
+
itemShape: {
|
|
228
|
+
kind: 'object',
|
|
229
|
+
properties: new Map([['message', { kind: 'primitive', primitiveType: 'string' }]]),
|
|
230
|
+
},
|
|
231
|
+
});
|
|
232
|
+
}
|
|
233
|
+
return { kind: 'object', properties };
|
|
220
234
|
}
|
|
221
235
|
}
|
|
222
236
|
return undefined;
|