@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.
- package/CHANGELOG.md +59 -0
- package/dist/checks/circular-render/index.d.ts +2 -0
- package/dist/checks/circular-render/index.js +164 -0
- package/dist/checks/circular-render/index.js.map +1 -0
- package/dist/checks/index.d.ts +1 -1
- package/dist/checks/index.js +6 -0
- package/dist/checks/index.js.map +1 -1
- 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/missing-page/index.d.ts +2 -0
- package/dist/checks/missing-page/index.js +73 -0
- package/dist/checks/missing-page/index.js.map +1 -0
- package/dist/checks/missing-partial/index.js +31 -31
- package/dist/checks/missing-partial/index.js.map +1 -1
- package/dist/checks/missing-render-partial-arguments/index.d.ts +2 -0
- package/dist/checks/missing-render-partial-arguments/index.js +37 -0
- package/dist/checks/missing-render-partial-arguments/index.js.map +1 -0
- package/dist/checks/nested-graphql-query/index.d.ts +2 -0
- package/dist/checks/nested-graphql-query/index.js +146 -0
- package/dist/checks/nested-graphql-query/index.js.map +1 -0
- package/dist/checks/translation-key-exists/index.js +16 -19
- package/dist/checks/translation-key-exists/index.js.map +1 -1
- package/dist/checks/translation-utils.d.ts +16 -0
- package/dist/checks/translation-utils.js +51 -0
- package/dist/checks/translation-utils.js.map +1 -0
- package/dist/checks/undefined-object/index.js +32 -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/checks/unused-translation-key/index.d.ts +4 -0
- package/dist/checks/unused-translation-key/index.js +85 -0
- package/dist/checks/unused-translation-key/index.js.map +1 -0
- package/dist/checks/valid-render-partial-argument-types/index.js +2 -1
- package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
- package/dist/context-utils.d.ts +2 -1
- package/dist/context-utils.js +31 -1
- package/dist/context-utils.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.js +2 -0
- package/dist/index.js.map +1 -1
- package/dist/liquid-doc/arguments.js +4 -0
- package/dist/liquid-doc/arguments.js.map +1 -1
- package/dist/liquid-doc/utils.d.ts +10 -2
- package/dist/liquid-doc/utils.js +26 -1
- package/dist/liquid-doc/utils.js.map +1 -1
- package/dist/to-source-code.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +8 -1
- package/dist/types.js.map +1 -1
- package/dist/url-helpers.d.ts +55 -0
- package/dist/url-helpers.js +334 -0
- package/dist/url-helpers.js.map +1 -0
- package/dist/utils/index.d.ts +1 -0
- package/dist/utils/index.js +1 -0
- package/dist/utils/index.js.map +1 -1
- package/dist/utils/levenshtein.d.ts +3 -0
- package/dist/utils/levenshtein.js +39 -0
- package/dist/utils/levenshtein.js.map +1 -0
- package/package.json +2 -2
- package/src/checks/graphql/index.spec.ts +2 -2
- package/src/checks/index.ts +6 -0
- 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/missing-page/index.spec.ts +755 -0
- package/src/checks/missing-page/index.ts +89 -0
- package/src/checks/missing-partial/index.spec.ts +361 -0
- package/src/checks/missing-partial/index.ts +39 -47
- package/src/checks/missing-render-partial-arguments/index.spec.ts +74 -0
- package/src/checks/missing-render-partial-arguments/index.ts +44 -0
- package/src/checks/nested-graphql-query/index.spec.ts +175 -0
- package/src/checks/nested-graphql-query/index.ts +203 -0
- package/src/checks/parser-blocking-script/index.spec.ts +7 -3
- package/src/checks/translation-key-exists/index.spec.ts +79 -2
- package/src/checks/translation-key-exists/index.ts +18 -27
- package/src/checks/translation-utils.ts +63 -0
- package/src/checks/undefined-object/index.spec.ts +194 -35
- package/src/checks/undefined-object/index.ts +40 -1
- package/src/checks/unknown-property/index.spec.ts +62 -0
- package/src/checks/unknown-property/index.ts +73 -2
- package/src/checks/unused-assign/index.spec.ts +1 -1
- package/src/checks/unused-doc-param/index.spec.ts +4 -2
- package/src/checks/valid-doc-param-types/index.spec.ts +1 -1
- package/src/checks/valid-render-partial-argument-types/index.spec.ts +24 -1
- package/src/checks/valid-render-partial-argument-types/index.ts +3 -2
- package/src/checks/variable-name/index.spec.ts +1 -1
- package/src/context-utils.ts +33 -1
- package/src/disabled-checks/index.spec.ts +4 -4
- package/src/index.ts +3 -0
- package/src/liquid-doc/arguments.ts +6 -0
- package/src/liquid-doc/utils.ts +26 -2
- package/src/types.ts +9 -1
- package/src/url-helpers.spec.ts +386 -0
- package/src/url-helpers.ts +363 -0
- package/src/utils/index.ts +1 -0
- 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
|
|
6
|
+
it('should use doc tag as complete param list when present', async () => {
|
|
7
7
|
const file = `
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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(
|
|
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
|
|
30
|
+
it('should report missing required doc params', async () => {
|
|
36
31
|
const file = `
|
|
37
|
-
|
|
38
|
-
|
|
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
|
|
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(
|
|
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
|
|
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
|
|
63
|
+
{% assign a = 5 | plus: variable %}
|
|
65
64
|
{{ a }}
|
|
66
65
|
`;
|
|
67
66
|
const file2 = `
|
|
68
|
-
{% function a = 'commands/call/fileToCall', variable: 2,
|
|
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
|
|
103
|
+
it('should allow passing doc-optional params without reporting unknown', async () => {
|
|
82
104
|
const file = `
|
|
83
105
|
{% doc %}
|
|
84
|
-
@param {
|
|
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
|
-
|
|
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
|
|
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
|
});
|