patram 0.1.1 → 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.
Files changed (35) hide show
  1. package/lib/build-graph-identity.js +39 -7
  2. package/lib/build-graph.js +14 -1
  3. package/lib/cli-help-metadata.js +552 -0
  4. package/lib/derived-summary.js +278 -0
  5. package/lib/format-derived-summary-row.js +9 -0
  6. package/lib/format-node-header.js +19 -0
  7. package/lib/format-output-item-block.js +22 -0
  8. package/lib/format-output-metadata.js +62 -0
  9. package/lib/layout-stored-queries.js +150 -2
  10. package/lib/load-patram-config.js +401 -2
  11. package/lib/load-patram-config.types.ts +31 -0
  12. package/lib/output-view.types.ts +15 -0
  13. package/lib/parse-cli-arguments-helpers.js +263 -90
  14. package/lib/parse-cli-arguments.js +160 -8
  15. package/lib/parse-cli-arguments.types.ts +48 -3
  16. package/lib/parse-where-clause.js +604 -209
  17. package/lib/parse-where-clause.types.ts +70 -0
  18. package/lib/patram-cli.js +144 -17
  19. package/lib/patram.js +6 -0
  20. package/lib/query-graph.js +231 -119
  21. package/lib/query-inspection.js +523 -0
  22. package/lib/render-check-output.js +1 -1
  23. package/lib/render-cli-help.js +419 -0
  24. package/lib/render-json-output.js +57 -4
  25. package/lib/render-output-view.js +37 -8
  26. package/lib/render-plain-output.js +31 -86
  27. package/lib/render-rich-output.js +34 -87
  28. package/lib/resolve-where-clause.js +18 -3
  29. package/lib/tagged-fenced-block-error.js +17 -0
  30. package/lib/tagged-fenced-block-markdown.js +111 -0
  31. package/lib/tagged-fenced-block-metadata.js +97 -0
  32. package/lib/tagged-fenced-block-parser.js +292 -0
  33. package/lib/tagged-fenced-blocks.js +100 -0
  34. package/lib/tagged-fenced-blocks.types.ts +38 -0
  35. package/package.json +8 -3
@@ -1,82 +1,125 @@
1
+ /* eslint-disable max-lines */
1
2
  /**
2
3
  * @typedef {import('./parse-cli-arguments.types.ts').CliCommandName} CliCommandName
3
4
  * @typedef {import('./parse-cli-arguments.types.ts').CliOutputMode} CliOutputMode
5
+ * @typedef {import('./parse-cli-arguments.types.ts').CliParseError} CliParseError
6
+ * @typedef {import('./parse-cli-arguments.types.ts').ParsedCliHelpRequest} ParsedCliHelpRequest
4
7
  * @typedef {{ kind: string, name?: string, rawName?: string, value?: string | boolean }} CliOptionToken
5
- * @typedef {{ color?: string, json?: boolean, limit?: string, 'no-color'?: boolean, offset?: string, plain?: boolean, where?: string }} CliOptionValues
8
+ * @typedef {{ color?: string, explain?: boolean, help?: boolean, json?: boolean, limit?: string, lint?: boolean, 'no-color'?: boolean, offset?: string, plain?: boolean, where?: string }} CliOptionValues
6
9
  * @typedef {{ option_tokens: CliOptionToken[], positionals: string[], values: CliOptionValues }} ParsedCommandLine
7
- * @typedef {{ allowed_option_names: Set<string>, extra_positionals_message: string, max_positionals: number, min_positionals: number, missing_positionals_message: string }} CommandSchema
8
10
  */
9
11
 
12
+ import {
13
+ findCommandSuggestion,
14
+ findHelpTargetSuggestion,
15
+ findOptionSuggestion,
16
+ getCommandDefinition,
17
+ GLOBAL_OPTION_NAMES,
18
+ isCommandName,
19
+ isHelpTopicName,
20
+ } from './cli-help-metadata.js';
10
21
  import { findInvalidColorMode } from './parse-cli-color-options.js';
11
22
  import { findInvalidQueryPagination } from './parse-cli-query-pagination.js';
12
23
 
13
- const GLOBAL_OPTION_NAMES = new Set(['plain', 'json', 'color', 'no-color']);
14
24
  export const CLI_OPTIONS = /** @type {const} */ ({
15
25
  color: { type: 'string' },
26
+ explain: { type: 'boolean' },
27
+ help: { type: 'boolean' },
16
28
  json: { type: 'boolean' },
17
29
  limit: { type: 'string' },
30
+ lint: { type: 'boolean' },
18
31
  'no-color': { type: 'boolean' },
19
32
  offset: { type: 'string' },
20
33
  plain: { type: 'boolean' },
21
34
  where: { type: 'string' },
22
35
  });
23
- /** @type {Record<CliCommandName, CommandSchema>} */
24
- const COMMAND_SCHEMAS = {
25
- check: createCommandSchema(0, 1, 'Check accepts at most one path.'),
26
- queries: createCommandSchema(
27
- 0,
28
- 0,
29
- 'Queries does not accept positional arguments.',
30
- ),
31
- query: {
32
- ...createCommandSchema(
33
- 0,
34
- 1,
35
- 'Query accepts either "--where" or a stored query name.',
36
- ),
37
- allowed_option_names: new Set(['limit', 'offset', 'where']),
38
- },
39
- show: createCommandSchema(
40
- 1,
41
- 1,
42
- 'Show accepts exactly one file path.',
43
- 'Show requires a file path.',
44
- ),
45
- };
36
+
46
37
  /**
47
- * @param {string | undefined} command_name
48
- * @returns {command_name is CliCommandName}
38
+ * @param {CliOptionToken[]} tokens
39
+ * @returns {CliOptionToken[]}
40
+ */
41
+ export function collectOptionTokens(tokens) {
42
+ return tokens.filter((token) => token.kind === 'option');
43
+ }
44
+
45
+ /**
46
+ * @param {ParsedCommandLine} command_line
47
+ * @returns {CliParseError | null}
49
48
  */
50
- export function isCommandName(command_name) {
49
+ export function validateRootCommandLine(command_line) {
51
50
  return (
52
- command_name === 'check' ||
53
- command_name === 'query' ||
54
- command_name === 'queries' ||
55
- command_name === 'show'
51
+ findUnknownOption(undefined, command_line.option_tokens) ??
52
+ findMissingOptionValue(command_line.option_tokens) ??
53
+ findOutputModeConflict(command_line.values) ??
54
+ createMessageParseError(findInvalidColorMode(command_line.option_tokens))
56
55
  );
57
56
  }
57
+
58
58
  /**
59
- * @param {CliOptionToken[]} tokens
60
- * @returns {CliOptionToken[]}
59
+ * @param {ParsedCommandLine} command_line
60
+ * @returns {CliParseError | null}
61
61
  */
62
- export function collectOptionTokens(tokens) {
63
- return tokens.filter((token) => token.kind === 'option');
62
+ export function validateHelpCommandLine(command_line) {
63
+ const help_validation_error = validateRootCommandLine(command_line);
64
+
65
+ if (help_validation_error) {
66
+ return help_validation_error;
67
+ }
68
+
69
+ if (command_line.positionals.length > 2) {
70
+ return createMessageParseError(
71
+ 'Help accepts at most one topic or command.',
72
+ );
73
+ }
74
+
75
+ return null;
76
+ }
77
+
78
+ /**
79
+ * @param {CliCommandName} command_name
80
+ * @param {ParsedCommandLine} command_line
81
+ * @returns {CliParseError | null}
82
+ */
83
+ export function validateCommandLineBeforeHelp(command_name, command_line) {
84
+ return (
85
+ findUnknownOption(command_name, command_line.option_tokens) ??
86
+ findMissingOptionValue(command_line.option_tokens) ??
87
+ findOutputModeConflict(command_line.values) ??
88
+ createMessageParseError(findInvalidColorMode(command_line.option_tokens))
89
+ );
64
90
  }
91
+
65
92
  /**
66
93
  * @param {CliCommandName} command_name
67
94
  * @param {ParsedCommandLine} command_line
68
- * @returns {string | null}
95
+ * @returns {CliParseError | null}
69
96
  */
70
97
  export function validateParsedCommand(command_name, command_line) {
71
98
  const command_positionals = command_line.positionals.slice(1);
72
99
 
100
+ if (
101
+ command_name === 'query' &&
102
+ command_line.values.where !== undefined &&
103
+ command_positionals.length === 0
104
+ ) {
105
+ return (
106
+ createMessageParseError(
107
+ findInvalidQueryPagination(command_line.option_tokens),
108
+ ) ??
109
+ findInvalidQueryMode(
110
+ command_name,
111
+ command_line.values,
112
+ command_positionals,
113
+ )
114
+ );
115
+ }
116
+
73
117
  return (
74
- findUnknownOption(command_line.option_tokens) ??
75
118
  findInvalidCommandOption(command_name, command_line.option_tokens) ??
76
- findMissingOptionValue(command_line.option_tokens) ??
77
- findOutputModeConflict(command_line.values) ??
78
- findInvalidColorMode(command_line.option_tokens) ??
79
- findInvalidQueryPagination(command_line.option_tokens) ??
119
+ createMessageParseError(
120
+ findInvalidQueryPagination(command_line.option_tokens),
121
+ ) ??
122
+ findInvalidQueryInspection(command_name, command_line.values) ??
80
123
  findInvalidQueryMode(
81
124
  command_name,
82
125
  command_line.values,
@@ -85,6 +128,7 @@ export function validateParsedCommand(command_name, command_line) {
85
128
  validateCommandPositionals(command_name, command_positionals)
86
129
  );
87
130
  }
131
+
88
132
  /**
89
133
  * @param {CliOptionValues} parsed_values
90
134
  * @returns {CliOutputMode}
@@ -100,6 +144,7 @@ export function resolveOutputMode(parsed_values) {
100
144
 
101
145
  return 'default';
102
146
  }
147
+
103
148
  /**
104
149
  * @param {CliCommandName} command_name
105
150
  * @param {string[]} command_positionals
@@ -119,31 +164,108 @@ export function buildCommandArguments(
119
164
  }
120
165
 
121
166
  /**
122
- * @param {string} message
123
- * @returns {{ message: string, success: false }}
167
+ * @param {string | undefined} help_target
168
+ * @returns {CliParseError}
169
+ */
170
+ export function createUnknownHelpTargetError(help_target) {
171
+ return {
172
+ code: 'unknown_help_target',
173
+ suggestion: help_target ? findHelpTargetSuggestion(help_target) : undefined,
174
+ token: help_target ?? '',
175
+ };
176
+ }
177
+
178
+ /**
179
+ * @param {string | undefined} command_name
180
+ * @returns {CliParseError}
124
181
  */
125
- export function createParseError(message) {
182
+ export function createUnknownCommandError(command_name) {
126
183
  return {
184
+ code: 'unknown_command',
185
+ suggestion:
186
+ command_name && command_name.length > 0
187
+ ? findCommandSuggestion(command_name)
188
+ : undefined,
189
+ token: command_name ?? '',
190
+ };
191
+ }
192
+
193
+ /**
194
+ * @param {CliCommandName} command_name
195
+ * @returns {ParsedCliHelpRequest}
196
+ */
197
+ export function createCommandHelpRequest(command_name) {
198
+ return {
199
+ kind: 'help',
200
+ target_kind: 'command',
201
+ target_name: command_name,
202
+ };
203
+ }
204
+
205
+ /**
206
+ * @param {string | undefined} help_target
207
+ * @returns {ParsedCliHelpRequest | null}
208
+ */
209
+ export function createNamedHelpRequest(help_target) {
210
+ if (isCommandName(help_target)) {
211
+ return createCommandHelpRequest(help_target);
212
+ }
213
+
214
+ if (isHelpTopicName(help_target)) {
215
+ return {
216
+ kind: 'help',
217
+ target_kind: 'topic',
218
+ target_name: help_target,
219
+ };
220
+ }
221
+
222
+ return null;
223
+ }
224
+
225
+ /**
226
+ * @returns {ParsedCliHelpRequest}
227
+ */
228
+ export function createRootHelpRequest() {
229
+ return {
230
+ kind: 'help',
231
+ target_kind: 'root',
232
+ };
233
+ }
234
+
235
+ /**
236
+ * @param {string | null} message
237
+ * @returns {CliParseError | null}
238
+ */
239
+ export function createMessageParseError(message) {
240
+ if (!message) {
241
+ return null;
242
+ }
243
+
244
+ return {
245
+ code: 'message',
127
246
  message,
128
- success: false,
129
247
  };
130
248
  }
131
249
 
132
250
  /**
251
+ * @param {CliCommandName | undefined} command_name
133
252
  * @param {CliOptionToken[]} option_tokens
134
- * @returns {string | null}
253
+ * @returns {CliParseError | null}
135
254
  */
136
- function findUnknownOption(option_tokens) {
255
+ function findUnknownOption(command_name, option_tokens) {
137
256
  for (const token of option_tokens) {
138
257
  if (
139
258
  token.name &&
140
259
  token.rawName &&
141
260
  !GLOBAL_OPTION_NAMES.has(token.name) &&
142
- token.name !== 'limit' &&
143
- token.name !== 'offset' &&
144
- token.name !== 'where'
261
+ !isKnownCommandOptionName(token.name)
145
262
  ) {
146
- return `Unknown option "${token.rawName}".`;
263
+ return {
264
+ code: 'unknown_option',
265
+ command_name,
266
+ suggestion: findOptionSuggestion(token.rawName, command_name),
267
+ token: token.rawName,
268
+ };
147
269
  }
148
270
  }
149
271
 
@@ -153,18 +275,22 @@ function findUnknownOption(option_tokens) {
153
275
  /**
154
276
  * @param {CliCommandName} command_name
155
277
  * @param {CliOptionToken[]} option_tokens
156
- * @returns {string | null}
278
+ * @returns {CliParseError | null}
157
279
  */
158
280
  function findInvalidCommandOption(command_name, option_tokens) {
159
- const command_schema = COMMAND_SCHEMAS[command_name];
281
+ const command_definition = getCommandDefinition(command_name);
160
282
 
161
283
  for (const token of option_tokens) {
162
284
  if (!token.name || !token.rawName || GLOBAL_OPTION_NAMES.has(token.name)) {
163
285
  continue;
164
286
  }
165
287
 
166
- if (!command_schema.allowed_option_names.has(token.name)) {
167
- return `Option "${token.rawName}" is not valid for "${command_name}".`;
288
+ if (!command_definition.allowed_option_names.has(token.name)) {
289
+ return {
290
+ code: 'option_not_valid_for_command',
291
+ command_name,
292
+ token: token.rawName,
293
+ };
168
294
  }
169
295
  }
170
296
 
@@ -173,24 +299,28 @@ function findInvalidCommandOption(command_name, option_tokens) {
173
299
 
174
300
  /**
175
301
  * @param {CliOptionToken[]} option_tokens
176
- * @returns {string | null}
302
+ * @returns {CliParseError | null}
177
303
  */
178
304
  function findMissingOptionValue(option_tokens) {
179
305
  for (const token of option_tokens) {
180
306
  if (token.name === 'where' && typeof token.value !== 'string') {
181
- return 'Query requires a where clause.';
307
+ return {
308
+ argument_label: '<name> or --where "<clause>"',
309
+ code: 'missing_required_argument',
310
+ command_name: 'query',
311
+ };
182
312
  }
183
313
 
184
314
  if (token.name === 'offset' && typeof token.value !== 'string') {
185
- return 'Offset requires a value.';
315
+ return createMessageParseError('Offset requires a value.');
186
316
  }
187
317
 
188
318
  if (token.name === 'limit' && typeof token.value !== 'string') {
189
- return 'Limit requires a value.';
319
+ return createMessageParseError('Limit requires a value.');
190
320
  }
191
321
 
192
322
  if (token.name === 'color' && typeof token.value !== 'string') {
193
- return 'Color requires a value.';
323
+ return createMessageParseError('Color requires a value.');
194
324
  }
195
325
  }
196
326
 
@@ -199,11 +329,13 @@ function findMissingOptionValue(option_tokens) {
199
329
 
200
330
  /**
201
331
  * @param {CliOptionValues} parsed_values
202
- * @returns {string | null}
332
+ * @returns {CliParseError | null}
203
333
  */
204
334
  function findOutputModeConflict(parsed_values) {
205
335
  if (parsed_values.plain && parsed_values.json) {
206
- return 'Output mode accepts at most one of "--plain" or "--json".';
336
+ return createMessageParseError(
337
+ 'Output mode accepts at most one of "--plain" or "--json".',
338
+ );
207
339
  }
208
340
 
209
341
  return null;
@@ -213,7 +345,7 @@ function findOutputModeConflict(parsed_values) {
213
345
  * @param {CliCommandName} command_name
214
346
  * @param {CliOptionValues} parsed_values
215
347
  * @param {string[]} command_positionals
216
- * @returns {string | null}
348
+ * @returns {CliParseError | null}
217
349
  */
218
350
  function findInvalidQueryMode(
219
351
  command_name,
@@ -225,7 +357,28 @@ function findInvalidQueryMode(
225
357
  parsed_values.where !== undefined &&
226
358
  command_positionals.length > 0
227
359
  ) {
228
- return 'Query accepts either "--where" or a stored query name.';
360
+ return createMessageParseError(
361
+ 'Query accepts either "--where" or a stored query name.',
362
+ );
363
+ }
364
+
365
+ return null;
366
+ }
367
+
368
+ /**
369
+ * @param {CliCommandName} command_name
370
+ * @param {CliOptionValues} parsed_values
371
+ * @returns {CliParseError | null}
372
+ */
373
+ function findInvalidQueryInspection(command_name, parsed_values) {
374
+ if (command_name !== 'query') {
375
+ return null;
376
+ }
377
+
378
+ if (parsed_values.explain && parsed_values.lint) {
379
+ return createMessageParseError(
380
+ 'Query accepts at most one of "--explain" or "--lint".',
381
+ );
229
382
  }
230
383
 
231
384
  return null;
@@ -234,40 +387,60 @@ function findInvalidQueryMode(
234
387
  /**
235
388
  * @param {CliCommandName} command_name
236
389
  * @param {string[]} command_positionals
237
- * @returns {string | null}
390
+ * @returns {CliParseError | null}
238
391
  */
239
392
  function validateCommandPositionals(command_name, command_positionals) {
240
- const command_schema = COMMAND_SCHEMAS[command_name];
393
+ const command_definition = getCommandDefinition(command_name);
394
+
395
+ if (command_positionals.length < command_definition.min_positionals) {
396
+ if (command_name === 'query' && command_definition.missing_argument_label) {
397
+ return {
398
+ argument_label: command_definition.missing_argument_label,
399
+ code: 'missing_required_argument',
400
+ command_name: 'query',
401
+ };
402
+ }
241
403
 
242
- if (command_positionals.length < command_schema.min_positionals) {
243
- return command_schema.missing_positionals_message;
404
+ if (command_name === 'show' && command_definition.missing_argument_label) {
405
+ return {
406
+ argument_label: command_definition.missing_argument_label,
407
+ code: 'missing_required_argument',
408
+ command_name: 'show',
409
+ };
410
+ }
411
+
412
+ return createMessageParseError(
413
+ command_definition.extra_positionals_message,
414
+ );
244
415
  }
245
416
 
246
- if (command_positionals.length > command_schema.max_positionals) {
247
- return command_schema.extra_positionals_message;
417
+ if (command_positionals.length > command_definition.max_positionals) {
418
+ return createMessageParseError(
419
+ command_definition.extra_positionals_message,
420
+ );
421
+ }
422
+
423
+ if (command_name === 'query' && command_positionals.length === 0) {
424
+ return {
425
+ argument_label: '<name> or --where "<clause>"',
426
+ code: 'missing_required_argument',
427
+ command_name: 'query',
428
+ };
248
429
  }
249
430
 
250
431
  return null;
251
432
  }
252
433
 
253
434
  /**
254
- * @param {number} min_positionals
255
- * @param {number} max_positionals
256
- * @param {string} extra_positionals_message
257
- * @param {string} missing_positionals_message
258
- * @returns {CommandSchema}
435
+ * @param {string} option_name
436
+ * @returns {boolean}
259
437
  */
260
- function createCommandSchema(
261
- min_positionals,
262
- max_positionals,
263
- extra_positionals_message,
264
- missing_positionals_message = '',
265
- ) {
266
- return {
267
- allowed_option_names: new Set(),
268
- extra_positionals_message,
269
- max_positionals,
270
- min_positionals,
271
- missing_positionals_message,
272
- };
438
+ function isKnownCommandOptionName(option_name) {
439
+ return (
440
+ option_name === 'explain' ||
441
+ option_name === 'limit' ||
442
+ option_name === 'lint' ||
443
+ option_name === 'offset' ||
444
+ option_name === 'where'
445
+ );
273
446
  }