@platformos/platformos-check-common 0.0.13 → 0.0.17
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 +34 -0
- package/dist/checks/metadata-params/extract-undefined-variables.d.ts +8 -0
- package/dist/checks/metadata-params/extract-undefined-variables.js +213 -0
- package/dist/checks/metadata-params/extract-undefined-variables.js.map +1 -0
- package/dist/checks/metadata-params/index.js +48 -33
- package/dist/checks/metadata-params/index.js.map +1 -1
- package/dist/checks/translation-utils.d.ts +3 -7
- package/dist/checks/translation-utils.js +2 -2
- package/dist/checks/translation-utils.js.map +1 -1
- package/dist/checks/undefined-object/index.js +11 -0
- package/dist/checks/undefined-object/index.js.map +1 -1
- package/dist/checks/unknown-property/index.js +84 -2
- package/dist/checks/unknown-property/index.js.map +1 -1
- package/dist/to-source-code.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/checks/metadata-params/extract-undefined-variables.spec.ts +115 -0
- package/src/checks/metadata-params/extract-undefined-variables.ts +286 -0
- package/src/checks/metadata-params/index.spec.ts +180 -26
- package/src/checks/metadata-params/index.ts +51 -34
- package/src/checks/translation-utils.ts +5 -5
- package/src/checks/undefined-object/index.spec.ts +165 -36
- package/src/checks/undefined-object/index.ts +13 -0
- package/src/checks/unknown-property/index.spec.ts +104 -0
- package/src/checks/unknown-property/index.ts +95 -2
- package/src/disabled-checks/index.spec.ts +4 -4
- package/src/url-helpers.spec.ts +145 -0
|
@@ -347,6 +347,110 @@ query {
|
|
|
347
347
|
});
|
|
348
348
|
});
|
|
349
349
|
|
|
350
|
+
describe('JSON literal validation', () => {
|
|
351
|
+
it('should report unknown property on hash literal', async () => {
|
|
352
|
+
const sourceCode = `{% assign a = {x: 5} %}{{ a.b }}`;
|
|
353
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
354
|
+
expect(offenses).toHaveLength(1);
|
|
355
|
+
expect(offenses[0].message).toEqual("Unknown property 'b' on 'a'.");
|
|
356
|
+
});
|
|
357
|
+
|
|
358
|
+
it('should not report for valid property on hash literal', async () => {
|
|
359
|
+
const sourceCode = `{% assign a = {x: 5, y: 10} %}{{ a.x }}{{ a.y }}`;
|
|
360
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
361
|
+
expect(offenses).toHaveLength(0);
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
it('should handle nested hash literals', async () => {
|
|
365
|
+
const sourceCode = `{% assign a = {x: {y: 1}} %}{{ a.x.y }}{{ a.x.z }}`;
|
|
366
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
367
|
+
expect(offenses).toHaveLength(1);
|
|
368
|
+
expect(offenses[0].message).toEqual("Unknown property 'z' on 'a.x'.");
|
|
369
|
+
});
|
|
370
|
+
|
|
371
|
+
it('should report unknown property access on array literal', async () => {
|
|
372
|
+
const sourceCode = `{% assign a = [2, 3] %}{{ a.asd }}`;
|
|
373
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
374
|
+
expect(offenses).toHaveLength(1);
|
|
375
|
+
expect(offenses[0].message).toEqual("Unknown property 'asd' on 'a'.");
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('should allow first/last/size on array literals', async () => {
|
|
379
|
+
const sourceCode = `{% assign a = [2, 3] %}{{ a.first }}{{ a.last }}{{ a.size }}`;
|
|
380
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
381
|
+
expect(offenses).toHaveLength(0);
|
|
382
|
+
});
|
|
383
|
+
|
|
384
|
+
it('should allow numeric index on array literals', async () => {
|
|
385
|
+
const sourceCode = `{% assign a = [2, 3] %}{{ a[0] }}{{ a[1] }}`;
|
|
386
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
387
|
+
expect(offenses).toHaveLength(0);
|
|
388
|
+
});
|
|
389
|
+
|
|
390
|
+
it('should report primitive access on array item from hash literal items', async () => {
|
|
391
|
+
const sourceCode = `{% assign a = [{x: 1}, {x: 2}] %}{{ a.first.x }}{{ a.first.y }}`;
|
|
392
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
393
|
+
expect(offenses).toHaveLength(1);
|
|
394
|
+
expect(offenses[0].message).toEqual("Unknown property 'y' on 'a.first'.");
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
it('should report unknown property when assigning from known-shape variable', async () => {
|
|
398
|
+
const sourceCode = `{% assign a = [2, 3] %}{% assign b = a.asd %}`;
|
|
399
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
400
|
+
expect(offenses).toHaveLength(1);
|
|
401
|
+
expect(offenses[0].message).toEqual("Unknown property 'asd' on 'a'.");
|
|
402
|
+
});
|
|
403
|
+
|
|
404
|
+
it('should handle hash literals with quoted string keys', async () => {
|
|
405
|
+
const sourceCode = `{% assign c = { "errors": {}, "valid": true } %}{{ c.valid }}{{ c.errors }}{{ c.missing }}`;
|
|
406
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
407
|
+
expect(offenses).toHaveLength(1);
|
|
408
|
+
expect(offenses[0].message).toEqual("Unknown property 'missing' on 'c'.");
|
|
409
|
+
});
|
|
410
|
+
|
|
411
|
+
it('should handle hash literals with mixed bare and quoted keys', async () => {
|
|
412
|
+
const sourceCode = `{% assign a = {x: 5, "y": 10} %}{{ a.x }}{{ a.y }}{{ a.z }}`;
|
|
413
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
414
|
+
expect(offenses).toHaveLength(1);
|
|
415
|
+
expect(offenses[0].message).toEqual("Unknown property 'z' on 'a'.");
|
|
416
|
+
});
|
|
417
|
+
|
|
418
|
+
it('should handle nested hash literals with quoted keys', async () => {
|
|
419
|
+
const sourceCode = `{% assign a = { "outer": { "inner": 1 } } %}{{ a.outer.inner }}{{ a.outer.missing }}`;
|
|
420
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
421
|
+
expect(offenses).toHaveLength(1);
|
|
422
|
+
expect(offenses[0].message).toEqual("Unknown property 'missing' on 'a.outer'.");
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
it('should report unknown property on hash literal assigned via another variable', async () => {
|
|
426
|
+
const sourceCode = `{% assign a = {a: 5} %}{% assign b = a.b %}`;
|
|
427
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
428
|
+
expect(offenses).toHaveLength(1);
|
|
429
|
+
expect(offenses[0].message).toEqual("Unknown property 'b' on 'a'.");
|
|
430
|
+
});
|
|
431
|
+
});
|
|
432
|
+
|
|
433
|
+
describe('function tag reassignment', () => {
|
|
434
|
+
it('should not report unknown property after function tag reassigns a variable', async () => {
|
|
435
|
+
const sourceCode = `{% assign object = { "valid": true, "id": id, "role": role } %}
|
|
436
|
+
{% function object = 'modules/core/commands/execute', object: object, mutation_name: 'some_mutation' %}
|
|
437
|
+
{% if object.errors == blank %}
|
|
438
|
+
{{ object.valid }}
|
|
439
|
+
{% endif %}`;
|
|
440
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
441
|
+
expect(offenses).toHaveLength(0);
|
|
442
|
+
});
|
|
443
|
+
|
|
444
|
+
it('should still report unknown property before function tag reassignment', async () => {
|
|
445
|
+
const sourceCode = `{% assign object = { "valid": true, "id": "123" } %}
|
|
446
|
+
{{ object.missing }}
|
|
447
|
+
{% function object = 'modules/core/commands/execute', object: object %}`;
|
|
448
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
449
|
+
expect(offenses).toHaveLength(1);
|
|
450
|
+
expect(offenses[0].message).toEqual("Unknown property 'missing' on 'object'.");
|
|
451
|
+
});
|
|
452
|
+
});
|
|
453
|
+
|
|
350
454
|
describe('error message formatting', () => {
|
|
351
455
|
it('should include variable name in error message', async () => {
|
|
352
456
|
const sourceCode = `{% assign myVar = '{"a": 1}' | parse_json %}
|
|
@@ -11,7 +11,10 @@ import {
|
|
|
11
11
|
TextNode,
|
|
12
12
|
GraphQLMarkup,
|
|
13
13
|
GraphQLInlineMarkup,
|
|
14
|
+
FunctionMarkup,
|
|
14
15
|
HashAssignMarkup,
|
|
16
|
+
JsonHashLiteral,
|
|
17
|
+
JsonArrayLiteral,
|
|
15
18
|
} from '@platformos/liquid-html-parser';
|
|
16
19
|
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
17
20
|
import { isError } from '../../utils';
|
|
@@ -85,8 +88,10 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
85
88
|
if (isLiquidTagAssign(node)) {
|
|
86
89
|
const markup = node.markup;
|
|
87
90
|
|
|
88
|
-
// Close any previous shape for this variable (reassignment)
|
|
89
|
-
|
|
91
|
+
// Close any previous shape for this variable (reassignment).
|
|
92
|
+
// Use the value expression's end so the RHS can still see the old shape
|
|
93
|
+
// (e.g. {% assign c = c.d %} — c.d must resolve against the previous shape of c).
|
|
94
|
+
closeShapeRange(markup.name, markup.value.position.end);
|
|
90
95
|
|
|
91
96
|
const hasParseJsonFilter =
|
|
92
97
|
markup.value.filters &&
|
|
@@ -127,6 +132,19 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
127
132
|
}
|
|
128
133
|
}
|
|
129
134
|
|
|
135
|
+
// {% assign x = {a: 5, b: {c: 1}} %} or {% assign x = [1, 2, 3] %}
|
|
136
|
+
const exprType = markup.value.expression.type;
|
|
137
|
+
if (exprType === NodeTypes.JsonHashLiteral || exprType === NodeTypes.JsonArrayLiteral) {
|
|
138
|
+
const shape = inferShapeFromLiteralNode(markup.value.expression);
|
|
139
|
+
if (shape) {
|
|
140
|
+
variableShapes.push({
|
|
141
|
+
name: markup.name,
|
|
142
|
+
shape,
|
|
143
|
+
range: [node.position.end],
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
130
148
|
// {% assign x = y | dig: "key1" | dig: "key2" %}
|
|
131
149
|
// Follow the dig path through the source variable's known shape.
|
|
132
150
|
const digFilters =
|
|
@@ -246,6 +264,17 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
246
264
|
}
|
|
247
265
|
}
|
|
248
266
|
|
|
267
|
+
// {% function object = 'partial_name', arg: value %}
|
|
268
|
+
// The function tag reassigns the variable to the return value of the partial,
|
|
269
|
+
// which has an unknown shape. Close any tracked shape so we don't false-positive.
|
|
270
|
+
if (isLiquidTagFunction(node)) {
|
|
271
|
+
const markup = node.markup;
|
|
272
|
+
const variableName = markup.name.name;
|
|
273
|
+
if (variableName) {
|
|
274
|
+
closeShapeRange(variableName, node.position.start);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
|
|
249
278
|
// {% hash_assign x["key"] = value %} or {% hash_assign x["a"]["b"] = value %}
|
|
250
279
|
if (isLiquidTagHashAssign(node)) {
|
|
251
280
|
const markup = node.markup;
|
|
@@ -514,6 +543,10 @@ function isLiquidTagGraphQL(
|
|
|
514
543
|
return node.name === NamedTags.graphql && typeof node.markup !== 'string';
|
|
515
544
|
}
|
|
516
545
|
|
|
546
|
+
function isLiquidTagFunction(node: LiquidTag): node is LiquidTag & { markup: FunctionMarkup } {
|
|
547
|
+
return node.name === NamedTags.function && typeof node.markup !== 'string';
|
|
548
|
+
}
|
|
549
|
+
|
|
517
550
|
function isLiquidTagHashAssign(node: LiquidTag): node is LiquidTag & { markup: HashAssignMarkup } {
|
|
518
551
|
return node.name === NamedTags.hash_assign && typeof node.markup !== 'string';
|
|
519
552
|
}
|
|
@@ -540,3 +573,63 @@ function isGraphQLInlineMarkup(
|
|
|
540
573
|
function isLiquidString(expr: LiquidString | LiquidVariableLookup): expr is LiquidString {
|
|
541
574
|
return expr.type === NodeTypes.String;
|
|
542
575
|
}
|
|
576
|
+
|
|
577
|
+
/**
|
|
578
|
+
* Infer a PropertyShape from a JsonHashLiteral or JsonArrayLiteral AST node.
|
|
579
|
+
*/
|
|
580
|
+
function inferShapeFromLiteralNode(
|
|
581
|
+
node: JsonHashLiteral | JsonArrayLiteral,
|
|
582
|
+
): PropertyShape | undefined {
|
|
583
|
+
if (node.type === NodeTypes.JsonArrayLiteral) {
|
|
584
|
+
let itemShape: PropertyShape | undefined;
|
|
585
|
+
for (const element of node.elements) {
|
|
586
|
+
const elShape = inferShapeFromExpressionNode(element);
|
|
587
|
+
if (elShape) {
|
|
588
|
+
itemShape = itemShape ? itemShape : elShape;
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
return { kind: 'array', itemShape };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
if (node.type === NodeTypes.JsonHashLiteral) {
|
|
595
|
+
const properties = new Map<string, PropertyShape>();
|
|
596
|
+
for (const entry of node.entries) {
|
|
597
|
+
// Keys can be VariableLookup nodes (bare keys) or String nodes (quoted keys)
|
|
598
|
+
let keyName: string | undefined;
|
|
599
|
+
if (entry.key.type === NodeTypes.VariableLookup && entry.key.name) {
|
|
600
|
+
keyName = entry.key.name;
|
|
601
|
+
} else if (entry.key.type === NodeTypes.String) {
|
|
602
|
+
keyName = entry.key.value;
|
|
603
|
+
}
|
|
604
|
+
if (keyName) {
|
|
605
|
+
const valueShape = inferShapeFromExpressionNode(entry.value);
|
|
606
|
+
properties.set(keyName, valueShape ?? { kind: 'primitive' });
|
|
607
|
+
}
|
|
608
|
+
}
|
|
609
|
+
return { kind: 'object', properties };
|
|
610
|
+
}
|
|
611
|
+
|
|
612
|
+
return undefined;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
/**
|
|
616
|
+
* Infer a PropertyShape from a Liquid expression or variable node.
|
|
617
|
+
*/
|
|
618
|
+
function inferShapeFromExpressionNode(node: LiquidExpression | LiquidVariable): PropertyShape {
|
|
619
|
+
if (node.type === NodeTypes.JsonHashLiteral) {
|
|
620
|
+
return inferShapeFromLiteralNode(node as JsonHashLiteral) ?? { kind: 'primitive' };
|
|
621
|
+
}
|
|
622
|
+
if (node.type === NodeTypes.JsonArrayLiteral) {
|
|
623
|
+
return inferShapeFromLiteralNode(node as JsonArrayLiteral) ?? { kind: 'primitive' };
|
|
624
|
+
}
|
|
625
|
+
if (node.type === NodeTypes.String) {
|
|
626
|
+
return { kind: 'primitive', primitiveType: 'string' };
|
|
627
|
+
}
|
|
628
|
+
if (node.type === NodeTypes.Number) {
|
|
629
|
+
return { kind: 'primitive', primitiveType: 'number' };
|
|
630
|
+
}
|
|
631
|
+
if (node.type === NodeTypes.LiquidLiteral) {
|
|
632
|
+
return { kind: 'primitive' };
|
|
633
|
+
}
|
|
634
|
+
return { kind: 'primitive' };
|
|
635
|
+
}
|
|
@@ -194,7 +194,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
it("should disable the parent node's next node if platformos-check is disabled as the last child node", async () => {
|
|
197
|
-
const file = `{% liquid
|
|
197
|
+
const file = `{% doc %}{% enddoc %}{% liquid
|
|
198
198
|
if condition
|
|
199
199
|
# platformos-check-disable-next-line
|
|
200
200
|
elsif other_condition
|
|
@@ -211,7 +211,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
it('should not disable any checks if platformos-check is disabled at the end', async () => {
|
|
214
|
-
const file = `{% liquid
|
|
214
|
+
const file = `{% doc %}{% enddoc %}{% liquid
|
|
215
215
|
echo hello
|
|
216
216
|
echo everyone
|
|
217
217
|
# platformos-check-disable-next-line
|
|
@@ -232,7 +232,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
it('should disable the next line if the content is an HTML tag with liquid', async () => {
|
|
235
|
-
const file = `{% # platformos-check-disable-next-line %}
|
|
235
|
+
const file = `{% doc %}{% enddoc %}{% # platformos-check-disable-next-line %}
|
|
236
236
|
<div class="{{ foo }}"></div>
|
|
237
237
|
<div class="{{ bar }}"></div>`;
|
|
238
238
|
|
|
@@ -246,7 +246,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
246
246
|
});
|
|
247
247
|
|
|
248
248
|
it('should not disable the next line if the specified rule does not exist', async () => {
|
|
249
|
-
const file = `{% # platformos-check-disable-next-line FAKE_RULE %}
|
|
249
|
+
const file = `{% doc %}{% enddoc %}{% # platformos-check-disable-next-line FAKE_RULE %}
|
|
250
250
|
<div class="{{ foo }}"></div>`;
|
|
251
251
|
|
|
252
252
|
const offenses = await check({ 'code.liquid': file }, [UndefinedObject]);
|
package/src/url-helpers.spec.ts
CHANGED
|
@@ -13,6 +13,8 @@ import {
|
|
|
13
13
|
extractUrlPattern,
|
|
14
14
|
isValuedAttrNode,
|
|
15
15
|
getAttrName,
|
|
16
|
+
buildVariableMap,
|
|
17
|
+
tryExtractAssignUrl,
|
|
16
18
|
ValuedAttrNode,
|
|
17
19
|
} from './url-helpers';
|
|
18
20
|
|
|
@@ -239,3 +241,146 @@ describe('extractUrlPattern with variableMap', () => {
|
|
|
239
241
|
expect(extractUrlPattern(attr, variableMap)).toBe('/about');
|
|
240
242
|
});
|
|
241
243
|
});
|
|
244
|
+
|
|
245
|
+
describe('tryExtractAssignUrl', () => {
|
|
246
|
+
function firstChild(source: string): LiquidHtmlNode {
|
|
247
|
+
return toLiquidHtmlAST(source).children[0];
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
it('returns null for a non-assign liquid tag', () => {
|
|
251
|
+
expect(tryExtractAssignUrl(firstChild('{% if true %}{% endif %}'))).toBe(null);
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
it('returns null for an HTML element', () => {
|
|
255
|
+
expect(tryExtractAssignUrl(firstChild('<a href="/about">link</a>'))).toBe(null);
|
|
256
|
+
});
|
|
257
|
+
|
|
258
|
+
it('extracts name and urlPattern from a simple string assign', () => {
|
|
259
|
+
const result = tryExtractAssignUrl(firstChild('{% assign url = "/about" %}'));
|
|
260
|
+
expect(result).toEqual({ name: 'url', urlPattern: '/about' });
|
|
261
|
+
});
|
|
262
|
+
|
|
263
|
+
it('extracts urlPattern from an assign with append filter', () => {
|
|
264
|
+
const result = tryExtractAssignUrl(
|
|
265
|
+
firstChild('{% assign url = "/users/" | append: user.id %}'),
|
|
266
|
+
);
|
|
267
|
+
expect(result).toEqual({ name: 'url', urlPattern: '/users/:_liquid_' });
|
|
268
|
+
});
|
|
269
|
+
|
|
270
|
+
it('returns null when the assign RHS is not a URL pattern (no leading /)', () => {
|
|
271
|
+
expect(tryExtractAssignUrl(firstChild('{% assign url = "about" %}'))).toBe(null);
|
|
272
|
+
});
|
|
273
|
+
|
|
274
|
+
it('returns null when the assign RHS uses an unsupported filter', () => {
|
|
275
|
+
expect(tryExtractAssignUrl(firstChild('{% assign url = "/ABOUT" | downcase %}'))).toBe(null);
|
|
276
|
+
});
|
|
277
|
+
|
|
278
|
+
it('returns null when assigning to a target with lookups (e.g. obj.field = ...)', () => {
|
|
279
|
+
// {% assign hash["key"] = "/about" %} — has lookups, not a plain variable
|
|
280
|
+
const ast = toLiquidHtmlAST('{% assign url = "/about" %}');
|
|
281
|
+
const node = ast.children[0] as LiquidTagAssign;
|
|
282
|
+
// Simulate lookups by checking the real code path: lookups.length > 0 returns null
|
|
283
|
+
const markup = node.markup as AssignMarkup;
|
|
284
|
+
// Normal assign has no lookups — just verify it returns non-null here
|
|
285
|
+
expect(markup.lookups.length).toBe(0);
|
|
286
|
+
expect(tryExtractAssignUrl(node)).not.toBe(null);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe('buildVariableMap', () => {
|
|
291
|
+
function parseChildren(source: string): LiquidHtmlNode[] {
|
|
292
|
+
return toLiquidHtmlAST(source).children;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
it('collects top-level assigns', () => {
|
|
296
|
+
const map = buildVariableMap(parseChildren('{% assign url = "/about" %}'));
|
|
297
|
+
expect(map.get('url')).toBe('/about');
|
|
298
|
+
});
|
|
299
|
+
|
|
300
|
+
it('collects multiple top-level assigns', () => {
|
|
301
|
+
const map = buildVariableMap(
|
|
302
|
+
parseChildren('{% assign a = "/first" %}{% assign b = "/second" %}'),
|
|
303
|
+
);
|
|
304
|
+
expect(map.get('a')).toBe('/first');
|
|
305
|
+
expect(map.get('b')).toBe('/second');
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it('later assign overwrites earlier one', () => {
|
|
309
|
+
const map = buildVariableMap(
|
|
310
|
+
parseChildren('{% assign url = "/first" %}{% assign url = "/second" %}'),
|
|
311
|
+
);
|
|
312
|
+
expect(map.get('url')).toBe('/second');
|
|
313
|
+
});
|
|
314
|
+
|
|
315
|
+
it('recurses into {% if %} block children', () => {
|
|
316
|
+
const map = buildVariableMap(
|
|
317
|
+
parseChildren('{% if true %}{% assign url = "/about" %}{% endif %}'),
|
|
318
|
+
);
|
|
319
|
+
expect(map.get('url')).toBe('/about');
|
|
320
|
+
});
|
|
321
|
+
|
|
322
|
+
it('recurses into {% for %} block children', () => {
|
|
323
|
+
const map = buildVariableMap(
|
|
324
|
+
parseChildren('{% for i in list %}{% assign url = "/about" %}{% endfor %}'),
|
|
325
|
+
);
|
|
326
|
+
expect(map.get('url')).toBe('/about');
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
it('recurses into {% liquid %} block markup', () => {
|
|
330
|
+
const map = buildVariableMap(parseChildren('{% liquid\n assign url = "/about"\n%}'));
|
|
331
|
+
expect(map.get('url')).toBe('/about');
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
describe('beforeOffset', () => {
|
|
335
|
+
it('excludes assigns that end after beforeOffset', () => {
|
|
336
|
+
// "{% assign url = "/about" %}" is 27 chars (positions 0-26, end=27)
|
|
337
|
+
const source = '{% assign url = "/about" %}';
|
|
338
|
+
const map = buildVariableMap(parseChildren(source), 26);
|
|
339
|
+
// assign.position.end === 27 > 26, so it should be excluded
|
|
340
|
+
expect(map.has('url')).toBe(false);
|
|
341
|
+
});
|
|
342
|
+
|
|
343
|
+
it('includes assigns that end at or before beforeOffset', () => {
|
|
344
|
+
const source = '{% assign url = "/about" %}';
|
|
345
|
+
// assign ends at 27; beforeOffset=27 means end <= offset → included
|
|
346
|
+
const map = buildVariableMap(parseChildren(source), 27);
|
|
347
|
+
expect(map.get('url')).toBe('/about');
|
|
348
|
+
});
|
|
349
|
+
|
|
350
|
+
it('includes assign and excludes later reassignment based on cursor position', () => {
|
|
351
|
+
// assign1 ends at 27, assign2 ends at 54; cursor between them
|
|
352
|
+
const source = '{% assign url = "/first" %}{% assign url = "/second" %}';
|
|
353
|
+
const map = buildVariableMap(parseChildren(source), 28);
|
|
354
|
+
expect(map.get('url')).toBe('/first');
|
|
355
|
+
});
|
|
356
|
+
|
|
357
|
+
// Regression test for bug where the top-level `continue` skipped recursion into
|
|
358
|
+
// block containers. A block that starts before the cursor but ends after it must
|
|
359
|
+
// still be recursed into so that assigns before the cursor within it are found.
|
|
360
|
+
it('includes assign inside a block that ends after beforeOffset', () => {
|
|
361
|
+
// {% if %}...{% assign url = "/about" %}...<a href>...{% endif %}
|
|
362
|
+
// The if block ends after <a>.position.start, but the assign ends before it.
|
|
363
|
+
const source =
|
|
364
|
+
'{% if true %}{% assign url = "/about" %}<a href="{{ url }}">About</a>{% endif %}';
|
|
365
|
+
const aStart = source.indexOf('<a href');
|
|
366
|
+
const map = buildVariableMap(parseChildren(source), aStart);
|
|
367
|
+
expect(map.get('url')).toBe('/about');
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
it('includes assign inside {% liquid %} block when block ends after beforeOffset', () => {
|
|
371
|
+
const source =
|
|
372
|
+
'{% if true %}{% liquid\n assign url = "/about"\n%}<a href="{{ url }}">About</a>{% endif %}';
|
|
373
|
+
const aStart = source.indexOf('<a href');
|
|
374
|
+
const map = buildVariableMap(parseChildren(source), aStart);
|
|
375
|
+
expect(map.get('url')).toBe('/about');
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it('excludes assign inside block that starts after beforeOffset', () => {
|
|
379
|
+
const source =
|
|
380
|
+
'<a href="{{ url }}">About</a>{% if true %}{% assign url = "/about" %}{% endif %}';
|
|
381
|
+
const aStart = source.indexOf('<a href');
|
|
382
|
+
const map = buildVariableMap(parseChildren(source), aStart);
|
|
383
|
+
expect(map.has('url')).toBe(false);
|
|
384
|
+
});
|
|
385
|
+
});
|
|
386
|
+
});
|