@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.
Files changed (46) hide show
  1. package/CHANGELOG.md +20 -0
  2. package/dist/checks/index.js +4 -0
  3. package/dist/checks/index.js.map +1 -1
  4. package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.d.ts +11 -0
  5. package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js +34 -1
  6. package/dist/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.js.map +1 -1
  7. package/dist/checks/liquid-html-syntax-error/index.js +8 -0
  8. package/dist/checks/liquid-html-syntax-error/index.js.map +1 -1
  9. package/dist/checks/missing-content-for-layout/index.d.ts +2 -0
  10. package/dist/checks/missing-content-for-layout/index.js +89 -0
  11. package/dist/checks/missing-content-for-layout/index.js.map +1 -0
  12. package/dist/checks/reserved-variable-name/index.d.ts +2 -0
  13. package/dist/checks/reserved-variable-name/index.js +82 -0
  14. package/dist/checks/reserved-variable-name/index.js.map +1 -0
  15. package/dist/checks/unused-assign/index.js +21 -0
  16. package/dist/checks/unused-assign/index.js.map +1 -1
  17. package/dist/checks/utils.d.ts +7 -0
  18. package/dist/checks/utils.js +8 -0
  19. package/dist/checks/utils.js.map +1 -1
  20. package/dist/find-root.js +15 -2
  21. package/dist/find-root.js.map +1 -1
  22. package/dist/index.d.ts +1 -1
  23. package/dist/index.js +3 -1
  24. package/dist/index.js.map +1 -1
  25. package/dist/liquid-doc/utils.d.ts +1 -1
  26. package/dist/to-source-code.d.ts +1 -1
  27. package/dist/tsconfig.tsbuildinfo +1 -1
  28. package/dist/types.d.ts +10 -0
  29. package/dist/types.js.map +1 -1
  30. package/package.json +2 -2
  31. package/src/checks/index.ts +4 -0
  32. package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.spec.ts +49 -0
  33. package/src/checks/liquid-html-syntax-error/checks/InvalidAssignSyntax.ts +36 -2
  34. package/src/checks/liquid-html-syntax-error/checks/InvalidTagSyntax.spec.ts +23 -0
  35. package/src/checks/liquid-html-syntax-error/index.ts +12 -1
  36. package/src/checks/missing-content-for-layout/index.spec.ts +84 -0
  37. package/src/checks/missing-content-for-layout/index.ts +94 -0
  38. package/src/checks/missing-page/index.spec.ts +94 -0
  39. package/src/checks/reserved-variable-name/index.spec.ts +142 -0
  40. package/src/checks/reserved-variable-name/index.ts +85 -0
  41. package/src/checks/unused-assign/index.spec.ts +61 -0
  42. package/src/checks/unused-assign/index.ts +19 -1
  43. package/src/checks/utils.ts +11 -0
  44. package/src/find-root.ts +17 -2
  45. package/src/index.ts +2 -0
  46. 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`,
@@ -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 app/ (app/modules/ is a valid subdirectory)
12
- and(fileExists(path.join(dir, 'modules')), Promise.resolve(path.basename(dir) !== 'app')),
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