@platformos/platformos-check-common 0.0.12 → 0.0.13

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 (85) hide show
  1. package/CHANGELOG.md +31 -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/missing-page/index.d.ts +2 -0
  9. package/dist/checks/missing-page/index.js +73 -0
  10. package/dist/checks/missing-page/index.js.map +1 -0
  11. package/dist/checks/missing-partial/index.js +31 -31
  12. package/dist/checks/missing-partial/index.js.map +1 -1
  13. package/dist/checks/missing-render-partial-arguments/index.d.ts +2 -0
  14. package/dist/checks/missing-render-partial-arguments/index.js +37 -0
  15. package/dist/checks/missing-render-partial-arguments/index.js.map +1 -0
  16. package/dist/checks/nested-graphql-query/index.d.ts +2 -0
  17. package/dist/checks/nested-graphql-query/index.js +146 -0
  18. package/dist/checks/nested-graphql-query/index.js.map +1 -0
  19. package/dist/checks/translation-key-exists/index.js +16 -19
  20. package/dist/checks/translation-key-exists/index.js.map +1 -1
  21. package/dist/checks/translation-utils.d.ts +20 -0
  22. package/dist/checks/translation-utils.js +51 -0
  23. package/dist/checks/translation-utils.js.map +1 -0
  24. package/dist/checks/undefined-object/index.js +21 -0
  25. package/dist/checks/undefined-object/index.js.map +1 -1
  26. package/dist/checks/unused-translation-key/index.d.ts +4 -0
  27. package/dist/checks/unused-translation-key/index.js +85 -0
  28. package/dist/checks/unused-translation-key/index.js.map +1 -0
  29. package/dist/checks/valid-render-partial-argument-types/index.js +2 -1
  30. package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
  31. package/dist/context-utils.d.ts +2 -1
  32. package/dist/context-utils.js +31 -1
  33. package/dist/context-utils.js.map +1 -1
  34. package/dist/index.d.ts +1 -0
  35. package/dist/index.js +2 -0
  36. package/dist/index.js.map +1 -1
  37. package/dist/liquid-doc/arguments.js +4 -0
  38. package/dist/liquid-doc/arguments.js.map +1 -1
  39. package/dist/liquid-doc/utils.d.ts +10 -2
  40. package/dist/liquid-doc/utils.js +26 -1
  41. package/dist/liquid-doc/utils.js.map +1 -1
  42. package/dist/tsconfig.tsbuildinfo +1 -1
  43. package/dist/types.d.ts +8 -1
  44. package/dist/types.js.map +1 -1
  45. package/dist/url-helpers.d.ts +55 -0
  46. package/dist/url-helpers.js +334 -0
  47. package/dist/url-helpers.js.map +1 -0
  48. package/dist/utils/index.d.ts +1 -0
  49. package/dist/utils/index.js +1 -0
  50. package/dist/utils/index.js.map +1 -1
  51. package/dist/utils/levenshtein.d.ts +3 -0
  52. package/dist/utils/levenshtein.js +39 -0
  53. package/dist/utils/levenshtein.js.map +1 -0
  54. package/package.json +2 -2
  55. package/src/checks/graphql/index.spec.ts +2 -2
  56. package/src/checks/index.ts +6 -0
  57. package/src/checks/missing-page/index.spec.ts +755 -0
  58. package/src/checks/missing-page/index.ts +89 -0
  59. package/src/checks/missing-partial/index.spec.ts +361 -0
  60. package/src/checks/missing-partial/index.ts +39 -47
  61. package/src/checks/missing-render-partial-arguments/index.spec.ts +74 -0
  62. package/src/checks/missing-render-partial-arguments/index.ts +44 -0
  63. package/src/checks/nested-graphql-query/index.spec.ts +175 -0
  64. package/src/checks/nested-graphql-query/index.ts +203 -0
  65. package/src/checks/parser-blocking-script/index.spec.ts +7 -3
  66. package/src/checks/translation-key-exists/index.spec.ts +79 -2
  67. package/src/checks/translation-key-exists/index.ts +18 -27
  68. package/src/checks/translation-utils.ts +63 -0
  69. package/src/checks/undefined-object/index.spec.ts +30 -0
  70. package/src/checks/undefined-object/index.ts +27 -1
  71. package/src/checks/unused-assign/index.spec.ts +1 -1
  72. package/src/checks/unused-doc-param/index.spec.ts +4 -2
  73. package/src/checks/valid-doc-param-types/index.spec.ts +1 -1
  74. package/src/checks/valid-render-partial-argument-types/index.spec.ts +24 -1
  75. package/src/checks/valid-render-partial-argument-types/index.ts +3 -2
  76. package/src/checks/variable-name/index.spec.ts +1 -1
  77. package/src/context-utils.ts +33 -1
  78. package/src/index.ts +3 -0
  79. package/src/liquid-doc/arguments.ts +6 -0
  80. package/src/liquid-doc/utils.ts +26 -2
  81. package/src/types.ts +9 -1
  82. package/src/url-helpers.spec.ts +241 -0
  83. package/src/url-helpers.ts +363 -0
  84. package/src/utils/index.ts +1 -0
  85. package/src/utils/levenshtein.ts +41 -0
package/CHANGELOG.md CHANGED
@@ -1,5 +1,36 @@
1
1
  # @platformos/theme-check-common
2
2
 
3
+ ## 0.0.13
4
+
5
+ ### Patch Changes
6
+
7
+ - **MissingRenderPartialArguments**: Reports an error when required `@param` arguments declared in a partial's LiquidDoc are not provided at the `{% render %}` call site.
8
+ - **NestedGraphQLQuery**: Detects N+1 query patterns — `{% graphql %}` tags inside `{% for %}`/`{% tablerow %}` loops. Also follows `{% function %}` and `{% render %}` calls transitively to detect indirect GraphQL queries. Skips loops wrapped in `{% cache %}` or `{% background %}`.
9
+ - Added **GraphQLFieldCompletionProvider**: Provides completions for GraphQL field names.
10
+ - Added **GraphQLFieldHoverProvider**: Shows hover documentation for GraphQL fields.
11
+ - Added `theme_render_rc` as a new document type, enabling the `{% theme_render_rc %}` tag to resolve partials through configurable `theme_search_paths` defined in `app/config.yml`.
12
+ - **DocumentsLocator**: New `locateWithSearchPaths()` method resolves partials using prioritized search paths, including dynamic paths with `{{ }}` Liquid expressions that expand by enumerating subdirectories.
13
+ - **loadSearchPaths()**: New utility to read and parse `theme_search_paths` from `app/config.yml`.
14
+ - **TranslationKeyExists**: Refactored to load all defined keys (app-level and module-level) in a single pass. Now suggests nearest matching keys using Levenshtein distance when a translation key is not found.
15
+ - Extracted shared translation utilities into `translation-utils.ts` for module discovery and key loading.
16
+ - Added `levenshtein.ts` utility for fuzzy key matching.
17
+ - Added support for `{% try %}...{% catch error %}` — the error variable in catch branches is now correctly registered as defined, preventing false-positive "undefined object" warnings.
18
+ - `null`/`nil` literals are now treated as compatible with any `@param` type, preventing false type-mismatch errors when passing null values to partials.
19
+ - `recursiveReadDirectory` now gracefully handles `ENOENT` errors instead of crashing when a directory doesn't exist.
20
+ - **MissingPartial** check updated to support `theme_render_rc` tag resolution through search paths.
21
+ - Extracted `tryExtractAssignUrl()` helper to deduplicate assign-to-URL resolution logic shared between `MissingPage` check and `buildVariableMap`.
22
+ - Fixed `buildVariableMap` to correctly recurse into block tags (`{% if %}`, `{% for %}`) whose position spans beyond the cursor offset — previously assigns inside such blocks could be missed.
23
+ - **SearchPathsLoader**: Now caches `theme_search_paths` per root URI to avoid re-reading `app/config.yml` on every request. Invalidated when file watchers detect config changes.
24
+ - Immediate cache invalidation on `app/config.yml` save (via `onDidSaveTextDocument`) so go-to-definition doesn't see stale data.
25
+ - Bulk file-watcher threshold extracted to `BULK_PAGE_CHANGE_THRESHOLD` constant.
26
+ - **RouteTable**: Added `routeCount()` method returning total number of route entries.
27
+ - Route table build errors are now properly handled — a failed build resets the cached promise so subsequent attempts can retry.
28
+ - `MissingPartial` check simplified with a shared `reportIfMissing()` helper, reducing code duplication across `RenderMarkup`, `FunctionMarkup`, and `GraphQLMarkup` visitors.
29
+ - AST traversal helpers (`getTraversableChildren`, `getTraversableMarkup`) extracted in `url-helpers.ts`.
30
+ - `MissingPage` check front-loads route table building in `onCodePathStart` instead of lazy-loading per element visit.
31
+ - Updated dependencies
32
+ - @platformos/liquid-html-parser@0.0.12
33
+
3
34
  ## 0.0.12
4
35
 
5
36
  ### Patch Changes
@@ -0,0 +1,2 @@
1
+ import { LiquidCheckDefinition } from '../../types';
2
+ export declare const CircularRender: LiquidCheckDefinition;
@@ -0,0 +1,164 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.CircularRender = void 0;
4
+ const liquid_html_parser_1 = require("@platformos/liquid-html-parser");
5
+ const types_1 = require("../../types");
6
+ const platformos_common_1 = require("@platformos/platformos-common");
7
+ const vscode_uri_1 = require("vscode-uri");
8
+ const MAX_DEPTH = 50;
9
+ // Module-level cache: URI -> partial references extracted from that file.
10
+ // Shared across all files in a check run to avoid re-parsing.
11
+ const parseCacheMap = new WeakMap();
12
+ function getParseCache(context) {
13
+ if (!parseCacheMap.has(context.fs)) {
14
+ parseCacheMap.set(context.fs, new Map());
15
+ }
16
+ return parseCacheMap.get(context.fs);
17
+ }
18
+ /**
19
+ * Extract partial references (render/function/include) from a Liquid source string.
20
+ * Returns an empty array on parse errors.
21
+ */
22
+ function extractPartialRefs(source) {
23
+ let ast;
24
+ try {
25
+ ast = (0, liquid_html_parser_1.toLiquidHtmlAST)(source);
26
+ }
27
+ catch {
28
+ return [];
29
+ }
30
+ const refs = [];
31
+ const stack = [ast];
32
+ while (stack.length > 0) {
33
+ const node = stack.pop();
34
+ if (!node || typeof node !== 'object')
35
+ continue;
36
+ if (node.type === liquid_html_parser_1.NodeTypes.LiquidTag &&
37
+ node.markup &&
38
+ typeof node.markup === 'object' &&
39
+ (node.markup.type === liquid_html_parser_1.NodeTypes.RenderMarkup ||
40
+ node.markup.type === liquid_html_parser_1.NodeTypes.FunctionMarkup) &&
41
+ node.markup.partial &&
42
+ node.markup.partial.type !== liquid_html_parser_1.NodeTypes.VariableLookup) {
43
+ refs.push({
44
+ name: node.markup.partial.value,
45
+ documentType: node.name || 'render',
46
+ startIndex: node.markup.partial.position.start,
47
+ endIndex: node.markup.partial.position.end,
48
+ });
49
+ }
50
+ // Traverse children (skip circular references like parentNode, prev, next)
51
+ for (const key of Object.keys(node)) {
52
+ if (liquid_html_parser_1.nonTraversableProperties.has(key))
53
+ continue;
54
+ const value = node[key];
55
+ if (Array.isArray(value)) {
56
+ for (const child of value) {
57
+ if (child && typeof child === 'object') {
58
+ stack.push(child);
59
+ }
60
+ }
61
+ }
62
+ else if (value && typeof value === 'object' && value.type) {
63
+ stack.push(value);
64
+ }
65
+ }
66
+ }
67
+ return refs;
68
+ }
69
+ exports.CircularRender = {
70
+ meta: {
71
+ code: 'CircularRender',
72
+ name: 'Prevent circular renders',
73
+ docs: {
74
+ description: 'Reports circular render/function/include chains that would cause infinite loops at runtime.',
75
+ recommended: true,
76
+ url: 'https://documentation.platformos.com/developer-guide/platformos-check/checks/circular-render',
77
+ },
78
+ type: types_1.SourceCodeType.LiquidHtml,
79
+ severity: types_1.Severity.ERROR,
80
+ schema: {},
81
+ targets: [],
82
+ },
83
+ create(context) {
84
+ const locator = new platformos_common_1.DocumentsLocator(context.fs);
85
+ const rootUri = vscode_uri_1.URI.parse(context.config.rootUri);
86
+ const parseCache = getParseCache(context);
87
+ const collectedRefs = [];
88
+ return {
89
+ async RenderMarkup(node, ancestors) {
90
+ if (node.partial.type === liquid_html_parser_1.NodeTypes.VariableLookup)
91
+ return;
92
+ const parent = ancestors.at(-1);
93
+ const documentType = parent?.name === 'include' ? 'include' : 'render';
94
+ collectedRefs.push({
95
+ name: node.partial.value,
96
+ documentType,
97
+ startIndex: node.partial.position.start,
98
+ endIndex: node.partial.position.end,
99
+ });
100
+ },
101
+ async FunctionMarkup(node) {
102
+ if (node.partial.type === liquid_html_parser_1.NodeTypes.VariableLookup)
103
+ return;
104
+ collectedRefs.push({
105
+ name: node.partial.value,
106
+ documentType: 'function',
107
+ startIndex: node.partial.position.start,
108
+ endIndex: node.partial.position.end,
109
+ });
110
+ },
111
+ async onCodePathEnd() {
112
+ const fileUri = context.file.uri;
113
+ for (const ref of collectedRefs) {
114
+ const cycle = await findCycle(locator, rootUri, context.fs, parseCache, fileUri, ref.name, ref.documentType, [fileUri], 0);
115
+ if (cycle) {
116
+ const cyclePath = cycle
117
+ .map((u) => {
118
+ const parts = u.split('/');
119
+ return parts.slice(-2).join('/');
120
+ })
121
+ .join(' -> ');
122
+ context.report({
123
+ message: `Circular render detected: ${cyclePath}. This will cause an infinite loop at runtime.`,
124
+ startIndex: ref.startIndex,
125
+ endIndex: ref.endIndex,
126
+ });
127
+ }
128
+ }
129
+ },
130
+ };
131
+ },
132
+ };
133
+ async function findCycle(locator, rootUri, fs, parseCache, originUri, partialName, documentType, path, depth) {
134
+ if (depth >= MAX_DEPTH)
135
+ return null;
136
+ const uri = await locator.locate(rootUri, documentType, partialName);
137
+ if (!uri)
138
+ return null;
139
+ if (uri === originUri) {
140
+ return [...path, uri];
141
+ }
142
+ if (path.includes(uri)) {
143
+ // This is a cycle, but it doesn't include the origin file — skip it
144
+ return null;
145
+ }
146
+ let refs = parseCache.get(uri);
147
+ if (!refs) {
148
+ try {
149
+ const source = await fs.readFile(uri);
150
+ refs = extractPartialRefs(source);
151
+ }
152
+ catch {
153
+ refs = [];
154
+ }
155
+ parseCache.set(uri, refs);
156
+ }
157
+ for (const dep of refs) {
158
+ const cycle = await findCycle(locator, rootUri, fs, parseCache, originUri, dep.name, dep.documentType, [...path, uri], depth + 1);
159
+ if (cycle)
160
+ return cycle;
161
+ }
162
+ return null;
163
+ }
164
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/checks/circular-render/index.ts"],"names":[],"mappings":"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"}
@@ -5,4 +5,4 @@ export declare const allChecks: (LiquidCheckDefinition | JSONCheckDefinition | G
5
5
  * - meta.docs.recommended: true
6
6
  * - Either no meta.targets list exist or if it does exist then Recommended is a target
7
7
  */
8
- export declare const recommended: (LiquidCheckDefinition | JSONCheckDefinition | GraphQLCheckDefinition | YAMLCheckDefinition)[];
8
+ export declare const recommended: (LiquidCheckDefinition | JSONCheckDefinition | YAMLCheckDefinition | GraphQLCheckDefinition)[];
@@ -32,6 +32,9 @@ const graphql_1 = require("./graphql");
32
32
  const unknown_property_1 = require("./unknown-property");
33
33
  const invalid_hash_assign_target_1 = require("./invalid-hash-assign-target");
34
34
  const duplicate_function_arguments_1 = require("./duplicate-function-arguments");
35
+ const missing_render_partial_arguments_1 = require("./missing-render-partial-arguments");
36
+ const nested_graphql_query_1 = require("./nested-graphql-query");
37
+ const missing_page_1 = require("./missing-page");
35
38
  exports.allChecks = [
36
39
  deprecated_filter_1.DeprecatedFilter,
37
40
  deprecated_tag_1.DeprecatedTag,
@@ -63,6 +66,9 @@ exports.allChecks = [
63
66
  graphql_1.GraphQLCheck,
64
67
  unknown_property_1.UnknownProperty,
65
68
  invalid_hash_assign_target_1.InvalidHashAssignTarget,
69
+ missing_render_partial_arguments_1.MissingRenderPartialArguments,
70
+ nested_graphql_query_1.NestedGraphQLQuery,
71
+ missing_page_1.MissingPage,
66
72
  ];
67
73
  /**
68
74
  * The recommended checks is populated by all checks with the following conditions:
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":";;;AAAA,oCAMkB;AAElB,2DAAuD;AACvD,qDAAiD;AACjD,6FAAuF;AACvF,iEAA2D;AAC3D,2DAAsD;AACtD,yEAAmE;AACnE,mEAA+D;AAC/D,mDAA+C;AAC/C,uDAAmD;AACnD,yDAAqD;AACrD,qEAAgE;AAChE,qEAAgE;AAChE,mEAA8D;AAC9D,yDAAqD;AACrD,qEAA+D;AAC/D,qDAAiD;AACjD,mGAA6F;AAC7F,mDAA+C;AAC/C,yDAAoD;AACpD,qEAAgE;AAChE,6CAAyC;AACzC,mEAA6D;AAC7D,+FAAwF;AACxF,mDAA+C;AAC/C,uDAAwD;AACxD,2DAA4D;AAC5D,uCAAyC;AACzC,yDAAqD;AACrD,6EAAuE;AACvE,iFAA4E;AAE/D,QAAA,SAAS,GAKhB;IACJ,oCAAgB;IAChB,8BAAa;IACb,yDAA0B;IAC1B,oEAA+B;IAC/B,wCAAiB;IACjB,mCAAe;IACf,gDAAqB;IACrB,4CAAoB;IACpB,4BAAY;IACZ,gCAAc;IACd,kCAAe;IACf,6CAAoB;IACpB,6CAAoB;IACpB,2CAAmB;IACnB,kCAAe;IACf,4CAAmB;IACnB,8BAAa;IACb,0EAAkC;IAClC,4BAAY;IACZ,iCAAc;IACd,6CAAoB;IACpB,sBAAS;IACT,0CAAkB;IAClB,qEAA+B;IAC/B,4BAAY;IACZ,qCAAmB;IACnB,yCAAqB;IACrB,sBAAY;IACZ,kCAAe;IACf,oDAAuB;CACxB,CAAC;AAEF;;;;GAIG;AACU,QAAA,WAAW,GAAG,iBAAS,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;IACpD,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;IAClD,MAAM,aAAa,GACjB,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO;QACnB,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM;QAC1B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAY,CAAC,WAAW,CAAC,CAAC;IAExD,OAAO,aAAa,IAAI,aAAa,CAAC;AACxC,CAAC,CAAC,CAAC"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../src/checks/index.ts"],"names":[],"mappings":";;;AAAA,oCAMkB;AAElB,2DAAuD;AACvD,qDAAiD;AACjD,6FAAuF;AACvF,iEAA2D;AAC3D,2DAAsD;AACtD,yEAAmE;AACnE,mEAA+D;AAC/D,mDAA+C;AAC/C,uDAAmD;AACnD,yDAAqD;AACrD,qEAAgE;AAChE,qEAAgE;AAChE,mEAA8D;AAC9D,yDAAqD;AACrD,qEAA+D;AAC/D,qDAAiD;AACjD,mGAA6F;AAC7F,mDAA+C;AAC/C,yDAAoD;AACpD,qEAAgE;AAChE,6CAAyC;AACzC,mEAA6D;AAC7D,+FAAwF;AACxF,mDAA+C;AAC/C,uDAAwD;AACxD,2DAA4D;AAC5D,uCAAyC;AACzC,yDAAqD;AACrD,6EAAuE;AACvE,iFAA4E;AAC5E,yFAAmF;AACnF,iEAA4D;AAC5D,iDAA6C;AAEhC,QAAA,SAAS,GAKhB;IACJ,oCAAgB;IAChB,8BAAa;IACb,yDAA0B;IAC1B,oEAA+B;IAC/B,wCAAiB;IACjB,mCAAe;IACf,gDAAqB;IACrB,4CAAoB;IACpB,4BAAY;IACZ,gCAAc;IACd,kCAAe;IACf,6CAAoB;IACpB,6CAAoB;IACpB,2CAAmB;IACnB,kCAAe;IACf,4CAAmB;IACnB,8BAAa;IACb,0EAAkC;IAClC,4BAAY;IACZ,iCAAc;IACd,6CAAoB;IACpB,sBAAS;IACT,0CAAkB;IAClB,qEAA+B;IAC/B,4BAAY;IACZ,qCAAmB;IACnB,yCAAqB;IACrB,sBAAY;IACZ,kCAAe;IACf,oDAAuB;IACvB,gEAA6B;IAC7B,yCAAkB;IAClB,0BAAW;CACZ,CAAC;AAEF;;;;GAIG;AACU,QAAA,WAAW,GAAG,iBAAS,CAAC,MAAM,CAAC,CAAC,KAAK,EAAE,EAAE;IACpD,MAAM,aAAa,GAAG,KAAK,CAAC,IAAI,CAAC,IAAI,CAAC,WAAW,CAAC;IAClD,MAAM,aAAa,GACjB,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO;QACnB,CAAC,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,MAAM;QAC1B,KAAK,CAAC,IAAI,CAAC,OAAO,CAAC,QAAQ,CAAC,oBAAY,CAAC,WAAW,CAAC,CAAC;IAExD,OAAO,aAAa,IAAI,aAAa,CAAC;AACxC,CAAC,CAAC,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { LiquidCheckDefinition } from '../../types';
2
+ export declare const MissingPage: LiquidCheckDefinition;
@@ -0,0 +1,73 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingPage = void 0;
4
+ const url_helpers_1 = require("../../url-helpers");
5
+ const types_1 = require("../../types");
6
+ const utils_1 = require("../utils");
7
+ exports.MissingPage = {
8
+ meta: {
9
+ code: 'MissingPage',
10
+ name: 'Missing page for route',
11
+ docs: {
12
+ description: 'Reports links and form actions that point to routes with no corresponding platformOS page.',
13
+ recommended: true,
14
+ url: 'https://documentation.platformos.com/developer-guide/platformos-check/checks/missing-page',
15
+ },
16
+ type: types_1.SourceCodeType.LiquidHtml,
17
+ severity: types_1.Severity.WARNING,
18
+ schema: {},
19
+ targets: [],
20
+ },
21
+ create(context) {
22
+ let routeTable;
23
+ // Tracks {% assign %} variable mappings incrementally in document order.
24
+ // This means each <a> / <form> sees only the assigns that precede it,
25
+ // and reassignments correctly shadow earlier values at their position.
26
+ const variableMap = new Map();
27
+ function checkUrlAttribute(attr, method) {
28
+ const urlPattern = (0, url_helpers_1.extractUrlPattern)(attr, variableMap);
29
+ if (urlPattern === null)
30
+ return;
31
+ if ((0, url_helpers_1.shouldSkipUrl)(urlPattern))
32
+ return;
33
+ if (!routeTable.hasMatch(urlPattern, method)) {
34
+ const methodLabel = method.toUpperCase();
35
+ context.report({
36
+ message: `No page found for route '${urlPattern}' (${methodLabel})`,
37
+ startIndex: attr.value[0].position.start,
38
+ endIndex: attr.value[attr.value.length - 1].position.end,
39
+ });
40
+ }
41
+ }
42
+ return {
43
+ async onCodePathStart() {
44
+ // Front-load the route table build so individual HtmlElement visits don't wait.
45
+ routeTable = await context.getRouteTable();
46
+ },
47
+ async LiquidTag(node) {
48
+ const extracted = (0, url_helpers_1.tryExtractAssignUrl)(node);
49
+ if (extracted) {
50
+ variableMap.set(extracted.name, extracted.urlPattern);
51
+ }
52
+ },
53
+ async HtmlElement(node) {
54
+ if ((0, utils_1.isHtmlTag)(node, 'a')) {
55
+ const hrefAttr = node.attributes.find((a) => (0, url_helpers_1.isValuedAttrNode)(a) && (0, url_helpers_1.getAttrName)(a) === 'href');
56
+ if (hrefAttr && (0, url_helpers_1.isValuedAttrNode)(hrefAttr)) {
57
+ checkUrlAttribute(hrefAttr, 'get');
58
+ }
59
+ }
60
+ else if ((0, utils_1.isHtmlTag)(node, 'form')) {
61
+ const actionAttr = node.attributes.find((a) => (0, url_helpers_1.isValuedAttrNode)(a) && (0, url_helpers_1.getAttrName)(a) === 'action');
62
+ if (actionAttr && (0, url_helpers_1.isValuedAttrNode)(actionAttr)) {
63
+ const method = (0, url_helpers_1.getEffectiveMethod)(node);
64
+ if (method !== null) {
65
+ checkUrlAttribute(actionAttr, method);
66
+ }
67
+ }
68
+ }
69
+ },
70
+ };
71
+ },
72
+ };
73
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/checks/missing-page/index.ts"],"names":[],"mappings":";;;AAEA,mDAO2B;AAC3B,uCAA8E;AAC9E,oCAAqC;AAExB,QAAA,WAAW,GAA0B;IAChD,IAAI,EAAE;QACJ,IAAI,EAAE,aAAa;QACnB,IAAI,EAAE,wBAAwB;QAC9B,IAAI,EAAE;YACJ,WAAW,EACT,4FAA4F;YAC9F,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,2FAA2F;SACjG;QACD,IAAI,EAAE,sBAAc,CAAC,UAAU;QAC/B,QAAQ,EAAE,gBAAQ,CAAC,OAAO;QAC1B,MAAM,EAAE,EAAE;QACV,OAAO,EAAE,EAAE;KACZ;IAED,MAAM,CAAC,OAAO;QACZ,IAAI,UAAsB,CAAC;QAC3B,yEAAyE;QACzE,sEAAsE;QACtE,uEAAuE;QACvE,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;QAE9C,SAAS,iBAAiB,CAAC,IAA6C,EAAE,MAAc;YACtF,MAAM,UAAU,GAAG,IAAA,+BAAiB,EAAC,IAAI,EAAE,WAAW,CAAC,CAAC;YACxD,IAAI,UAAU,KAAK,IAAI;gBAAE,OAAO;YAChC,IAAI,IAAA,2BAAa,EAAC,UAAU,CAAC;gBAAE,OAAO;YAEtC,IAAI,CAAC,UAAU,CAAC,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC,EAAE,CAAC;gBAC7C,MAAM,WAAW,GAAG,MAAM,CAAC,WAAW,EAAE,CAAC;gBACzC,OAAO,CAAC,MAAM,CAAC;oBACb,OAAO,EAAE,4BAA4B,UAAU,MAAM,WAAW,GAAG;oBACnE,UAAU,EAAE,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,KAAK;oBACxC,QAAQ,EAAE,IAAI,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC,QAAQ,CAAC,GAAG;iBACzD,CAAC,CAAC;YACL,CAAC;QACH,CAAC;QAED,OAAO;YACL,KAAK,CAAC,eAAe;gBACnB,gFAAgF;gBAChF,UAAU,GAAG,MAAM,OAAO,CAAC,aAAa,EAAE,CAAC;YAC7C,CAAC;YAED,KAAK,CAAC,SAAS,CAAC,IAAe;gBAC7B,MAAM,SAAS,GAAG,IAAA,iCAAmB,EAAC,IAAI,CAAC,CAAC;gBAC5C,IAAI,SAAS,EAAE,CAAC;oBACd,WAAW,CAAC,GAAG,CAAC,SAAS,CAAC,IAAI,EAAE,SAAS,CAAC,UAAU,CAAC,CAAC;gBACxD,CAAC;YACH,CAAC;YAED,KAAK,CAAC,WAAW,CAAC,IAAI;gBACpB,IAAI,IAAA,iBAAS,EAAC,IAAI,EAAE,GAAG,CAAC,EAAE,CAAC;oBACzB,MAAM,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CACnC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,8BAAgB,EAAC,CAAC,CAAC,IAAI,IAAA,yBAAW,EAAC,CAAC,CAAC,KAAK,MAAM,CACxD,CAAC;oBAEF,IAAI,QAAQ,IAAI,IAAA,8BAAgB,EAAC,QAAQ,CAAC,EAAE,CAAC;wBAC3C,iBAAiB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;oBACrC,CAAC;gBACH,CAAC;qBAAM,IAAI,IAAA,iBAAS,EAAC,IAAI,EAAE,MAAM,CAAC,EAAE,CAAC;oBACnC,MAAM,UAAU,GAAG,IAAI,CAAC,UAAU,CAAC,IAAI,CACrC,CAAC,CAAC,EAAE,EAAE,CAAC,IAAA,8BAAgB,EAAC,CAAC,CAAC,IAAI,IAAA,yBAAW,EAAC,CAAC,CAAC,KAAK,QAAQ,CAC1D,CAAC;oBAEF,IAAI,UAAU,IAAI,IAAA,8BAAgB,EAAC,UAAU,CAAC,EAAE,CAAC;wBAC/C,MAAM,MAAM,GAAG,IAAA,gCAAkB,EAAC,IAAmB,CAAC,CAAC;wBACvD,IAAI,MAAM,KAAK,IAAI,EAAE,CAAC;4BACpB,iBAAiB,CAAC,UAAU,EAAE,MAAM,CAAC,CAAC;wBACxC,CAAC;oBACH,CAAC;gBACH,CAAC;YACH,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -5,6 +5,13 @@ const liquid_html_parser_1 = require("@platformos/liquid-html-parser");
5
5
  const types_1 = require("../../types");
6
6
  const platformos_common_1 = require("@platformos/platformos-common");
7
7
  const vscode_uri_1 = require("vscode-uri");
8
+ function getTagName(ancestors) {
9
+ const parent = ancestors.at(-1);
10
+ if (parent?.type === liquid_html_parser_1.NodeTypes.LiquidTag && parent.name === liquid_html_parser_1.NamedTags.theme_render_rc) {
11
+ return 'theme_render_rc';
12
+ }
13
+ return 'render';
14
+ }
8
15
  const schema = {
9
16
  ignoreMissing: types_1.SchemaProp.array(types_1.SchemaProp.string(), []),
10
17
  };
@@ -24,44 +31,37 @@ exports.MissingPartial = {
24
31
  },
25
32
  create(context) {
26
33
  const locator = new platformos_common_1.DocumentsLocator(context.fs);
34
+ const rootUri = vscode_uri_1.URI.parse(context.config.rootUri);
35
+ let searchPathsPromise;
36
+ function getSearchPaths() {
37
+ searchPathsPromise ??= (0, platformos_common_1.loadSearchPaths)(context.fs, rootUri);
38
+ return searchPathsPromise;
39
+ }
40
+ async function reportIfMissing(docType, name, position) {
41
+ const searchPaths = docType === 'theme_render_rc' ? await getSearchPaths() : null;
42
+ const location = await locator.locate(rootUri, docType, name, searchPaths);
43
+ if (!location) {
44
+ context.report({
45
+ message: `'${name}' does not exist`,
46
+ startIndex: position.start,
47
+ endIndex: position.end,
48
+ });
49
+ }
50
+ }
27
51
  return {
28
- async RenderMarkup(node) {
29
- if (node.partial.type === liquid_html_parser_1.NodeTypes.VariableLookup)
30
- return;
31
- const partial = node.partial;
32
- const location = await locator.locate(vscode_uri_1.URI.parse(context.config.rootUri), 'render', partial.value);
33
- if (!location) {
34
- context.report({
35
- message: `'${partial.value}' does not exist`,
36
- startIndex: node.partial.position.start,
37
- endIndex: node.partial.position.end,
38
- });
52
+ async RenderMarkup(node, ancestors) {
53
+ if (node.partial.type !== liquid_html_parser_1.NodeTypes.VariableLookup) {
54
+ await reportIfMissing(getTagName(ancestors), node.partial.value, node.partial.position);
39
55
  }
40
56
  },
41
57
  async FunctionMarkup(node) {
42
- if (node.partial.type === liquid_html_parser_1.NodeTypes.VariableLookup)
43
- return;
44
- const partial = node.partial;
45
- const location = await locator.locate(vscode_uri_1.URI.parse(context.config.rootUri), 'function', partial.value);
46
- if (!location) {
47
- context.report({
48
- message: `'${partial.value}' does not exist`,
49
- startIndex: node.partial.position.start,
50
- endIndex: node.partial.position.end,
51
- });
58
+ if (node.partial.type !== liquid_html_parser_1.NodeTypes.VariableLookup) {
59
+ await reportIfMissing('function', node.partial.value, node.partial.position);
52
60
  }
53
61
  },
54
62
  async GraphQLMarkup(node) {
55
- if (node.graphql.type === liquid_html_parser_1.NodeTypes.VariableLookup)
56
- return;
57
- const graphql = node.graphql;
58
- const location = await locator.locate(vscode_uri_1.URI.parse(context.config.rootUri), 'graphql', graphql.value);
59
- if (!location) {
60
- context.report({
61
- message: `'${graphql.value}' does not exist`,
62
- startIndex: node.graphql.position.start,
63
- endIndex: node.graphql.position.end,
64
- });
63
+ if (node.graphql.type !== liquid_html_parser_1.NodeTypes.VariableLookup) {
64
+ await reportIfMissing('graphql', node.graphql.value, node.graphql.position);
65
65
  }
66
66
  },
67
67
  };
@@ -1 +1 @@
1
- {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/checks/missing-partial/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/checks/missing-partial/index.ts"],"names":[],"mappings":"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"}
@@ -0,0 +1,2 @@
1
+ import { LiquidCheckDefinition } from '../../types';
2
+ export declare const MissingRenderPartialArguments: LiquidCheckDefinition;
@@ -0,0 +1,37 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MissingRenderPartialArguments = void 0;
4
+ const types_1 = require("../../types");
5
+ const arguments_1 = require("../../liquid-doc/arguments");
6
+ exports.MissingRenderPartialArguments = {
7
+ meta: {
8
+ code: 'MissingRenderPartialArguments',
9
+ name: 'Missing Required Render Partial Arguments',
10
+ aliases: ['MissingRenderPartialParams'],
11
+ docs: {
12
+ description: 'This check ensures that all required @param arguments declared by a partial are provided at the call site.',
13
+ recommended: true,
14
+ url: 'https://documentation.platformos.com/developer-guide/platformos-check/checks/missing-render-partial-arguments',
15
+ },
16
+ type: types_1.SourceCodeType.LiquidHtml,
17
+ severity: types_1.Severity.ERROR,
18
+ schema: {},
19
+ targets: [],
20
+ },
21
+ create(context) {
22
+ return {
23
+ async RenderMarkup(node) {
24
+ const partialName = (0, arguments_1.getPartialName)(node);
25
+ if (!partialName)
26
+ return;
27
+ const liquidDocParameters = await (0, arguments_1.getLiquidDocParams)(context, partialName);
28
+ if (!liquidDocParameters)
29
+ return;
30
+ const providedNames = new Set(node.args.map((a) => a.name));
31
+ const missingRequired = [...liquidDocParameters.values()].filter((p) => p.required && !providedNames.has(p.name));
32
+ (0, arguments_1.reportMissingArguments)(context, node, missingRequired, partialName);
33
+ },
34
+ };
35
+ },
36
+ };
37
+ //# sourceMappingURL=index.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.js","sourceRoot":"","sources":["../../../src/checks/missing-render-partial-arguments/index.ts"],"names":[],"mappings":";;;AACA,uCAA8E;AAC9E,0DAIoC;AAEvB,QAAA,6BAA6B,GAA0B;IAClE,IAAI,EAAE;QACJ,IAAI,EAAE,+BAA+B;QACrC,IAAI,EAAE,2CAA2C;QACjD,OAAO,EAAE,CAAC,4BAA4B,CAAC;QACvC,IAAI,EAAE;YACJ,WAAW,EACT,4GAA4G;YAC9G,WAAW,EAAE,IAAI;YACjB,GAAG,EAAE,+GAA+G;SACrH;QACD,IAAI,EAAE,sBAAc,CAAC,UAAU;QAC/B,QAAQ,EAAE,gBAAQ,CAAC,KAAK;QACxB,MAAM,EAAE,EAAE;QACV,OAAO,EAAE,EAAE;KACZ;IAED,MAAM,CAAC,OAAO;QACZ,OAAO;YACL,KAAK,CAAC,YAAY,CAAC,IAAkB;gBACnC,MAAM,WAAW,GAAG,IAAA,0BAAc,EAAC,IAAI,CAAC,CAAC;gBACzC,IAAI,CAAC,WAAW;oBAAE,OAAO;gBAEzB,MAAM,mBAAmB,GAAG,MAAM,IAAA,8BAAkB,EAAC,OAAO,EAAE,WAAW,CAAC,CAAC;gBAC3E,IAAI,CAAC,mBAAmB;oBAAE,OAAO;gBAEjC,MAAM,aAAa,GAAG,IAAI,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;gBAC5D,MAAM,eAAe,GAAG,CAAC,GAAG,mBAAmB,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAC9D,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,IAAI,CAAC,aAAa,CAAC,GAAG,CAAC,CAAC,CAAC,IAAI,CAAC,CAChD,CAAC;gBAEF,IAAA,kCAAsB,EAAC,OAAO,EAAE,IAAI,EAAE,eAAe,EAAE,WAAW,CAAC,CAAC;YACtE,CAAC;SACF,CAAC;IACJ,CAAC;CACF,CAAC"}
@@ -0,0 +1,2 @@
1
+ import { LiquidCheckDefinition } from '../../types';
2
+ export declare const NestedGraphQLQuery: LiquidCheckDefinition;
@@ -0,0 +1,146 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.NestedGraphQLQuery = void 0;
4
+ const liquid_html_parser_1 = require("@platformos/liquid-html-parser");
5
+ const platformos_common_1 = require("@platformos/platformos-common");
6
+ const vscode_uri_1 = require("vscode-uri");
7
+ const types_1 = require("../../types");
8
+ const utils_1 = require("../utils");
9
+ const SKIP_IF_ANCESTOR_TAGS = [liquid_html_parser_1.NamedTags.cache];
10
+ function findNodesInAST(ast) {
11
+ const results = [];
12
+ const stack = [...ast];
13
+ while (stack.length > 0) {
14
+ const node = stack.pop();
15
+ if (node.type === liquid_html_parser_1.NodeTypes.LiquidTag) {
16
+ if (node.name === liquid_html_parser_1.NamedTags.graphql) {
17
+ results.push({ type: 'graphql', partialChain: [] });
18
+ }
19
+ else if ((node.name === liquid_html_parser_1.NamedTags.function || node.name === liquid_html_parser_1.NamedTags.render) &&
20
+ typeof node.markup !== 'string' &&
21
+ 'partial' in node.markup &&
22
+ node.markup.partial.type !== liquid_html_parser_1.NodeTypes.VariableLookup) {
23
+ results.push({ type: node.name, partialName: node.markup.partial.value });
24
+ }
25
+ if ('children' in node && Array.isArray(node.children)) {
26
+ stack.push(...node.children);
27
+ }
28
+ }
29
+ else if ('children' in node && Array.isArray(node.children)) {
30
+ stack.push(...node.children);
31
+ }
32
+ }
33
+ return results;
34
+ }
35
+ async function containsGraphQLTransitively(locator, fs, rootUri, partialName, tagType, visited) {
36
+ if (visited.has(partialName))
37
+ return null;
38
+ visited.add(partialName);
39
+ const location = await locator.locate(rootUri, tagType, partialName);
40
+ if (!location)
41
+ return null;
42
+ let source;
43
+ try {
44
+ source = await fs.readFile(location);
45
+ }
46
+ catch {
47
+ return null;
48
+ }
49
+ let ast;
50
+ try {
51
+ ast = (0, liquid_html_parser_1.toLiquidHtmlAST)(source);
52
+ }
53
+ catch {
54
+ return null;
55
+ }
56
+ const nodes = findNodesInAST(ast.children);
57
+ for (const found of nodes) {
58
+ if (found.type === 'graphql') {
59
+ return [partialName];
60
+ }
61
+ }
62
+ for (const found of nodes) {
63
+ if (found.type === 'function' || found.type === 'render') {
64
+ const chain = await containsGraphQLTransitively(locator, fs, rootUri, found.partialName, found.type, visited);
65
+ if (chain) {
66
+ return [partialName, ...chain];
67
+ }
68
+ }
69
+ }
70
+ return null;
71
+ }
72
+ exports.NestedGraphQLQuery = {
73
+ meta: {
74
+ code: 'NestedGraphQLQuery',
75
+ name: 'Prevent N+1 GraphQL queries in loops',
76
+ docs: {
77
+ description: 'This check detects {% graphql %} tags placed inside loop tags ({% for %}, {% tablerow %}), which causes one database request per loop iteration (N+1 pattern). It also follows {% function %} and {% render %} calls transitively to detect indirect GraphQL queries.',
78
+ recommended: true,
79
+ url: 'https://documentation.platformos.com/developer-guide/platformos-check/checks/nested-graphql-query',
80
+ },
81
+ type: types_1.SourceCodeType.LiquidHtml,
82
+ severity: types_1.Severity.WARNING,
83
+ schema: {},
84
+ targets: [],
85
+ },
86
+ create(context) {
87
+ const locator = new platformos_common_1.DocumentsLocator(context.fs);
88
+ const rootUri = vscode_uri_1.URI.parse(context.config.rootUri);
89
+ function isInsideLoopWithoutCacheOrBackground(ancestors) {
90
+ const ancestorTags = ancestors.filter((a) => a.type === liquid_html_parser_1.NodeTypes.LiquidTag);
91
+ const loopAncestor = ancestorTags.find(utils_1.isLoopLiquidTag);
92
+ if (!loopAncestor)
93
+ return null;
94
+ const inBackground = ancestorTags.some((a) => a.name === liquid_html_parser_1.NamedTags.background);
95
+ if (inBackground)
96
+ return null;
97
+ const shouldSkip = ancestorTags.some((a) => SKIP_IF_ANCESTOR_TAGS.map((a) => a.toString()).includes(a.name));
98
+ if (shouldSkip)
99
+ return null;
100
+ return loopAncestor;
101
+ }
102
+ return {
103
+ async LiquidTag(node, ancestors) {
104
+ if (node.name === liquid_html_parser_1.NamedTags.graphql) {
105
+ const loopAncestor = isInsideLoopWithoutCacheOrBackground(ancestors);
106
+ if (!loopAncestor)
107
+ return;
108
+ let resultName = '';
109
+ if (typeof node.markup !== 'string' &&
110
+ (node.markup.type === liquid_html_parser_1.NodeTypes.GraphQLMarkup ||
111
+ node.markup.type === liquid_html_parser_1.NodeTypes.GraphQLInlineMarkup)) {
112
+ resultName = node.markup.name ? ` result = '${node.markup.name}'` : '';
113
+ }
114
+ const graphqlStr = resultName ? `{% graphql${resultName} %}` : '{% graphql %}';
115
+ context.report({
116
+ message: `N+1 pattern: ${graphqlStr} is inside a {% ${loopAncestor.name} %} loop. This executes at least one database request per iteration. Move the query before the loop and pass data as a variable.`,
117
+ startIndex: node.position.start,
118
+ endIndex: node.position.end,
119
+ });
120
+ }
121
+ else if (node.name === liquid_html_parser_1.NamedTags.function || node.name === liquid_html_parser_1.NamedTags.render) {
122
+ const loopAncestor = isInsideLoopWithoutCacheOrBackground(ancestors);
123
+ if (!loopAncestor)
124
+ return;
125
+ if (typeof node.markup === 'string' ||
126
+ !('partial' in node.markup) ||
127
+ node.markup.partial.type === liquid_html_parser_1.NodeTypes.VariableLookup) {
128
+ return;
129
+ }
130
+ const partialName = node.markup.partial.value;
131
+ const visited = new Set();
132
+ const chain = await containsGraphQLTransitively(locator, context.fs, rootUri, partialName, node.name, visited);
133
+ if (chain) {
134
+ const chainStr = chain.join(' → ');
135
+ context.report({
136
+ message: `N+1 pattern: {% ${node.name} '${partialName}' %} inside a {% ${loopAncestor.name} %} loop transitively calls a GraphQL query (${chainStr}). Move the query before the loop and pass data as a variable.`,
137
+ startIndex: node.position.start,
138
+ endIndex: node.position.end,
139
+ });
140
+ }
141
+ }
142
+ },
143
+ };
144
+ },
145
+ };
146
+ //# sourceMappingURL=index.js.map