@platformos/platformos-check-common 0.0.16 → 0.0.18
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +14 -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 +21 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js +60 -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 +17 -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/unknown-property/index.js +22 -2
- package/dist/checks/unknown-property/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 +422 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.ts +63 -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 +19 -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/unknown-property/index.spec.ts +42 -0
- package/src/checks/unknown-property/index.ts +24 -2
- 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
package/src/checks/{metadata-params → partial-call-arguments}/extract-undefined-variables.ts
RENAMED
|
@@ -8,6 +8,7 @@ import {
|
|
|
8
8
|
LiquidTagIncrement,
|
|
9
9
|
LiquidTagTablerow,
|
|
10
10
|
LiquidVariableLookup,
|
|
11
|
+
LiquidVariable,
|
|
11
12
|
NamedTags,
|
|
12
13
|
NodeTypes,
|
|
13
14
|
Position,
|
|
@@ -25,7 +26,10 @@ type Scope = { start?: number; end?: number };
|
|
|
25
26
|
|
|
26
27
|
/**
|
|
27
28
|
* Parses a Liquid source string and returns a deduplicated list of variable names
|
|
28
|
-
* that are used but never defined. Returns [] on parse errors.
|
|
29
|
+
* that are used but never defined. Returns `{ required: [], optional: [] }` on parse errors.
|
|
30
|
+
*
|
|
31
|
+
* Variables used exclusively with `| default` filter (e.g. `assign x = x | default: val`)
|
|
32
|
+
* are returned in `optional` — the partial handles the missing-argument case itself.
|
|
29
33
|
*
|
|
30
34
|
* This mirrors the variable tracking logic from the UndefinedObject check but
|
|
31
35
|
* packaged as a standalone synchronous function.
|
|
@@ -33,17 +37,18 @@ type Scope = { start?: number; end?: number };
|
|
|
33
37
|
export function extractUndefinedVariables(
|
|
34
38
|
source: string,
|
|
35
39
|
globalObjectNames: string[] = [],
|
|
36
|
-
): string[] {
|
|
40
|
+
): { required: string[]; optional: string[] } {
|
|
37
41
|
let ast;
|
|
38
42
|
try {
|
|
39
43
|
ast = toLiquidHtmlAST(source);
|
|
40
44
|
} catch {
|
|
41
|
-
return [];
|
|
45
|
+
return { required: [], optional: [] };
|
|
42
46
|
}
|
|
43
47
|
|
|
44
48
|
const scopedVariables: Map<string, Scope[]> = new Map();
|
|
45
49
|
const fileScopedVariables: Set<string> = new Set(globalObjectNames);
|
|
46
50
|
const variables: LiquidVariableLookup[] = [];
|
|
51
|
+
const variablesWithDefault: Set<string> = new Set();
|
|
47
52
|
|
|
48
53
|
function indexVariableScope(variableName: string | null, scope: Scope) {
|
|
49
54
|
if (!variableName) return;
|
|
@@ -166,13 +171,25 @@ export function extractUndefinedVariables(
|
|
|
166
171
|
if (isHashAssignMarkup(parent) && parent.target === node) return;
|
|
167
172
|
|
|
168
173
|
variables.push(node);
|
|
174
|
+
|
|
175
|
+
// Detect `x | default: ...` — the variable is the expression of a LiquidVariable
|
|
176
|
+
// that has a `default` filter, meaning the partial handles the missing case itself.
|
|
177
|
+
if (
|
|
178
|
+
node.name &&
|
|
179
|
+
isLiquidVariable(parent) &&
|
|
180
|
+
parent.expression === node &&
|
|
181
|
+
parent.filters.some((f) => f.name === 'default')
|
|
182
|
+
) {
|
|
183
|
+
variablesWithDefault.add(node.name);
|
|
184
|
+
}
|
|
169
185
|
}
|
|
170
186
|
|
|
171
187
|
walk(ast, []);
|
|
172
188
|
|
|
173
189
|
// Determine undefined variables
|
|
174
190
|
const seen = new Set<string>();
|
|
175
|
-
const
|
|
191
|
+
const required: string[] = [];
|
|
192
|
+
const optional: string[] = [];
|
|
176
193
|
|
|
177
194
|
for (const variable of variables) {
|
|
178
195
|
if (!variable.name) continue;
|
|
@@ -187,11 +204,15 @@ export function extractUndefinedVariables(
|
|
|
187
204
|
|
|
188
205
|
if (!isVariableDefined) {
|
|
189
206
|
seen.add(variable.name);
|
|
190
|
-
|
|
207
|
+
if (variablesWithDefault.has(variable.name)) {
|
|
208
|
+
optional.push(variable.name);
|
|
209
|
+
} else {
|
|
210
|
+
required.push(variable.name);
|
|
211
|
+
}
|
|
191
212
|
}
|
|
192
213
|
}
|
|
193
214
|
|
|
194
|
-
return
|
|
215
|
+
return { required, optional };
|
|
195
216
|
}
|
|
196
217
|
|
|
197
218
|
function isNode(x: any): x is LiquidHtmlNode {
|
|
@@ -284,3 +305,7 @@ function isLiquidBranchCatch(
|
|
|
284
305
|
): node is LiquidHtmlNode & { type: typeof NodeTypes.LiquidBranch; name: 'catch'; markup: any } {
|
|
285
306
|
return node?.type === NodeTypes.LiquidBranch && (node as any).name === NamedTags.catch;
|
|
286
307
|
}
|
|
308
|
+
|
|
309
|
+
function isLiquidVariable(node?: LiquidHtmlNode): node is LiquidVariable {
|
|
310
|
+
return node?.type === NodeTypes.LiquidVariable;
|
|
311
|
+
}
|
|
@@ -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
|
});
|