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.
Files changed (67) hide show
  1. package/bin/patram.js +25 -147
  2. package/lib/build-graph-identity.js +270 -0
  3. package/lib/build-graph.js +156 -77
  4. package/lib/check-graph.js +23 -7
  5. package/lib/claim-helpers.js +55 -0
  6. package/lib/cli-help-metadata.js +552 -0
  7. package/lib/command-output.js +83 -0
  8. package/lib/derived-summary.js +278 -0
  9. package/lib/format-derived-summary-row.js +9 -0
  10. package/lib/format-node-header.js +19 -0
  11. package/lib/format-output-item-block.js +22 -0
  12. package/lib/format-output-metadata.js +62 -0
  13. package/lib/layout-stored-queries.js +361 -0
  14. package/lib/list-queries.js +18 -0
  15. package/lib/list-source-files.js +50 -15
  16. package/lib/load-patram-config.js +505 -18
  17. package/lib/load-patram-config.types.ts +40 -0
  18. package/lib/load-project-graph.js +124 -0
  19. package/lib/output-view.types.ts +88 -0
  20. package/lib/parse-claims.js +38 -158
  21. package/lib/parse-claims.types.ts +7 -0
  22. package/lib/parse-cli-arguments-helpers.js +446 -0
  23. package/lib/parse-cli-arguments.js +266 -0
  24. package/lib/parse-cli-arguments.types.ts +69 -0
  25. package/lib/parse-cli-color-options.js +44 -0
  26. package/lib/parse-cli-query-pagination.js +49 -0
  27. package/lib/parse-jsdoc-blocks.js +184 -0
  28. package/lib/parse-jsdoc-claims.js +280 -0
  29. package/lib/parse-jsdoc-prose.js +111 -0
  30. package/lib/parse-markdown-claims.js +242 -0
  31. package/lib/parse-markdown-directives.js +136 -0
  32. package/lib/parse-where-clause.js +707 -0
  33. package/lib/parse-where-clause.types.ts +70 -0
  34. package/lib/patram-cli.js +464 -0
  35. package/lib/patram-config.js +3 -1
  36. package/lib/patram-config.types.ts +2 -1
  37. package/lib/patram.js +6 -0
  38. package/lib/query-graph.js +368 -0
  39. package/lib/query-inspection.js +523 -0
  40. package/lib/render-check-output.js +315 -0
  41. package/lib/render-cli-help.js +419 -0
  42. package/lib/render-json-output.js +161 -0
  43. package/lib/render-output-view.js +222 -0
  44. package/lib/render-plain-output.js +182 -0
  45. package/lib/render-rich-output.js +240 -0
  46. package/lib/render-rich-source.js +1333 -0
  47. package/lib/resolve-check-target.js +190 -0
  48. package/lib/resolve-output-mode.js +60 -0
  49. package/lib/resolve-patram-graph-config.js +88 -0
  50. package/lib/resolve-where-clause.js +66 -0
  51. package/lib/show-document.js +311 -0
  52. package/lib/source-file-defaults.js +28 -0
  53. package/lib/tagged-fenced-block-error.js +17 -0
  54. package/lib/tagged-fenced-block-markdown.js +111 -0
  55. package/lib/tagged-fenced-block-metadata.js +97 -0
  56. package/lib/tagged-fenced-block-parser.js +292 -0
  57. package/lib/tagged-fenced-blocks.js +100 -0
  58. package/lib/tagged-fenced-blocks.types.ts +38 -0
  59. package/lib/write-paged-output.js +87 -0
  60. package/package.json +28 -12
  61. package/bin/patram.test.js +0 -184
  62. package/lib/build-graph.test.js +0 -141
  63. package/lib/check-graph.test.js +0 -103
  64. package/lib/list-source-files.test.js +0 -101
  65. package/lib/load-patram-config.test.js +0 -211
  66. package/lib/parse-claims.test.js +0 -113
  67. package/lib/patram-config.test.js +0 -147
@@ -0,0 +1,315 @@
1
+ /**
2
+ * @import { PatramDiagnostic } from './load-patram-config.types.ts';
3
+ * @import { ResolvedOutputMode } from './output-view.types.ts';
4
+ */
5
+
6
+ import { Ansis } from 'ansis';
7
+ import stringWidth from 'string-width';
8
+
9
+ /**
10
+ * Render check diagnostics for one output mode.
11
+ *
12
+ * @param {PatramDiagnostic[]} diagnostics
13
+ * @param {ResolvedOutputMode} output_mode
14
+ * @returns {string}
15
+ */
16
+ export function renderCheckDiagnostics(diagnostics, output_mode) {
17
+ if (output_mode.renderer_name === 'json') {
18
+ return `${JSON.stringify(
19
+ {
20
+ diagnostics: diagnostics.map(formatJsonDiagnostic),
21
+ },
22
+ null,
23
+ 2,
24
+ )}\n`;
25
+ }
26
+
27
+ if (output_mode.renderer_name === 'plain') {
28
+ return renderPlainCheckDiagnostics(diagnostics);
29
+ }
30
+
31
+ return renderRichCheckDiagnostics(diagnostics, createAnsi(output_mode));
32
+ }
33
+
34
+ /**
35
+ * Render the success summary for one check run.
36
+ *
37
+ * @param {number} source_file_count
38
+ * @param {ResolvedOutputMode} output_mode
39
+ * @returns {string}
40
+ */
41
+ export function renderCheckSuccess(source_file_count, output_mode) {
42
+ if (output_mode.renderer_name === 'json') {
43
+ return `${JSON.stringify({ diagnostics: [] }, null, 2)}\n`;
44
+ }
45
+
46
+ if (output_mode.renderer_name === 'plain') {
47
+ return formatCheckSuccess(source_file_count);
48
+ }
49
+
50
+ const ansi = createAnsi(output_mode);
51
+ return `${ansi.green('Check passed.')}\n${ansi.gray(`Scanned ${source_file_count} ${pluralize('file', source_file_count)}. Found 0 errors.`)}\n`;
52
+ }
53
+
54
+ /**
55
+ * @param {PatramDiagnostic[]} diagnostics
56
+ * @returns {string}
57
+ */
58
+ function renderPlainCheckDiagnostics(diagnostics) {
59
+ const grouped_diagnostics = groupDiagnosticsByPath(diagnostics);
60
+ const diagnostic_prefix_width = measureMaxDiagnosticPrefixWidth(diagnostics);
61
+ return `${grouped_diagnostics
62
+ .map((diagnostic_group) =>
63
+ formatPlainDiagnosticGroup(diagnostic_group, diagnostic_prefix_width),
64
+ )
65
+ .join('\n\n')}\n\n${formatPlainSummary(diagnostics)}\n`;
66
+ }
67
+
68
+ /**
69
+ * @param {PatramDiagnostic[]} diagnostics
70
+ * @param {Ansis} ansi
71
+ * @returns {string}
72
+ */
73
+ function renderRichCheckDiagnostics(diagnostics, ansi) {
74
+ const grouped_diagnostics = groupDiagnosticsByPath(diagnostics);
75
+ const diagnostic_prefix_width = measureMaxDiagnosticPrefixWidth(diagnostics);
76
+ return `${grouped_diagnostics
77
+ .map((diagnostic_group) =>
78
+ formatRichDiagnosticGroup(
79
+ diagnostic_group,
80
+ diagnostic_prefix_width,
81
+ ansi,
82
+ ),
83
+ )
84
+ .join('\n\n')}\n\n${formatRichSummary(diagnostics, ansi)}\n`;
85
+ }
86
+
87
+ /**
88
+ * @param {PatramDiagnostic[]} diagnostics
89
+ * @returns {Array<{ diagnostics: PatramDiagnostic[], path: string }>}
90
+ */
91
+ function groupDiagnosticsByPath(diagnostics) {
92
+ /** @type {Array<{ diagnostics: PatramDiagnostic[], path: string }>} */
93
+ const grouped_diagnostics = [];
94
+ /** @type {Map<string, { diagnostics: PatramDiagnostic[], path: string }>} */
95
+ const grouped_diagnostics_by_path = new Map();
96
+
97
+ for (const diagnostic of diagnostics) {
98
+ let diagnostic_group = grouped_diagnostics_by_path.get(diagnostic.path);
99
+
100
+ if (!diagnostic_group) {
101
+ diagnostic_group = {
102
+ diagnostics: [],
103
+ path: diagnostic.path,
104
+ };
105
+ grouped_diagnostics_by_path.set(diagnostic.path, diagnostic_group);
106
+ grouped_diagnostics.push(diagnostic_group);
107
+ }
108
+
109
+ diagnostic_group.diagnostics.push(diagnostic);
110
+ }
111
+
112
+ return grouped_diagnostics;
113
+ }
114
+
115
+ /**
116
+ * @param {{ diagnostics: PatramDiagnostic[], path: string }} diagnostic_group
117
+ * @param {number} diagnostic_prefix_width
118
+ * @returns {string}
119
+ */
120
+ function formatPlainDiagnosticGroup(diagnostic_group, diagnostic_prefix_width) {
121
+ return `${formatDiagnosticGroupHeader(diagnostic_group.path)}\n${diagnostic_group.diagnostics
122
+ .map((diagnostic) =>
123
+ formatPlainDiagnosticRow(diagnostic, diagnostic_prefix_width),
124
+ )
125
+ .join('\n')}`;
126
+ }
127
+
128
+ /**
129
+ * @param {PatramDiagnostic} diagnostic
130
+ * @param {number} diagnostic_prefix_width
131
+ * @returns {string}
132
+ */
133
+ function formatPlainDiagnosticRow(diagnostic, diagnostic_prefix_width) {
134
+ const diagnostic_prefix = formatDiagnosticPrefix(diagnostic);
135
+ return `${diagnostic_prefix}${createDiagnosticCodePadding(diagnostic_prefix, diagnostic_prefix_width)} ${diagnostic.code}`;
136
+ }
137
+
138
+ /**
139
+ * @param {PatramDiagnostic[]} diagnostics
140
+ * @returns {string}
141
+ */
142
+ function formatPlainSummary(diagnostics) {
143
+ const error_count = countDiagnosticsByLevel(diagnostics, 'error');
144
+ const warning_count = countDiagnosticsByLevel(diagnostics, 'warning');
145
+ const problem_count = diagnostics.length;
146
+
147
+ return `\u2716 ${problem_count} ${pluralize('problem', problem_count)} (${error_count} ${pluralize('error', error_count)}, ${warning_count} ${pluralize('warning', warning_count)})`;
148
+ }
149
+
150
+ /**
151
+ * @param {{ diagnostics: PatramDiagnostic[], path: string }} diagnostic_group
152
+ * @param {number} diagnostic_prefix_width
153
+ * @param {Ansis} ansi
154
+ * @returns {string}
155
+ */
156
+ function formatRichDiagnosticGroup(
157
+ diagnostic_group,
158
+ diagnostic_prefix_width,
159
+ ansi,
160
+ ) {
161
+ return `${ansi.green(formatDiagnosticGroupHeader(diagnostic_group.path))}\n${diagnostic_group.diagnostics
162
+ .map((diagnostic) =>
163
+ formatRichDiagnosticRow(diagnostic, diagnostic_prefix_width, ansi),
164
+ )
165
+ .join('\n')}`;
166
+ }
167
+
168
+ /**
169
+ * @param {PatramDiagnostic} diagnostic
170
+ * @param {number} diagnostic_prefix_width
171
+ * @param {Ansis} ansi
172
+ * @returns {string}
173
+ */
174
+ function formatRichDiagnosticRow(diagnostic, diagnostic_prefix_width, ansi) {
175
+ const location = `${diagnostic.line}:${diagnostic.column}`;
176
+ const diagnostic_prefix = formatDiagnosticPrefix(diagnostic);
177
+ return ` ${ansi.gray(location)} ${formatRichDiagnosticLevel(diagnostic.level, ansi)} ${diagnostic.message}${createDiagnosticCodePadding(diagnostic_prefix, diagnostic_prefix_width)} ${ansi.gray(diagnostic.code)}`;
178
+ }
179
+
180
+ /**
181
+ * @param {PatramDiagnostic[]} diagnostics
182
+ * @param {Ansis} ansi
183
+ * @returns {string}
184
+ */
185
+ function formatRichSummary(diagnostics, ansi) {
186
+ return ansi.red(formatPlainSummary(diagnostics));
187
+ }
188
+
189
+ /**
190
+ * @param {PatramDiagnostic} diagnostic
191
+ * @returns {{ code: string, column: number, level: 'error', line: number, message: string, path: string }}
192
+ */
193
+ function formatJsonDiagnostic(diagnostic) {
194
+ return {
195
+ path: diagnostic.path,
196
+ line: diagnostic.line,
197
+ column: diagnostic.column,
198
+ level: diagnostic.level,
199
+ code: diagnostic.code,
200
+ message: diagnostic.message,
201
+ };
202
+ }
203
+
204
+ /**
205
+ * @param {number} source_file_count
206
+ * @returns {string}
207
+ */
208
+ function formatCheckSuccess(source_file_count) {
209
+ return `Check passed.\nScanned ${source_file_count} ${pluralize('file', source_file_count)}. Found 0 errors.\n`;
210
+ }
211
+
212
+ /**
213
+ * @param {PatramDiagnostic} diagnostic
214
+ */
215
+ function formatDiagnosticPrefix(diagnostic) {
216
+ return ` ${diagnostic.line}:${diagnostic.column} ${diagnostic.level} ${diagnostic.message}`;
217
+ }
218
+
219
+ /**
220
+ * @param {PatramDiagnostic[]} diagnostics
221
+ */
222
+ function measureMaxDiagnosticPrefixWidth(diagnostics) {
223
+ let max_width = 0;
224
+
225
+ for (const diagnostic of diagnostics) {
226
+ const diagnostic_prefix_width = stringWidth(
227
+ formatDiagnosticPrefix(diagnostic),
228
+ );
229
+
230
+ if (diagnostic_prefix_width > max_width) {
231
+ max_width = diagnostic_prefix_width;
232
+ }
233
+ }
234
+
235
+ return max_width;
236
+ }
237
+
238
+ /**
239
+ * @param {string} diagnostic_prefix
240
+ * @param {number} diagnostic_prefix_width
241
+ */
242
+ function createDiagnosticCodePadding(
243
+ diagnostic_prefix,
244
+ diagnostic_prefix_width,
245
+ ) {
246
+ return ' '.repeat(
247
+ Math.max(0, diagnostic_prefix_width - stringWidth(diagnostic_prefix)),
248
+ );
249
+ }
250
+
251
+ /**
252
+ * @param {PatramDiagnostic[]} diagnostics
253
+ * @param {'error' | 'warning'} level
254
+ */
255
+ function countDiagnosticsByLevel(diagnostics, level) {
256
+ let count = 0;
257
+
258
+ for (const diagnostic of diagnostics) {
259
+ if (diagnostic.level === level) {
260
+ count += 1;
261
+ }
262
+ }
263
+
264
+ return count;
265
+ }
266
+
267
+ /**
268
+ * @param {'error' | 'warning'} level
269
+ * @param {Ansis} ansi
270
+ * @returns {string}
271
+ */
272
+ function formatRichDiagnosticLevel(level, ansi) {
273
+ if (level === 'warning') {
274
+ return ansi.yellow(level);
275
+ }
276
+
277
+ return ansi.red(level);
278
+ }
279
+
280
+ /**
281
+ * @param {string} noun
282
+ * @param {number} count
283
+ */
284
+ function pluralize(noun, count) {
285
+ if (count === 1) {
286
+ return noun;
287
+ }
288
+
289
+ return `${noun}s`;
290
+ }
291
+
292
+ /**
293
+ * @param {ResolvedOutputMode} output_mode
294
+ */
295
+ function createAnsi(output_mode) {
296
+ return new Ansis(output_mode.color_enabled ? 3 : 0);
297
+ }
298
+
299
+ /**
300
+ * @param {string} path
301
+ */
302
+ function formatDiagnosticGroupHeader(path) {
303
+ return `${resolveDiagnosticPathType(path)} ${path}`;
304
+ }
305
+
306
+ /**
307
+ * @param {string} path
308
+ */
309
+ function resolveDiagnosticPathType(path) {
310
+ if (path.endsWith('.json') || path.startsWith('<')) {
311
+ return 'file';
312
+ }
313
+
314
+ return 'document';
315
+ }