patram 0.0.2 → 0.2.0
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/bin/patram.js +25 -147
- package/lib/build-graph-identity.js +270 -0
- package/lib/build-graph.js +156 -77
- package/lib/check-graph.js +23 -7
- package/lib/claim-helpers.js +55 -0
- package/lib/cli-help-metadata.js +552 -0
- package/lib/command-output.js +83 -0
- package/lib/derived-summary.js +278 -0
- package/lib/format-derived-summary-row.js +9 -0
- package/lib/format-node-header.js +19 -0
- package/lib/format-output-item-block.js +22 -0
- package/lib/format-output-metadata.js +62 -0
- package/lib/layout-stored-queries.js +361 -0
- package/lib/list-queries.js +18 -0
- package/lib/list-source-files.js +50 -15
- package/lib/load-patram-config.js +505 -18
- package/lib/load-patram-config.types.ts +40 -0
- package/lib/load-project-graph.js +124 -0
- package/lib/output-view.types.ts +88 -0
- package/lib/parse-claims.js +38 -158
- package/lib/parse-claims.types.ts +7 -0
- package/lib/parse-cli-arguments-helpers.js +446 -0
- package/lib/parse-cli-arguments.js +266 -0
- package/lib/parse-cli-arguments.types.ts +69 -0
- package/lib/parse-cli-color-options.js +44 -0
- package/lib/parse-cli-query-pagination.js +49 -0
- package/lib/parse-jsdoc-blocks.js +184 -0
- package/lib/parse-jsdoc-claims.js +280 -0
- package/lib/parse-jsdoc-prose.js +111 -0
- package/lib/parse-markdown-claims.js +242 -0
- package/lib/parse-markdown-directives.js +136 -0
- package/lib/parse-where-clause.js +707 -0
- package/lib/parse-where-clause.types.ts +70 -0
- package/lib/patram-cli.js +464 -0
- package/lib/patram-config.js +3 -1
- package/lib/patram-config.types.ts +2 -1
- package/lib/patram.js +6 -0
- package/lib/query-graph.js +368 -0
- package/lib/query-inspection.js +523 -0
- package/lib/render-check-output.js +315 -0
- package/lib/render-cli-help.js +419 -0
- package/lib/render-json-output.js +161 -0
- package/lib/render-output-view.js +222 -0
- package/lib/render-plain-output.js +182 -0
- package/lib/render-rich-output.js +240 -0
- package/lib/render-rich-source.js +1333 -0
- package/lib/resolve-check-target.js +190 -0
- package/lib/resolve-output-mode.js +60 -0
- package/lib/resolve-patram-graph-config.js +88 -0
- package/lib/resolve-where-clause.js +66 -0
- package/lib/show-document.js +311 -0
- package/lib/source-file-defaults.js +28 -0
- package/lib/tagged-fenced-block-error.js +17 -0
- package/lib/tagged-fenced-block-markdown.js +111 -0
- package/lib/tagged-fenced-block-metadata.js +97 -0
- package/lib/tagged-fenced-block-parser.js +292 -0
- package/lib/tagged-fenced-blocks.js +100 -0
- package/lib/tagged-fenced-blocks.types.ts +38 -0
- package/lib/write-paged-output.js +87 -0
- package/package.json +28 -12
- package/bin/patram.test.js +0 -184
- package/lib/build-graph.test.js +0 -141
- package/lib/check-graph.test.js +0 -103
- package/lib/list-source-files.test.js +0 -101
- package/lib/load-patram-config.test.js +0 -211
- package/lib/parse-claims.test.js +0 -113
- package/lib/patram-config.test.js +0 -147
|
@@ -0,0 +1,707 @@
|
|
|
1
|
+
/* eslint-disable max-lines */
|
|
2
|
+
/**
|
|
3
|
+
* @import { PatramDiagnostic } from './load-patram-config.types.ts';
|
|
4
|
+
* @import {
|
|
5
|
+
* ParseWhereClauseResult,
|
|
6
|
+
* ParsedAggregateComparison,
|
|
7
|
+
* ParsedAggregateName,
|
|
8
|
+
* ParsedAggregateTerm,
|
|
9
|
+
* ParsedClause,
|
|
10
|
+
* ParsedFieldName,
|
|
11
|
+
* ParsedTerm,
|
|
12
|
+
* ParsedTraversalTerm,
|
|
13
|
+
* } from './parse-where-clause.types.ts';
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* @typedef {{ index: number, where_clause: string }} ParserState
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* @typedef {{
|
|
22
|
+
* success: true,
|
|
23
|
+
* term: ParsedTerm,
|
|
24
|
+
* } | {
|
|
25
|
+
* diagnostic: PatramDiagnostic,
|
|
26
|
+
* success: false,
|
|
27
|
+
* }} ParseTermResult
|
|
28
|
+
*/
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Parse one where clause into structured clauses.
|
|
32
|
+
*
|
|
33
|
+
* @param {string} where_clause
|
|
34
|
+
* @returns {ParseWhereClauseResult}
|
|
35
|
+
*/
|
|
36
|
+
export function parseWhereClause(where_clause) {
|
|
37
|
+
/** @type {ParserState} */
|
|
38
|
+
const parser_state = { index: 0, where_clause };
|
|
39
|
+
|
|
40
|
+
skipWhitespace(parser_state);
|
|
41
|
+
|
|
42
|
+
if (isAtEnd(parser_state)) {
|
|
43
|
+
return fail(1, 'Query must not be empty.');
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const clauses_result = parseClauses(parser_state, null);
|
|
47
|
+
|
|
48
|
+
if (!clauses_result.success) {
|
|
49
|
+
return clauses_result;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
skipWhitespace(parser_state);
|
|
53
|
+
|
|
54
|
+
if (!isAtEnd(parser_state)) {
|
|
55
|
+
return failToken(parser_state);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
return {
|
|
59
|
+
clauses: clauses_result.clauses,
|
|
60
|
+
success: true,
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* @param {ParserState} parser_state
|
|
66
|
+
* @param {')' | null} stop_character
|
|
67
|
+
* @returns {ParseWhereClauseResult}
|
|
68
|
+
*/
|
|
69
|
+
function parseClauses(parser_state, stop_character) {
|
|
70
|
+
/** @type {ParsedClause[]} */
|
|
71
|
+
const clauses = [];
|
|
72
|
+
let is_first_clause = true;
|
|
73
|
+
|
|
74
|
+
while (true) {
|
|
75
|
+
skipWhitespace(parser_state);
|
|
76
|
+
|
|
77
|
+
if (
|
|
78
|
+
currentCharacter(parser_state) === stop_character ||
|
|
79
|
+
isAtEnd(parser_state)
|
|
80
|
+
) {
|
|
81
|
+
return is_first_clause
|
|
82
|
+
? fail(parser_state.where_clause.length + 1, 'Expected a query term.')
|
|
83
|
+
: { clauses, success: true };
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (!is_first_clause) {
|
|
87
|
+
if (!consumeKeyword(parser_state, 'and')) {
|
|
88
|
+
return failToken(parser_state);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
skipWhitespace(parser_state);
|
|
92
|
+
|
|
93
|
+
if (
|
|
94
|
+
currentCharacter(parser_state) === stop_character ||
|
|
95
|
+
isAtEnd(parser_state)
|
|
96
|
+
) {
|
|
97
|
+
return fail(
|
|
98
|
+
parser_state.where_clause.length + 1,
|
|
99
|
+
'Expected a query term.',
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const clause_result = parseClause(parser_state);
|
|
105
|
+
|
|
106
|
+
if (!clause_result.success) {
|
|
107
|
+
return clause_result;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
clauses.push(clause_result.clause);
|
|
111
|
+
is_first_clause = false;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
/**
|
|
116
|
+
* @param {ParserState} parser_state
|
|
117
|
+
* @returns {{ clause: ParsedClause, success: true } | { diagnostic: PatramDiagnostic, success: false }}
|
|
118
|
+
*/
|
|
119
|
+
function parseClause(parser_state) {
|
|
120
|
+
const clause_start = parser_state.index;
|
|
121
|
+
const is_negated = consumeKeyword(parser_state, 'not');
|
|
122
|
+
|
|
123
|
+
if (is_negated && !consumeRequiredWhitespace(parser_state)) {
|
|
124
|
+
return fail(
|
|
125
|
+
clause_start + 1,
|
|
126
|
+
`Unsupported query token "${readToken(parser_state, clause_start)}".`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const term_result = parseTerm(parser_state);
|
|
131
|
+
|
|
132
|
+
if (!term_result.success) {
|
|
133
|
+
return term_result;
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return {
|
|
137
|
+
clause: {
|
|
138
|
+
is_negated,
|
|
139
|
+
term: term_result.term,
|
|
140
|
+
},
|
|
141
|
+
success: true,
|
|
142
|
+
};
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* @param {ParserState} parser_state
|
|
147
|
+
* @returns {ParseTermResult}
|
|
148
|
+
*/
|
|
149
|
+
function parseTerm(parser_state) {
|
|
150
|
+
return parseAggregate(parser_state) ?? parseAtomicTerm(parser_state);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* @param {ParserState} parser_state
|
|
155
|
+
* @returns {ParseTermResult | null}
|
|
156
|
+
*/
|
|
157
|
+
function parseAggregate(parser_state) {
|
|
158
|
+
const start_index = parser_state.index;
|
|
159
|
+
const aggregate_name = parseIdentifier(parser_state);
|
|
160
|
+
|
|
161
|
+
if (
|
|
162
|
+
aggregate_name !== 'any' &&
|
|
163
|
+
aggregate_name !== 'count' &&
|
|
164
|
+
aggregate_name !== 'none'
|
|
165
|
+
) {
|
|
166
|
+
parser_state.index = start_index;
|
|
167
|
+
return null;
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
skipWhitespace(parser_state);
|
|
171
|
+
|
|
172
|
+
if (currentCharacter(parser_state) !== '(') {
|
|
173
|
+
parser_state.index = start_index;
|
|
174
|
+
return null;
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
parser_state.index += 1;
|
|
178
|
+
skipWhitespace(parser_state);
|
|
179
|
+
|
|
180
|
+
const traversal_result = parseTraversal(parser_state);
|
|
181
|
+
|
|
182
|
+
if (!traversal_result.success) {
|
|
183
|
+
return traversal_result;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
if (!consumeOperator(parser_state, ',')) {
|
|
187
|
+
return failToken(parser_state);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
skipWhitespace(parser_state);
|
|
191
|
+
const clauses_result = parseClauses(parser_state, ')');
|
|
192
|
+
|
|
193
|
+
if (!clauses_result.success) {
|
|
194
|
+
return clauses_result;
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (!consumeOperator(parser_state, ')')) {
|
|
198
|
+
return failToken(parser_state);
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
return createAggregateTerm(
|
|
202
|
+
parser_state,
|
|
203
|
+
aggregate_name,
|
|
204
|
+
traversal_result.traversal,
|
|
205
|
+
clauses_result.clauses,
|
|
206
|
+
);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* @param {ParserState} parser_state
|
|
211
|
+
* @returns {ParseTermResult}
|
|
212
|
+
*/
|
|
213
|
+
function parseAtomicTerm(parser_state) {
|
|
214
|
+
const start_index = parser_state.index;
|
|
215
|
+
const field_or_relation_name = parseIdentifier(parser_state);
|
|
216
|
+
|
|
217
|
+
if (!field_or_relation_name) {
|
|
218
|
+
return failToken(parser_state);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return (
|
|
222
|
+
parseFieldSet(parser_state, field_or_relation_name) ??
|
|
223
|
+
parseOperatorTerm(parser_state, start_index, field_or_relation_name)
|
|
224
|
+
);
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
/**
|
|
228
|
+
* @param {ParserState} parser_state
|
|
229
|
+
* @param {ParsedAggregateName} aggregate_name
|
|
230
|
+
* @param {ParsedTraversalTerm} traversal
|
|
231
|
+
* @param {ParsedClause[]} clauses
|
|
232
|
+
* @returns {ParseTermResult}
|
|
233
|
+
*/
|
|
234
|
+
function createAggregateTerm(parser_state, aggregate_name, traversal, clauses) {
|
|
235
|
+
/** @type {ParsedAggregateTerm} */
|
|
236
|
+
const aggregate_term = {
|
|
237
|
+
aggregate_name,
|
|
238
|
+
clauses,
|
|
239
|
+
kind: 'aggregate',
|
|
240
|
+
traversal,
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
if (aggregate_name !== 'count') {
|
|
244
|
+
return success(aggregate_term);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
const count_tail = parseCountTail(parser_state);
|
|
248
|
+
|
|
249
|
+
if (!count_tail) {
|
|
250
|
+
return failToken(parser_state);
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return success({
|
|
254
|
+
...aggregate_term,
|
|
255
|
+
comparison: count_tail.comparison,
|
|
256
|
+
value: count_tail.value,
|
|
257
|
+
});
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/**
|
|
261
|
+
* @param {ParserState} parser_state
|
|
262
|
+
* @param {ParsedFieldName | string} field_name
|
|
263
|
+
* @returns {ParseTermResult | null}
|
|
264
|
+
*/
|
|
265
|
+
function parseFieldSet(parser_state, field_name) {
|
|
266
|
+
if (!isSupportedFieldName(field_name)) {
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
const start_index = parser_state.index;
|
|
271
|
+
|
|
272
|
+
if (!consumeRequiredWhitespace(parser_state)) {
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
const operator = parseSetOperator(parser_state);
|
|
277
|
+
|
|
278
|
+
if (!operator) {
|
|
279
|
+
parser_state.index = start_index;
|
|
280
|
+
return null;
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
if (!consumeRequiredWhitespace(parser_state)) {
|
|
284
|
+
return failToken(parser_state);
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
const values = parseList(parser_state);
|
|
288
|
+
|
|
289
|
+
return values
|
|
290
|
+
? success({ field_name, kind: 'field_set', operator, values })
|
|
291
|
+
: failToken(parser_state);
|
|
292
|
+
}
|
|
293
|
+
|
|
294
|
+
/**
|
|
295
|
+
* @param {ParserState} parser_state
|
|
296
|
+
* @param {number} start_index
|
|
297
|
+
* @param {string} field_or_relation_name
|
|
298
|
+
* @returns {ParseTermResult}
|
|
299
|
+
*/
|
|
300
|
+
function parseOperatorTerm(parser_state, start_index, field_or_relation_name) {
|
|
301
|
+
const prefix_term = parsePrefixTerm(parser_state, field_or_relation_name);
|
|
302
|
+
|
|
303
|
+
if (prefix_term) {
|
|
304
|
+
return prefix_term;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
const contains_term = parseContainsTerm(parser_state, field_or_relation_name);
|
|
308
|
+
|
|
309
|
+
if (contains_term) {
|
|
310
|
+
return contains_term;
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
const equality_term = parseEqualityTerm(
|
|
314
|
+
parser_state,
|
|
315
|
+
start_index,
|
|
316
|
+
field_or_relation_name,
|
|
317
|
+
);
|
|
318
|
+
|
|
319
|
+
if (equality_term) {
|
|
320
|
+
return equality_term;
|
|
321
|
+
}
|
|
322
|
+
|
|
323
|
+
if (consumeOperator(parser_state, ':*')) {
|
|
324
|
+
return success({
|
|
325
|
+
column: start_index + 1,
|
|
326
|
+
kind: 'relation',
|
|
327
|
+
relation_name: field_or_relation_name,
|
|
328
|
+
});
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
parser_state.index = start_index;
|
|
332
|
+
return failToken(parser_state);
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* @param {ParserState} parser_state
|
|
337
|
+
* @returns {{ comparison: ParsedAggregateComparison, value: number } | null}
|
|
338
|
+
*/
|
|
339
|
+
function parseCountTail(parser_state) {
|
|
340
|
+
if (!consumeRequiredWhitespace(parser_state)) {
|
|
341
|
+
return null;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const comparison = parseComparison(parser_state);
|
|
345
|
+
|
|
346
|
+
if (!comparison || !consumeRequiredWhitespace(parser_state)) {
|
|
347
|
+
return null;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
const value = parseInteger(parser_state);
|
|
351
|
+
|
|
352
|
+
return value === null ? null : { comparison, value };
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
/**
|
|
356
|
+
* @param {ParserState} parser_state
|
|
357
|
+
* @returns {{ success: true, traversal: ParsedTraversalTerm } | { diagnostic: PatramDiagnostic, success: false }}
|
|
358
|
+
*/
|
|
359
|
+
function parseTraversal(parser_state) {
|
|
360
|
+
const column = parser_state.index + 1;
|
|
361
|
+
const direction = parseIdentifier(parser_state);
|
|
362
|
+
|
|
363
|
+
if (
|
|
364
|
+
(direction !== 'in' && direction !== 'out') ||
|
|
365
|
+
!consumeOperator(parser_state, ':')
|
|
366
|
+
) {
|
|
367
|
+
return failToken(parser_state);
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
const relation_name = parseIdentifier(parser_state);
|
|
371
|
+
|
|
372
|
+
return relation_name
|
|
373
|
+
? { success: true, traversal: { column, direction, relation_name } }
|
|
374
|
+
: failToken(parser_state);
|
|
375
|
+
}
|
|
376
|
+
|
|
377
|
+
/**
|
|
378
|
+
* @param {ParserState} parser_state
|
|
379
|
+
* @returns {'in' | 'not in' | null}
|
|
380
|
+
*/
|
|
381
|
+
function parseSetOperator(parser_state) {
|
|
382
|
+
if (consumeKeyword(parser_state, 'in')) {
|
|
383
|
+
return 'in';
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
if (
|
|
387
|
+
!consumeKeyword(parser_state, 'not') ||
|
|
388
|
+
!consumeRequiredWhitespace(parser_state)
|
|
389
|
+
) {
|
|
390
|
+
return null;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
return consumeKeyword(parser_state, 'in') ? 'not in' : null;
|
|
394
|
+
}
|
|
395
|
+
|
|
396
|
+
/**
|
|
397
|
+
* @param {ParserState} parser_state
|
|
398
|
+
* @param {string} field_name
|
|
399
|
+
* @returns {ParseTermResult | null}
|
|
400
|
+
*/
|
|
401
|
+
function parsePrefixTerm(parser_state, field_name) {
|
|
402
|
+
if (field_name !== 'id' && field_name !== 'path') {
|
|
403
|
+
return null;
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
if (!consumeOperator(parser_state, '^=')) {
|
|
407
|
+
return null;
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
const value = parseBareValue(parser_state);
|
|
411
|
+
|
|
412
|
+
return value
|
|
413
|
+
? success({ field_name, kind: 'field', operator: '^=', value })
|
|
414
|
+
: failToken(parser_state);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* @param {ParserState} parser_state
|
|
419
|
+
* @param {string} field_name
|
|
420
|
+
* @returns {ParseTermResult | null}
|
|
421
|
+
*/
|
|
422
|
+
function parseContainsTerm(parser_state, field_name) {
|
|
423
|
+
if (field_name !== 'title' || !consumeOperator(parser_state, '~')) {
|
|
424
|
+
return null;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const value = parseBareValue(parser_state);
|
|
428
|
+
|
|
429
|
+
return value
|
|
430
|
+
? success({ field_name: 'title', kind: 'field', operator: '~', value })
|
|
431
|
+
: failToken(parser_state);
|
|
432
|
+
}
|
|
433
|
+
|
|
434
|
+
/**
|
|
435
|
+
* @param {ParserState} parser_state
|
|
436
|
+
* @param {number} start_index
|
|
437
|
+
* @param {string} field_or_relation_name
|
|
438
|
+
* @returns {ParseTermResult | null}
|
|
439
|
+
*/
|
|
440
|
+
function parseEqualityTerm(parser_state, start_index, field_or_relation_name) {
|
|
441
|
+
if (!consumeOperator(parser_state, '=')) {
|
|
442
|
+
return null;
|
|
443
|
+
}
|
|
444
|
+
|
|
445
|
+
const value = parseBareValue(parser_state);
|
|
446
|
+
|
|
447
|
+
if (!value) {
|
|
448
|
+
return failToken(parser_state);
|
|
449
|
+
}
|
|
450
|
+
|
|
451
|
+
if (isExactMatchField(field_or_relation_name)) {
|
|
452
|
+
return success({
|
|
453
|
+
field_name: field_or_relation_name,
|
|
454
|
+
kind: 'field',
|
|
455
|
+
operator: '=',
|
|
456
|
+
value,
|
|
457
|
+
});
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
if (value.includes(':')) {
|
|
461
|
+
return success({
|
|
462
|
+
column: start_index + 1,
|
|
463
|
+
kind: 'relation_target',
|
|
464
|
+
relation_name: field_or_relation_name,
|
|
465
|
+
target_id: value,
|
|
466
|
+
});
|
|
467
|
+
}
|
|
468
|
+
|
|
469
|
+
parser_state.index = start_index;
|
|
470
|
+
return failToken(parser_state);
|
|
471
|
+
}
|
|
472
|
+
|
|
473
|
+
/**
|
|
474
|
+
* @param {ParserState} parser_state
|
|
475
|
+
* @returns {string[] | null}
|
|
476
|
+
*/
|
|
477
|
+
function parseList(parser_state) {
|
|
478
|
+
if (!consumeOperator(parser_state, '[')) {
|
|
479
|
+
return null;
|
|
480
|
+
}
|
|
481
|
+
|
|
482
|
+
/** @type {string[]} */
|
|
483
|
+
const values = [];
|
|
484
|
+
|
|
485
|
+
while (true) {
|
|
486
|
+
skipWhitespace(parser_state);
|
|
487
|
+
|
|
488
|
+
if (consumeOperator(parser_state, ']')) {
|
|
489
|
+
return values.length > 0 ? values : null;
|
|
490
|
+
}
|
|
491
|
+
|
|
492
|
+
const list_value = parseListValue(parser_state);
|
|
493
|
+
|
|
494
|
+
if (!list_value) {
|
|
495
|
+
return null;
|
|
496
|
+
}
|
|
497
|
+
|
|
498
|
+
values.push(list_value);
|
|
499
|
+
skipWhitespace(parser_state);
|
|
500
|
+
|
|
501
|
+
if (consumeOperator(parser_state, ']')) {
|
|
502
|
+
return values;
|
|
503
|
+
}
|
|
504
|
+
|
|
505
|
+
if (!consumeOperator(parser_state, ',')) {
|
|
506
|
+
return null;
|
|
507
|
+
}
|
|
508
|
+
}
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
/**
|
|
512
|
+
* @param {ParserState} parser_state
|
|
513
|
+
* @returns {ParsedAggregateComparison | null}
|
|
514
|
+
*/
|
|
515
|
+
function parseComparison(parser_state) {
|
|
516
|
+
/** @type {ParsedAggregateComparison[]} */
|
|
517
|
+
const comparisons = ['>=', '<=', '!=', '=', '>', '<'];
|
|
518
|
+
|
|
519
|
+
return (
|
|
520
|
+
comparisons.find((value) => consumeOperator(parser_state, value)) ?? null
|
|
521
|
+
);
|
|
522
|
+
}
|
|
523
|
+
|
|
524
|
+
/**
|
|
525
|
+
* @param {ParserState} parser_state
|
|
526
|
+
* @returns {string | null}
|
|
527
|
+
*/
|
|
528
|
+
function parseIdentifier(parser_state) {
|
|
529
|
+
return readMatch(parser_state, /^[a-z_]+/u);
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
/**
|
|
533
|
+
* @param {ParserState} parser_state
|
|
534
|
+
* @returns {number | null}
|
|
535
|
+
*/
|
|
536
|
+
function parseInteger(parser_state) {
|
|
537
|
+
const numeric_text = readMatch(parser_state, /^\d+/u);
|
|
538
|
+
return numeric_text ? Number.parseInt(numeric_text, 10) : null;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* @param {ParserState} parser_state
|
|
543
|
+
* @returns {string | null}
|
|
544
|
+
*/
|
|
545
|
+
function parseBareValue(parser_state) {
|
|
546
|
+
return readMatch(parser_state, /^[^\s\],)]+/u);
|
|
547
|
+
}
|
|
548
|
+
|
|
549
|
+
/**
|
|
550
|
+
* @param {ParserState} parser_state
|
|
551
|
+
* @returns {string | null}
|
|
552
|
+
*/
|
|
553
|
+
function parseListValue(parser_state) {
|
|
554
|
+
const list_value = readMatch(parser_state, /^[^\s\],][^\],)]*/u);
|
|
555
|
+
return list_value?.trim() || null;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
/**
|
|
559
|
+
* @param {ParserState} parser_state
|
|
560
|
+
* @param {RegExp} pattern
|
|
561
|
+
* @returns {string | null}
|
|
562
|
+
*/
|
|
563
|
+
function readMatch(parser_state, pattern) {
|
|
564
|
+
const match = parser_state.where_clause
|
|
565
|
+
.slice(parser_state.index)
|
|
566
|
+
.match(pattern);
|
|
567
|
+
|
|
568
|
+
if (!match) {
|
|
569
|
+
return null;
|
|
570
|
+
}
|
|
571
|
+
|
|
572
|
+
parser_state.index += match[0].length;
|
|
573
|
+
return match[0];
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
/**
|
|
577
|
+
* @param {ParserState} parser_state
|
|
578
|
+
* @param {string} operator
|
|
579
|
+
* @returns {boolean}
|
|
580
|
+
*/
|
|
581
|
+
function consumeOperator(parser_state, operator) {
|
|
582
|
+
skipWhitespace(parser_state);
|
|
583
|
+
|
|
584
|
+
if (!parser_state.where_clause.startsWith(operator, parser_state.index)) {
|
|
585
|
+
return false;
|
|
586
|
+
}
|
|
587
|
+
|
|
588
|
+
parser_state.index += operator.length;
|
|
589
|
+
return true;
|
|
590
|
+
}
|
|
591
|
+
|
|
592
|
+
/**
|
|
593
|
+
* @param {ParserState} parser_state
|
|
594
|
+
* @param {string} keyword
|
|
595
|
+
* @returns {boolean}
|
|
596
|
+
*/
|
|
597
|
+
function consumeKeyword(parser_state, keyword) {
|
|
598
|
+
if (!parser_state.where_clause.startsWith(keyword, parser_state.index)) {
|
|
599
|
+
return false;
|
|
600
|
+
}
|
|
601
|
+
|
|
602
|
+
const next_character =
|
|
603
|
+
parser_state.where_clause[parser_state.index + keyword.length];
|
|
604
|
+
|
|
605
|
+
if (next_character && /[a-z_]/u.test(next_character)) {
|
|
606
|
+
return false;
|
|
607
|
+
}
|
|
608
|
+
|
|
609
|
+
parser_state.index += keyword.length;
|
|
610
|
+
return true;
|
|
611
|
+
}
|
|
612
|
+
|
|
613
|
+
/**
|
|
614
|
+
* @param {ParserState} parser_state
|
|
615
|
+
* @returns {boolean}
|
|
616
|
+
*/
|
|
617
|
+
function consumeRequiredWhitespace(parser_state) {
|
|
618
|
+
return skipWhitespace(parser_state) > 0;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
/**
|
|
622
|
+
* @param {ParserState} parser_state
|
|
623
|
+
* @returns {number}
|
|
624
|
+
*/
|
|
625
|
+
function skipWhitespace(parser_state) {
|
|
626
|
+
const whitespace = readMatch(parser_state, /^\s+/u);
|
|
627
|
+
return whitespace?.length ?? 0;
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
/**
|
|
631
|
+
* @param {ParserState} parser_state
|
|
632
|
+
* @returns {string | undefined}
|
|
633
|
+
*/
|
|
634
|
+
function currentCharacter(parser_state) {
|
|
635
|
+
return parser_state.where_clause[parser_state.index];
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
/**
|
|
639
|
+
* @param {ParserState} parser_state
|
|
640
|
+
* @returns {boolean}
|
|
641
|
+
*/
|
|
642
|
+
function isAtEnd(parser_state) {
|
|
643
|
+
return parser_state.index >= parser_state.where_clause.length;
|
|
644
|
+
}
|
|
645
|
+
|
|
646
|
+
/**
|
|
647
|
+
* @param {string} field_name
|
|
648
|
+
* @returns {field_name is ParsedFieldName}
|
|
649
|
+
*/
|
|
650
|
+
function isSupportedFieldName(field_name) {
|
|
651
|
+
return ['id', 'kind', 'path', 'status', 'title'].includes(field_name);
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
/**
|
|
655
|
+
* @param {string} field_name
|
|
656
|
+
* @returns {field_name is 'id' | 'kind' | 'path' | 'status'}
|
|
657
|
+
*/
|
|
658
|
+
function isExactMatchField(field_name) {
|
|
659
|
+
return ['id', 'kind', 'path', 'status'].includes(field_name);
|
|
660
|
+
}
|
|
661
|
+
|
|
662
|
+
/**
|
|
663
|
+
* @param {ParsedTerm} term
|
|
664
|
+
* @returns {ParseTermResult}
|
|
665
|
+
*/
|
|
666
|
+
function success(term) {
|
|
667
|
+
return { success: true, term };
|
|
668
|
+
}
|
|
669
|
+
|
|
670
|
+
/**
|
|
671
|
+
* @param {ParserState} parser_state
|
|
672
|
+
* @returns {{ diagnostic: PatramDiagnostic, success: false }}
|
|
673
|
+
*/
|
|
674
|
+
function failToken(parser_state) {
|
|
675
|
+
return fail(
|
|
676
|
+
parser_state.index + 1,
|
|
677
|
+
`Unsupported query token "${readToken(parser_state, parser_state.index)}".`,
|
|
678
|
+
);
|
|
679
|
+
}
|
|
680
|
+
|
|
681
|
+
/**
|
|
682
|
+
* @param {number} column
|
|
683
|
+
* @param {string} message
|
|
684
|
+
* @returns {{ diagnostic: PatramDiagnostic, success: false }}
|
|
685
|
+
*/
|
|
686
|
+
function fail(column, message) {
|
|
687
|
+
return {
|
|
688
|
+
diagnostic: {
|
|
689
|
+
code: 'query.invalid',
|
|
690
|
+
column,
|
|
691
|
+
level: 'error',
|
|
692
|
+
line: 1,
|
|
693
|
+
message,
|
|
694
|
+
path: '<query>',
|
|
695
|
+
},
|
|
696
|
+
success: false,
|
|
697
|
+
};
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/**
|
|
701
|
+
* @param {ParserState} parser_state
|
|
702
|
+
* @param {number} start_index
|
|
703
|
+
* @returns {string}
|
|
704
|
+
*/
|
|
705
|
+
function readToken(parser_state, start_index) {
|
|
706
|
+
return parser_state.where_clause.slice(start_index).match(/^\S+/u)?.[0] ?? '';
|
|
707
|
+
}
|