patram 0.9.0 → 0.11.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 (33) hide show
  1. package/lib/cli/arguments.types.d.ts +64 -0
  2. package/lib/cli/commands/check.js +27 -15
  3. package/lib/cli/commands/queries.js +189 -1
  4. package/lib/cli/commands/query.js +6 -3
  5. package/lib/cli/help-metadata.js +45 -110
  6. package/lib/cli/parse-arguments-helpers.js +295 -39
  7. package/lib/cli/render-help.js +87 -0
  8. package/lib/config/load-patram-config.d.ts +11 -0
  9. package/lib/config/load-patram-config.js +9 -88
  10. package/lib/config/manage-stored-queries-helpers.d.ts +69 -0
  11. package/lib/config/manage-stored-queries-helpers.js +262 -0
  12. package/lib/config/manage-stored-queries-jsonc.d.ts +31 -0
  13. package/lib/config/manage-stored-queries-jsonc.js +95 -0
  14. package/lib/config/manage-stored-queries.d.ts +77 -0
  15. package/lib/config/manage-stored-queries.js +294 -0
  16. package/lib/config/schema.d.ts +2 -0
  17. package/lib/config/schema.js +4 -0
  18. package/lib/config/validate-patram-config-value.d.ts +13 -0
  19. package/lib/config/validate-patram-config-value.js +119 -0
  20. package/lib/find-close-match.d.ts +8 -0
  21. package/lib/find-close-match.js +98 -0
  22. package/lib/graph/query/resolve.d.ts +9 -5
  23. package/lib/graph/query/resolve.js +41 -4
  24. package/lib/output/layout-stored-queries.js +18 -2
  25. package/lib/output/list-queries.js +2 -1
  26. package/lib/output/renderers/json.js +9 -5
  27. package/lib/output/renderers/plain.js +15 -26
  28. package/lib/output/renderers/rich.js +22 -26
  29. package/lib/output/resolve-check-target.js +120 -11
  30. package/lib/output/view-model/index.js +5 -18
  31. package/lib/patram.d.ts +8 -0
  32. package/lib/scan/discover-fields.js +136 -10
  33. package/package.json +2 -1
@@ -10,7 +10,7 @@ const MIN_TERM_COLUMN_WIDTH = 20;
10
10
  const STORED_QUERY_COLUMN_GAP = 2;
11
11
 
12
12
  /**
13
- * @typedef {'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain'} StoredQuerySegmentKind
13
+ * @typedef {'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain'} StoredQuerySegmentKind
14
14
  */
15
15
 
16
16
  /**
@@ -56,7 +56,8 @@ function layoutStoredQuery(output_item, name_column_width, term_column_width) {
56
56
  name_column_width + STORED_QUERY_COLUMN_GAP,
57
57
  );
58
58
 
59
- return term_lines.map((line_segments, line_index) => {
59
+ /** @type {StoredQuerySegment[][]} */
60
+ const output_lines = term_lines.map((line_segments, line_index) => {
60
61
  if (line_index === 0) {
61
62
  return [
62
63
  {
@@ -79,6 +80,21 @@ function layoutStoredQuery(output_item, name_column_width, term_column_width) {
79
80
  ...line_segments,
80
81
  ];
81
82
  });
83
+
84
+ if (output_item.description) {
85
+ for (const description_line of output_item.description.split('\n')) {
86
+ output_lines.push([
87
+ {
88
+ kind: 'description',
89
+ text: `${' '.repeat(name_column_width + STORED_QUERY_COLUMN_GAP)}${description_line}`,
90
+ },
91
+ ]);
92
+ }
93
+ }
94
+
95
+ output_lines.push([]);
96
+
97
+ return output_lines;
82
98
  }
83
99
 
84
100
  /**
@@ -6,12 +6,13 @@
6
6
  * List stored queries in stable name order.
7
7
  *
8
8
  * @param {Record<string, StoredQueryConfig>} stored_queries
9
- * @returns {{ name: string, where: string }[]}
9
+ * @returns {{ name: string, where: string, description?: string }[]}
10
10
  */
11
11
  export function listQueries(stored_queries) {
12
12
  return Object.entries(stored_queries)
13
13
  .sort(([left_name], [right_name]) => left_name.localeCompare(right_name))
14
14
  .map(([name, stored_query]) => ({
15
+ description: stored_query.description,
15
16
  name,
16
17
  where: stored_query.where,
17
18
  }));
@@ -92,9 +92,6 @@ function renderJsonRefsOutput(output_view) {
92
92
  function renderJsonShowOutput(output_view) {
93
93
  return `${JSON.stringify(
94
94
  {
95
- document: output_view.document
96
- ? formatJsonNodeItem(output_view.document)
97
- : undefined,
98
95
  incoming_summary: output_view.incoming_summary,
99
96
  source: output_view.source,
100
97
  resolved_links: output_view.items.map(formatJsonResolvedLink),
@@ -144,13 +141,20 @@ function formatJsonNodeItem(output_item) {
144
141
 
145
142
  /**
146
143
  * @param {OutputStoredQueryItem} output_item
147
- * @returns {{ name: string, where: string }}
144
+ * @returns {{ name: string, where: string, description?: string }}
148
145
  */
149
146
  function formatJsonStoredQuery(output_item) {
150
- return {
147
+ /** @type {{ description?: string, name: string, where: string }} */
148
+ const stored_query = {
151
149
  name: output_item.name,
152
150
  where: output_item.where,
153
151
  };
152
+
153
+ if (output_item.description) {
154
+ stored_query.description = output_item.description;
155
+ }
156
+
157
+ return stored_query;
154
158
  }
155
159
 
156
160
  /**
@@ -24,7 +24,7 @@ export function renderPlainOutput(output_view) {
24
24
  }
25
25
 
26
26
  if (output_view.command === 'queries') {
27
- return renderPlainStoredQueries(output_view.items);
27
+ return renderPlainStoredQueries(output_view);
28
28
  }
29
29
 
30
30
  if (output_view.command === 'refs') {
@@ -69,17 +69,23 @@ function renderPlainEmptyQuery(footer) {
69
69
  }
70
70
 
71
71
  /**
72
- * @param {OutputStoredQueryItem[]} output_items
72
+ * @param {Extract<OutputView, { command: 'queries' }>} output_view
73
73
  * @returns {string}
74
74
  */
75
- function renderPlainStoredQueries(output_items) {
76
- if (output_items.length === 0) {
75
+ function renderPlainStoredQueries(output_view) {
76
+ if (output_view.items.length === 0) {
77
77
  return '';
78
78
  }
79
79
 
80
- return `${layoutStoredQueries(output_items)
81
- .map(formatPlainStoredQueryLine)
82
- .join('\n')}\n`;
80
+ const output_lines = layoutStoredQueries(output_view.items).map(
81
+ formatPlainStoredQueryLine,
82
+ );
83
+
84
+ if (output_view.hints.length === 0) {
85
+ return `${output_lines.join('\n')}\n`;
86
+ }
87
+
88
+ return `${output_lines.join('\n')}\n${output_view.hints.join('\n')}\n`;
83
89
  }
84
90
 
85
91
  /**
@@ -88,29 +94,12 @@ function renderPlainStoredQueries(output_items) {
88
94
  */
89
95
  function renderPlainShowOutput(output_view) {
90
96
  const rendered_source = trimTrailingLineBreaks(output_view.rendered_source);
91
- const document_summary = output_view.document
92
- ? formatPlainNodeItem(output_view.document)
93
- : '';
94
97
  const incoming_summary = renderPlainIncomingSummary(output_view);
95
98
 
96
- if (
97
- document_summary.length === 0 &&
98
- output_view.items.length === 0 &&
99
- incoming_summary.length === 0
100
- ) {
99
+ if (output_view.items.length === 0 && incoming_summary.length === 0) {
101
100
  return `${rendered_source}\n`;
102
101
  }
103
-
104
- /** @type {string[]} */
105
- const summary_items = [];
106
-
107
- if (document_summary.length > 0) {
108
- summary_items.push(document_summary);
109
- }
110
-
111
- summary_items.push(...output_view.items.map(formatPlainResolvedLinkItem));
112
-
113
- const summary_blocks = summary_items.filter((summary_item) => summary_item);
102
+ const summary_blocks = output_view.items.map(formatPlainResolvedLinkItem);
114
103
 
115
104
  if (incoming_summary.length > 0) {
116
105
  summary_blocks.push(incoming_summary);
@@ -33,7 +33,7 @@ export async function renderRichOutput(output_view, render_options) {
33
33
  }
34
34
 
35
35
  if (output_view.command === 'queries') {
36
- return renderRichStoredQueries(output_view.items, ansi);
36
+ return renderRichStoredQueries(output_view, ansi);
37
37
  }
38
38
 
39
39
  if (output_view.command === 'refs') {
@@ -83,18 +83,24 @@ function renderRichEmptyQuery(footer, ansi) {
83
83
  }
84
84
 
85
85
  /**
86
- * @param {OutputStoredQueryItem[]} output_items
86
+ * @param {Extract<OutputView, { command: 'queries' }>} output_view
87
87
  * @param {Ansis} ansi
88
88
  * @returns {string}
89
89
  */
90
- function renderRichStoredQueries(output_items, ansi) {
91
- if (output_items.length === 0) {
90
+ function renderRichStoredQueries(output_view, ansi) {
91
+ if (output_view.items.length === 0) {
92
92
  return '';
93
93
  }
94
94
 
95
- return `${layoutStoredQueries(output_items)
96
- .map((line_segments) => formatRichStoredQueryLine(line_segments, ansi))
97
- .join('\n')}\n`;
95
+ const output_lines = layoutStoredQueries(output_view.items).map(
96
+ (line_segments) => formatRichStoredQueryLine(line_segments, ansi),
97
+ );
98
+
99
+ if (output_view.hints.length === 0) {
100
+ return `${output_lines.join('\n')}\n`;
101
+ }
102
+
103
+ return `${output_lines.join('\n')}\n${output_view.hints.map((hint) => ansi.gray(hint)).join('\n')}\n`;
98
104
  }
99
105
 
100
106
  /**
@@ -107,28 +113,14 @@ async function renderRichShowOutput(output_view, render_options, ansi) {
107
113
  const rendered_source = trimTrailingLineBreaks(
108
114
  await renderRichSource(output_view, render_options),
109
115
  );
110
- const document_summary = output_view.document
111
- ? formatRichNodeItem(output_view.document, ansi)
112
- : '';
113
116
  const incoming_summary = renderRichIncomingSummary(output_view, ansi);
114
117
 
115
- if (
116
- document_summary.length === 0 &&
117
- output_view.items.length === 0 &&
118
- incoming_summary.length === 0
119
- ) {
118
+ if (output_view.items.length === 0 && incoming_summary.length === 0) {
120
119
  return `${rendered_source}\n`;
121
120
  }
122
121
 
123
- /** @type {string[]} */
124
- const summary_items = [];
125
-
126
- if (document_summary.length > 0) {
127
- summary_items.push(document_summary);
128
- }
129
-
130
- summary_items.push(
131
- ...output_view.items.map((item) => formatRichResolvedLinkItem(item, ansi)),
122
+ const summary_items = output_view.items.map((item) =>
123
+ formatRichResolvedLinkItem(item, ansi),
132
124
  );
133
125
 
134
126
  if (incoming_summary.length > 0) {
@@ -173,7 +165,7 @@ function formatRichNodeItem(output_item, ansi) {
173
165
  }
174
166
 
175
167
  /**
176
- * @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }[]} line_segments
168
+ * @param {{ kind: 'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }[]} line_segments
177
169
  * @param {Ansis} ansi
178
170
  * @returns {string}
179
171
  */
@@ -232,7 +224,7 @@ function createAnsi(color_enabled) {
232
224
  }
233
225
 
234
226
  /**
235
- * @param {{ kind: 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }} line_segment
227
+ * @param {{ kind: 'description' | 'field_name' | 'keyword' | 'literal' | 'name' | 'operator' | 'plain', text: string }} line_segment
236
228
  * @param {Ansis} ansi
237
229
  * @returns {string}
238
230
  */
@@ -241,6 +233,10 @@ function styleStoredQuerySegment(line_segment, ansi) {
241
233
  return ansi.green(line_segment.text);
242
234
  }
243
235
 
236
+ if (line_segment.kind === 'description') {
237
+ return ansi.gray(line_segment.text);
238
+ }
239
+
244
240
  if (line_segment.kind === 'operator') {
245
241
  return ansi.gray(line_segment.text);
246
242
  }
@@ -21,7 +21,7 @@ const CONFIG_FILE_NAME = '.patram.json';
21
21
  * @param {string | undefined} target_argument
22
22
  * @returns {Promise<ResolvedCheckTarget>}
23
23
  */
24
- export async function resolveCheckTarget(target_argument) {
24
+ async function resolveCheckTarget(target_argument) {
25
25
  if (target_argument === undefined) {
26
26
  return {
27
27
  project_directory: process.cwd(),
@@ -35,23 +35,19 @@ export async function resolveCheckTarget(target_argument) {
35
35
  ? absolute_target_path
36
36
  : dirname(absolute_target_path);
37
37
  const project_directory = await findProjectDirectory(target_directory);
38
-
39
38
  if (target_stats.isFile()) {
40
39
  const target_path = normalizeRepoRelativePath(
41
40
  relative(project_directory, absolute_target_path),
42
41
  );
43
-
44
42
  return {
45
43
  project_directory,
46
44
  target_kind: 'file',
47
45
  target_path,
48
46
  };
49
47
  }
50
-
51
48
  const target_path = normalizeRepoRelativePath(
52
49
  relative(project_directory, absolute_target_path),
53
50
  );
54
-
55
51
  if (target_path.length === 0) {
56
52
  return {
57
53
  project_directory,
@@ -65,7 +61,36 @@ export async function resolveCheckTarget(target_argument) {
65
61
  target_path,
66
62
  };
67
63
  }
64
+ /**
65
+ * @param {string[]} target_arguments
66
+ * @returns {Promise<ResolvedCheckTarget[]>}
67
+ */
68
+ export async function resolveCheckTargets(target_arguments) {
69
+ if (target_arguments.length === 0) {
70
+ return [await resolveCheckTarget(undefined)];
71
+ }
68
72
 
73
+ return Promise.all(
74
+ target_arguments.map((target_argument) =>
75
+ resolveCheckTarget(target_argument),
76
+ ),
77
+ );
78
+ }
79
+ /** @param {ResolvedCheckTarget[]} resolved_targets
80
+ * @returns {string | null}
81
+ */
82
+ export function resolveCheckTargetProjectDirectory(resolved_targets) {
83
+ const project_directory = resolved_targets[0]?.project_directory;
84
+ if (!project_directory) {
85
+ return null;
86
+ }
87
+ for (const resolved_target of resolved_targets) {
88
+ if (resolved_target.project_directory !== project_directory) {
89
+ return null;
90
+ }
91
+ }
92
+ return project_directory;
93
+ }
69
94
  /**
70
95
  * Select the source files covered by one resolved `check` target.
71
96
  *
@@ -73,10 +98,7 @@ export async function resolveCheckTarget(target_argument) {
73
98
  * @param {ResolvedCheckTarget} resolved_target
74
99
  * @returns {string[]}
75
100
  */
76
- export function selectCheckTargetSourceFiles(
77
- source_file_paths,
78
- resolved_target,
79
- ) {
101
+ function selectCheckTargetSourceFiles(source_file_paths, resolved_target) {
80
102
  if (resolved_target.target_kind === 'project') {
81
103
  return source_file_paths;
82
104
  }
@@ -91,7 +113,22 @@ export function selectCheckTargetSourceFiles(
91
113
  isPathInsideDirectory(source_file_path, resolved_target.target_path),
92
114
  );
93
115
  }
94
-
116
+ /**
117
+ * @param {string[]} source_file_paths
118
+ * @param {ResolvedCheckTarget[]} resolved_targets
119
+ * @returns {string[]}
120
+ */
121
+ export function selectCheckTargetsSourceFiles(
122
+ source_file_paths,
123
+ resolved_targets,
124
+ ) {
125
+ return selectCheckTargetValues(
126
+ source_file_paths,
127
+ resolved_targets,
128
+ selectCheckTargetSourceFiles,
129
+ (source_file_path) => source_file_path,
130
+ );
131
+ }
95
132
  /**
96
133
  * Filter diagnostics to one resolved `check` target.
97
134
  *
@@ -99,7 +136,7 @@ export function selectCheckTargetSourceFiles(
99
136
  * @param {ResolvedCheckTarget} resolved_target
100
137
  * @returns {PatramDiagnostic[]}
101
138
  */
102
- export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
139
+ function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
103
140
  if (resolved_target.target_kind === 'project') {
104
141
  return diagnostics;
105
142
  }
@@ -115,6 +152,20 @@ export function selectCheckTargetDiagnostics(diagnostics, resolved_target) {
115
152
  );
116
153
  }
117
154
 
155
+ /**
156
+ * @param {PatramDiagnostic[]} diagnostics
157
+ * @param {ResolvedCheckTarget[]} resolved_targets
158
+ * @returns {PatramDiagnostic[]}
159
+ */
160
+ export function selectCheckTargetsDiagnostics(diagnostics, resolved_targets) {
161
+ return selectCheckTargetValues(
162
+ diagnostics,
163
+ resolved_targets,
164
+ selectCheckTargetDiagnostics,
165
+ createDiagnosticKey,
166
+ );
167
+ }
168
+
118
169
  /**
119
170
  * @param {string} start_directory
120
171
  * @returns {Promise<string>}
@@ -155,6 +206,49 @@ async function hasConfigFile(directory_path) {
155
206
  return true;
156
207
  }
157
208
 
209
+ /**
210
+ * @template ValueType
211
+ * @param {ValueType[]} values
212
+ * @param {ResolvedCheckTarget[]} resolved_targets
213
+ * @param {(values: ValueType[], resolved_target: ResolvedCheckTarget) => ValueType[]} select_values
214
+ * @param {(value: ValueType) => string} get_value_key
215
+ * @returns {ValueType[]}
216
+ */
217
+ function selectCheckTargetValues(
218
+ values,
219
+ resolved_targets,
220
+ select_values,
221
+ get_value_key,
222
+ ) {
223
+ if (
224
+ resolved_targets.some(
225
+ (resolved_target) => resolved_target.target_kind === 'project',
226
+ )
227
+ ) {
228
+ return values;
229
+ }
230
+
231
+ /** @type {Set<string>} */
232
+ const selected_keys = new Set();
233
+ /** @type {ValueType[]} */
234
+ const selected_values = [];
235
+
236
+ for (const resolved_target of resolved_targets) {
237
+ for (const value of select_values(values, resolved_target)) {
238
+ const value_key = get_value_key(value);
239
+
240
+ if (selected_keys.has(value_key)) {
241
+ continue;
242
+ }
243
+
244
+ selected_keys.add(value_key);
245
+ selected_values.push(value);
246
+ }
247
+ }
248
+
249
+ return selected_values;
250
+ }
251
+
158
252
  /**
159
253
  * @param {string} source_path
160
254
  * @param {string} directory_path
@@ -175,6 +269,21 @@ function normalizeRepoRelativePath(source_path) {
175
269
  return source_path.replaceAll('\\', '/');
176
270
  }
177
271
 
272
+ /**
273
+ * @param {PatramDiagnostic} diagnostic
274
+ * @returns {string}
275
+ */
276
+ function createDiagnosticKey(diagnostic) {
277
+ return [
278
+ diagnostic.path,
279
+ diagnostic.line,
280
+ diagnostic.column,
281
+ diagnostic.level,
282
+ diagnostic.code,
283
+ diagnostic.message,
284
+ ].join(':');
285
+ }
286
+
178
287
  /**
179
288
  * @param {unknown} error
180
289
  * @returns {error is NodeJS.ErrnoException}
@@ -12,7 +12,7 @@ import { resolveDocumentNodeId } from '../../graph/build-graph-identity.js';
12
12
  * Create a shared output view from one command result.
13
13
  *
14
14
  * @param {'query' | 'queries'} command_name
15
- * @param {GraphNode[] | { name: string, where: string }[]} command_items
15
+ * @param {GraphNode[] | { name: string, where: string, description?: string }[]} command_items
16
16
  * @param {{ derived_summary_evaluator?: DerivedSummaryEvaluator, hints?: string[], limit?: number, offset?: number, repo_config?: PatramRepoConfig, total_count?: number }=} command_options
17
17
  * @returns {OutputView}
18
18
  */
@@ -41,23 +41,8 @@ export function createOutputView(command_name, command_items, command_options) {
41
41
  * @returns {ShowOutputView}
42
42
  */
43
43
  export function createShowOutputView(show_output, command_options = {}) {
44
- const shown_document_node = resolveDocumentGraphNode(
45
- command_options.graph_nodes,
46
- command_options.document_node_ids,
47
- show_output.path,
48
- );
49
-
50
44
  return {
51
45
  command: 'show',
52
- document: shown_document_node
53
- ? createOutputNodeItem(
54
- shown_document_node,
55
- command_options.derived_summary_evaluator?.evaluate(
56
- shown_document_node,
57
- ) ?? null,
58
- command_options.repo_config?.fields ?? {},
59
- )
60
- : undefined,
61
46
  hints: [],
62
47
  incoming_summary: show_output.incoming_summary,
63
48
  items: show_output.resolved_links.map((resolved_link) =>
@@ -144,17 +129,19 @@ function createQueryOutputView(graph_nodes, command_options = {}) {
144
129
  }
145
130
 
146
131
  /**
147
- * @param {{ name: string, where: string }[]} stored_queries
132
+ * @param {{ name: string, where: string, description?: string }[]} stored_queries
148
133
  * @returns {OutputView}
149
134
  */
150
135
  function createStoredQueriesOutputView(stored_queries) {
151
136
  return {
152
137
  command: 'queries',
153
- hints: [],
138
+ hints:
139
+ stored_queries.length === 0 ? [] : ['Hint: patram help query-language'],
154
140
  items: stored_queries.map((stored_query) => ({
155
141
  kind: 'stored_query',
156
142
  name: stored_query.name,
157
143
  where: stored_query.where,
144
+ description: stored_query.description,
158
145
  })),
159
146
  summary: {
160
147
  count: stored_queries.length,
package/lib/patram.d.ts CHANGED
@@ -48,6 +48,8 @@ export type PatramParsedTerm =
48
48
  import('./graph/parse-where-clause.types.d.ts').ParsedTerm;
49
49
  export type PatramParsedExpression =
50
50
  import('./graph/parse-where-clause.types.d.ts').ParsedExpression;
51
+ export type PatramParseResult =
52
+ import('./graph/parse-where-clause.types.d.ts').ParseWhereClauseResult;
51
53
  export type PatramParseWhereClauseResult =
52
54
  import('./graph/parse-where-clause.types.d.ts').ParseWhereClauseResult;
53
55
  export type PatramQuerySource =
@@ -59,6 +61,12 @@ export type PatramQuerySource =
59
61
  name: string;
60
62
  };
61
63
 
64
+ export interface PatramQueryGraphOptions {
65
+ bindings?: Record<string, string>;
66
+ limit?: number;
67
+ offset?: number;
68
+ }
69
+
62
70
  export interface PatramProjectGraphResult {
63
71
  claims: import('./parse/parse-claims.types.d.ts').PatramClaim[];
64
72
  config: import('./config/load-patram-config.types.d.ts').PatramRepoConfig;