@platformos/platformos-check-common 0.0.18 → 0.0.20
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 +20 -0
- package/dist/checks/index.js +4 -0
- package/dist/checks/index.js.map +1 -1
- package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.d.ts +11 -0
- package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js +34 -1
- package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js.map +1 -1
- package/dist/checks/liquid-html-syntax-error/index.js +8 -0
- package/dist/checks/liquid-html-syntax-error/index.js.map +1 -1
- package/dist/checks/missing-content-for-layout/index.d.ts +2 -0
- package/dist/checks/missing-content-for-layout/index.js +89 -0
- package/dist/checks/missing-content-for-layout/index.js.map +1 -0
- package/dist/checks/reserved-variable-name/index.d.ts +2 -0
- package/dist/checks/reserved-variable-name/index.js +82 -0
- package/dist/checks/reserved-variable-name/index.js.map +1 -0
- package/dist/checks/unused-assign/index.js +21 -0
- package/dist/checks/unused-assign/index.js.map +1 -1
- package/dist/checks/utils.d.ts +7 -0
- package/dist/checks/utils.js +8 -0
- package/dist/checks/utils.js.map +1 -1
- package/dist/find-root.js +15 -2
- package/dist/find-root.js.map +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +3 -1
- package/dist/index.js.map +1 -1
- package/dist/liquid-doc/utils.d.ts +1 -1
- package/dist/to-source-code.d.ts +1 -1
- package/dist/tsconfig.tsbuildinfo +1 -1
- package/dist/types.d.ts +10 -0
- package/dist/types.js.map +1 -1
- package/package.json +2 -2
- package/src/checks/index.ts +4 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.spec.ts +49 -0
- package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.ts +36 -2
- package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.spec.ts +23 -0
- package/src/checks/liquid-html-syntax-error/index.ts +12 -1
- package/src/checks/missing-content-for-layout/index.spec.ts +84 -0
- package/src/checks/missing-content-for-layout/index.ts +94 -0
- package/src/checks/missing-page/index.spec.ts +94 -0
- package/src/checks/reserved-variable-name/index.spec.ts +142 -0
- package/src/checks/reserved-variable-name/index.ts +85 -0
- package/src/checks/unused-assign/index.spec.ts +61 -0
- package/src/checks/unused-assign/index.ts +19 -1
- package/src/checks/utils.ts +11 -0
- package/src/find-root.ts +17 -2
- package/src/index.ts +2 -0
- package/src/types.ts +19 -0
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { LiquidVariableLookup, NamedTags, NodeTypes } from '@platformos/liquid-html-parser';
|
|
2
|
+
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
3
|
+
import { RESERVED_VARIABLE_NAMES } from '../utils';
|
|
4
|
+
|
|
5
|
+
export const ReservedVariableName: LiquidCheckDefinition = {
|
|
6
|
+
meta: {
|
|
7
|
+
code: 'ReservedVariableName',
|
|
8
|
+
name: 'Reserved variable name',
|
|
9
|
+
docs: {
|
|
10
|
+
description:
|
|
11
|
+
"Disallows using reserved Liquid literals ('true', 'false', 'nil', 'null', 'empty', 'blank') as variable names. Reading such a name always returns the built-in literal, never the assigned value.",
|
|
12
|
+
recommended: true,
|
|
13
|
+
url: undefined,
|
|
14
|
+
},
|
|
15
|
+
type: SourceCodeType.LiquidHtml,
|
|
16
|
+
severity: Severity.ERROR,
|
|
17
|
+
schema: {},
|
|
18
|
+
targets: [],
|
|
19
|
+
},
|
|
20
|
+
|
|
21
|
+
create(context) {
|
|
22
|
+
const report = (name: string, startIndex: number, endIndex: number) => {
|
|
23
|
+
context.report({
|
|
24
|
+
message: `'${name}' is a reserved Liquid literal and cannot be used as a variable name — reading '${name}' always returns the literal, never the assigned value`,
|
|
25
|
+
startIndex,
|
|
26
|
+
endIndex,
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
// Targets whose name is a plain string always sit at the start of their markup node
|
|
31
|
+
const checkStringTarget = (name: string, markup: { position: { start: number } }) => {
|
|
32
|
+
if (RESERVED_VARIABLE_NAMES.has(name)) {
|
|
33
|
+
report(name, markup.position.start, markup.position.start + name.length);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const checkLookupTarget = (lookup: LiquidVariableLookup | null) => {
|
|
38
|
+
if (lookup?.name && RESERVED_VARIABLE_NAMES.has(lookup.name)) {
|
|
39
|
+
report(lookup.name, lookup.position.start, lookup.position.end);
|
|
40
|
+
}
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
return {
|
|
44
|
+
async LiquidTag(node) {
|
|
45
|
+
if (typeof node.markup === 'string') return;
|
|
46
|
+
|
|
47
|
+
switch (node.name) {
|
|
48
|
+
case NamedTags.assign:
|
|
49
|
+
checkStringTarget(node.markup.name, node.markup);
|
|
50
|
+
break;
|
|
51
|
+
case NamedTags.capture:
|
|
52
|
+
case NamedTags.parse_json:
|
|
53
|
+
case NamedTags.increment:
|
|
54
|
+
case NamedTags.decrement:
|
|
55
|
+
checkLookupTarget(node.markup);
|
|
56
|
+
break;
|
|
57
|
+
case NamedTags.function:
|
|
58
|
+
checkLookupTarget(node.markup.name);
|
|
59
|
+
break;
|
|
60
|
+
case NamedTags.graphql:
|
|
61
|
+
checkStringTarget(node.markup.name, node.markup);
|
|
62
|
+
break;
|
|
63
|
+
case NamedTags.hash_assign:
|
|
64
|
+
checkLookupTarget(node.markup.target);
|
|
65
|
+
break;
|
|
66
|
+
case NamedTags.for:
|
|
67
|
+
case NamedTags.tablerow:
|
|
68
|
+
checkStringTarget(node.markup.variableName, node.markup);
|
|
69
|
+
break;
|
|
70
|
+
case NamedTags.background:
|
|
71
|
+
if (node.markup.type === NodeTypes.BackgroundMarkup) {
|
|
72
|
+
checkStringTarget(node.markup.jobId, node.markup);
|
|
73
|
+
}
|
|
74
|
+
break;
|
|
75
|
+
}
|
|
76
|
+
},
|
|
77
|
+
|
|
78
|
+
async LiquidBranch(node) {
|
|
79
|
+
if (node.name === NamedTags.catch && typeof node.markup !== 'string') {
|
|
80
|
+
checkLookupTarget(node.markup);
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
};
|
|
84
|
+
},
|
|
85
|
+
};
|
|
@@ -201,4 +201,65 @@ describe('Module: UnusedAssign', () => {
|
|
|
201
201
|
expect(offenses).to.have.lengthOf(1);
|
|
202
202
|
expect(offenses[0].message).to.equal("The variable 'arr' is assigned but not used");
|
|
203
203
|
});
|
|
204
|
+
|
|
205
|
+
it('should not report variables used inside a filter array-literal argument', async () => {
|
|
206
|
+
const sourceCode = `
|
|
207
|
+
{% assign k = "key" %}
|
|
208
|
+
{% assign v = "value" %}
|
|
209
|
+
{% assign name = arr | default: [ "hi", k, v] %}
|
|
210
|
+
{{ name }}
|
|
211
|
+
`;
|
|
212
|
+
|
|
213
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
214
|
+
expect(offenses).to.have.lengthOf(0);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it('should not report variables used inside a filter hash-literal argument', async () => {
|
|
218
|
+
const sourceCode = `
|
|
219
|
+
{% assign k = 1 %}
|
|
220
|
+
{% assign name = arr | default: { a: k } %}
|
|
221
|
+
{{ name }}
|
|
222
|
+
`;
|
|
223
|
+
|
|
224
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
225
|
+
expect(offenses).to.have.lengthOf(0);
|
|
226
|
+
});
|
|
227
|
+
|
|
228
|
+
it('should not report variables used inside a named-argument value', async () => {
|
|
229
|
+
const sourceCode = `
|
|
230
|
+
{% assign k = 0 %}
|
|
231
|
+
{% assign name = arr | slice: start: k %}
|
|
232
|
+
{{ name }}
|
|
233
|
+
`;
|
|
234
|
+
|
|
235
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
236
|
+
expect(offenses).to.have.lengthOf(0);
|
|
237
|
+
});
|
|
238
|
+
|
|
239
|
+
it('should not report assignments to reserved literal names (handled by ReservedVariableName)', async () => {
|
|
240
|
+
const sourceCode = `
|
|
241
|
+
{% liquid
|
|
242
|
+
assign empty = '{}' | parse_json
|
|
243
|
+
function invalid = 'modules/blog/commands/blog_instances/build', object: empty
|
|
244
|
+
echo invalid
|
|
245
|
+
%}
|
|
246
|
+
`;
|
|
247
|
+
|
|
248
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
249
|
+
expect(offenses).toEqual([]);
|
|
250
|
+
});
|
|
251
|
+
|
|
252
|
+
it('should not report variables referenced in a string-markup fallback assign (parse failure)', async () => {
|
|
253
|
+
// The stray `}` before `%}` causes the tolerant parser to fall back to
|
|
254
|
+
// string markup. The AST contains no VariableLookup nodes for k/v, so
|
|
255
|
+
// without a textual fallback scan the check would wrongly flag them.
|
|
256
|
+
const sourceCode = `
|
|
257
|
+
{% assign k = "key" %}
|
|
258
|
+
{% assign v = "value" %}
|
|
259
|
+
{% assign name = arr | default: [ "hi", k, v] } %}
|
|
260
|
+
`;
|
|
261
|
+
|
|
262
|
+
const offenses = await runLiquidCheck(UnusedAssign, sourceCode);
|
|
263
|
+
expect(offenses).to.have.lengthOf(0);
|
|
264
|
+
});
|
|
204
265
|
});
|
|
@@ -6,7 +6,7 @@ import {
|
|
|
6
6
|
NodeTypes,
|
|
7
7
|
} from '@platformos/liquid-html-parser';
|
|
8
8
|
import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
|
|
9
|
-
import { isWithinRawTagThatDoesNotParseItsContents } from '../utils';
|
|
9
|
+
import { isWithinRawTagThatDoesNotParseItsContents, RESERVED_VARIABLE_NAMES } from '../utils';
|
|
10
10
|
|
|
11
11
|
export const UnusedAssign: LiquidCheckDefinition = {
|
|
12
12
|
meta: {
|
|
@@ -31,6 +31,12 @@ export const UnusedAssign: LiquidCheckDefinition = {
|
|
|
31
31
|
// effects on the original, so they count as "using" the variable.
|
|
32
32
|
const referenceAssignedVariables: Set<string> = new Set();
|
|
33
33
|
const usedVariables: Set<string> = new Set();
|
|
34
|
+
// Concatenated markup of LiquidTags whose parsing fell back to a raw string
|
|
35
|
+
// (e.g. a typo like `{% assign name = x | default: [...] } %}`). The AST has
|
|
36
|
+
// no VariableLookup nodes to traverse inside them, so onCodePathEnd scans
|
|
37
|
+
// this text for assigned-variable names — avoids compounding the user's
|
|
38
|
+
// syntax error with false "unused" warnings.
|
|
39
|
+
let fallbackText = '';
|
|
34
40
|
|
|
35
41
|
function checkVariableUsage(node: any) {
|
|
36
42
|
if (node.type === NodeTypes.VariableLookup) {
|
|
@@ -66,6 +72,8 @@ export const UnusedAssign: LiquidCheckDefinition = {
|
|
|
66
72
|
}
|
|
67
73
|
} else if (isLiquidTagCapture(node) && node.markup.name) {
|
|
68
74
|
assignedVariables.set(node.markup.name, node);
|
|
75
|
+
} else if (typeof node.markup === 'string' && node.markup) {
|
|
76
|
+
fallbackText += '\n' + node.markup;
|
|
69
77
|
}
|
|
70
78
|
},
|
|
71
79
|
|
|
@@ -79,7 +87,17 @@ export const UnusedAssign: LiquidCheckDefinition = {
|
|
|
79
87
|
},
|
|
80
88
|
|
|
81
89
|
async onCodePathEnd() {
|
|
90
|
+
if (fallbackText) {
|
|
91
|
+
for (const name of assignedVariables.keys()) {
|
|
92
|
+
if (usedVariables.has(name)) continue;
|
|
93
|
+
if (new RegExp(`\\b${name}\\b`).test(fallbackText)) usedVariables.add(name);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
82
97
|
for (const [variable, node] of assignedVariables.entries()) {
|
|
98
|
+
// Reserved literal names are reported by ReservedVariableName instead —
|
|
99
|
+
// "unused" would be misleading when the source visibly references the name.
|
|
100
|
+
if (RESERVED_VARIABLE_NAMES.has(variable)) continue;
|
|
83
101
|
if (!usedVariables.has(variable) && !variable.startsWith('_')) {
|
|
84
102
|
context.report({
|
|
85
103
|
message: `The variable '${variable}' is assigned but not used`,
|
package/src/checks/utils.ts
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
AttrUnquoted,
|
|
10
10
|
LiquidHtmlNode,
|
|
11
11
|
LiquidBranch,
|
|
12
|
+
LiquidLiteralValues,
|
|
12
13
|
LiquidTagFor,
|
|
13
14
|
LiquidTagTablerow,
|
|
14
15
|
LiquidTag,
|
|
@@ -16,6 +17,16 @@ import {
|
|
|
16
17
|
} from '@platformos/liquid-html-parser';
|
|
17
18
|
import { LiquidHtmlNodeOfType as NodeOfType } from '../types';
|
|
18
19
|
|
|
20
|
+
/**
|
|
21
|
+
* Names that Liquid resolves as built-in literals before looking up variables
|
|
22
|
+
* (Liquid::Expression::LITERALS in the backend, `LiquidLiteralValues` in the
|
|
23
|
+
* parser grammar). Assigning to them succeeds silently, but reading them
|
|
24
|
+
* always returns the literal, never the assigned value.
|
|
25
|
+
*/
|
|
26
|
+
export const RESERVED_VARIABLE_NAMES: ReadonlySet<string> = new Set(
|
|
27
|
+
Object.keys(LiquidLiteralValues),
|
|
28
|
+
);
|
|
29
|
+
|
|
19
30
|
type ElementType<T> = T extends (infer E)[] ? E : never;
|
|
20
31
|
|
|
21
32
|
export type ValuedHtmlAttribute = AttrSingleQuoted | AttrDoubleQuoted | AttrUnquoted;
|
package/src/find-root.ts
CHANGED
|
@@ -3,13 +3,28 @@ import * as path from './path';
|
|
|
3
3
|
|
|
4
4
|
type FileExists = (uri: string) => Promise<boolean>;
|
|
5
5
|
|
|
6
|
+
function isInsideApp(dir: UriString): boolean {
|
|
7
|
+
let current = dir;
|
|
8
|
+
while (true) {
|
|
9
|
+
const parent = path.dirname(current);
|
|
10
|
+
if (parent === current) return false;
|
|
11
|
+
if (path.basename(parent) === 'app') return true;
|
|
12
|
+
current = parent;
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
|
|
6
16
|
async function isRoot(dir: UriString, fileExists: FileExists) {
|
|
7
17
|
return or(
|
|
8
18
|
fileExists(path.join(dir, '.pos')),
|
|
9
19
|
fileExists(path.join(dir, '.platformos-check.yml')),
|
|
10
20
|
fileExists(path.join(dir, 'app')),
|
|
11
|
-
// modules/ is a root indicator only when not inside
|
|
12
|
-
|
|
21
|
+
// modules/ is a root indicator only when not inside an app/ subtree.
|
|
22
|
+
// app/modules/ is a valid subdirectory, and app/views/partials/modules/ is a
|
|
23
|
+
// legitimate partial organization that should not be mistaken for a project root.
|
|
24
|
+
and(
|
|
25
|
+
fileExists(path.join(dir, 'modules')),
|
|
26
|
+
Promise.resolve(path.basename(dir) !== 'app' && !isInsideApp(dir)),
|
|
27
|
+
),
|
|
13
28
|
);
|
|
14
29
|
}
|
|
15
30
|
|
package/src/index.ts
CHANGED
|
@@ -60,11 +60,13 @@ export {
|
|
|
60
60
|
isFormConfiguration,
|
|
61
61
|
isKnownGraphQLFile,
|
|
62
62
|
isKnownLiquidFile,
|
|
63
|
+
isKnownYAMLFile,
|
|
63
64
|
isLayout,
|
|
64
65
|
isMigration,
|
|
65
66
|
isPage,
|
|
66
67
|
isPartial,
|
|
67
68
|
isSms,
|
|
69
|
+
isSupportedSourceFile,
|
|
68
70
|
PlatformOSFileType,
|
|
69
71
|
} from '@platformos/platformos-common';
|
|
70
72
|
export * from './frontmatter';
|
package/src/types.ts
CHANGED
|
@@ -315,6 +315,22 @@ export type Translations = {
|
|
|
315
315
|
* target: 'file:///app/views/partials/child.liquid'
|
|
316
316
|
* }
|
|
317
317
|
*/
|
|
318
|
+
/**
|
|
319
|
+
* The semantic Liquid construct that created a {@link Reference} edge.
|
|
320
|
+
*
|
|
321
|
+
* Optional/additive: older producers may omit it. Lets consumers distinguish a
|
|
322
|
+
* `{% render %}` edge from an `{% include %}` / `{% function %}` / `{% graphql %}`
|
|
323
|
+
* / `{% background %}` / asset / layout-association edge without re-parsing.
|
|
324
|
+
*/
|
|
325
|
+
export type ReferenceKind =
|
|
326
|
+
| 'render'
|
|
327
|
+
| 'include'
|
|
328
|
+
| 'function'
|
|
329
|
+
| 'background'
|
|
330
|
+
| 'graphql'
|
|
331
|
+
| 'asset'
|
|
332
|
+
| 'layout';
|
|
333
|
+
|
|
318
334
|
export type Reference = {
|
|
319
335
|
source: Location;
|
|
320
336
|
target: Location;
|
|
@@ -322,6 +338,9 @@ export type Reference = {
|
|
|
322
338
|
type:
|
|
323
339
|
| 'direct' // explicit dependency, e.g. {% render 'partial' %}
|
|
324
340
|
| 'indirect'; // indirect dependency
|
|
341
|
+
|
|
342
|
+
/** Which Liquid construct produced this edge. Optional for backwards compatibility. */
|
|
343
|
+
kind?: ReferenceKind;
|
|
325
344
|
};
|
|
326
345
|
|
|
327
346
|
export type Range = [start: number, end: number]; // represents a range in the source code
|