@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
|
@@ -12,6 +12,8 @@ import {
|
|
|
12
12
|
GraphQLMarkup,
|
|
13
13
|
GraphQLInlineMarkup,
|
|
14
14
|
HashAssignMarkup,
|
|
15
|
+
JsonHashLiteral,
|
|
16
|
+
JsonArrayLiteral,
|
|
15
17
|
} from '@platformos/liquid-html-parser';
|
|
16
18
|
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
17
19
|
import { isError } from '../../utils';
|
|
@@ -85,8 +87,10 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
85
87
|
if (isLiquidTagAssign(node)) {
|
|
86
88
|
const markup = node.markup;
|
|
87
89
|
|
|
88
|
-
// Close any previous shape for this variable (reassignment)
|
|
89
|
-
|
|
90
|
+
// Close any previous shape for this variable (reassignment).
|
|
91
|
+
// Use the value expression's end so the RHS can still see the old shape
|
|
92
|
+
// (e.g. {% assign c = c.d %} — c.d must resolve against the previous shape of c).
|
|
93
|
+
closeShapeRange(markup.name, markup.value.position.end);
|
|
90
94
|
|
|
91
95
|
const hasParseJsonFilter =
|
|
92
96
|
markup.value.filters &&
|
|
@@ -127,6 +131,19 @@ export const UnknownProperty: LiquidCheckDefinition = {
|
|
|
127
131
|
}
|
|
128
132
|
}
|
|
129
133
|
|
|
134
|
+
// {% assign x = {a: 5, b: {c: 1}} %} or {% assign x = [1, 2, 3] %}
|
|
135
|
+
const exprType = markup.value.expression.type;
|
|
136
|
+
if (exprType === NodeTypes.JsonHashLiteral || exprType === NodeTypes.JsonArrayLiteral) {
|
|
137
|
+
const shape = inferShapeFromLiteralNode(markup.value.expression);
|
|
138
|
+
if (shape) {
|
|
139
|
+
variableShapes.push({
|
|
140
|
+
name: markup.name,
|
|
141
|
+
shape,
|
|
142
|
+
range: [node.position.end],
|
|
143
|
+
});
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
130
147
|
// {% assign x = y | dig: "key1" | dig: "key2" %}
|
|
131
148
|
// Follow the dig path through the source variable's known shape.
|
|
132
149
|
const digFilters =
|
|
@@ -540,3 +557,57 @@ function isGraphQLInlineMarkup(
|
|
|
540
557
|
function isLiquidString(expr: LiquidString | LiquidVariableLookup): expr is LiquidString {
|
|
541
558
|
return expr.type === NodeTypes.String;
|
|
542
559
|
}
|
|
560
|
+
|
|
561
|
+
/**
|
|
562
|
+
* Infer a PropertyShape from a JsonHashLiteral or JsonArrayLiteral AST node.
|
|
563
|
+
*/
|
|
564
|
+
function inferShapeFromLiteralNode(
|
|
565
|
+
node: JsonHashLiteral | JsonArrayLiteral,
|
|
566
|
+
): PropertyShape | undefined {
|
|
567
|
+
if (node.type === NodeTypes.JsonArrayLiteral) {
|
|
568
|
+
let itemShape: PropertyShape | undefined;
|
|
569
|
+
for (const element of node.elements) {
|
|
570
|
+
const elShape = inferShapeFromExpressionNode(element);
|
|
571
|
+
if (elShape) {
|
|
572
|
+
itemShape = itemShape ? itemShape : elShape;
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
return { kind: 'array', itemShape };
|
|
576
|
+
}
|
|
577
|
+
|
|
578
|
+
if (node.type === NodeTypes.JsonHashLiteral) {
|
|
579
|
+
const properties = new Map<string, PropertyShape>();
|
|
580
|
+
for (const entry of node.entries) {
|
|
581
|
+
// Keys are VariableLookup nodes where the name is the key string
|
|
582
|
+
if (entry.key.type === NodeTypes.VariableLookup && entry.key.name) {
|
|
583
|
+
const valueShape = inferShapeFromExpressionNode(entry.value);
|
|
584
|
+
properties.set(entry.key.name, valueShape ?? { kind: 'primitive' });
|
|
585
|
+
}
|
|
586
|
+
}
|
|
587
|
+
return { kind: 'object', properties };
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
return undefined;
|
|
591
|
+
}
|
|
592
|
+
|
|
593
|
+
/**
|
|
594
|
+
* Infer a PropertyShape from a Liquid expression or variable node.
|
|
595
|
+
*/
|
|
596
|
+
function inferShapeFromExpressionNode(node: LiquidExpression | LiquidVariable): PropertyShape {
|
|
597
|
+
if (node.type === NodeTypes.JsonHashLiteral) {
|
|
598
|
+
return inferShapeFromLiteralNode(node as JsonHashLiteral) ?? { kind: 'primitive' };
|
|
599
|
+
}
|
|
600
|
+
if (node.type === NodeTypes.JsonArrayLiteral) {
|
|
601
|
+
return inferShapeFromLiteralNode(node as JsonArrayLiteral) ?? { kind: 'primitive' };
|
|
602
|
+
}
|
|
603
|
+
if (node.type === NodeTypes.String) {
|
|
604
|
+
return { kind: 'primitive', primitiveType: 'string' };
|
|
605
|
+
}
|
|
606
|
+
if (node.type === NodeTypes.Number) {
|
|
607
|
+
return { kind: 'primitive', primitiveType: 'number' };
|
|
608
|
+
}
|
|
609
|
+
if (node.type === NodeTypes.LiquidLiteral) {
|
|
610
|
+
return { kind: 'primitive' };
|
|
611
|
+
}
|
|
612
|
+
return { kind: 'primitive' };
|
|
613
|
+
}
|
|
@@ -56,7 +56,7 @@ describe('Module: UnusedAssign', () => {
|
|
|
56
56
|
{{ usedVar }}
|
|
57
57
|
`;
|
|
58
58
|
|
|
59
|
-
expect(suggestions).to.
|
|
59
|
+
expect(suggestions).to.deep.equal([expectedFixedCode]);
|
|
60
60
|
});
|
|
61
61
|
|
|
62
62
|
it('should not report unused assigns for things used in a capture tag', async () => {
|
|
@@ -51,14 +51,16 @@ describe('Module: UnusedDocParam', () => {
|
|
|
51
51
|
const offenses = await runLiquidCheck(UnusedDocParam, sourceCode);
|
|
52
52
|
const suggestions = applySuggestions(sourceCode, offenses[0]);
|
|
53
53
|
|
|
54
|
-
expect(suggestions).to.
|
|
54
|
+
expect(suggestions).to.deep.equal([
|
|
55
|
+
`
|
|
55
56
|
{% doc %}
|
|
56
57
|
@param param1 - Example param
|
|
57
58
|
|
|
58
59
|
{% enddoc %}
|
|
59
60
|
|
|
60
61
|
{{ param1 }}
|
|
61
|
-
|
|
62
|
+
`,
|
|
63
|
+
]);
|
|
62
64
|
});
|
|
63
65
|
|
|
64
66
|
LoopNamedTags.forEach((tag) => {
|
|
@@ -69,7 +69,7 @@ describe('Module: ValidDocParamTypes', () => {
|
|
|
69
69
|
expect(offenses).to.have.length(1);
|
|
70
70
|
const suggestions = applySuggestions(source, offenses[0]);
|
|
71
71
|
|
|
72
|
-
expect(suggestions).to.
|
|
72
|
+
expect(suggestions).to.deep.equal([`{% doc %} @param param1 - Example param {% enddoc %}`]);
|
|
73
73
|
}
|
|
74
74
|
});
|
|
75
75
|
});
|
|
@@ -34,7 +34,7 @@ describe('Module: ValidRenderPartialParamTypes', () => {
|
|
|
34
34
|
{ value: "'hello'", actualType: BasicParamTypes.String },
|
|
35
35
|
{ value: '123', actualType: BasicParamTypes.Number },
|
|
36
36
|
{ value: 'true', actualType: BasicParamTypes.Boolean },
|
|
37
|
-
{ value: 'empty', actualType: BasicParamTypes.
|
|
37
|
+
{ value: 'empty', actualType: BasicParamTypes.String },
|
|
38
38
|
],
|
|
39
39
|
},
|
|
40
40
|
];
|
|
@@ -142,6 +142,29 @@ describe('Module: ValidRenderPartialParamTypes', () => {
|
|
|
142
142
|
expect(offenses).toHaveLength(0);
|
|
143
143
|
});
|
|
144
144
|
|
|
145
|
+
it('should not report null/nil as type mismatch for any type', async () => {
|
|
146
|
+
for (const type of ['string', 'number', 'object', 'boolean']) {
|
|
147
|
+
for (const literal of ['nil', 'null']) {
|
|
148
|
+
const sourceCode = `{% render 'card', param: ${literal} %}`;
|
|
149
|
+
const offenses = await runLiquidCheck(
|
|
150
|
+
ValidRenderPartialArgumentTypes,
|
|
151
|
+
sourceCode,
|
|
152
|
+
undefined,
|
|
153
|
+
{},
|
|
154
|
+
{
|
|
155
|
+
'app/views/partials/card.liquid': `
|
|
156
|
+
{% doc %}
|
|
157
|
+
@param {${type}} param - Description
|
|
158
|
+
{% enddoc %}
|
|
159
|
+
<div>{{ param }}</div>
|
|
160
|
+
`,
|
|
161
|
+
},
|
|
162
|
+
);
|
|
163
|
+
expect(offenses, `${literal} should be valid for ${type}`).toHaveLength(0);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
});
|
|
167
|
+
|
|
145
168
|
it('should not enforce unsupported types', async () => {
|
|
146
169
|
const sourceCode = `{% render 'card', title: 123 %}`;
|
|
147
170
|
const offenses = await runLiquidCheck(
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
2
2
|
import { NodeTypes, RenderMarkup } from '@platformos/liquid-html-parser';
|
|
3
3
|
import { LiquidDocParameter } from '../../liquid-doc/liquidDoc';
|
|
4
|
-
import { inferArgumentType, isTypeCompatible } from '../../liquid-doc/utils';
|
|
4
|
+
import { inferArgumentType, isNullLiteral, isTypeCompatible } from '../../liquid-doc/utils';
|
|
5
5
|
import {
|
|
6
6
|
findTypeMismatchParams,
|
|
7
7
|
generateTypeMismatchSuggestions,
|
|
@@ -41,7 +41,8 @@ export const ValidRenderPartialArgumentTypes: LiquidCheckDefinition = {
|
|
|
41
41
|
if (
|
|
42
42
|
node.alias &&
|
|
43
43
|
node.variable?.name &&
|
|
44
|
-
node.variable.name.type !== NodeTypes.VariableLookup
|
|
44
|
+
node.variable.name.type !== NodeTypes.VariableLookup &&
|
|
45
|
+
!isNullLiteral(node.variable.name)
|
|
45
46
|
) {
|
|
46
47
|
const paramIsDefinedWithType = liquidDocParameters
|
|
47
48
|
.get(node.alias.value)
|
|
@@ -40,7 +40,7 @@ describe('Module: VariableName', () => {
|
|
|
40
40
|
|
|
41
41
|
const expectedFixedCode = `{% assign variable_name = "value" %}`;
|
|
42
42
|
|
|
43
|
-
expect(suggestions).to.
|
|
43
|
+
expect(suggestions).to.deep.equal([expectedFixedCode]);
|
|
44
44
|
});
|
|
45
45
|
|
|
46
46
|
it('should not report an error for variables starting with underscore', async () => {
|
package/src/context-utils.ts
CHANGED
|
@@ -3,6 +3,7 @@ import {
|
|
|
3
3
|
AbstractFileSystem,
|
|
4
4
|
FileTuple,
|
|
5
5
|
FileType,
|
|
6
|
+
RouteTable,
|
|
6
7
|
TranslationProvider,
|
|
7
8
|
UriString,
|
|
8
9
|
} from '@platformos/platformos-common';
|
|
@@ -131,6 +132,31 @@ function getDefaultTranslationsFromBuffer(app: App): Translations | undefined {
|
|
|
131
132
|
}
|
|
132
133
|
}
|
|
133
134
|
|
|
135
|
+
export function makeGetRouteTable(
|
|
136
|
+
fs: AbstractFileSystem,
|
|
137
|
+
rootUri: string,
|
|
138
|
+
existingTable?: RouteTable,
|
|
139
|
+
): () => Promise<RouteTable> {
|
|
140
|
+
const table = existingTable ?? new RouteTable(fs);
|
|
141
|
+
let buildPromise: Promise<RouteTable> | null = null;
|
|
142
|
+
return () => {
|
|
143
|
+
if (!buildPromise) {
|
|
144
|
+
if (table.isBuilt()) {
|
|
145
|
+
buildPromise = Promise.resolve(table);
|
|
146
|
+
} else {
|
|
147
|
+
buildPromise = table
|
|
148
|
+
.build(URI.parse(rootUri))
|
|
149
|
+
.then(() => table)
|
|
150
|
+
.catch((err) => {
|
|
151
|
+
buildPromise = null;
|
|
152
|
+
throw err;
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
return buildPromise;
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
134
160
|
function cached<T>(fn: () => Promise<T>): () => Promise<T>;
|
|
135
161
|
function cached<T>(fn: (...args: any[]) => Promise<T>): (...args: any[]) => Promise<T> {
|
|
136
162
|
let cachedPromise: Promise<T>;
|
|
@@ -145,7 +171,13 @@ export async function recursiveReadDirectory(
|
|
|
145
171
|
uri: string,
|
|
146
172
|
filter: (fileTuple: FileTuple) => boolean,
|
|
147
173
|
): Promise<UriString[]> {
|
|
148
|
-
|
|
174
|
+
let allFiles: FileTuple[];
|
|
175
|
+
try {
|
|
176
|
+
allFiles = await fs.readDirectory(uri);
|
|
177
|
+
} catch (err: any) {
|
|
178
|
+
if (err?.code === 'ENOENT') return [];
|
|
179
|
+
throw err;
|
|
180
|
+
}
|
|
149
181
|
const files = allFiles.filter((ft) => !isIgnored(ft) && (isDirectory(ft) || filter(ft)));
|
|
150
182
|
|
|
151
183
|
const results = await Promise.all(
|
|
@@ -194,7 +194,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
194
194
|
});
|
|
195
195
|
|
|
196
196
|
it("should disable the parent node's next node if platformos-check is disabled as the last child node", async () => {
|
|
197
|
-
const file = `{% liquid
|
|
197
|
+
const file = `{% doc %}{% enddoc %}{% liquid
|
|
198
198
|
if condition
|
|
199
199
|
# platformos-check-disable-next-line
|
|
200
200
|
elsif other_condition
|
|
@@ -211,7 +211,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
211
211
|
});
|
|
212
212
|
|
|
213
213
|
it('should not disable any checks if platformos-check is disabled at the end', async () => {
|
|
214
|
-
const file = `{% liquid
|
|
214
|
+
const file = `{% doc %}{% enddoc %}{% liquid
|
|
215
215
|
echo hello
|
|
216
216
|
echo everyone
|
|
217
217
|
# platformos-check-disable-next-line
|
|
@@ -232,7 +232,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
232
232
|
});
|
|
233
233
|
|
|
234
234
|
it('should disable the next line if the content is an HTML tag with liquid', async () => {
|
|
235
|
-
const file = `{% # platformos-check-disable-next-line %}
|
|
235
|
+
const file = `{% doc %}{% enddoc %}{% # platformos-check-disable-next-line %}
|
|
236
236
|
<div class="{{ foo }}"></div>
|
|
237
237
|
<div class="{{ bar }}"></div>`;
|
|
238
238
|
|
|
@@ -246,7 +246,7 @@ ${buildComment('platformos-check-enable')}
|
|
|
246
246
|
});
|
|
247
247
|
|
|
248
248
|
it('should not disable the next line if the specified rule does not exist', async () => {
|
|
249
|
-
const file = `{% # platformos-check-disable-next-line FAKE_RULE %}
|
|
249
|
+
const file = `{% doc %}{% enddoc %}{% # platformos-check-disable-next-line FAKE_RULE %}
|
|
250
250
|
<div class="{{ foo }}"></div>`;
|
|
251
251
|
|
|
252
252
|
const offenses = await check({ 'code.liquid': file }, [UndefinedObject]);
|
package/src/index.ts
CHANGED
|
@@ -5,6 +5,7 @@ import {
|
|
|
5
5
|
makeFileSize,
|
|
6
6
|
makeGetDefaultLocale,
|
|
7
7
|
makeGetDefaultTranslations,
|
|
8
|
+
makeGetRouteTable,
|
|
8
9
|
makeGetTranslationsForBase,
|
|
9
10
|
} from './context-utils';
|
|
10
11
|
import { createDisabledChecksModule } from './disabled-checks';
|
|
@@ -80,6 +81,7 @@ export * from './utils/object';
|
|
|
80
81
|
export * from './visitor';
|
|
81
82
|
export * from './liquid-doc/liquidDoc';
|
|
82
83
|
export * from './liquid-doc/utils';
|
|
84
|
+
export * from './url-helpers';
|
|
83
85
|
|
|
84
86
|
const defaultErrorHandler = (_error: Error): void => {
|
|
85
87
|
// Silently ignores errors by default.
|
|
@@ -101,6 +103,7 @@ export async function check(
|
|
|
101
103
|
getDefaultLocale: makeGetDefaultLocale(fs, rootUri),
|
|
102
104
|
getDefaultTranslations: makeGetDefaultTranslations(fs, app, rootUri),
|
|
103
105
|
getTranslationsForBase: makeGetTranslationsForBase(fs, app),
|
|
106
|
+
getRouteTable: makeGetRouteTable(fs, rootUri, injectedDependencies.routeTable),
|
|
104
107
|
};
|
|
105
108
|
|
|
106
109
|
const { DisabledChecksVisitor, isDisabled } = createDisabledChecksModule();
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
BasicParamTypes,
|
|
14
14
|
getDefaultValueForType,
|
|
15
15
|
inferArgumentType,
|
|
16
|
+
isNullLiteral,
|
|
16
17
|
isTypeCompatible,
|
|
17
18
|
} from './utils';
|
|
18
19
|
import { isLiquidString } from '../checks/utils';
|
|
@@ -133,6 +134,11 @@ export function findTypeMismatchParams(
|
|
|
133
134
|
continue;
|
|
134
135
|
}
|
|
135
136
|
|
|
137
|
+
// null/nil is compatible with any type — skip type checking
|
|
138
|
+
if (isNullLiteral(arg.value)) {
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
|
|
136
142
|
const liquidDocParamDef = liquidDocParameters.get(arg.name);
|
|
137
143
|
if (liquidDocParamDef && liquidDocParamDef.type) {
|
|
138
144
|
const paramType = liquidDocParamDef.type.toLowerCase();
|
package/src/liquid-doc/utils.ts
CHANGED
|
@@ -18,6 +18,11 @@ export enum BasicParamTypes {
|
|
|
18
18
|
Object = 'object',
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
/** Inferred type for null/nil literals — not a valid @param type, only used in type mismatch messages. */
|
|
22
|
+
export const InferredNull = 'null' as const;
|
|
23
|
+
|
|
24
|
+
export type InferredParamType = BasicParamTypes | typeof InferredNull;
|
|
25
|
+
|
|
21
26
|
export enum SupportedDocTagTypes {
|
|
22
27
|
Param = 'param',
|
|
23
28
|
Example = 'example',
|
|
@@ -44,7 +49,7 @@ export function getDefaultValueForType(type: string | null) {
|
|
|
44
49
|
/**
|
|
45
50
|
* Casts the value of a LiquidNamedArgument to a string representing the type of the value.
|
|
46
51
|
*/
|
|
47
|
-
export function inferArgumentType(arg: LiquidExpression | LiquidVariable):
|
|
52
|
+
export function inferArgumentType(arg: LiquidExpression | LiquidVariable): InferredParamType {
|
|
48
53
|
if (arg.type === NodeTypes.LiquidVariable) {
|
|
49
54
|
// A variable with filters — delegate to the base expression if there are no filters,
|
|
50
55
|
// otherwise we can't statically determine the filtered output type.
|
|
@@ -59,6 +64,8 @@ export function inferArgumentType(arg: LiquidExpression | LiquidVariable): Basic
|
|
|
59
64
|
case NodeTypes.Number:
|
|
60
65
|
return BasicParamTypes.Number;
|
|
61
66
|
case NodeTypes.LiquidLiteral:
|
|
67
|
+
if (arg.value === null) return InferredNull;
|
|
68
|
+
if (arg.value === '') return BasicParamTypes.String;
|
|
62
69
|
return BasicParamTypes.Boolean;
|
|
63
70
|
case NodeTypes.Range:
|
|
64
71
|
case NodeTypes.VariableLookup:
|
|
@@ -71,12 +78,29 @@ export function inferArgumentType(arg: LiquidExpression | LiquidVariable): Basic
|
|
|
71
78
|
}
|
|
72
79
|
}
|
|
73
80
|
|
|
81
|
+
/**
|
|
82
|
+
* Checks if a LiquidExpression is a null/nil literal.
|
|
83
|
+
* null/nil is compatible with any type — it represents "no value".
|
|
84
|
+
*/
|
|
85
|
+
export function isNullLiteral(arg: LiquidExpression | LiquidVariable): boolean {
|
|
86
|
+
if (arg.type === NodeTypes.LiquidVariable) {
|
|
87
|
+
if (arg.filters.length > 0) return false;
|
|
88
|
+
const expr = arg.expression;
|
|
89
|
+
if (expr.type === NodeTypes.BooleanExpression) return false;
|
|
90
|
+
return isNullLiteral(expr);
|
|
91
|
+
}
|
|
92
|
+
if (arg.type === NodeTypes.LiquidLiteral) {
|
|
93
|
+
return arg.value === null;
|
|
94
|
+
}
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
|
|
74
98
|
/**
|
|
75
99
|
* Checks if the provided argument type is compatible with the expected type.
|
|
76
100
|
* Makes certain types more permissive:
|
|
77
101
|
* - Boolean accepts any value, since everything is truthy / falsy in Liquid
|
|
78
102
|
*/
|
|
79
|
-
export function isTypeCompatible(expectedType: string, actualType:
|
|
103
|
+
export function isTypeCompatible(expectedType: string, actualType: InferredParamType): boolean {
|
|
80
104
|
const normalizedExpectedType = expectedType.toLowerCase();
|
|
81
105
|
|
|
82
106
|
if (normalizedExpectedType === BasicParamTypes.Boolean) {
|
package/src/types.ts
CHANGED
|
@@ -2,7 +2,7 @@ import { LiquidHtmlNode, NodeTypes as LiquidHtmlNodeTypes } from '@platformos/li
|
|
|
2
2
|
|
|
3
3
|
import { Schema, Settings } from './types/schema-prop-factory';
|
|
4
4
|
|
|
5
|
-
import { AbstractFileSystem, UriString } from '@platformos/platformos-common';
|
|
5
|
+
import { AbstractFileSystem, RouteTable, UriString } from '@platformos/platformos-common';
|
|
6
6
|
import { JSONCorrector, StringCorrector } from './fixes';
|
|
7
7
|
|
|
8
8
|
import {
|
|
@@ -355,6 +355,12 @@ export interface Dependencies {
|
|
|
355
355
|
* Returns an empty array if no files reference this file
|
|
356
356
|
*/
|
|
357
357
|
getReferences?: (uri: string) => Promise<Reference[]>;
|
|
358
|
+
|
|
359
|
+
/**
|
|
360
|
+
* Optional pre-built RouteTable. When provided (e.g. by the LSP),
|
|
361
|
+
* the check pipeline reuses it instead of building a new one per run.
|
|
362
|
+
*/
|
|
363
|
+
routeTable?: RouteTable;
|
|
358
364
|
}
|
|
359
365
|
|
|
360
366
|
export type ValidateJSON = (
|
|
@@ -377,6 +383,8 @@ export interface AugmentedDependencies extends Dependencies {
|
|
|
377
383
|
* Covers both `{base}/{locale}.yml` and `{base}/{locale}/*.yml`.
|
|
378
384
|
*/
|
|
379
385
|
getTranslationsForBase(translationBaseUri: string, locale: string): Promise<Translations>;
|
|
386
|
+
/** Lazily builds and returns a shared RouteTable for the current check run. */
|
|
387
|
+
getRouteTable(): Promise<RouteTable>;
|
|
380
388
|
}
|
|
381
389
|
|
|
382
390
|
type StaticContextProperties<T extends SourceCodeType> = T extends SourceCodeType
|