@platformos/platformos-check-common 0.0.13 → 0.0.16
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 +28 -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 +64 -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 +62 -0
- package/src/checks/unknown-property/index.ts +73 -2
- package/src/disabled-checks/index.spec.ts +4 -4
- package/src/url-helpers.spec.ts +145 -0
|
@@ -1,31 +1,9 @@
|
|
|
1
1
|
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
2
|
-
import yaml from 'js-yaml';
|
|
3
2
|
import { DocumentsLocator, DocumentType } from '@platformos/platformos-common';
|
|
4
3
|
import { URI } from 'vscode-uri';
|
|
5
4
|
import { LiquidNamedArgument, Position } from '@platformos/liquid-html-parser';
|
|
6
5
|
import { relative } from '../../path';
|
|
7
|
-
|
|
8
|
-
type Metadata = {
|
|
9
|
-
metadata: {
|
|
10
|
-
params: Record<string, unknown>;
|
|
11
|
-
};
|
|
12
|
-
};
|
|
13
|
-
|
|
14
|
-
function extractMetadataParams(source: string): string[] | null {
|
|
15
|
-
source = source.trim();
|
|
16
|
-
if (!source.startsWith('---')) return null;
|
|
17
|
-
|
|
18
|
-
const end = source.indexOf('---', 3);
|
|
19
|
-
if (end === -1) return null;
|
|
20
|
-
|
|
21
|
-
const yamlBlock = source.slice(3, end).trim();
|
|
22
|
-
try {
|
|
23
|
-
const result = yaml.load(yamlBlock) as Metadata;
|
|
24
|
-
return Object.keys(result.metadata.params);
|
|
25
|
-
} catch (e) {
|
|
26
|
-
return null;
|
|
27
|
-
}
|
|
28
|
-
}
|
|
6
|
+
import { extractUndefinedVariables } from './extract-undefined-variables';
|
|
29
7
|
|
|
30
8
|
export const MetadataParamsCheck: LiquidCheckDefinition = {
|
|
31
9
|
meta: {
|
|
@@ -33,7 +11,7 @@ export const MetadataParamsCheck: LiquidCheckDefinition = {
|
|
|
33
11
|
name: 'Metadata Params Check',
|
|
34
12
|
docs: {
|
|
35
13
|
description:
|
|
36
|
-
'Ensures that parameters referenced in the document exist in
|
|
14
|
+
'Ensures that parameters referenced in the document exist in the doc tag or are inferred from undefined variables.',
|
|
37
15
|
recommended: true,
|
|
38
16
|
url: undefined,
|
|
39
17
|
},
|
|
@@ -61,21 +39,60 @@ export const MetadataParamsCheck: LiquidCheckDefinition = {
|
|
|
61
39
|
if (!locatedFile) {
|
|
62
40
|
return;
|
|
63
41
|
}
|
|
64
|
-
let params = extractMetadataParams(await context.fs.readFile(locatedFile));
|
|
65
|
-
if (!params) {
|
|
66
|
-
if (!context.getDocDefinition) return;
|
|
67
|
-
const relativePath = relative(locatedFile, context.config.rootUri);
|
|
68
|
-
const docDef = await context.getDocDefinition(relativePath);
|
|
69
|
-
if (!docDef?.liquidDoc?.parameters) return;
|
|
70
|
-
const liquidDocParameters = new Map(docDef.liquidDoc.parameters.map((p) => [p.name, p]));
|
|
71
42
|
|
|
72
|
-
|
|
43
|
+
const source = await context.fs.readFile(locatedFile);
|
|
44
|
+
const relativePath = relative(locatedFile, context.config.rootUri);
|
|
45
|
+
|
|
46
|
+
let requiredParams: string[];
|
|
47
|
+
let allowedParams: string[];
|
|
48
|
+
|
|
49
|
+
// Check for @doc tag first — if present, it's the complete param list
|
|
50
|
+
const docDef = context.getDocDefinition
|
|
51
|
+
? await context.getDocDefinition(relativePath)
|
|
52
|
+
: undefined;
|
|
53
|
+
|
|
54
|
+
if (docDef?.liquidDoc?.parameters) {
|
|
55
|
+
const globalObjectNames: string[] = [];
|
|
56
|
+
if (context.platformosDocset) {
|
|
57
|
+
const objects = await context.platformosDocset.objects();
|
|
58
|
+
for (const obj of objects) {
|
|
59
|
+
if (!obj.access || obj.access.global === true || obj.access.template.length > 0) {
|
|
60
|
+
globalObjectNames.push(obj.name);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
const undefinedVars = extractUndefinedVariables(source, globalObjectNames);
|
|
65
|
+
const docRequiredNames = docDef.liquidDoc.parameters
|
|
73
66
|
.filter((p) => p.required)
|
|
74
67
|
.map((p) => p.name);
|
|
68
|
+
requiredParams = docRequiredNames.filter((name) => undefinedVars.includes(name));
|
|
69
|
+
allowedParams = docDef.liquidDoc.parameters.map((p) => p.name);
|
|
70
|
+
} else {
|
|
71
|
+
// No @doc — scan for undefined variables, treat all as required
|
|
72
|
+
const globalObjectNames: string[] = [];
|
|
73
|
+
if (context.platformosDocset) {
|
|
74
|
+
const objects = await context.platformosDocset.objects();
|
|
75
|
+
for (const obj of objects) {
|
|
76
|
+
if (!obj.access || obj.access.global === true || obj.access.template.length > 0) {
|
|
77
|
+
globalObjectNames.push(obj.name);
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
if (relativePath.includes('views/partials/') || relativePath.includes('/lib/')) {
|
|
82
|
+
if (!globalObjectNames.includes('app')) {
|
|
83
|
+
globalObjectNames.push('app');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const undefinedVars = extractUndefinedVariables(source, globalObjectNames);
|
|
88
|
+
if (undefinedVars.length === 0) return;
|
|
89
|
+
|
|
90
|
+
requiredParams = undefinedVars;
|
|
91
|
+
allowedParams = undefinedVars;
|
|
75
92
|
}
|
|
76
93
|
|
|
77
94
|
args
|
|
78
|
-
.filter((arg) => !
|
|
95
|
+
.filter((arg) => !allowedParams.includes(arg.name))
|
|
79
96
|
.forEach((arg) => {
|
|
80
97
|
context.report({
|
|
81
98
|
message: `Unknown parameter ${arg.name} passed to ${nodeType} call`,
|
|
@@ -84,7 +101,7 @@ export const MetadataParamsCheck: LiquidCheckDefinition = {
|
|
|
84
101
|
});
|
|
85
102
|
});
|
|
86
103
|
|
|
87
|
-
|
|
104
|
+
requiredParams
|
|
88
105
|
.filter((param) => !args.find((arg) => arg.name === param))
|
|
89
106
|
.forEach((param) => {
|
|
90
107
|
context.report({
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { FileType, TranslationProvider } from '@platformos/platformos-common';
|
|
1
|
+
import { AbstractFileSystem, FileType, TranslationProvider } from '@platformos/platformos-common';
|
|
2
2
|
import { flattenTranslationKeys } from '../utils/levenshtein';
|
|
3
3
|
|
|
4
4
|
/**
|
|
@@ -6,7 +6,7 @@ import { flattenTranslationKeys } from '../utils/levenshtein';
|
|
|
6
6
|
* Returns a deduplicated set of module names.
|
|
7
7
|
*/
|
|
8
8
|
export async function discoverModules(
|
|
9
|
-
fs:
|
|
9
|
+
fs: AbstractFileSystem,
|
|
10
10
|
...moduleDirUris: string[]
|
|
11
11
|
): Promise<Set<string>> {
|
|
12
12
|
const modules = new Set<string>();
|
|
@@ -18,15 +18,15 @@ export async function discoverModules(
|
|
|
18
18
|
modules.add(entryUri.split('/').pop()!);
|
|
19
19
|
}
|
|
20
20
|
}
|
|
21
|
-
} catch
|
|
22
|
-
|
|
21
|
+
} catch {
|
|
22
|
+
// Directory doesn't exist or isn't accessible — skip
|
|
23
23
|
}
|
|
24
24
|
}
|
|
25
25
|
return modules;
|
|
26
26
|
}
|
|
27
27
|
|
|
28
28
|
export interface TranslationContext {
|
|
29
|
-
fs:
|
|
29
|
+
fs: AbstractFileSystem;
|
|
30
30
|
toUri(relativePath: string): string;
|
|
31
31
|
getTranslationsForBase(uri: string, locale: string): Promise<Record<string, any>>;
|
|
32
32
|
}
|
|
@@ -4,13 +4,25 @@ import { runLiquidCheck, highlightedOffenses } from '../../test';
|
|
|
4
4
|
import { Offense } from '../../types';
|
|
5
5
|
|
|
6
6
|
describe('Module: UndefinedObject', () => {
|
|
7
|
-
it('should report
|
|
7
|
+
it('should not report offenses when no doc tag is present', async () => {
|
|
8
8
|
const sourceCode = `
|
|
9
9
|
{{ my_var }}
|
|
10
10
|
`;
|
|
11
11
|
|
|
12
12
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
13
13
|
|
|
14
|
+
expect(offenses).toHaveLength(0);
|
|
15
|
+
});
|
|
16
|
+
|
|
17
|
+
it('should report an offense when object is undefined and doc tag is present', async () => {
|
|
18
|
+
const sourceCode = `
|
|
19
|
+
{% doc %}
|
|
20
|
+
{% enddoc %}
|
|
21
|
+
{{ my_var }}
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
25
|
+
|
|
14
26
|
expect(offenses).toHaveLength(1);
|
|
15
27
|
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_var' used."]);
|
|
16
28
|
|
|
@@ -18,8 +30,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
18
30
|
expect(highlights).toEqual(['my_var']);
|
|
19
31
|
});
|
|
20
32
|
|
|
21
|
-
it('should report an offense when object with an attribute is undefined', async () => {
|
|
33
|
+
it('should report an offense when object with an attribute is undefined and doc tag is present', async () => {
|
|
22
34
|
const sourceCode = `
|
|
35
|
+
{% doc %}
|
|
36
|
+
{% enddoc %}
|
|
23
37
|
{{ my_var.my_attr }}
|
|
24
38
|
`;
|
|
25
39
|
|
|
@@ -29,8 +43,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
29
43
|
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_var' used."]);
|
|
30
44
|
});
|
|
31
45
|
|
|
32
|
-
it('should report an offense when undefined object is used as an argument', async () => {
|
|
46
|
+
it('should report an offense when undefined object is used as an argument with doc tag', async () => {
|
|
33
47
|
const sourceCode = `
|
|
48
|
+
{% doc %}
|
|
49
|
+
{% enddoc %}
|
|
34
50
|
{{ product[my_object] }}
|
|
35
51
|
{{ product[my_object] }}
|
|
36
52
|
|
|
@@ -47,12 +63,14 @@ describe('Module: UndefinedObject', () => {
|
|
|
47
63
|
]);
|
|
48
64
|
});
|
|
49
65
|
|
|
50
|
-
it('should report an offense when object is undefined in a Liquid tag', async () => {
|
|
66
|
+
it('should report an offense when object is undefined in a Liquid tag with doc tag', async () => {
|
|
51
67
|
const sourceCode = `
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
68
|
+
{% doc %}
|
|
69
|
+
{% enddoc %}
|
|
70
|
+
{% liquid
|
|
71
|
+
echo my_var
|
|
72
|
+
%}
|
|
73
|
+
`;
|
|
56
74
|
|
|
57
75
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
58
76
|
|
|
@@ -121,8 +139,11 @@ describe('Module: UndefinedObject', () => {
|
|
|
121
139
|
expect(offenses).toHaveLength(0);
|
|
122
140
|
});
|
|
123
141
|
|
|
124
|
-
it('should report an offense when object is defined in a for loop but used outside of the scope', async () => {
|
|
142
|
+
it('should report an offense when object is defined in a for loop but used outside of the scope with doc tag', async () => {
|
|
125
143
|
const sourceCode = `
|
|
144
|
+
{% doc %}
|
|
145
|
+
@param {Array} collections
|
|
146
|
+
{% enddoc %}
|
|
126
147
|
{% for c in collections %}
|
|
127
148
|
{{ c }}
|
|
128
149
|
{% endfor %}{{ c }}
|
|
@@ -145,8 +166,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
145
166
|
expect(offenses).toHaveLength(0);
|
|
146
167
|
});
|
|
147
168
|
|
|
148
|
-
it('should report an offense when function result variable is used before its definition', async () => {
|
|
169
|
+
it('should report an offense when function result variable is used before its definition with doc tag', async () => {
|
|
149
170
|
const sourceCode = `
|
|
171
|
+
{% doc %}
|
|
172
|
+
{% enddoc %}
|
|
150
173
|
{{ a }}
|
|
151
174
|
{% function a = 'test' %}
|
|
152
175
|
`;
|
|
@@ -183,15 +206,14 @@ describe('Module: UndefinedObject', () => {
|
|
|
183
206
|
expect(offenses).toHaveLength(0);
|
|
184
207
|
});
|
|
185
208
|
|
|
186
|
-
it('should report
|
|
209
|
+
it('should not report offenses for undefined partials without doc tag', async () => {
|
|
187
210
|
const sourceCode = `
|
|
188
211
|
{% include undefined_partial %}
|
|
189
212
|
`;
|
|
190
213
|
|
|
191
214
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
192
215
|
|
|
193
|
-
expect(offenses).toHaveLength(
|
|
194
|
-
expect(offenses[0].message).toBe("Unknown object 'undefined_partial' used.");
|
|
216
|
+
expect(offenses).toHaveLength(0);
|
|
195
217
|
});
|
|
196
218
|
|
|
197
219
|
it('should not report an offense when a variable partial in include is defined', async () => {
|
|
@@ -205,19 +227,20 @@ describe('Module: UndefinedObject', () => {
|
|
|
205
227
|
expect(offenses).toHaveLength(0);
|
|
206
228
|
});
|
|
207
229
|
|
|
208
|
-
it('should report
|
|
230
|
+
it('should not report offenses for undefined function partials without doc tag', async () => {
|
|
209
231
|
const sourceCode = `
|
|
210
232
|
{% function result = undefined_partial %}
|
|
211
233
|
`;
|
|
212
234
|
|
|
213
235
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
214
236
|
|
|
215
|
-
expect(offenses).toHaveLength(
|
|
216
|
-
expect(offenses[0].message).toBe("Unknown object 'undefined_partial' used.");
|
|
237
|
+
expect(offenses).toHaveLength(0);
|
|
217
238
|
});
|
|
218
239
|
|
|
219
240
|
it('should not report an offense for the result variable itself in function tag', async () => {
|
|
220
241
|
const sourceCode = `
|
|
242
|
+
{% doc %}
|
|
243
|
+
{% enddoc %}
|
|
221
244
|
{% function result = undefined_partial %}
|
|
222
245
|
`;
|
|
223
246
|
|
|
@@ -227,8 +250,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
227
250
|
expect(offenses.every((o) => o.message !== "Unknown object 'result' used.")).toBe(true);
|
|
228
251
|
});
|
|
229
252
|
|
|
230
|
-
it('should report offenses for lookup key variables in function result target and partial', async () => {
|
|
253
|
+
it('should report offenses for lookup key variables in function result target and partial with doc tag', async () => {
|
|
231
254
|
const sourceCode = `
|
|
255
|
+
{% doc %}
|
|
256
|
+
{% enddoc %}
|
|
232
257
|
{% parse_json my_hash %}{}{% endparse_json %}
|
|
233
258
|
{% function my_hash[lookup_key] = my_hash[path_var] %}
|
|
234
259
|
`;
|
|
@@ -242,8 +267,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
242
267
|
expect(messages).not.toContain("Unknown object 'my_hash' used.");
|
|
243
268
|
});
|
|
244
269
|
|
|
245
|
-
it('should check the partial variable in function but not the hash-access result target base', async () => {
|
|
270
|
+
it('should check the partial variable in function but not the hash-access result target base with doc tag', async () => {
|
|
246
271
|
const sourceCode = `
|
|
272
|
+
{% doc %}
|
|
273
|
+
{% enddoc %}
|
|
247
274
|
{% parse_json my_hash %}{}{% endparse_json %}
|
|
248
275
|
{% function my_hash['key'] = undefined_partial %}
|
|
249
276
|
`;
|
|
@@ -255,8 +282,11 @@ describe('Module: UndefinedObject', () => {
|
|
|
255
282
|
expect(messages).not.toContain("Unknown object 'my_hash' used.");
|
|
256
283
|
});
|
|
257
284
|
|
|
258
|
-
it('should report an offense when object is defined in a for loop but used outside of the scope (
|
|
285
|
+
it('should report an offense when object is defined in a for loop but used outside of the scope (multiple scopes) with doc tag', async () => {
|
|
259
286
|
const sourceCode = `
|
|
287
|
+
{% doc %}
|
|
288
|
+
@param {Array} collections
|
|
289
|
+
{% enddoc %}
|
|
260
290
|
{% for c in collections %}
|
|
261
291
|
{% comment %} -- Scope 1 -- {% endcomment %}
|
|
262
292
|
{{ c }}
|
|
@@ -278,8 +308,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
278
308
|
]);
|
|
279
309
|
});
|
|
280
310
|
|
|
281
|
-
it('should report an offense when undefined object defines another object', async () => {
|
|
311
|
+
it('should report an offense when undefined object defines another object with doc tag', async () => {
|
|
282
312
|
const sourceCode = `
|
|
313
|
+
{% doc %}
|
|
314
|
+
{% enddoc %}
|
|
283
315
|
{% assign my_object = my_var %}
|
|
284
316
|
{{ my_object }}
|
|
285
317
|
`;
|
|
@@ -302,8 +334,11 @@ describe('Module: UndefinedObject', () => {
|
|
|
302
334
|
expect(offenses).toHaveLength(0);
|
|
303
335
|
});
|
|
304
336
|
|
|
305
|
-
it('should report an offense when object is defined in a tablerow loop but used outside of the scope', async () => {
|
|
337
|
+
it('should report an offense when object is defined in a tablerow loop but used outside of the scope with doc tag', async () => {
|
|
306
338
|
const sourceCode = `
|
|
339
|
+
{% doc %}
|
|
340
|
+
@param {Array} collections
|
|
341
|
+
{% enddoc %}
|
|
307
342
|
{% tablerow c in collections %}
|
|
308
343
|
{{ c }}
|
|
309
344
|
{% endtablerow %}{{ c }}
|
|
@@ -315,8 +350,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
315
350
|
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'c' used."]);
|
|
316
351
|
});
|
|
317
352
|
|
|
318
|
-
it('should contextually report on the undefined nature of the form object
|
|
353
|
+
it('should contextually report on the undefined nature of the form object with doc tag', async () => {
|
|
319
354
|
const sourceCode = `
|
|
355
|
+
{% doc %}
|
|
356
|
+
{% enddoc %}
|
|
320
357
|
{% form "cart" %}
|
|
321
358
|
{{ form }}
|
|
322
359
|
{% endform %}{{ form }}
|
|
@@ -328,8 +365,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
328
365
|
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'form' used."]);
|
|
329
366
|
});
|
|
330
367
|
|
|
331
|
-
it('should support {% layout none %}', async () => {
|
|
368
|
+
it('should support {% layout none %} with doc tag', async () => {
|
|
332
369
|
const sourceCode = `
|
|
370
|
+
{% doc %}
|
|
371
|
+
{% enddoc %}
|
|
333
372
|
{% layout none %}
|
|
334
373
|
{{ none }}
|
|
335
374
|
`;
|
|
@@ -352,7 +391,7 @@ describe('Module: UndefinedObject', () => {
|
|
|
352
391
|
}
|
|
353
392
|
});
|
|
354
393
|
|
|
355
|
-
it('should report an offense when object is undefined in a
|
|
394
|
+
it('should report an offense when object is undefined in a partial file with empty doc tag', async () => {
|
|
356
395
|
const sourceCode = `
|
|
357
396
|
{% doc %}
|
|
358
397
|
{% enddoc %}
|
|
@@ -384,8 +423,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
384
423
|
expect(offenses).toHaveLength(0);
|
|
385
424
|
});
|
|
386
425
|
|
|
387
|
-
it('should report an offense when object is not global', async () => {
|
|
426
|
+
it('should report an offense when object is not global with doc tag', async () => {
|
|
388
427
|
const sourceCode = `
|
|
428
|
+
{% doc %}
|
|
429
|
+
{% enddoc %}
|
|
389
430
|
{{ image }}
|
|
390
431
|
`;
|
|
391
432
|
|
|
@@ -409,19 +450,21 @@ describe('Module: UndefinedObject', () => {
|
|
|
409
450
|
}
|
|
410
451
|
});
|
|
411
452
|
|
|
412
|
-
it('should
|
|
453
|
+
it('should not report offenses for contextual objects without doc tag', async () => {
|
|
413
454
|
let offenses: Offense[];
|
|
414
455
|
const contexts: [string, string][] = [['app', 'app/views/partials/theme-app-extension.liquid']];
|
|
415
456
|
for (const [object, goodPath] of contexts) {
|
|
416
457
|
offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, goodPath);
|
|
417
458
|
expect(offenses).toHaveLength(0);
|
|
418
459
|
offenses = await runLiquidCheck(UndefinedObject, `{{ ${object} }}`, 'file.liquid');
|
|
419
|
-
expect(offenses).toHaveLength(
|
|
460
|
+
expect(offenses).toHaveLength(0);
|
|
420
461
|
}
|
|
421
462
|
});
|
|
422
463
|
|
|
423
|
-
it('should report an offense for forloop/tablerowloop used outside of context', async () => {
|
|
464
|
+
it('should report an offense for forloop/tablerowloop used outside of context with doc tag', async () => {
|
|
424
465
|
const sourceCode = `
|
|
466
|
+
{% doc %}
|
|
467
|
+
{% enddoc %}
|
|
425
468
|
{{ forloop }}
|
|
426
469
|
{{ tablerowloop }}
|
|
427
470
|
`;
|
|
@@ -458,8 +501,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
458
501
|
expect(offenses).toHaveLength(0);
|
|
459
502
|
});
|
|
460
503
|
|
|
461
|
-
it('should report an offense when assigning an undefined variable to itself', async () => {
|
|
504
|
+
it('should report an offense when assigning an undefined variable to itself with doc tag', async () => {
|
|
462
505
|
const sourceCode = `
|
|
506
|
+
{% doc %}
|
|
507
|
+
{% enddoc %}
|
|
463
508
|
{% assign my_var = my_var | default: "fallback" %}
|
|
464
509
|
`;
|
|
465
510
|
|
|
@@ -469,7 +514,7 @@ describe('Module: UndefinedObject', () => {
|
|
|
469
514
|
expect(offenses[0].message).toBe("Unknown object 'my_var' used.");
|
|
470
515
|
});
|
|
471
516
|
|
|
472
|
-
it('should report
|
|
517
|
+
it('should not report offenses for undefined variables inside background block without doc tag', async () => {
|
|
473
518
|
const sourceCode = `
|
|
474
519
|
{% background source_type: 'some form' %}
|
|
475
520
|
{{ undefined_var }}
|
|
@@ -478,7 +523,7 @@ describe('Module: UndefinedObject', () => {
|
|
|
478
523
|
|
|
479
524
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
480
525
|
|
|
481
|
-
expect(offenses).toHaveLength(
|
|
526
|
+
expect(offenses).toHaveLength(0);
|
|
482
527
|
});
|
|
483
528
|
|
|
484
529
|
it('should not report an offense when job_id is used after background file-based tag', async () => {
|
|
@@ -503,7 +548,7 @@ describe('Module: UndefinedObject', () => {
|
|
|
503
548
|
expect(offenses).toHaveLength(0);
|
|
504
549
|
});
|
|
505
550
|
|
|
506
|
-
it('should report
|
|
551
|
+
it('should not report offenses for job_id used before background tag without doc tag', async () => {
|
|
507
552
|
const sourceCode = `
|
|
508
553
|
{{ my_job }}
|
|
509
554
|
{% background my_job = 'some_partial' %}
|
|
@@ -511,8 +556,7 @@ describe('Module: UndefinedObject', () => {
|
|
|
511
556
|
|
|
512
557
|
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
513
558
|
|
|
514
|
-
expect(offenses).toHaveLength(
|
|
515
|
-
expect(offenses.map((e) => e.message)).toEqual(["Unknown object 'my_job' used."]);
|
|
559
|
+
expect(offenses).toHaveLength(0);
|
|
516
560
|
});
|
|
517
561
|
|
|
518
562
|
it('should not report an offense when object is defined with a parse_json tag', async () => {
|
|
@@ -528,8 +572,10 @@ describe('Module: UndefinedObject', () => {
|
|
|
528
572
|
expect(offenses).toHaveLength(0);
|
|
529
573
|
});
|
|
530
574
|
|
|
531
|
-
it('should report an offense when parse_json variable is used before the tag', async () => {
|
|
575
|
+
it('should report an offense when parse_json variable is used before the tag with doc tag', async () => {
|
|
532
576
|
const sourceCode = `
|
|
577
|
+
{% doc %}
|
|
578
|
+
{% enddoc %}
|
|
533
579
|
{{ groups_data }}
|
|
534
580
|
{% parse_json groups_data %}
|
|
535
581
|
{ "hello": "world" }
|
|
@@ -556,8 +602,91 @@ describe('Module: UndefinedObject', () => {
|
|
|
556
602
|
expect(offenses).toHaveLength(0);
|
|
557
603
|
});
|
|
558
604
|
|
|
559
|
-
it('should report an offense for
|
|
605
|
+
it('should report an offense for undefined variables in a page file even without doc tag', async () => {
|
|
606
|
+
const sourceCode = `
|
|
607
|
+
{{ my_var }}
|
|
608
|
+
`;
|
|
609
|
+
|
|
610
|
+
const offenses = await runLiquidCheck(
|
|
611
|
+
UndefinedObject,
|
|
612
|
+
sourceCode,
|
|
613
|
+
'app/views/pages/home.liquid',
|
|
614
|
+
);
|
|
615
|
+
|
|
616
|
+
expect(offenses).toHaveLength(1);
|
|
617
|
+
expect(offenses[0].message).toBe("Unknown object 'my_var' used.");
|
|
618
|
+
});
|
|
619
|
+
|
|
620
|
+
it('should report an offense for undefined variables in a module page file even without doc tag', async () => {
|
|
621
|
+
const sourceCode = `
|
|
622
|
+
{{ my_var }}
|
|
623
|
+
`;
|
|
624
|
+
|
|
625
|
+
const modulePaths = [
|
|
626
|
+
'modules/my_module/public/views/pages/home.liquid',
|
|
627
|
+
'modules/my_module/private/views/pages/home.liquid',
|
|
628
|
+
'app/modules/my_module/public/views/pages/home.liquid',
|
|
629
|
+
'app/modules/my_module/private/views/pages/home.liquid',
|
|
630
|
+
];
|
|
631
|
+
|
|
632
|
+
for (const pagePath of modulePaths) {
|
|
633
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode, pagePath);
|
|
634
|
+
|
|
635
|
+
expect(offenses).toHaveLength(1);
|
|
636
|
+
expect(offenses[0].message).toBe("Unknown object 'my_var' used.");
|
|
637
|
+
}
|
|
638
|
+
});
|
|
639
|
+
|
|
640
|
+
it('should not report offenses for global objects in a page file without doc tag', async () => {
|
|
641
|
+
const sourceCode = `
|
|
642
|
+
{{ collections }}
|
|
643
|
+
`;
|
|
644
|
+
|
|
645
|
+
const offenses = await runLiquidCheck(
|
|
646
|
+
UndefinedObject,
|
|
647
|
+
sourceCode,
|
|
648
|
+
'app/views/pages/home.liquid',
|
|
649
|
+
);
|
|
650
|
+
|
|
651
|
+
expect(offenses).toHaveLength(0);
|
|
652
|
+
});
|
|
653
|
+
|
|
654
|
+
it('should not report offenses for assigned variables in a page file without doc tag', async () => {
|
|
655
|
+
const sourceCode = `
|
|
656
|
+
{% assign my_var = "hello" %}
|
|
657
|
+
{{ my_var }}
|
|
658
|
+
`;
|
|
659
|
+
|
|
660
|
+
const offenses = await runLiquidCheck(
|
|
661
|
+
UndefinedObject,
|
|
662
|
+
sourceCode,
|
|
663
|
+
'app/views/pages/home.liquid',
|
|
664
|
+
);
|
|
665
|
+
|
|
666
|
+
expect(offenses).toHaveLength(0);
|
|
667
|
+
});
|
|
668
|
+
|
|
669
|
+
it('should respect @param in a page file with doc tag', async () => {
|
|
670
|
+
const sourceCode = `
|
|
671
|
+
{% doc %}
|
|
672
|
+
@param {string} text
|
|
673
|
+
{% enddoc %}
|
|
674
|
+
{{ text }}
|
|
675
|
+
`;
|
|
676
|
+
|
|
677
|
+
const offenses = await runLiquidCheck(
|
|
678
|
+
UndefinedObject,
|
|
679
|
+
sourceCode,
|
|
680
|
+
'app/views/pages/home.liquid',
|
|
681
|
+
);
|
|
682
|
+
|
|
683
|
+
expect(offenses).toHaveLength(0);
|
|
684
|
+
});
|
|
685
|
+
|
|
686
|
+
it('should report an offense for catch variable used outside catch block with doc tag', async () => {
|
|
560
687
|
const sourceCode = `
|
|
688
|
+
{% doc %}
|
|
689
|
+
{% enddoc %}
|
|
561
690
|
{% try %}
|
|
562
691
|
{{ "something" }}
|
|
563
692
|
{% catch error %}
|
|
@@ -22,6 +22,7 @@ import {
|
|
|
22
22
|
import { LiquidCheckDefinition, Severity, SourceCodeType, PlatformOSDocset } from '../../types';
|
|
23
23
|
import { isError, last } from '../../utils';
|
|
24
24
|
import { isWithinRawTagThatDoesNotParseItsContents } from '../utils';
|
|
25
|
+
import { isPage } from '../../path';
|
|
25
26
|
import yaml from 'js-yaml';
|
|
26
27
|
|
|
27
28
|
type Scope = { start?: number; end?: number };
|
|
@@ -58,6 +59,7 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
58
59
|
const scopedVariables: Map<string, Scope[]> = new Map();
|
|
59
60
|
const fileScopedVariables: Set<string> = new Set();
|
|
60
61
|
const variables: LiquidVariableLookup[] = [];
|
|
62
|
+
let hasDocTag = false;
|
|
61
63
|
|
|
62
64
|
function indexVariableScope(variableName: string | null, scope: Scope) {
|
|
63
65
|
if (!variableName) return;
|
|
@@ -74,6 +76,12 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
74
76
|
}
|
|
75
77
|
},
|
|
76
78
|
|
|
79
|
+
async LiquidRawTag(node) {
|
|
80
|
+
if (node.name === 'doc') {
|
|
81
|
+
hasDocTag = true;
|
|
82
|
+
}
|
|
83
|
+
},
|
|
84
|
+
|
|
77
85
|
async LiquidTag(node, ancestors) {
|
|
78
86
|
if (isWithinRawTagThatDoesNotParseItsContents(ancestors)) return;
|
|
79
87
|
|
|
@@ -191,6 +199,11 @@ export const UndefinedObject: LiquidCheckDefinition = {
|
|
|
191
199
|
},
|
|
192
200
|
|
|
193
201
|
async onCodePathEnd() {
|
|
202
|
+
const fileIsPage = isPage(context.file.uri);
|
|
203
|
+
|
|
204
|
+
// If no @doc tag and not a page, assume undefined variables are params from caller
|
|
205
|
+
if (!hasDocTag && !fileIsPage) return;
|
|
206
|
+
|
|
194
207
|
const objects = await globalObjects(platformosDocset, relativePath);
|
|
195
208
|
|
|
196
209
|
objects.forEach((obj) => fileScopedVariables.add(obj.name));
|
|
@@ -347,6 +347,68 @@ 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 report unknown property on hash literal assigned via another variable', async () => {
|
|
405
|
+
const sourceCode = `{% assign a = {a: 5} %}{% assign b = a.b %}`;
|
|
406
|
+
const offenses = await runLiquidCheck(UnknownProperty, sourceCode);
|
|
407
|
+
expect(offenses).toHaveLength(1);
|
|
408
|
+
expect(offenses[0].message).toEqual("Unknown property 'b' on 'a'.");
|
|
409
|
+
});
|
|
410
|
+
});
|
|
411
|
+
|
|
350
412
|
describe('error message formatting', () => {
|
|
351
413
|
it('should include variable name in error message', async () => {
|
|
352
414
|
const sourceCode = `{% assign myVar = '{"a": 1}' | parse_json %}
|