@platformos/platformos-check-common 0.0.17 → 0.0.19
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 +16 -0
- package/dist/checks/index.d.ts +1 -1
- package/dist/checks/index.js +6 -2
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/json-literal-quote-style/index.d.ts +2 -0
- package/dist/checks/json-literal-quote-style/index.js +42 -0
- package/dist/checks/json-literal-quote-style/index.js.map +1 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.d.ts +32 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js +93 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js.map +1 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidOutputPush.d.ts +17 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidOutputPush.js +37 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidOutputPush.js.map +1 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.js +1 -1
- package/dist/checks/liquid-html-syntax-error/index.js +25 -0
- package/dist/checks/liquid-html-syntax-error/index.js.map +1 -1
- package/dist/checks/partial-call-arguments/extract-undefined-variables.d.ts +14 -0
- package/dist/checks/partial-call-arguments/extract-undefined-variables.js +234 -0
- package/dist/checks/partial-call-arguments/extract-undefined-variables.js.map +1 -0
- package/dist/checks/partial-call-arguments/index.d.ts +2 -0
- package/dist/checks/partial-call-arguments/index.js +117 -0
- package/dist/checks/partial-call-arguments/index.js.map +1 -0
- package/dist/checks/unused-assign/index.js +17 -0
- package/dist/checks/unused-assign/index.js.map +1 -1
- package/dist/checks/valid-frontmatter/index.d.ts +2 -0
- package/dist/checks/valid-frontmatter/index.js +279 -0
- package/dist/checks/valid-frontmatter/index.js.map +1 -0
- package/dist/frontmatter/index.d.ts +1 -59
- package/dist/frontmatter/index.js +6 -298
- package/dist/frontmatter/index.js.map +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/package.json +2 -2
- package/src/checks/index.ts +6 -2
- package/src/checks/json-literal-quote-style/index.spec.ts +129 -0
- package/src/checks/json-literal-quote-style/index.ts +45 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.spec.ts +471 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.ts +97 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidOutputPush.spec.ts +104 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidOutputPush.ts +39 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.spec.ts +86 -2
- package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.ts +1 -1
- package/src/checks/liquid-html-syntax-error/index.ts +30 -0
- package/src/checks/partial-call-arguments/extract-undefined-variables.spec.ts +218 -0
- package/src/checks/{metadata-params → partial-call-arguments}/extract-undefined-variables.ts +31 -6
- package/src/checks/partial-call-arguments/index.spec.ts +436 -0
- package/src/checks/{metadata-params → partial-call-arguments}/index.ts +18 -11
- package/src/checks/undefined-object/index.spec.ts +101 -0
- package/src/checks/unused-assign/index.spec.ts +48 -0
- package/src/checks/unused-assign/index.ts +15 -0
- package/src/checks/valid-frontmatter/index.spec.ts +666 -0
- package/src/checks/valid-frontmatter/index.ts +344 -0
- package/src/frontmatter/index.ts +9 -344
- package/src/checks/metadata-params/extract-undefined-variables.spec.ts +0 -115
- package/src/checks/metadata-params/index.spec.ts +0 -257
|
@@ -0,0 +1,436 @@
|
|
|
1
|
+
import { expect, describe, it } from 'vitest';
|
|
2
|
+
import { PartialCallArguments } from '.';
|
|
3
|
+
import { check } from '../../test';
|
|
4
|
+
|
|
5
|
+
describe('Module: PartialCallArguments', () => {
|
|
6
|
+
// ─── @doc-based validation ───────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
it('should use doc tag as complete param list when present', async () => {
|
|
9
|
+
const file = `
|
|
10
|
+
{% doc %}
|
|
11
|
+
@param {Number} variable - param with description
|
|
12
|
+
@param {Number} variable2 - param with description
|
|
13
|
+
{% enddoc %}
|
|
14
|
+
|
|
15
|
+
{% assign a = 5 | plus: variable | plus: variable2 %}
|
|
16
|
+
{{ a }}
|
|
17
|
+
`;
|
|
18
|
+
const file2 = `
|
|
19
|
+
{% function a = 'commands/call/fileToCall', variable: 2, variable2: 12 %}
|
|
20
|
+
{{ a }}
|
|
21
|
+
`;
|
|
22
|
+
const files = {
|
|
23
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
24
|
+
'app/lib/caller.liquid': file2,
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
28
|
+
|
|
29
|
+
expect(offenses).to.have.length(0);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should report missing required doc params', async () => {
|
|
33
|
+
const file = `
|
|
34
|
+
{% doc %}
|
|
35
|
+
@param {Number} variable - param with description
|
|
36
|
+
@param {Number} variable2 - param with description
|
|
37
|
+
{% enddoc %}
|
|
38
|
+
|
|
39
|
+
{% assign a = 5 | plus: variable | plus: variable2 %}
|
|
40
|
+
{{ a }}
|
|
41
|
+
`;
|
|
42
|
+
const file2 = `
|
|
43
|
+
{% function a = 'commands/call/fileToCall', variable: 2 %}
|
|
44
|
+
{{ a }}
|
|
45
|
+
`;
|
|
46
|
+
const files = {
|
|
47
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
48
|
+
'app/lib/caller.liquid': file2,
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
52
|
+
|
|
53
|
+
expect(offenses).to.have.length(1);
|
|
54
|
+
expect(offenses).to.containOffense(
|
|
55
|
+
'Required parameter variable2 must be passed to function call',
|
|
56
|
+
);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('should report unknown params not in doc', async () => {
|
|
60
|
+
const file = `
|
|
61
|
+
{% doc %}
|
|
62
|
+
@param {Number} variable - param with description
|
|
63
|
+
{% enddoc %}
|
|
64
|
+
|
|
65
|
+
{% assign a = 5 | plus: variable %}
|
|
66
|
+
{{ a }}
|
|
67
|
+
`;
|
|
68
|
+
const file2 = `
|
|
69
|
+
{% function a = 'commands/call/fileToCall', variable: 2, extra: 12 %}
|
|
70
|
+
{{ a }}
|
|
71
|
+
`;
|
|
72
|
+
const files = {
|
|
73
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
74
|
+
'app/lib/caller.liquid': file2,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
78
|
+
|
|
79
|
+
expect(offenses).to.have.length(1);
|
|
80
|
+
expect(offenses).to.containOffense('Unknown parameter extra passed to function call');
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
it('should allow doc-optional params without requiring them', async () => {
|
|
84
|
+
const file = `
|
|
85
|
+
{% doc %}
|
|
86
|
+
@param {String} a - required
|
|
87
|
+
@param {String} [b] - optional
|
|
88
|
+
{% enddoc %}
|
|
89
|
+
{{ a }}{{ b }}
|
|
90
|
+
`;
|
|
91
|
+
const file2 = `
|
|
92
|
+
{% function res = 'commands/call/fileToCall', a: 'hello' %}
|
|
93
|
+
{{ res }}
|
|
94
|
+
`;
|
|
95
|
+
const files = {
|
|
96
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
97
|
+
'app/lib/caller.liquid': file2,
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
101
|
+
|
|
102
|
+
expect(offenses).to.have.length(0);
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
it('should allow passing doc-optional params without reporting unknown', async () => {
|
|
106
|
+
const file = `
|
|
107
|
+
{% doc %}
|
|
108
|
+
@param {String} a - required
|
|
109
|
+
@param {String} [b] - optional
|
|
110
|
+
{% enddoc %}
|
|
111
|
+
{{ a }}{{ b }}
|
|
112
|
+
`;
|
|
113
|
+
const file2 = `
|
|
114
|
+
{% function res = 'commands/call/fileToCall', a: 'hello', b: 'world' %}
|
|
115
|
+
{{ res }}
|
|
116
|
+
`;
|
|
117
|
+
const files = {
|
|
118
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
119
|
+
'app/lib/caller.liquid': file2,
|
|
120
|
+
};
|
|
121
|
+
|
|
122
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
123
|
+
|
|
124
|
+
expect(offenses).to.have.length(0);
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
it('should not require doc params that are not used in source', async () => {
|
|
128
|
+
const file = `
|
|
129
|
+
{% doc %}
|
|
130
|
+
@param {String} a - required param
|
|
131
|
+
@param {String} unused - required but not used in source
|
|
132
|
+
{% enddoc %}
|
|
133
|
+
{{ a }}
|
|
134
|
+
`;
|
|
135
|
+
const file2 = `
|
|
136
|
+
{% function res = 'commands/call/fileToCall', a: 'hello' %}
|
|
137
|
+
{{ res }}
|
|
138
|
+
`;
|
|
139
|
+
const files = {
|
|
140
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
141
|
+
'app/lib/caller.liquid': file2,
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
145
|
+
|
|
146
|
+
expect(offenses).to.have.length(0);
|
|
147
|
+
});
|
|
148
|
+
|
|
149
|
+
it('should still require a doc-required param even when implementation uses | default', async () => {
|
|
150
|
+
// The @doc annotation is the public API contract; internal fallbacks do not change it.
|
|
151
|
+
const file = `
|
|
152
|
+
{% doc %}
|
|
153
|
+
@param {String} message - required by contract
|
|
154
|
+
{% enddoc %}
|
|
155
|
+
{% assign message = message | default: 'fallback' %}
|
|
156
|
+
{{ message }}
|
|
157
|
+
`;
|
|
158
|
+
const file2 = `
|
|
159
|
+
{% function res = 'commands/call/fileToCall' %}
|
|
160
|
+
{{ res }}
|
|
161
|
+
`;
|
|
162
|
+
const files = {
|
|
163
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
164
|
+
'app/lib/caller.liquid': file2,
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
168
|
+
|
|
169
|
+
expect(offenses).to.have.length(1);
|
|
170
|
+
expect(offenses).to.containOffense(
|
|
171
|
+
'Required parameter message must be passed to function call',
|
|
172
|
+
);
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
// ─── Inferred validation (no @doc) ───────────────────────────────────────
|
|
176
|
+
|
|
177
|
+
it('should infer required params from undefined variables when no doc', async () => {
|
|
178
|
+
const file = `
|
|
179
|
+
{% assign b = a %}
|
|
180
|
+
{{ b }}
|
|
181
|
+
`;
|
|
182
|
+
const file2 = `
|
|
183
|
+
{% function res = 'commands/call/fileToCall', a: 'hello' %}
|
|
184
|
+
{{ res }}
|
|
185
|
+
`;
|
|
186
|
+
const files = {
|
|
187
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
188
|
+
'app/lib/caller.liquid': file2,
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
192
|
+
|
|
193
|
+
expect(offenses).to.have.length(0);
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it('should report missing inferred params when no doc', async () => {
|
|
197
|
+
const file = `
|
|
198
|
+
{% assign b = a %}
|
|
199
|
+
{{ b }}
|
|
200
|
+
`;
|
|
201
|
+
const file2 = `
|
|
202
|
+
{% function res = 'commands/call/fileToCall' %}
|
|
203
|
+
{{ res }}
|
|
204
|
+
`;
|
|
205
|
+
const files = {
|
|
206
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
207
|
+
'app/lib/caller.liquid': file2,
|
|
208
|
+
};
|
|
209
|
+
|
|
210
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
211
|
+
|
|
212
|
+
expect(offenses).to.have.length(1);
|
|
213
|
+
expect(offenses).to.containOffense('Required parameter a must be passed to function call');
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should report unknown params when passing args not in inferred set', async () => {
|
|
217
|
+
const file = `
|
|
218
|
+
{% assign b = a %}
|
|
219
|
+
{{ b }}
|
|
220
|
+
`;
|
|
221
|
+
const file2 = `
|
|
222
|
+
{% function res = 'commands/call/fileToCall', a: 'hello', extra: 'world' %}
|
|
223
|
+
{{ res }}
|
|
224
|
+
`;
|
|
225
|
+
const files = {
|
|
226
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
227
|
+
'app/lib/caller.liquid': file2,
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
231
|
+
|
|
232
|
+
expect(offenses).to.have.length(1);
|
|
233
|
+
expect(offenses).to.containOffense('Unknown parameter extra passed to function call');
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it('should not include global objects like context in inferred params', async () => {
|
|
237
|
+
const file = `
|
|
238
|
+
{{ context.session }}
|
|
239
|
+
{{ a }}
|
|
240
|
+
`;
|
|
241
|
+
const file2 = `
|
|
242
|
+
{% function res = 'commands/call/fileToCall', a: 'hello' %}
|
|
243
|
+
{{ res }}
|
|
244
|
+
`;
|
|
245
|
+
const files = {
|
|
246
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
247
|
+
'app/lib/caller.liquid': file2,
|
|
248
|
+
};
|
|
249
|
+
|
|
250
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
251
|
+
|
|
252
|
+
expect(offenses).to.have.length(0);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it('should work with render tags too', async () => {
|
|
256
|
+
const file = `{{ a }}`;
|
|
257
|
+
const file2 = `{% render 'fileToRender' %}`;
|
|
258
|
+
const files = {
|
|
259
|
+
'app/views/partials/fileToRender.liquid': file,
|
|
260
|
+
'app/views/pages/caller.liquid': file2,
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
264
|
+
|
|
265
|
+
expect(offenses).to.have.length(1);
|
|
266
|
+
expect(offenses).to.containOffense('Required parameter a must be passed to render call');
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it('should skip validation when no doc and no undefined vars', async () => {
|
|
270
|
+
const file = `
|
|
271
|
+
{% assign a = 5 %}
|
|
272
|
+
{{ a }}
|
|
273
|
+
`;
|
|
274
|
+
const file2 = `
|
|
275
|
+
{% function res = 'commands/call/fileToCall', extra: 'hello' %}
|
|
276
|
+
{{ res }}
|
|
277
|
+
`;
|
|
278
|
+
const files = {
|
|
279
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
280
|
+
'app/lib/caller.liquid': file2,
|
|
281
|
+
};
|
|
282
|
+
|
|
283
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
284
|
+
|
|
285
|
+
expect(offenses).to.have.length(0);
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
// ─── | default — inferred optional params ────────────────────────────────
|
|
289
|
+
|
|
290
|
+
it('should treat assign x = x | default: val as optional (no error when omitted)', async () => {
|
|
291
|
+
const file = `
|
|
292
|
+
{% assign message = message | default: null %}
|
|
293
|
+
{% assign required_param = required_param %}
|
|
294
|
+
{{ message }}{{ required_param }}
|
|
295
|
+
`;
|
|
296
|
+
const file2 = `
|
|
297
|
+
{% function res = 'commands/call/fileToCall', required_param: 'hello' %}
|
|
298
|
+
{{ res }}
|
|
299
|
+
`;
|
|
300
|
+
const files = {
|
|
301
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
302
|
+
'app/lib/caller.liquid': file2,
|
|
303
|
+
};
|
|
304
|
+
|
|
305
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
306
|
+
|
|
307
|
+
expect(offenses).to.have.length(0);
|
|
308
|
+
});
|
|
309
|
+
|
|
310
|
+
it('should allow passing inferred optional params without reporting unknown', async () => {
|
|
311
|
+
const file = `
|
|
312
|
+
{% assign message = message | default: null %}
|
|
313
|
+
{% assign required_param = required_param %}
|
|
314
|
+
{{ message }}{{ required_param }}
|
|
315
|
+
`;
|
|
316
|
+
const file2 = `
|
|
317
|
+
{% function res = 'commands/call/fileToCall', required_param: 'hello', message: 'hi' %}
|
|
318
|
+
{{ res }}
|
|
319
|
+
`;
|
|
320
|
+
const files = {
|
|
321
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
322
|
+
'app/lib/caller.liquid': file2,
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
326
|
+
|
|
327
|
+
expect(offenses).to.have.length(0);
|
|
328
|
+
});
|
|
329
|
+
|
|
330
|
+
it('should treat inline output {{ x | default: val }} as optional', async () => {
|
|
331
|
+
const file = `{{ message | default: 'fallback' }}`;
|
|
332
|
+
const file2 = `
|
|
333
|
+
{% function res = 'commands/call/fileToCall' %}
|
|
334
|
+
{{ res }}
|
|
335
|
+
`;
|
|
336
|
+
const files = {
|
|
337
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
338
|
+
'app/lib/caller.liquid': file2,
|
|
339
|
+
};
|
|
340
|
+
|
|
341
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
342
|
+
|
|
343
|
+
expect(offenses).to.have.length(0);
|
|
344
|
+
});
|
|
345
|
+
|
|
346
|
+
it('should treat assign y = x | default: val as x optional (different lhs/rhs)', async () => {
|
|
347
|
+
const file = `{% assign label = title | default: 'Untitled' %}{{ label }}`;
|
|
348
|
+
const file2 = `
|
|
349
|
+
{% function res = 'commands/call/fileToCall' %}
|
|
350
|
+
{{ res }}
|
|
351
|
+
`;
|
|
352
|
+
const files = {
|
|
353
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
354
|
+
'app/lib/caller.liquid': file2,
|
|
355
|
+
};
|
|
356
|
+
|
|
357
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
358
|
+
|
|
359
|
+
expect(offenses).to.have.length(0);
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
it('should still report unknown when caller passes arg not in optional or required set', async () => {
|
|
363
|
+
const file = `
|
|
364
|
+
{% assign message = message | default: null %}
|
|
365
|
+
{{ message }}
|
|
366
|
+
`;
|
|
367
|
+
const file2 = `
|
|
368
|
+
{% function res = 'commands/call/fileToCall', unknown_param: 'oops' %}
|
|
369
|
+
{{ res }}
|
|
370
|
+
`;
|
|
371
|
+
const files = {
|
|
372
|
+
'app/lib/commands/call/fileToCall.liquid': file,
|
|
373
|
+
'app/lib/caller.liquid': file2,
|
|
374
|
+
};
|
|
375
|
+
|
|
376
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
377
|
+
|
|
378
|
+
expect(offenses).to.have.length(1);
|
|
379
|
+
expect(offenses).to.containOffense('Unknown parameter unknown_param passed to function call');
|
|
380
|
+
});
|
|
381
|
+
|
|
382
|
+
it('should handle the real register_error pattern: required + optional params', async () => {
|
|
383
|
+
const file = `
|
|
384
|
+
{% liquid
|
|
385
|
+
assign key = key | default: null
|
|
386
|
+
assign message = message | default: null
|
|
387
|
+
assign errors = contract.errors
|
|
388
|
+
assign field_errors = errors[field_name] | default: blank
|
|
389
|
+
assign field_errors << message
|
|
390
|
+
assign errors[field_name] = field_errors
|
|
391
|
+
assign contract.valid = false
|
|
392
|
+
return contract
|
|
393
|
+
%}
|
|
394
|
+
`;
|
|
395
|
+
const file2 = `
|
|
396
|
+
{% function c = 'helpers/register_error', contract: c, field_name: field_name, key: key %}
|
|
397
|
+
`;
|
|
398
|
+
const files = {
|
|
399
|
+
'app/lib/helpers/register_error.liquid': file,
|
|
400
|
+
'app/lib/caller.liquid': file2,
|
|
401
|
+
};
|
|
402
|
+
|
|
403
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
404
|
+
|
|
405
|
+
expect(offenses).to.have.length(0);
|
|
406
|
+
});
|
|
407
|
+
|
|
408
|
+
it('should still require required params even when optional ones are present', async () => {
|
|
409
|
+
const file = `
|
|
410
|
+
{% liquid
|
|
411
|
+
assign key = key | default: null
|
|
412
|
+
assign errors = contract.errors
|
|
413
|
+
assign field_errors = errors[field_name] | default: blank
|
|
414
|
+
return contract
|
|
415
|
+
%}
|
|
416
|
+
`;
|
|
417
|
+
// omits both required: contract and field_name
|
|
418
|
+
const file2 = `
|
|
419
|
+
{% function c = 'helpers/register_error', key: 'some_key' %}
|
|
420
|
+
`;
|
|
421
|
+
const files = {
|
|
422
|
+
'app/lib/helpers/register_error.liquid': file,
|
|
423
|
+
'app/lib/caller.liquid': file2,
|
|
424
|
+
};
|
|
425
|
+
|
|
426
|
+
const offenses = await check(files, [PartialCallArguments]);
|
|
427
|
+
|
|
428
|
+
expect(offenses).to.have.length(2);
|
|
429
|
+
expect(offenses).to.containOffense(
|
|
430
|
+
'Required parameter contract must be passed to function call',
|
|
431
|
+
);
|
|
432
|
+
expect(offenses).to.containOffense(
|
|
433
|
+
'Required parameter field_name must be passed to function call',
|
|
434
|
+
);
|
|
435
|
+
});
|
|
436
|
+
});
|
|
@@ -5,15 +5,16 @@ import { LiquidNamedArgument, Position } from '@platformos/liquid-html-parser';
|
|
|
5
5
|
import { relative } from '../../path';
|
|
6
6
|
import { extractUndefinedVariables } from './extract-undefined-variables';
|
|
7
7
|
|
|
8
|
-
export const
|
|
8
|
+
export const PartialCallArguments: LiquidCheckDefinition = {
|
|
9
9
|
meta: {
|
|
10
|
-
code: '
|
|
11
|
-
|
|
10
|
+
code: 'PartialCallArguments',
|
|
11
|
+
aliases: ['MetadataParamsCheck'],
|
|
12
|
+
name: 'Partial Call Arguments',
|
|
12
13
|
docs: {
|
|
13
14
|
description:
|
|
14
|
-
'Ensures that
|
|
15
|
+
'Ensures that all required arguments are passed at render/function call sites, and that no unknown arguments are passed. Required vs optional is determined from the {% doc %} block when present, or inferred from undefined variables in the partial source otherwise. Variables used with | default are treated as optional.',
|
|
15
16
|
recommended: true,
|
|
16
|
-
url:
|
|
17
|
+
url: 'https://documentation.platformos.com/developer-guide/platformos-check/checks/partial-call-arguments',
|
|
17
18
|
},
|
|
18
19
|
type: SourceCodeType.LiquidHtml,
|
|
19
20
|
severity: Severity.ERROR,
|
|
@@ -61,14 +62,17 @@ export const MetadataParamsCheck: LiquidCheckDefinition = {
|
|
|
61
62
|
}
|
|
62
63
|
}
|
|
63
64
|
}
|
|
64
|
-
const
|
|
65
|
+
const { required: undefinedRequiredVars, optional: undefinedOptionalVars } =
|
|
66
|
+
extractUndefinedVariables(source, globalObjectNames);
|
|
67
|
+
const undefinedVars = [...undefinedRequiredVars, ...undefinedOptionalVars];
|
|
65
68
|
const docRequiredNames = docDef.liquidDoc.parameters
|
|
66
69
|
.filter((p) => p.required)
|
|
67
70
|
.map((p) => p.name);
|
|
68
71
|
requiredParams = docRequiredNames.filter((name) => undefinedVars.includes(name));
|
|
69
72
|
allowedParams = docDef.liquidDoc.parameters.map((p) => p.name);
|
|
70
73
|
} else {
|
|
71
|
-
// No @doc —
|
|
74
|
+
// No @doc — infer required vs optional from undefined variables in the source.
|
|
75
|
+
// Variables used with `| default` are treated as optional.
|
|
72
76
|
const globalObjectNames: string[] = [];
|
|
73
77
|
if (context.platformosDocset) {
|
|
74
78
|
const objects = await context.platformosDocset.objects();
|
|
@@ -84,11 +88,14 @@ export const MetadataParamsCheck: LiquidCheckDefinition = {
|
|
|
84
88
|
}
|
|
85
89
|
}
|
|
86
90
|
|
|
87
|
-
const
|
|
88
|
-
|
|
91
|
+
const { required: requiredVars, optional: optionalVars } = extractUndefinedVariables(
|
|
92
|
+
source,
|
|
93
|
+
globalObjectNames,
|
|
94
|
+
);
|
|
95
|
+
if (requiredVars.length === 0 && optionalVars.length === 0) return;
|
|
89
96
|
|
|
90
|
-
requiredParams =
|
|
91
|
-
allowedParams =
|
|
97
|
+
requiredParams = requiredVars;
|
|
98
|
+
allowedParams = [...requiredVars, ...optionalVars];
|
|
92
99
|
}
|
|
93
100
|
|
|
94
101
|
args
|
|
@@ -700,4 +700,105 @@ describe('Module: UndefinedObject', () => {
|
|
|
700
700
|
expect(offenses).toHaveLength(1);
|
|
701
701
|
expect(offenses[0].message).toBe("Unknown object 'error' used.");
|
|
702
702
|
});
|
|
703
|
+
|
|
704
|
+
describe('JSON literals in assign tag', () => {
|
|
705
|
+
it('should report undefined bare key AND bare value in a hash literal', async () => {
|
|
706
|
+
// bare keys and bare values are both VariableLookups in platformOS semantics —
|
|
707
|
+
// they are evaluated at runtime, so both are subject to undefined-object checks
|
|
708
|
+
const sourceCode = `
|
|
709
|
+
{% doc %}
|
|
710
|
+
{% enddoc %}
|
|
711
|
+
{% assign hash = { bare_key: bare_val } %}
|
|
712
|
+
`;
|
|
713
|
+
|
|
714
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
715
|
+
|
|
716
|
+
expect(offenses.map((o) => o.message).sort()).toEqual([
|
|
717
|
+
"Unknown object 'bare_key' used.",
|
|
718
|
+
"Unknown object 'bare_val' used.",
|
|
719
|
+
]);
|
|
720
|
+
});
|
|
721
|
+
|
|
722
|
+
it('should NOT report string keys in a hash literal (they are literals, not variables)', async () => {
|
|
723
|
+
const sourceCode = `
|
|
724
|
+
{% doc %}
|
|
725
|
+
{% enddoc %}
|
|
726
|
+
{% assign hash = { "key": "val" } %}
|
|
727
|
+
`;
|
|
728
|
+
|
|
729
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
730
|
+
|
|
731
|
+
expect(offenses).toHaveLength(0);
|
|
732
|
+
});
|
|
733
|
+
|
|
734
|
+
it('should report undefined bare value under a string key', async () => {
|
|
735
|
+
const sourceCode = `
|
|
736
|
+
{% doc %}
|
|
737
|
+
{% enddoc %}
|
|
738
|
+
{% assign hash = { "key": undefined_var } %}
|
|
739
|
+
`;
|
|
740
|
+
|
|
741
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
742
|
+
|
|
743
|
+
expect(offenses).toHaveLength(1);
|
|
744
|
+
expect(offenses[0].message).toBe("Unknown object 'undefined_var' used.");
|
|
745
|
+
});
|
|
746
|
+
|
|
747
|
+
it('should report undefined elements in an array literal', async () => {
|
|
748
|
+
const sourceCode = `
|
|
749
|
+
{% doc %}
|
|
750
|
+
{% enddoc %}
|
|
751
|
+
{% assign arr = [a, undefined_var] %}
|
|
752
|
+
`;
|
|
753
|
+
|
|
754
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
755
|
+
|
|
756
|
+
expect(offenses.map((o) => o.message).sort()).toEqual([
|
|
757
|
+
"Unknown object 'a' used.",
|
|
758
|
+
"Unknown object 'undefined_var' used.",
|
|
759
|
+
]);
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
it('should NOT report string elements in an array literal', async () => {
|
|
763
|
+
const sourceCode = `
|
|
764
|
+
{% doc %}
|
|
765
|
+
{% enddoc %}
|
|
766
|
+
{% assign arr = ["a", "b", "c"] %}
|
|
767
|
+
`;
|
|
768
|
+
|
|
769
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
770
|
+
|
|
771
|
+
expect(offenses).toHaveLength(0);
|
|
772
|
+
});
|
|
773
|
+
|
|
774
|
+
it('should NOT report bare keys/values when they refer to defined variables', async () => {
|
|
775
|
+
const sourceCode = `
|
|
776
|
+
{% doc %}
|
|
777
|
+
{% enddoc %}
|
|
778
|
+
{% assign bare_key = "k" %}
|
|
779
|
+
{% assign bare_val = "v" %}
|
|
780
|
+
{% assign hash = { bare_key: bare_val } %}
|
|
781
|
+
`;
|
|
782
|
+
|
|
783
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
784
|
+
|
|
785
|
+
expect(offenses).toHaveLength(0);
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
it('should report undefined bare keys/values in nested hash literals', async () => {
|
|
789
|
+
const sourceCode = `
|
|
790
|
+
{% doc %}
|
|
791
|
+
{% enddoc %}
|
|
792
|
+
{% assign hash = { outer: { inner: value } } %}
|
|
793
|
+
`;
|
|
794
|
+
|
|
795
|
+
const offenses = await runLiquidCheck(UndefinedObject, sourceCode);
|
|
796
|
+
|
|
797
|
+
expect(offenses.map((o) => o.message).sort()).toEqual([
|
|
798
|
+
"Unknown object 'inner' used.",
|
|
799
|
+
"Unknown object 'outer' used.",
|
|
800
|
+
"Unknown object 'value' used.",
|
|
801
|
+
]);
|
|
802
|
+
});
|
|
803
|
+
});
|
|
703
804
|
});
|
|
@@ -201,4 +201,52 @@ describe('Module: UnusedAssign', () => {
|
|
|
201
201
|
expect(offenses).to.have.lengthOf(1);
|
|
202
202
|
expect(offenses[0].message).to.equal("The variable 'arr' is assigned but not used");
|
|
203
203
|
});
|
|
204
|
+
|
|
205
|
+
it('should not report variables used inside a filter array-literal argument', async () => {
|
|
206
|
+
const sourceCode = `
|
|
207
|
+
{% assign k = "key" %}
|
|
208
|
+
{% assign v = "value" %}
|
|
209
|
+
{% assign name = arr | default: [ "hi", k, v] %}
|
|
210
|
+
{{ name }}
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
214
|
+
expect(offenses).to.have.lengthOf(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should not report variables used inside a filter hash-literal argument', async () => {
|
|
218
|
+
const sourceCode = `
|
|
219
|
+
{% assign k = 1 %}
|
|
220
|
+
{% assign name = arr | default: { a: k } %}
|
|
221
|
+
{{ name }}
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
225
|
+
expect(offenses).to.have.lengthOf(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should not report variables used inside a named-argument value', async () => {
|
|
229
|
+
const sourceCode = `
|
|
230
|
+
{% assign k = 0 %}
|
|
231
|
+
{% assign name = arr | slice: start: k %}
|
|
232
|
+
{{ name }}
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
236
|
+
expect(offenses).to.have.lengthOf(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should not report variables referenced in a string-markup fallback assign (parse failure)', async () => {
|
|
240
|
+
// The stray `}` before `%}` causes the tolerant parser to fall back to
|
|
241
|
+
// string markup. The AST contains no VariableLookup nodes for k/v, so
|
|
242
|
+
// without a textual fallback scan the check would wrongly flag them.
|
|
243
|
+
const sourceCode = `
|
|
244
|
+
{% assign k = "key" %}
|
|
245
|
+
{% assign v = "value" %}
|
|
246
|
+
{% assign name = arr | default: [ "hi", k, v] } %}
|
|
247
|
+
`;
|
|
248
|
+
|
|
249
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
250
|
+
expect(offenses).to.have.lengthOf(0);
|
|
251
|
+
});
|
|
204
252
|
});
|
|
@@ -31,6 +31,12 @@ export const UnusedAssign: LiquidCheckDefinition = {
|
|
|
31
31
|
// effects on the original, so they count as "using" the variable.
|
|
32
32
|
const referenceAssignedVariables: Set<string> = new Set();
|
|
33
33
|
const usedVariables: Set<string> = new Set();
|
|
34
|
+
// Concatenated markup of LiquidTags whose parsing fell back to a raw string
|
|
35
|
+
// (e.g. a typo like `{% assign name = x | default: [...] } %}`). The AST has
|
|
36
|
+
// no VariableLookup nodes to traverse inside them, so onCodePathEnd scans
|
|
37
|
+
// this text for assigned-variable names — avoids compounding the user's
|
|
38
|
+
// syntax error with false "unused" warnings.
|
|
39
|
+
let fallbackText = '';
|
|
34
40
|
|
|
35
41
|
function checkVariableUsage(node: any) {
|
|
36
42
|
if (node.type === NodeTypes.VariableLookup) {
|
|
@@ -66,6 +72,8 @@ export const UnusedAssign: LiquidCheckDefinition = {
|
|
|
66
72
|
}
|
|
67
73
|
} else if (isLiquidTagCapture(node) && node.markup.name) {
|
|
68
74
|
assignedVariables.set(node.markup.name, node);
|
|
75
|
+
} else if (typeof node.markup === 'string' && node.markup) {
|
|
76
|
+
fallbackText += '\n' + node.markup;
|
|
69
77
|
}
|
|
70
78
|
},
|
|
71
79
|
|
|
@@ -79,6 +87,13 @@ export const UnusedAssign: LiquidCheckDefinition = {
|
|
|
79
87
|
},
|
|
80
88
|
|
|
81
89
|
async onCodePathEnd() {
|
|
90
|
+
if (fallbackText) {
|
|
91
|
+
for (const name of assignedVariables.keys()) {
|
|
92
|
+
if (usedVariables.has(name)) continue;
|
|
93
|
+
if (new RegExp(`\\b${name}\\b`).test(fallbackText)) usedVariables.add(name);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
for (const [variable, node] of assignedVariables.entries()) {
|
|
83
98
|
if (!usedVariables.has(variable) && !variable.startsWith('_')) {
|
|
84
99
|
context.report({
|