patram 0.0.2 → 0.1.1

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 (51) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +238 -0
  3. package/lib/build-graph.js +143 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/command-output.js +83 -0
  7. package/lib/layout-stored-queries.js +213 -0
  8. package/lib/list-queries.js +18 -0
  9. package/lib/list-source-files.js +50 -15
  10. package/lib/load-patram-config.js +106 -18
  11. package/lib/load-patram-config.types.ts +9 -0
  12. package/lib/load-project-graph.js +124 -0
  13. package/lib/output-view.types.ts +73 -0
  14. package/lib/parse-claims.js +38 -158
  15. package/lib/parse-claims.types.ts +7 -0
  16. package/lib/parse-cli-arguments-helpers.js +273 -0
  17. package/lib/parse-cli-arguments.js +114 -0
  18. package/lib/parse-cli-arguments.types.ts +24 -0
  19. package/lib/parse-cli-color-options.js +44 -0
  20. package/lib/parse-cli-query-pagination.js +49 -0
  21. package/lib/parse-jsdoc-blocks.js +184 -0
  22. package/lib/parse-jsdoc-claims.js +280 -0
  23. package/lib/parse-jsdoc-prose.js +111 -0
  24. package/lib/parse-markdown-claims.js +242 -0
  25. package/lib/parse-markdown-directives.js +136 -0
  26. package/lib/parse-where-clause.js +312 -0
  27. package/lib/patram-cli.js +337 -0
  28. package/lib/patram-config.js +3 -1
  29. package/lib/patram-config.types.ts +2 -1
  30. package/lib/query-graph.js +256 -0
  31. package/lib/render-check-output.js +315 -0
  32. package/lib/render-json-output.js +108 -0
  33. package/lib/render-output-view.js +193 -0
  34. package/lib/render-plain-output.js +237 -0
  35. package/lib/render-rich-output.js +293 -0
  36. package/lib/render-rich-source.js +1333 -0
  37. package/lib/resolve-check-target.js +190 -0
  38. package/lib/resolve-output-mode.js +60 -0
  39. package/lib/resolve-patram-graph-config.js +88 -0
  40. package/lib/resolve-where-clause.js +51 -0
  41. package/lib/show-document.js +311 -0
  42. package/lib/source-file-defaults.js +28 -0
  43. package/lib/write-paged-output.js +87 -0
  44. package/package.json +21 -10
  45. package/bin/patram.test.js +0 -184
  46. package/lib/build-graph.test.js +0 -141
  47. package/lib/check-graph.test.js +0 -103
  48. package/lib/list-source-files.test.js +0 -101
  49. package/lib/load-patram-config.test.js +0 -211
  50. package/lib/parse-claims.test.js +0 -113
  51. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,136 @@
1
+ /**
2
+ * @import { PatramClaimFields } from './parse-claims.types.ts';
3
+ */
4
+
5
+ const FRONT_MATTER_BOUNDARY_PATTERN = /^---$/du;
6
+ const FRONT_MATTER_DIRECTIVE_PATTERN = /^([A-Za-z][A-Za-z0-9 _-]*):\s+(.+)$/du;
7
+ const MARKDOWN_HIDDEN_DIRECTIVE_PATTERN =
8
+ /^\[patram\s+([A-Za-z][A-Za-z0-9 _-]*)=(.+)\]:\s*#\s*$/du;
9
+ const VISIBLE_DIRECTIVE_PATTERN = /^(?:-\s+)?([A-Z][A-Za-z _-]*):\s+(.+)$/du;
10
+
11
+ /**
12
+ * @param {string} file_path
13
+ * @param {string[]} lines
14
+ * @returns {{ body_start: number, directive_fields: PatramClaimFields[] }}
15
+ */
16
+ export function parseFrontMatterDirectiveFields(file_path, lines) {
17
+ if (lines[0] !== '---') {
18
+ return {
19
+ body_start: 0,
20
+ directive_fields: [],
21
+ };
22
+ }
23
+
24
+ const closing_line_index = findFrontMatterClosingLineIndex(lines);
25
+
26
+ if (closing_line_index < 0) {
27
+ return {
28
+ body_start: 0,
29
+ directive_fields: [],
30
+ };
31
+ }
32
+
33
+ /** @type {PatramClaimFields[]} */
34
+ const directive_fields = [];
35
+
36
+ for (let line_index = 1; line_index < closing_line_index; line_index += 1) {
37
+ const directive_fields_match = matchDirectiveFields(
38
+ FRONT_MATTER_DIRECTIVE_PATTERN,
39
+ file_path,
40
+ lines[line_index],
41
+ line_index + 1,
42
+ );
43
+
44
+ if (!directive_fields_match) {
45
+ continue;
46
+ }
47
+
48
+ directive_fields.push(directive_fields_match);
49
+ }
50
+
51
+ return {
52
+ body_start: closing_line_index + 1,
53
+ directive_fields,
54
+ };
55
+ }
56
+
57
+ /**
58
+ * @param {string} file_path
59
+ * @param {string} line
60
+ * @param {number} line_number
61
+ * @returns {PatramClaimFields | null}
62
+ */
63
+ export function matchVisibleDirectiveFields(file_path, line, line_number) {
64
+ return matchDirectiveFields(
65
+ VISIBLE_DIRECTIVE_PATTERN,
66
+ file_path,
67
+ line,
68
+ line_number,
69
+ );
70
+ }
71
+
72
+ /**
73
+ * @param {string} file_path
74
+ * @param {string} line
75
+ * @param {number} line_number
76
+ * @returns {PatramClaimFields | null}
77
+ */
78
+ export function matchHiddenDirectiveFields(file_path, line, line_number) {
79
+ return matchDirectiveFields(
80
+ MARKDOWN_HIDDEN_DIRECTIVE_PATTERN,
81
+ file_path,
82
+ line,
83
+ line_number,
84
+ );
85
+ }
86
+
87
+ /**
88
+ * @param {string[]} lines
89
+ * @returns {number}
90
+ */
91
+ function findFrontMatterClosingLineIndex(lines) {
92
+ for (let line_index = 1; line_index < lines.length; line_index += 1) {
93
+ if (FRONT_MATTER_BOUNDARY_PATTERN.test(lines[line_index])) {
94
+ return line_index;
95
+ }
96
+ }
97
+
98
+ return -1;
99
+ }
100
+
101
+ /**
102
+ * @param {RegExp} pattern
103
+ * @param {string} file_path
104
+ * @param {string} line
105
+ * @param {number} line_number
106
+ * @returns {PatramClaimFields | null}
107
+ */
108
+ function matchDirectiveFields(pattern, file_path, line, line_number) {
109
+ const directive_match = line.match(pattern);
110
+
111
+ if (!directive_match) {
112
+ return null;
113
+ }
114
+
115
+ return {
116
+ name: normalizeDirectiveName(directive_match[1]),
117
+ origin: {
118
+ column: 1,
119
+ line: line_number,
120
+ path: file_path,
121
+ },
122
+ parser: 'markdown',
123
+ value: directive_match[2].trim(),
124
+ };
125
+ }
126
+
127
+ /**
128
+ * @param {string} directive_label
129
+ * @returns {string}
130
+ */
131
+ export function normalizeDirectiveName(directive_label) {
132
+ return directive_label
133
+ .trim()
134
+ .toLowerCase()
135
+ .replaceAll(/[\s-]+/dgu, '_');
136
+ }
@@ -0,0 +1,312 @@
1
+ /**
2
+ * @import { PatramDiagnostic } from './load-patram-config.types.ts';
3
+ */
4
+
5
+ /**
6
+ * @typedef {{
7
+ * field_name: 'id' | 'kind' | 'path' | 'status' | 'title',
8
+ * kind: 'field',
9
+ * operator: '=' | '^=' | '~',
10
+ * value: string,
11
+ * }} ParsedFieldTerm
12
+ */
13
+
14
+ /**
15
+ * @typedef {{
16
+ * kind: 'relation',
17
+ * relation_name: string,
18
+ * }} ParsedRelationTerm
19
+ */
20
+
21
+ /**
22
+ * @typedef {{
23
+ * kind: 'relation_target',
24
+ * relation_name: string,
25
+ * target_id: string,
26
+ * }} ParsedRelationTargetTerm
27
+ */
28
+
29
+ /**
30
+ * @typedef {{
31
+ * is_negated: boolean,
32
+ * term: ParsedFieldTerm | ParsedRelationTerm | ParsedRelationTargetTerm,
33
+ * }} ParsedClause
34
+ */
35
+
36
+ /**
37
+ * @typedef {{
38
+ * clause: ParsedClause,
39
+ * success: true,
40
+ * } | {
41
+ * diagnostic: PatramDiagnostic,
42
+ * success: false,
43
+ * }} CreateClauseResult
44
+ */
45
+
46
+ /**
47
+ * @typedef {{
48
+ * clauses: ParsedClause[],
49
+ * success: true,
50
+ * } | {
51
+ * diagnostic: PatramDiagnostic,
52
+ * success: false,
53
+ * }} ParseWhereClauseResult
54
+ */
55
+
56
+ /**
57
+ * Parse one v0 where clause into structured clauses.
58
+ *
59
+ * @param {string} where_clause
60
+ * @returns {ParseWhereClauseResult}
61
+ */
62
+ export function parseWhereClause(where_clause) {
63
+ const tokens = tokenizeWhereClause(where_clause);
64
+
65
+ if (tokens.length === 0) {
66
+ return {
67
+ diagnostic: createQueryDiagnostic(1, 'Query must not be empty.'),
68
+ success: false,
69
+ };
70
+ }
71
+
72
+ return parseTokens(tokens, where_clause);
73
+ }
74
+
75
+ /**
76
+ * @param {{ value: string, column: number }[]} tokens
77
+ * @param {string} where_clause
78
+ * @returns {ParseWhereClauseResult}
79
+ */
80
+ function parseTokens(tokens, where_clause) {
81
+ /** @type {ParsedClause[]} */
82
+ const clauses = [];
83
+ let should_expect_term = true;
84
+ let is_negated = false;
85
+
86
+ for (const token of tokens) {
87
+ if (token.value === 'and') {
88
+ if (should_expect_term) {
89
+ return {
90
+ diagnostic: createQueryDiagnostic(
91
+ token.column,
92
+ `Unsupported query token "${token.value}".`,
93
+ ),
94
+ success: false,
95
+ };
96
+ }
97
+
98
+ should_expect_term = true;
99
+ continue;
100
+ }
101
+
102
+ if (!should_expect_term) {
103
+ return {
104
+ diagnostic: createQueryDiagnostic(
105
+ token.column,
106
+ `Unsupported query token "${token.value}".`,
107
+ ),
108
+ success: false,
109
+ };
110
+ }
111
+
112
+ if (token.value === 'not') {
113
+ is_negated = true;
114
+ continue;
115
+ }
116
+
117
+ const clause_result = createClause(token, is_negated);
118
+
119
+ if (!clause_result.success) {
120
+ return clause_result;
121
+ }
122
+
123
+ clauses.push(clause_result.clause);
124
+ is_negated = false;
125
+ should_expect_term = false;
126
+ }
127
+
128
+ if (should_expect_term) {
129
+ return {
130
+ diagnostic: createQueryDiagnostic(
131
+ where_clause.length + 1,
132
+ 'Expected a query term.',
133
+ ),
134
+ success: false,
135
+ };
136
+ }
137
+
138
+ return {
139
+ clauses,
140
+ success: true,
141
+ };
142
+ }
143
+
144
+ /**
145
+ * @param {{ value: string, column: number }} token
146
+ * @param {boolean} is_negated
147
+ * @returns {CreateClauseResult}
148
+ */
149
+ function createClause(token, is_negated) {
150
+ const field_term = createFieldTerm(token.value);
151
+
152
+ if (field_term) {
153
+ return {
154
+ clause: {
155
+ is_negated,
156
+ term: field_term,
157
+ },
158
+ success: true,
159
+ };
160
+ }
161
+
162
+ const relation_target_term = createRelationTargetTerm(token.value);
163
+
164
+ if (relation_target_term) {
165
+ return {
166
+ clause: {
167
+ is_negated,
168
+ term: relation_target_term,
169
+ },
170
+ success: true,
171
+ };
172
+ }
173
+
174
+ if (/^[a-z_]+:\*$/u.test(token.value)) {
175
+ return {
176
+ clause: {
177
+ is_negated,
178
+ term: {
179
+ kind: 'relation',
180
+ relation_name: token.value.slice(0, -2),
181
+ },
182
+ },
183
+ success: true,
184
+ };
185
+ }
186
+
187
+ return {
188
+ diagnostic: createQueryDiagnostic(
189
+ token.column,
190
+ `Unsupported query token "${token.value}".`,
191
+ ),
192
+ success: false,
193
+ };
194
+ }
195
+
196
+ /**
197
+ * @param {string} query_term
198
+ * @returns {ParsedFieldTerm | null}
199
+ */
200
+ function createFieldTerm(query_term) {
201
+ if (query_term.startsWith('id=')) {
202
+ return {
203
+ field_name: 'id',
204
+ kind: 'field',
205
+ operator: '=',
206
+ value: query_term.slice('id='.length),
207
+ };
208
+ }
209
+
210
+ if (query_term.startsWith('id^=')) {
211
+ return {
212
+ field_name: 'id',
213
+ kind: 'field',
214
+ operator: '^=',
215
+ value: query_term.slice('id^='.length),
216
+ };
217
+ }
218
+
219
+ if (query_term.startsWith('kind=')) {
220
+ return {
221
+ field_name: 'kind',
222
+ kind: 'field',
223
+ operator: '=',
224
+ value: query_term.slice('kind='.length),
225
+ };
226
+ }
227
+
228
+ if (query_term.startsWith('status=')) {
229
+ return {
230
+ field_name: 'status',
231
+ kind: 'field',
232
+ operator: '=',
233
+ value: query_term.slice('status='.length),
234
+ };
235
+ }
236
+
237
+ if (query_term.startsWith('path=')) {
238
+ return {
239
+ field_name: 'path',
240
+ kind: 'field',
241
+ operator: '=',
242
+ value: query_term.slice('path='.length),
243
+ };
244
+ }
245
+
246
+ if (query_term.startsWith('path^=')) {
247
+ return {
248
+ field_name: 'path',
249
+ kind: 'field',
250
+ operator: '^=',
251
+ value: query_term.slice('path^='.length),
252
+ };
253
+ }
254
+
255
+ if (query_term.startsWith('title~')) {
256
+ return {
257
+ field_name: 'title',
258
+ kind: 'field',
259
+ operator: '~',
260
+ value: query_term.slice('title~'.length),
261
+ };
262
+ }
263
+
264
+ return null;
265
+ }
266
+
267
+ /**
268
+ * @param {string} query_term
269
+ * @returns {ParsedRelationTargetTerm | null}
270
+ */
271
+ function createRelationTargetTerm(query_term) {
272
+ const relation_target_match = query_term.match(
273
+ /^(?<relation_name>[a-z_]+)=(?<target_id>[^\s=:]+:[^\s]+)$/u,
274
+ );
275
+
276
+ if (!relation_target_match?.groups) {
277
+ return null;
278
+ }
279
+
280
+ return {
281
+ kind: 'relation_target',
282
+ relation_name: relation_target_match.groups.relation_name,
283
+ target_id: relation_target_match.groups.target_id,
284
+ };
285
+ }
286
+
287
+ /**
288
+ * @param {number} column_number
289
+ * @param {string} message
290
+ * @returns {PatramDiagnostic}
291
+ */
292
+ function createQueryDiagnostic(column_number, message) {
293
+ return {
294
+ code: 'query.invalid',
295
+ column: column_number,
296
+ level: 'error',
297
+ line: 1,
298
+ message,
299
+ path: '<query>',
300
+ };
301
+ }
302
+
303
+ /**
304
+ * @param {string} where_clause
305
+ * @returns {{ value: string, column: number }[]}
306
+ */
307
+ function tokenizeWhereClause(where_clause) {
308
+ return [...where_clause.matchAll(/\S+/gu)].map((token_match) => ({
309
+ column: (token_match.index ?? 0) + 1,
310
+ value: token_match[0],
311
+ }));
312
+ }