@platformos/platformos-check-common 0.0.12 → 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.
Files changed (100) hide show
  1. package/CHANGELOG.md +59 -0
  2. package/dist/checks/circular-render/index.d.ts +2 -0
  3. package/dist/checks/circular-render/index.js +164 -0
  4. package/dist/checks/circular-render/index.js.map +1 -0
  5. package/dist/checks/index.d.ts +1 -1
  6. package/dist/checks/index.js +6 -0
  7. package/dist/checks/index.js.map +1 -1
  8. package/dist/checks/metadata-params/extract-undefined-variables.d.ts +8 -0
  9. package/dist/checks/metadata-params/extract-undefined-variables.js +213 -0
  10. package/dist/checks/metadata-params/extract-undefined-variables.js.map +1 -0
  11. package/dist/checks/metadata-params/index.js +48 -33
  12. package/dist/checks/metadata-params/index.js.map +1 -1
  13. package/dist/checks/missing-page/index.d.ts +2 -0
  14. package/dist/checks/missing-page/index.js +73 -0
  15. package/dist/checks/missing-page/index.js.map +1 -0
  16. package/dist/checks/missing-partial/index.js +31 -31
  17. package/dist/checks/missing-partial/index.js.map +1 -1
  18. package/dist/checks/missing-render-partial-arguments/index.d.ts +2 -0
  19. package/dist/checks/missing-render-partial-arguments/index.js +37 -0
  20. package/dist/checks/missing-render-partial-arguments/index.js.map +1 -0
  21. package/dist/checks/nested-graphql-query/index.d.ts +2 -0
  22. package/dist/checks/nested-graphql-query/index.js +146 -0
  23. package/dist/checks/nested-graphql-query/index.js.map +1 -0
  24. package/dist/checks/translation-key-exists/index.js +16 -19
  25. package/dist/checks/translation-key-exists/index.js.map +1 -1
  26. package/dist/checks/translation-utils.d.ts +16 -0
  27. package/dist/checks/translation-utils.js +51 -0
  28. package/dist/checks/translation-utils.js.map +1 -0
  29. package/dist/checks/undefined-object/index.js +32 -0
  30. package/dist/checks/undefined-object/index.js.map +1 -1
  31. package/dist/checks/unknown-property/index.js +64 -2
  32. package/dist/checks/unknown-property/index.js.map +1 -1
  33. package/dist/checks/unused-translation-key/index.d.ts +4 -0
  34. package/dist/checks/unused-translation-key/index.js +85 -0
  35. package/dist/checks/unused-translation-key/index.js.map +1 -0
  36. package/dist/checks/valid-render-partial-argument-types/index.js +2 -1
  37. package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
  38. package/dist/context-utils.d.ts +2 -1
  39. package/dist/context-utils.js +31 -1
  40. package/dist/context-utils.js.map +1 -1
  41. package/dist/index.d.ts +1 -0
  42. package/dist/index.js +2 -0
  43. package/dist/index.js.map +1 -1
  44. package/dist/liquid-doc/arguments.js +4 -0
  45. package/dist/liquid-doc/arguments.js.map +1 -1
  46. package/dist/liquid-doc/utils.d.ts +10 -2
  47. package/dist/liquid-doc/utils.js +26 -1
  48. package/dist/liquid-doc/utils.js.map +1 -1
  49. package/dist/to-source-code.d.ts +1 -1
  50. package/dist/tsconfig.tsbuildinfo +1 -1
  51. package/dist/types.d.ts +8 -1
  52. package/dist/types.js.map +1 -1
  53. package/dist/url-helpers.d.ts +55 -0
  54. package/dist/url-helpers.js +334 -0
  55. package/dist/url-helpers.js.map +1 -0
  56. package/dist/utils/index.d.ts +1 -0
  57. package/dist/utils/index.js +1 -0
  58. package/dist/utils/index.js.map +1 -1
  59. package/dist/utils/levenshtein.d.ts +3 -0
  60. package/dist/utils/levenshtein.js +39 -0
  61. package/dist/utils/levenshtein.js.map +1 -0
  62. package/package.json +2 -2
  63. package/src/checks/graphql/index.spec.ts +2 -2
  64. package/src/checks/index.ts +6 -0
  65. package/src/checks/metadata-params/extract-undefined-variables.spec.ts +115 -0
  66. package/src/checks/metadata-params/extract-undefined-variables.ts +286 -0
  67. package/src/checks/metadata-params/index.spec.ts +180 -26
  68. package/src/checks/metadata-params/index.ts +51 -34
  69. package/src/checks/missing-page/index.spec.ts +755 -0
  70. package/src/checks/missing-page/index.ts +89 -0
  71. package/src/checks/missing-partial/index.spec.ts +361 -0
  72. package/src/checks/missing-partial/index.ts +39 -47
  73. package/src/checks/missing-render-partial-arguments/index.spec.ts +74 -0
  74. package/src/checks/missing-render-partial-arguments/index.ts +44 -0
  75. package/src/checks/nested-graphql-query/index.spec.ts +175 -0
  76. package/src/checks/nested-graphql-query/index.ts +203 -0
  77. package/src/checks/parser-blocking-script/index.spec.ts +7 -3
  78. package/src/checks/translation-key-exists/index.spec.ts +79 -2
  79. package/src/checks/translation-key-exists/index.ts +18 -27
  80. package/src/checks/translation-utils.ts +63 -0
  81. package/src/checks/undefined-object/index.spec.ts +194 -35
  82. package/src/checks/undefined-object/index.ts +40 -1
  83. package/src/checks/unknown-property/index.spec.ts +62 -0
  84. package/src/checks/unknown-property/index.ts +73 -2
  85. package/src/checks/unused-assign/index.spec.ts +1 -1
  86. package/src/checks/unused-doc-param/index.spec.ts +4 -2
  87. package/src/checks/valid-doc-param-types/index.spec.ts +1 -1
  88. package/src/checks/valid-render-partial-argument-types/index.spec.ts +24 -1
  89. package/src/checks/valid-render-partial-argument-types/index.ts +3 -2
  90. package/src/checks/variable-name/index.spec.ts +1 -1
  91. package/src/context-utils.ts +33 -1
  92. package/src/disabled-checks/index.spec.ts +4 -4
  93. package/src/index.ts +3 -0
  94. package/src/liquid-doc/arguments.ts +6 -0
  95. package/src/liquid-doc/utils.ts +26 -2
  96. package/src/types.ts +9 -1
  97. package/src/url-helpers.spec.ts +386 -0
  98. package/src/url-helpers.ts +363 -0
  99. package/src/utils/index.ts +1 -0
  100. package/src/utils/levenshtein.ts +41 -0
@@ -0,0 +1,115 @@
1
+ import { describe, it, expect } from 'vitest';
2
+ import { extractUndefinedVariables } from './extract-undefined-variables';
3
+
4
+ describe('extractUndefinedVariables', () => {
5
+ it('should return variables used but not defined', () => {
6
+ const source = `{% liquid
7
+ assign b = a
8
+ %}
9
+ {{ b }}`;
10
+ const result = extractUndefinedVariables(source);
11
+ expect(result).to.deep.equal(['a']);
12
+ });
13
+
14
+ it('should not include assigned variables', () => {
15
+ const source = `{% assign x = 1 %}{{ x }}`;
16
+ const result = extractUndefinedVariables(source);
17
+ expect(result).to.deep.equal([]);
18
+ });
19
+
20
+ it('should not include captured variables', () => {
21
+ const source = `{% capture x %}hello{% endcapture %}{{ x }}`;
22
+ const result = extractUndefinedVariables(source);
23
+ expect(result).to.deep.equal([]);
24
+ });
25
+
26
+ it('should not include for loop variables', () => {
27
+ const source = `{% for item in items %}{{ item }}{% endfor %}`;
28
+ const result = extractUndefinedVariables(source);
29
+ expect(result).to.deep.equal(['items']);
30
+ });
31
+
32
+ it('should not include forloop variable', () => {
33
+ const source = `{% for item in items %}{{ forloop.index }}{% endfor %}`;
34
+ const result = extractUndefinedVariables(source);
35
+ expect(result).to.deep.equal(['items']);
36
+ });
37
+
38
+ it('should handle function result variables', () => {
39
+ const source = `{% function res = 'my_partial' %}{{ res }}`;
40
+ const result = extractUndefinedVariables(source);
41
+ expect(result).to.deep.equal([]);
42
+ });
43
+
44
+ it('should handle graphql result variables', () => {
45
+ const source = `{% graphql res = 'my_query' %}{{ res }}`;
46
+ const result = extractUndefinedVariables(source);
47
+ expect(result).to.deep.equal([]);
48
+ });
49
+
50
+ it('should handle inline graphql result variables', () => {
51
+ const source = `{% graphql res %}{ users { id } }{% endgraphql %}{{ res }}`;
52
+ const result = extractUndefinedVariables(source);
53
+ expect(result).to.deep.equal([]);
54
+ });
55
+
56
+ it('should handle parse_json result variables', () => {
57
+ const source = `{% parse_json data %}{"a":1}{% endparse_json %}{{ data }}`;
58
+ const result = extractUndefinedVariables(source);
59
+ expect(result).to.deep.equal([]);
60
+ });
61
+
62
+ it('should not include global objects', () => {
63
+ const source = `{{ context.session }}`;
64
+ const result = extractUndefinedVariables(source, [
65
+ 'context',
66
+ 'null',
67
+ 'true',
68
+ 'false',
69
+ 'blank',
70
+ 'empty',
71
+ ]);
72
+ expect(result).to.deep.equal([]);
73
+ });
74
+
75
+ it('should deduplicate results', () => {
76
+ const source = `{{ a }}{{ a }}`;
77
+ const result = extractUndefinedVariables(source);
78
+ expect(result).to.deep.equal(['a']);
79
+ });
80
+
81
+ it('should return empty array if source fails to parse', () => {
82
+ const source = `{% invalid unclosed`;
83
+ const result = extractUndefinedVariables(source);
84
+ expect(result).to.deep.equal([]);
85
+ });
86
+
87
+ it('should handle increment/decrement as definitions', () => {
88
+ const source = `{% increment counter %}{{ counter }}`;
89
+ const result = extractUndefinedVariables(source);
90
+ expect(result).to.deep.equal([]);
91
+ });
92
+
93
+ it('should handle background file-based result variables', () => {
94
+ const source = `{% background my_job = 'some_partial' %}{{ my_job }}`;
95
+ const result = extractUndefinedVariables(source);
96
+ expect(result).to.deep.equal([]);
97
+ });
98
+
99
+ it('should handle inline background tag without job_id', () => {
100
+ const source = `{% background source_name: 'my_task' %}echo "hello"{% endbackground %}{{ my_job }}`;
101
+ const result = extractUndefinedVariables(source);
102
+ expect(result).to.deep.equal(['my_job']);
103
+ });
104
+
105
+ it('should not include doc param names', () => {
106
+ const source = `
107
+ {% doc %}
108
+ @param {String} name - a name
109
+ {% enddoc %}
110
+ {{ name }}
111
+ `;
112
+ const result = extractUndefinedVariables(source);
113
+ expect(result).to.deep.equal(['name']);
114
+ });
115
+ });
@@ -0,0 +1,286 @@
1
+ import {
2
+ LiquidHtmlNode,
3
+ LiquidTag,
4
+ LiquidTagAssign,
5
+ LiquidTagCapture,
6
+ LiquidTagDecrement,
7
+ LiquidTagFor,
8
+ LiquidTagIncrement,
9
+ LiquidTagTablerow,
10
+ LiquidVariableLookup,
11
+ NamedTags,
12
+ NodeTypes,
13
+ Position,
14
+ FunctionMarkup,
15
+ LiquidTagHashAssign,
16
+ HashAssignMarkup,
17
+ LiquidTagGraphQL,
18
+ LiquidTagParseJson,
19
+ LiquidTagBackground,
20
+ BackgroundMarkup,
21
+ toLiquidHtmlAST,
22
+ } from '@platformos/liquid-html-parser';
23
+
24
+ type Scope = { start?: number; end?: number };
25
+
26
+ /**
27
+ * 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
+ *
30
+ * This mirrors the variable tracking logic from the UndefinedObject check but
31
+ * packaged as a standalone synchronous function.
32
+ */
33
+ export function extractUndefinedVariables(
34
+ source: string,
35
+ globalObjectNames: string[] = [],
36
+ ): string[] {
37
+ let ast;
38
+ try {
39
+ ast = toLiquidHtmlAST(source);
40
+ } catch {
41
+ return [];
42
+ }
43
+
44
+ const scopedVariables: Map<string, Scope[]> = new Map();
45
+ const fileScopedVariables: Set<string> = new Set(globalObjectNames);
46
+ const variables: LiquidVariableLookup[] = [];
47
+
48
+ function indexVariableScope(variableName: string | null, scope: Scope) {
49
+ if (!variableName) return;
50
+ const indexedScope = scopedVariables.get(variableName) ?? [];
51
+ scopedVariables.set(variableName, indexedScope.concat(scope));
52
+ }
53
+
54
+ function walk(node: LiquidHtmlNode, ancestors: LiquidHtmlNode[]) {
55
+ // Process definitions from LiquidTag nodes
56
+ if (node.type === NodeTypes.LiquidTag) {
57
+ handleLiquidTag(node, ancestors);
58
+ }
59
+
60
+ // Process definitions from LiquidBranch nodes (catch)
61
+ if (node.type === NodeTypes.LiquidBranch) {
62
+ handleLiquidBranch(node);
63
+ }
64
+
65
+ // Process variable usages
66
+ if (node.type === NodeTypes.VariableLookup) {
67
+ handleVariableLookup(node, ancestors);
68
+ }
69
+
70
+ // Recurse into children
71
+ const newAncestors = ancestors.concat(node);
72
+ for (const value of Object.values(node)) {
73
+ if (Array.isArray(value)) {
74
+ for (const item of value) {
75
+ if (isNode(item)) {
76
+ walk(item, newAncestors);
77
+ }
78
+ }
79
+ } else if (isNode(value)) {
80
+ walk(value, newAncestors);
81
+ }
82
+ }
83
+ }
84
+
85
+ function handleLiquidTag(node: LiquidTag, _ancestors: LiquidHtmlNode[]) {
86
+ if (isLiquidTagAssign(node) || isLiquidTagGraphQL(node) || isLiquidTagParseJson(node)) {
87
+ indexVariableScope(node.markup.name, {
88
+ start: node.blockStartPosition.end,
89
+ });
90
+ }
91
+
92
+ if (isLiquidTagHashAssign(node) && node.markup.target.name) {
93
+ indexVariableScope(node.markup.target.name, {
94
+ start: node.blockStartPosition.end,
95
+ });
96
+ }
97
+
98
+ if (isLiquidTagCapture(node)) {
99
+ indexVariableScope(node.markup.name, {
100
+ start: node.blockEndPosition?.end,
101
+ });
102
+ }
103
+
104
+ if (node.name === 'form') {
105
+ indexVariableScope(node.name, {
106
+ start: node.blockStartPosition.end,
107
+ end: node.blockEndPosition?.start,
108
+ });
109
+ }
110
+
111
+ if (node.name === 'function') {
112
+ const fnName = (node.markup as FunctionMarkup).name;
113
+ if (fnName.lookups.length === 0 && fnName.name !== null) {
114
+ indexVariableScope(fnName.name, {
115
+ start: node.position.end,
116
+ });
117
+ }
118
+ }
119
+
120
+ if ((isLiquidTagIncrement(node) || isLiquidTagDecrement(node)) && node.markup.name !== null) {
121
+ indexVariableScope(node.markup.name, {
122
+ start: node.position.start,
123
+ });
124
+ }
125
+
126
+ if (isLiquidForTag(node) || isLiquidTableRowTag(node)) {
127
+ indexVariableScope(node.markup.variableName, {
128
+ start: node.blockStartPosition.end,
129
+ end: node.blockEndPosition?.start,
130
+ });
131
+ indexVariableScope(node.name === 'for' ? 'forloop' : 'tablerowloop', {
132
+ start: node.blockStartPosition.end,
133
+ end: node.blockEndPosition?.start,
134
+ });
135
+ }
136
+
137
+ if (isLiquidTagBackground(node)) {
138
+ indexVariableScope(node.markup.jobId, {
139
+ start: node.position.end,
140
+ });
141
+ }
142
+ }
143
+
144
+ function handleLiquidBranch(node: LiquidHtmlNode & { type: typeof NodeTypes.LiquidBranch }) {
145
+ if (
146
+ node.name === NamedTags.catch &&
147
+ node.markup &&
148
+ typeof node.markup !== 'string' &&
149
+ 'name' in node.markup &&
150
+ (node.markup as any).name
151
+ ) {
152
+ indexVariableScope((node.markup as any).name, {
153
+ start: (node as any).blockStartPosition.end,
154
+ end: (node as any).blockEndPosition?.start,
155
+ });
156
+ }
157
+ }
158
+
159
+ function handleVariableLookup(node: LiquidVariableLookup, ancestors: LiquidHtmlNode[]) {
160
+ const parent = ancestors[ancestors.length - 1];
161
+
162
+ if (isLiquidTag(parent) && isLiquidTagCapture(parent)) return;
163
+ if (isLiquidTag(parent) && isLiquidTagParseJson(parent)) return;
164
+ if (isFunctionMarkup(parent) && parent.name === node) return;
165
+ if (isLiquidBranchCatch(parent) && parent.markup === node) return;
166
+ if (isHashAssignMarkup(parent) && parent.target === node) return;
167
+
168
+ variables.push(node);
169
+ }
170
+
171
+ walk(ast, []);
172
+
173
+ // Determine undefined variables
174
+ const seen = new Set<string>();
175
+ const result: string[] = [];
176
+
177
+ for (const variable of variables) {
178
+ if (!variable.name) continue;
179
+ if (seen.has(variable.name)) continue;
180
+
181
+ const isVariableDefined = isDefined(
182
+ variable.name,
183
+ variable.position,
184
+ fileScopedVariables,
185
+ scopedVariables,
186
+ );
187
+
188
+ if (!isVariableDefined) {
189
+ seen.add(variable.name);
190
+ result.push(variable.name);
191
+ }
192
+ }
193
+
194
+ return result;
195
+ }
196
+
197
+ function isNode(x: any): x is LiquidHtmlNode {
198
+ return x !== null && typeof x === 'object' && typeof x.type === 'string';
199
+ }
200
+
201
+ function isDefined(
202
+ variableName: string,
203
+ variablePosition: Position,
204
+ fileScopedVariables: Set<string>,
205
+ scopedVariables: Map<string, Scope[]>,
206
+ ): boolean {
207
+ if (fileScopedVariables.has(variableName)) {
208
+ return true;
209
+ }
210
+
211
+ const scopes = scopedVariables.get(variableName);
212
+ if (!scopes) {
213
+ return false;
214
+ }
215
+
216
+ return scopes.some((scope) => {
217
+ const start = variablePosition.start;
218
+ const isVariableAfterScopeStart = !scope.start || start > scope.start;
219
+ const isVariableBeforeScopeEnd = !scope.end || start < scope.end;
220
+ return isVariableAfterScopeStart && isVariableBeforeScopeEnd;
221
+ });
222
+ }
223
+
224
+ function isLiquidTag(node?: LiquidHtmlNode): node is LiquidTag {
225
+ return node?.type === NodeTypes.LiquidTag;
226
+ }
227
+
228
+ function isLiquidTagCapture(node: LiquidTag): node is LiquidTagCapture {
229
+ return node.name === NamedTags.capture;
230
+ }
231
+
232
+ function isLiquidTagAssign(node: LiquidTag): node is LiquidTagAssign {
233
+ return node.name === NamedTags.assign && typeof node.markup !== 'string';
234
+ }
235
+
236
+ function isLiquidTagHashAssign(node: LiquidTag): node is LiquidTagHashAssign {
237
+ return node.name === NamedTags.hash_assign && typeof node.markup !== 'string';
238
+ }
239
+
240
+ function isLiquidTagGraphQL(node: LiquidTag): node is LiquidTagGraphQL {
241
+ return node.name === NamedTags.graphql && typeof node.markup !== 'string';
242
+ }
243
+
244
+ function isLiquidTagParseJson(node: LiquidTag): node is LiquidTagParseJson {
245
+ return node.name === NamedTags.parse_json && typeof node.markup !== 'string';
246
+ }
247
+
248
+ function isLiquidForTag(node: LiquidTag): node is LiquidTagFor {
249
+ return node.name === NamedTags.for && typeof node.markup !== 'string';
250
+ }
251
+
252
+ function isLiquidTableRowTag(node: LiquidTag): node is LiquidTagTablerow {
253
+ return node.name === NamedTags.tablerow && typeof node.markup !== 'string';
254
+ }
255
+
256
+ function isLiquidTagIncrement(node: LiquidTag): node is LiquidTagIncrement {
257
+ return node.name === NamedTags.increment && typeof node.markup !== 'string';
258
+ }
259
+
260
+ function isLiquidTagDecrement(node: LiquidTag): node is LiquidTagDecrement {
261
+ return node.name === NamedTags.decrement && typeof node.markup !== 'string';
262
+ }
263
+
264
+ function isLiquidTagBackground(
265
+ node: LiquidTag,
266
+ ): node is LiquidTagBackground & { markup: BackgroundMarkup } {
267
+ return (
268
+ node.name === NamedTags.background &&
269
+ typeof node.markup !== 'string' &&
270
+ node.markup.type === NodeTypes.BackgroundMarkup
271
+ );
272
+ }
273
+
274
+ function isHashAssignMarkup(node?: LiquidHtmlNode): node is HashAssignMarkup {
275
+ return node?.type === NodeTypes.HashAssignMarkup;
276
+ }
277
+
278
+ function isFunctionMarkup(node?: LiquidHtmlNode): node is FunctionMarkup {
279
+ return node?.type === NodeTypes.FunctionMarkup;
280
+ }
281
+
282
+ function isLiquidBranchCatch(
283
+ node?: LiquidHtmlNode,
284
+ ): node is LiquidHtmlNode & { type: typeof NodeTypes.LiquidBranch; name: 'catch'; markup: any } {
285
+ return node?.type === NodeTypes.LiquidBranch && (node as any).name === NamedTags.catch;
286
+ }
@@ -3,14 +3,13 @@ import { MetadataParamsCheck } from '.';
3
3
  import { check } from '../../test';
4
4
 
5
5
  describe('Module: MetadataParamsCheck', () => {
6
- it('should report the missing variable when not defined but passed', async () => {
6
+ it('should use doc tag as complete param list when present', async () => {
7
7
  const file = `
8
- ---
9
- metadata:
10
- params:
11
- variable: {}
12
- variable3: {}
13
- ---
8
+ {% doc %}
9
+ @param {Number} variable - param with description
10
+ @param {Number} variable2 - param with description
11
+ {% enddoc %}
12
+
14
13
  {% assign a = 5 | plus: variable | plus: variable2 %}
15
14
  {{ a }}
16
15
  `;
@@ -25,23 +24,21 @@ describe('Module: MetadataParamsCheck', () => {
25
24
 
26
25
  const offenses = await check(files, [MetadataParamsCheck]);
27
26
 
28
- expect(offenses).to.have.length(2);
29
- expect(offenses).to.containOffense('Unknown parameter variable2 passed to function call');
30
- expect(offenses).to.containOffense(
31
- 'Required parameter variable3 must be passed to function call',
32
- );
27
+ expect(offenses).to.have.length(0);
33
28
  });
34
29
 
35
- it('should ignore if metadata not defined', async () => {
30
+ it('should report missing required doc params', async () => {
36
31
  const file = `
37
- ---
38
- metadata:
39
- ---
32
+ {% doc %}
33
+ @param {Number} variable - param with description
34
+ @param {Number} variable2 - param with description
35
+ {% enddoc %}
36
+
40
37
  {% assign a = 5 | plus: variable | plus: variable2 %}
41
38
  {{ a }}
42
39
  `;
43
40
  const file2 = `
44
- {% function a = 'commands/call/fileToCall', variable: 2, variable2: 12 %}
41
+ {% function a = 'commands/call/fileToCall', variable: 2 %}
45
42
  {{ a }}
46
43
  `;
47
44
  const files = {
@@ -51,21 +48,23 @@ describe('Module: MetadataParamsCheck', () => {
51
48
 
52
49
  const offenses = await check(files, [MetadataParamsCheck]);
53
50
 
54
- expect(offenses).to.have.length(0);
51
+ expect(offenses).to.have.length(1);
52
+ expect(offenses).to.containOffense(
53
+ 'Required parameter variable2 must be passed to function call',
54
+ );
55
55
  });
56
56
 
57
- it('should accept doc tag if metadata not defined', async () => {
57
+ it('should report unknown params not in doc', async () => {
58
58
  const file = `
59
59
  {% doc %}
60
60
  @param {Number} variable - param with description
61
- @param {Number} variable2 - param with description
62
61
  {% enddoc %}
63
62
 
64
- {% assign a = 5 | plus: variable | plus: variable2 %}
63
+ {% assign a = 5 | plus: variable %}
65
64
  {{ a }}
66
65
  `;
67
66
  const file2 = `
68
- {% function a = 'commands/call/fileToCall', variable: 2, variable2: 12 %}
67
+ {% function a = 'commands/call/fileToCall', variable: 2, extra: 12 %}
69
68
  {{ a }}
70
69
  `;
71
70
  const files = {
@@ -75,22 +74,144 @@ describe('Module: MetadataParamsCheck', () => {
75
74
 
76
75
  const offenses = await check(files, [MetadataParamsCheck]);
77
76
 
77
+ expect(offenses).to.have.length(1);
78
+ expect(offenses).to.containOffense('Unknown parameter extra passed to function call');
79
+ });
80
+
81
+ it('should allow doc-optional params without requiring them', async () => {
82
+ const file = `
83
+ {% doc %}
84
+ @param {String} a - required
85
+ @param {String} [b] - optional
86
+ {% enddoc %}
87
+ {{ a }}{{ b }}
88
+ `;
89
+ const file2 = `
90
+ {% function res = 'commands/call/fileToCall', a: 'hello' %}
91
+ {{ res }}
92
+ `;
93
+ const files = {
94
+ 'app/lib/commands/call/fileToCall.liquid': file,
95
+ 'app/lib/caller.liquid': file2,
96
+ };
97
+
98
+ const offenses = await check(files, [MetadataParamsCheck]);
99
+
78
100
  expect(offenses).to.have.length(0);
79
101
  });
80
102
 
81
- it('should reject if doc tag is missing params', async () => {
103
+ it('should allow passing doc-optional params without reporting unknown', async () => {
82
104
  const file = `
83
105
  {% doc %}
84
- @param {Number} variable - param with description
106
+ @param {String} a - required
107
+ @param {String} [b] - optional
85
108
  {% enddoc %}
109
+ {{ a }}{{ b }}
110
+ `;
111
+ const file2 = `
112
+ {% function res = 'commands/call/fileToCall', a: 'hello', b: 'world' %}
113
+ {{ res }}
114
+ `;
115
+ const files = {
116
+ 'app/lib/commands/call/fileToCall.liquid': file,
117
+ 'app/lib/caller.liquid': file2,
118
+ };
86
119
 
87
- {% assign a = 5 | plus: variable | plus: variable2 %}
120
+ const offenses = await check(files, [MetadataParamsCheck]);
121
+
122
+ expect(offenses).to.have.length(0);
123
+ });
124
+
125
+ it('should not require doc params that are not used in source', async () => {
126
+ const file = `
127
+ {% doc %}
128
+ @param {String} a - required param
129
+ @param {String} unused - required but not used in source
130
+ {% enddoc %}
88
131
  {{ a }}
89
132
  `;
90
133
  const file2 = `
91
- {% function a = 'commands/call/fileToCall', variable: 2, variable2: 12 %}
134
+ {% function res = 'commands/call/fileToCall', a: 'hello' %}
135
+ {{ res }}
136
+ `;
137
+ const files = {
138
+ 'app/lib/commands/call/fileToCall.liquid': file,
139
+ 'app/lib/caller.liquid': file2,
140
+ };
141
+
142
+ const offenses = await check(files, [MetadataParamsCheck]);
143
+
144
+ expect(offenses).to.have.length(0);
145
+ });
146
+
147
+ it('should infer required params from undefined variables when no doc', async () => {
148
+ const file = `
149
+ {% assign b = a %}
150
+ {{ b }}
151
+ `;
152
+ const file2 = `
153
+ {% function res = 'commands/call/fileToCall', a: 'hello' %}
154
+ {{ res }}
155
+ `;
156
+ const files = {
157
+ 'app/lib/commands/call/fileToCall.liquid': file,
158
+ 'app/lib/caller.liquid': file2,
159
+ };
160
+
161
+ const offenses = await check(files, [MetadataParamsCheck]);
162
+
163
+ expect(offenses).to.have.length(0);
164
+ });
165
+
166
+ it('should report missing inferred params when no doc', async () => {
167
+ const file = `
168
+ {% assign b = a %}
169
+ {{ b }}
170
+ `;
171
+ const file2 = `
172
+ {% function res = 'commands/call/fileToCall' %}
173
+ {{ res }}
174
+ `;
175
+ const files = {
176
+ 'app/lib/commands/call/fileToCall.liquid': file,
177
+ 'app/lib/caller.liquid': file2,
178
+ };
179
+
180
+ const offenses = await check(files, [MetadataParamsCheck]);
181
+
182
+ expect(offenses).to.have.length(1);
183
+ expect(offenses).to.containOffense('Required parameter a must be passed to function call');
184
+ });
185
+
186
+ it('should report unknown params when passing args not in inferred set', async () => {
187
+ const file = `
188
+ {% assign b = a %}
189
+ {{ b }}
190
+ `;
191
+ const file2 = `
192
+ {% function res = 'commands/call/fileToCall', a: 'hello', extra: 'world' %}
193
+ {{ res }}
194
+ `;
195
+ const files = {
196
+ 'app/lib/commands/call/fileToCall.liquid': file,
197
+ 'app/lib/caller.liquid': file2,
198
+ };
199
+
200
+ const offenses = await check(files, [MetadataParamsCheck]);
201
+
202
+ expect(offenses).to.have.length(1);
203
+ expect(offenses).to.containOffense('Unknown parameter extra passed to function call');
204
+ });
205
+
206
+ it('should not include global objects like context in inferred params', async () => {
207
+ const file = `
208
+ {{ context.session }}
92
209
  {{ a }}
93
210
  `;
211
+ const file2 = `
212
+ {% function res = 'commands/call/fileToCall', a: 'hello' %}
213
+ {{ res }}
214
+ `;
94
215
  const files = {
95
216
  'app/lib/commands/call/fileToCall.liquid': file,
96
217
  'app/lib/caller.liquid': file2,
@@ -98,6 +219,39 @@ describe('Module: MetadataParamsCheck', () => {
98
219
 
99
220
  const offenses = await check(files, [MetadataParamsCheck]);
100
221
 
222
+ expect(offenses).to.have.length(0);
223
+ });
224
+
225
+ it('should work with render tags too', async () => {
226
+ const file = `{{ a }}`;
227
+ const file2 = `{% render 'fileToRender' %}`;
228
+ const files = {
229
+ 'app/views/partials/fileToRender.liquid': file,
230
+ 'app/views/pages/caller.liquid': file2,
231
+ };
232
+
233
+ const offenses = await check(files, [MetadataParamsCheck]);
234
+
101
235
  expect(offenses).to.have.length(1);
236
+ expect(offenses).to.containOffense('Required parameter a must be passed to render call');
237
+ });
238
+
239
+ it('should skip validation when no doc and no undefined vars', async () => {
240
+ const file = `
241
+ {% assign a = 5 %}
242
+ {{ a }}
243
+ `;
244
+ const file2 = `
245
+ {% function res = 'commands/call/fileToCall', extra: 'hello' %}
246
+ {{ res }}
247
+ `;
248
+ const files = {
249
+ 'app/lib/commands/call/fileToCall.liquid': file,
250
+ 'app/lib/caller.liquid': file2,
251
+ };
252
+
253
+ const offenses = await check(files, [MetadataParamsCheck]);
254
+
255
+ expect(offenses).to.have.length(0);
102
256
  });
103
257
  });