@platformos/platformos-check-common 0.0.11 → 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 (140) hide show
  1. package/CHANGELOG.md +39 -0
  2. package/CLAUDE.md +150 -0
  3. package/dist/AugmentedPlatformOSDocset.js +1 -0
  4. package/dist/AugmentedPlatformOSDocset.js.map +1 -1
  5. package/dist/checks/circular-render/index.d.ts +2 -0
  6. package/dist/checks/circular-render/index.js +164 -0
  7. package/dist/checks/circular-render/index.js.map +1 -0
  8. package/dist/checks/deprecated-filter/index.js +15 -0
  9. package/dist/checks/deprecated-filter/index.js.map +1 -1
  10. package/dist/checks/duplicate-content-for-arguments/index.js +1 -1
  11. package/dist/checks/duplicate-content-for-arguments/index.js.map +1 -1
  12. package/dist/checks/graphql/index.d.ts +1 -0
  13. package/dist/checks/graphql/index.js +20 -7
  14. package/dist/checks/graphql/index.js.map +1 -1
  15. package/dist/checks/index.d.ts +1 -1
  16. package/dist/checks/index.js +6 -0
  17. package/dist/checks/index.js.map +1 -1
  18. package/dist/checks/invalid-hash-assign-target/index.js +4 -3
  19. package/dist/checks/invalid-hash-assign-target/index.js.map +1 -1
  20. package/dist/checks/missing-content-for-arguments/index.js +1 -1
  21. package/dist/checks/missing-content-for-arguments/index.js.map +1 -1
  22. package/dist/checks/missing-page/index.d.ts +2 -0
  23. package/dist/checks/missing-page/index.js +73 -0
  24. package/dist/checks/missing-page/index.js.map +1 -0
  25. package/dist/checks/missing-partial/index.js +31 -31
  26. package/dist/checks/missing-partial/index.js.map +1 -1
  27. package/dist/checks/missing-render-partial-arguments/index.d.ts +2 -0
  28. package/dist/checks/missing-render-partial-arguments/index.js +37 -0
  29. package/dist/checks/missing-render-partial-arguments/index.js.map +1 -0
  30. package/dist/checks/nested-graphql-query/index.d.ts +2 -0
  31. package/dist/checks/nested-graphql-query/index.js +146 -0
  32. package/dist/checks/nested-graphql-query/index.js.map +1 -0
  33. package/dist/checks/pagination-size/index.js +1 -1
  34. package/dist/checks/pagination-size/index.js.map +1 -1
  35. package/dist/checks/translation-key-exists/index.js +16 -19
  36. package/dist/checks/translation-key-exists/index.js.map +1 -1
  37. package/dist/checks/translation-utils.d.ts +20 -0
  38. package/dist/checks/translation-utils.js +51 -0
  39. package/dist/checks/translation-utils.js.map +1 -0
  40. package/dist/checks/undefined-object/index.js +35 -13
  41. package/dist/checks/undefined-object/index.js.map +1 -1
  42. package/dist/checks/unknown-property/index.js +75 -10
  43. package/dist/checks/unknown-property/index.js.map +1 -1
  44. package/dist/checks/unknown-property/property-shape.js +14 -1
  45. package/dist/checks/unknown-property/property-shape.js.map +1 -1
  46. package/dist/checks/unrecognized-content-for-arguments/index.js +1 -1
  47. package/dist/checks/unrecognized-content-for-arguments/index.js.map +1 -1
  48. package/dist/checks/unused-assign/index.js +23 -1
  49. package/dist/checks/unused-assign/index.js.map +1 -1
  50. package/dist/checks/unused-translation-key/index.d.ts +4 -0
  51. package/dist/checks/unused-translation-key/index.js +85 -0
  52. package/dist/checks/unused-translation-key/index.js.map +1 -0
  53. package/dist/checks/valid-content-for-argument-types/index.js +1 -1
  54. package/dist/checks/valid-content-for-argument-types/index.js.map +1 -1
  55. package/dist/checks/valid-render-partial-argument-types/index.js +2 -1
  56. package/dist/checks/valid-render-partial-argument-types/index.js.map +1 -1
  57. package/dist/checks/variable-name/index.js +4 -0
  58. package/dist/checks/variable-name/index.js.map +1 -1
  59. package/dist/context-utils.d.ts +2 -1
  60. package/dist/context-utils.js +31 -1
  61. package/dist/context-utils.js.map +1 -1
  62. package/dist/doc-generator/DocBlockGenerator.d.ts +16 -0
  63. package/dist/doc-generator/DocBlockGenerator.js +464 -0
  64. package/dist/doc-generator/DocBlockGenerator.js.map +1 -0
  65. package/dist/doc-generator/index.d.ts +1 -0
  66. package/dist/doc-generator/index.js +6 -0
  67. package/dist/doc-generator/index.js.map +1 -0
  68. package/dist/frontmatter/index.d.ts +59 -0
  69. package/dist/frontmatter/index.js +301 -0
  70. package/dist/frontmatter/index.js.map +1 -0
  71. package/dist/index.d.ts +3 -1
  72. package/dist/index.js +6 -1
  73. package/dist/index.js.map +1 -1
  74. package/dist/liquid-doc/arguments.js +9 -0
  75. package/dist/liquid-doc/arguments.js.map +1 -1
  76. package/dist/liquid-doc/utils.d.ts +10 -2
  77. package/dist/liquid-doc/utils.js +26 -1
  78. package/dist/liquid-doc/utils.js.map +1 -1
  79. package/dist/path.d.ts +1 -1
  80. package/dist/path.js +3 -1
  81. package/dist/path.js.map +1 -1
  82. package/dist/to-schema.d.ts +1 -1
  83. package/dist/tsconfig.tsbuildinfo +1 -1
  84. package/dist/types.d.ts +8 -1
  85. package/dist/types.js.map +1 -1
  86. package/dist/url-helpers.d.ts +55 -0
  87. package/dist/url-helpers.js +334 -0
  88. package/dist/url-helpers.js.map +1 -0
  89. package/dist/utils/block.js.map +1 -1
  90. package/dist/utils/index.d.ts +1 -0
  91. package/dist/utils/index.js +1 -0
  92. package/dist/utils/index.js.map +1 -1
  93. package/dist/utils/levenshtein.d.ts +3 -0
  94. package/dist/utils/levenshtein.js +39 -0
  95. package/dist/utils/levenshtein.js.map +1 -0
  96. package/package.json +2 -2
  97. package/src/AugmentedPlatformOSDocset.ts +1 -0
  98. package/src/checks/deprecated-filter/index.spec.ts +41 -1
  99. package/src/checks/deprecated-filter/index.ts +17 -0
  100. package/src/checks/graphql/index.spec.ts +173 -0
  101. package/src/checks/graphql/index.ts +21 -10
  102. package/src/checks/index.ts +6 -0
  103. package/src/checks/invalid-hash-assign-target/index.spec.ts +26 -0
  104. package/src/checks/invalid-hash-assign-target/index.ts +6 -4
  105. package/src/checks/missing-page/index.spec.ts +755 -0
  106. package/src/checks/missing-page/index.ts +89 -0
  107. package/src/checks/missing-partial/index.spec.ts +361 -0
  108. package/src/checks/missing-partial/index.ts +39 -47
  109. package/src/checks/missing-render-partial-arguments/index.spec.ts +74 -0
  110. package/src/checks/missing-render-partial-arguments/index.ts +44 -0
  111. package/src/checks/nested-graphql-query/index.spec.ts +175 -0
  112. package/src/checks/nested-graphql-query/index.ts +203 -0
  113. package/src/checks/parser-blocking-script/index.spec.ts +7 -3
  114. package/src/checks/translation-key-exists/index.spec.ts +79 -2
  115. package/src/checks/translation-key-exists/index.ts +18 -27
  116. package/src/checks/translation-utils.ts +63 -0
  117. package/src/checks/undefined-object/index.spec.ts +153 -19
  118. package/src/checks/undefined-object/index.ts +43 -19
  119. package/src/checks/unknown-property/index.spec.ts +133 -0
  120. package/src/checks/unknown-property/index.ts +84 -10
  121. package/src/checks/unknown-property/property-shape.ts +15 -1
  122. package/src/checks/unused-assign/index.spec.ts +75 -1
  123. package/src/checks/unused-assign/index.ts +26 -1
  124. package/src/checks/unused-doc-param/index.spec.ts +4 -2
  125. package/src/checks/valid-doc-param-types/index.spec.ts +1 -1
  126. package/src/checks/valid-render-partial-argument-types/index.spec.ts +24 -1
  127. package/src/checks/valid-render-partial-argument-types/index.ts +3 -2
  128. package/src/checks/variable-name/index.spec.ts +10 -1
  129. package/src/checks/variable-name/index.ts +5 -0
  130. package/src/context-utils.ts +33 -1
  131. package/src/frontmatter/index.ts +344 -0
  132. package/src/index.ts +6 -0
  133. package/src/liquid-doc/arguments.ts +9 -0
  134. package/src/liquid-doc/utils.ts +26 -2
  135. package/src/path.ts +2 -0
  136. package/src/types.ts +9 -1
  137. package/src/url-helpers.spec.ts +241 -0
  138. package/src/url-helpers.ts +363 -0
  139. package/src/utils/index.ts +1 -0
  140. package/src/utils/levenshtein.ts +41 -0
@@ -39,10 +39,27 @@ export const DeprecatedFilter: LiquidCheckDefinition = {
39
39
 
40
40
  const message = deprecatedFilterMessage(deprecatedFilter, recommendedFilter);
41
41
 
42
+ const filterText = node.source.slice(node.position.start, node.position.end);
43
+ const afterPipeIdx = filterText.indexOf('|') + 1;
44
+ const nameIdx =
45
+ afterPipeIdx + filterText.slice(afterPipeIdx).indexOf(deprecatedFilter.name);
46
+ const filterNameStart = node.position.start + nameIdx;
47
+ const filterNameEnd = filterNameStart + deprecatedFilter.name.length;
48
+
42
49
  context.report({
43
50
  message,
44
51
  startIndex: node.position.start + 1,
45
52
  endIndex: node.position.end,
53
+ suggest: recommendedFilter
54
+ ? [
55
+ {
56
+ message: `Replace '${deprecatedFilter.name}' with '${recommendedFilter.name}'`,
57
+ fix: (corrector) => {
58
+ corrector.replace(filterNameStart, filterNameEnd, recommendedFilter.name);
59
+ },
60
+ },
61
+ ]
62
+ : undefined,
46
63
  });
47
64
  },
48
65
  };
@@ -0,0 +1,173 @@
1
+ import { expect, describe, it } from 'vitest';
2
+ import { GraphQLCheck, lineToRange } from './index';
3
+ import { check } from '../../test';
4
+
5
+ const SCHEMA = `
6
+ type Query {
7
+ hello: String
8
+ users: [User]
9
+ }
10
+
11
+ type User {
12
+ id: ID
13
+ name: String
14
+ }
15
+ `;
16
+
17
+ const mockDependencies = {
18
+ platformosDocset: {
19
+ async graphQL() {
20
+ return SCHEMA;
21
+ },
22
+ async filters() {
23
+ return [];
24
+ },
25
+ async objects() {
26
+ return [];
27
+ },
28
+ async liquidDrops() {
29
+ return [];
30
+ },
31
+ async tags() {
32
+ return [];
33
+ },
34
+ },
35
+ };
36
+
37
+ const noDeps = {
38
+ platformosDocset: {
39
+ async graphQL() {
40
+ return null;
41
+ },
42
+ async filters() {
43
+ return [];
44
+ },
45
+ async objects() {
46
+ return [];
47
+ },
48
+ async liquidDrops() {
49
+ return [];
50
+ },
51
+ async tags() {
52
+ return [];
53
+ },
54
+ },
55
+ };
56
+
57
+ describe('Module: GraphQLCheck', () => {
58
+ it('reports no offenses for a valid query', async () => {
59
+ const files = {
60
+ 'app/graphql/my_query.graphql': '{ hello }',
61
+ };
62
+
63
+ const offenses = await check(files, [GraphQLCheck], mockDependencies);
64
+ expect(offenses).to.be.empty;
65
+ });
66
+
67
+ it('reports an offense for an unknown field', async () => {
68
+ const files = {
69
+ 'app/graphql/my_query.graphql': '{ unknownField }',
70
+ };
71
+
72
+ const offenses = await check(files, [GraphQLCheck], mockDependencies);
73
+ expect(offenses).to.have.length(1);
74
+ expect(offenses[0].message).to.equal('Cannot query field "unknownField" on type "Query".');
75
+ });
76
+
77
+ it('offense for unknown field spans only the affected line, not the entire file', async () => {
78
+ const query = `{
79
+ unknownField
80
+ }`;
81
+ const files = {
82
+ 'app/graphql/my_query.graphql': query,
83
+ };
84
+
85
+ const offenses = await check(files, [GraphQLCheck], mockDependencies);
86
+ expect(offenses).to.have.length(1);
87
+
88
+ // Should point to line 2 (1-based), which is " unknownField"
89
+ // and NOT span to the end of the file
90
+ expect(offenses[0].start.line).to.equal(1); // 0-based: line index 1 = " unknownField"
91
+ expect(offenses[0].end.line).to.equal(1);
92
+ });
93
+
94
+ it('reports a syntax error offense instead of swallowing it', async () => {
95
+ const files = {
96
+ 'app/graphql/my_query.graphql': '{ unclosed {',
97
+ };
98
+
99
+ const offenses = await check(files, [GraphQLCheck], mockDependencies);
100
+ expect(offenses).to.have.length(1);
101
+ expect(offenses[0].message).to.equal('Syntax Error: Expected Name, found <EOF>.');
102
+ });
103
+
104
+ it('syntax error offense points to the actual error line, not the whole file', async () => {
105
+ // unclosed brace on line 3 causes a parse error — graphql-js will report the exact location
106
+ const query = `{
107
+ hello
108
+ unclosed {
109
+ `;
110
+ const files = {
111
+ 'app/graphql/my_query.graphql': query,
112
+ };
113
+
114
+ const offenses = await check(files, [GraphQLCheck], mockDependencies);
115
+ expect(offenses).to.have.length(1);
116
+ expect(offenses[0].message).to.equal('Syntax Error: Expected Name, found <EOF>.');
117
+ // Offense spans exactly one line (the error line), NOT the whole file
118
+ expect(offenses[0].start.line).to.equal(offenses[0].end.line);
119
+ // And that line is not the last line of the file (i.e. not spanning to the end)
120
+ expect(offenses[0].end.line).to.be.lessThan(3); // file has 4 lines (0-indexed: 0-3)
121
+ });
122
+
123
+ it('reports no offenses when platformosDocset.graphQL returns null', async () => {
124
+ const files = {
125
+ 'app/graphql/my_query.graphql': '{ unknownField }',
126
+ };
127
+
128
+ const offenses = await check(files, [GraphQLCheck], noDeps);
129
+ expect(offenses).to.be.empty;
130
+ });
131
+ });
132
+
133
+ describe('Unit: lineToRange', () => {
134
+ const TEXT = 'line1\nline2\nline3';
135
+
136
+ it('returns correct range for line 1', () => {
137
+ expect(lineToRange(TEXT, 1)).to.eql([0, 5]); // "line1"
138
+ });
139
+
140
+ it('returns correct range for line 2', () => {
141
+ expect(lineToRange(TEXT, 2)).to.eql([6, 11]); // "line2"
142
+ });
143
+
144
+ it('returns correct range for line 3', () => {
145
+ expect(lineToRange(TEXT, 3)).to.eql([12, 17]); // "line3"
146
+ });
147
+
148
+ it('clamps line 0 to first line instead of spanning the whole file', () => {
149
+ const [start, end] = lineToRange(TEXT, 0);
150
+ expect(start).to.equal(0);
151
+ expect(end).to.equal(5); // "line1" length = 5, not TEXT.length (17)
152
+ });
153
+
154
+ it('clamps line beyond last to last line instead of spanning the whole file', () => {
155
+ const [start, end] = lineToRange(TEXT, 999);
156
+ expect(start).to.equal(12);
157
+ expect(end).to.equal(17); // "line3"
158
+ });
159
+
160
+ it('handles single-line text with line 0', () => {
161
+ const [start, end] = lineToRange('hello', 0);
162
+ expect(start).to.equal(0);
163
+ expect(end).to.equal(5); // entire single line, NOT text.length (which happens to be the same here)
164
+ });
165
+
166
+ it('does not return the whole file when line is 0', () => {
167
+ const longText = 'first line\nsecond line\nthird line';
168
+ const [, end] = lineToRange(longText, 0);
169
+ // Should be end of first line (10), not end of whole text (33)
170
+ expect(end).to.equal(10);
171
+ expect(end).to.not.equal(longText.length);
172
+ });
173
+ });
@@ -1,20 +1,17 @@
1
1
  import { GraphQLCheckDefinition, Severity, SourceCodeType } from '../../types';
2
2
  import { parse } from 'graphql/language';
3
- import { buildSchema, validate } from 'graphql';
3
+ import { buildSchema, GraphQLError, validate } from 'graphql';
4
4
 
5
- function lineToRange(text: string, line: number): [number, number] {
5
+ export function lineToRange(text: string, line: number): [number, number] {
6
6
  const lines = text.split(/\r?\n/);
7
-
8
- if (line < 1 || line > lines.length) {
9
- return [0, text.length];
10
- }
7
+ const clampedLine = Math.max(1, Math.min(line, lines.length));
11
8
 
12
9
  let start = 0;
13
- for (let i = 0; i < line - 1; i++) {
10
+ for (let i = 0; i < clampedLine - 1; i++) {
14
11
  start += lines[i].length + 1;
15
12
  }
16
13
 
17
- const end = start + lines[line - 1].length;
14
+ const end = start + lines[clampedLine - 1].length;
18
15
  return [start, end];
19
16
  }
20
17
 
@@ -42,11 +39,25 @@ export const GraphQLCheck: GraphQLCheckDefinition = {
42
39
 
43
40
  const graphQLSchema = buildSchema(graphQLSchemaString);
44
41
 
45
- const document = parse(content);
42
+ let document;
43
+ try {
44
+ document = parse(content);
45
+ } catch (e) {
46
+ if (e instanceof GraphQLError) {
47
+ const [start, end] = lineToRange(content, e.locations?.[0]?.line ?? 1);
48
+ context.report({
49
+ message: e.message,
50
+ startIndex: start,
51
+ endIndex: end,
52
+ });
53
+ }
54
+ return;
55
+ }
56
+
46
57
  const errors = validate(graphQLSchema, document);
47
58
 
48
59
  errors.forEach((error) => {
49
- const [start, end] = lineToRange(content, error.locations?.[0].line ?? 0);
60
+ const [start, end] = lineToRange(content, error.locations?.[0]?.line ?? 0);
50
61
  context.report({
51
62
  message: error.message,
52
63
  startIndex: start,
@@ -36,6 +36,9 @@ import { GraphQLCheck } from './graphql';
36
36
  import { UnknownProperty } from './unknown-property';
37
37
  import { InvalidHashAssignTarget } from './invalid-hash-assign-target';
38
38
  import { DuplicateFunctionArguments } from './duplicate-function-arguments';
39
+ import { MissingRenderPartialArguments } from './missing-render-partial-arguments';
40
+ import { NestedGraphQLQuery } from './nested-graphql-query';
41
+ import { MissingPage } from './missing-page';
39
42
 
40
43
  export const allChecks: (
41
44
  | LiquidCheckDefinition
@@ -73,6 +76,9 @@ export const allChecks: (
73
76
  GraphQLCheck,
74
77
  UnknownProperty,
75
78
  InvalidHashAssignTarget,
79
+ MissingRenderPartialArguments,
80
+ NestedGraphQLQuery,
81
+ MissingPage,
76
82
  ];
77
83
 
78
84
  /**
@@ -119,6 +119,32 @@ describe('Module: InvalidHashAssignTarget', () => {
119
119
  expect(offenses).toHaveLength(0);
120
120
  });
121
121
 
122
+ it('should not report an error when hash_assign is used on a function return with variable partial', async () => {
123
+ const app: MockApp = {
124
+ 'file.liquid': `
125
+ {% assign partial_name = 'lib/get_data' %}
126
+ {% function data = partial_name %}
127
+ {% hash_assign data['extra'] = 'value' %}
128
+ `,
129
+ };
130
+
131
+ const offenses = await check(app, [InvalidHashAssignTarget]);
132
+ expect(offenses).toHaveLength(0);
133
+ });
134
+
135
+ it('should not report an error when function uses hash-access result target and hash_assign follows', async () => {
136
+ const app: MockApp = {
137
+ 'file.liquid': `
138
+ {% parse_json my_hash %}{}{% endparse_json %}
139
+ {% function my_hash['result'] = 'lib/get_data' %}
140
+ {% hash_assign my_hash['extra'] = 'value' %}
141
+ `,
142
+ };
143
+
144
+ const offenses = await check(app, [InvalidHashAssignTarget]);
145
+ expect(offenses).toHaveLength(0);
146
+ });
147
+
122
148
  it('should track reassignment and report error on new type', async () => {
123
149
  const app: MockApp = {
124
150
  'file.liquid': `
@@ -7,6 +7,7 @@ import {
7
7
  HashAssignMarkup,
8
8
  GraphQLMarkup,
9
9
  GraphQLInlineMarkup,
10
+ FunctionMarkup,
10
11
  } from '@platformos/liquid-html-parser';
11
12
  import { LiquidCheckDefinition, Severity, SourceCodeType } from '../../types';
12
13
  import { isError } from '../../utils';
@@ -229,12 +230,13 @@ export const InvalidHashAssignTarget: LiquidCheckDefinition = {
229
230
 
230
231
  // {% function result = 'path' %}
231
232
  if (node.name === NamedTags.function && typeof node.markup !== 'string') {
232
- const markup = node.markup as { name: string };
233
- if (markup.name) {
234
- closeTypeRange(markup.name, node.position.start);
233
+ const markup = node.markup as FunctionMarkup;
234
+ const varName = markup.name.name;
235
+ if (varName) {
236
+ closeTypeRange(varName, node.position.start);
235
237
  // Function returns are untyped unless we can infer them
236
238
  variableTypes.push({
237
- name: markup.name,
239
+ name: varName,
238
240
  type: 'untyped',
239
241
  range: [node.position.end],
240
242
  });