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,69 @@
1
+ export type CliCommandName = 'check' | 'query' | 'queries' | 'show';
2
+ export type CliHelpTopicName = 'query-language';
3
+ export type CliHelpTargetKind = 'root' | 'command' | 'topic';
4
+
5
+ export type CliOutputMode = 'default' | 'plain' | 'json';
6
+
7
+ export type CliColorMode = 'auto' | 'always' | 'never';
8
+
9
+ export interface ParsedCliCommandRequest {
10
+ kind?: 'command';
11
+ color_mode: CliColorMode;
12
+ command_arguments: string[];
13
+ command_name: CliCommandName;
14
+ output_mode: CliOutputMode;
15
+ query_inspection_mode?: 'explain' | 'lint';
16
+ query_limit?: number;
17
+ query_offset?: number;
18
+ }
19
+
20
+ export interface ParsedCliHelpRequest {
21
+ kind: 'help';
22
+ target_kind: CliHelpTargetKind;
23
+ target_name?: CliCommandName | CliHelpTopicName;
24
+ }
25
+
26
+ export type ParsedCliArguments = ParsedCliCommandRequest;
27
+ export type ParsedCliRequest = ParsedCliCommandRequest | ParsedCliHelpRequest;
28
+
29
+ export type CliParseError =
30
+ | {
31
+ code: 'message';
32
+ message: string;
33
+ }
34
+ | {
35
+ code: 'missing_required_argument';
36
+ argument_label: string;
37
+ command_name: 'query' | 'show';
38
+ }
39
+ | {
40
+ code: 'option_not_valid_for_command';
41
+ command_name: CliCommandName;
42
+ token: string;
43
+ }
44
+ | {
45
+ code: 'unknown_command';
46
+ suggestion?: CliCommandName;
47
+ token: string;
48
+ }
49
+ | {
50
+ code: 'unknown_help_target';
51
+ suggestion?: CliCommandName | CliHelpTopicName;
52
+ token: string;
53
+ }
54
+ | {
55
+ code: 'unknown_option';
56
+ command_name?: CliCommandName;
57
+ suggestion?: string;
58
+ token: string;
59
+ };
60
+
61
+ export type ParseCliArgumentsResult =
62
+ | {
63
+ success: true;
64
+ value: ParsedCliRequest;
65
+ }
66
+ | {
67
+ error: CliParseError;
68
+ success: false;
69
+ };
@@ -0,0 +1,44 @@
1
+ /**
2
+ * @typedef {import('./parse-cli-arguments-helpers.js').CliOptionToken} CliOptionToken
3
+ * @typedef {import('./parse-cli-arguments.types.ts').CliColorMode} CliColorMode
4
+ */
5
+
6
+ const VALID_COLOR_MODES = new Set(['auto', 'always', 'never']);
7
+
8
+ /**
9
+ * @param {CliOptionToken[]} option_tokens
10
+ * @returns {CliColorMode}
11
+ */
12
+ export function resolveColorMode(option_tokens) {
13
+ let color_mode = 'auto';
14
+
15
+ for (const token of option_tokens) {
16
+ if (token.name === 'no-color') {
17
+ color_mode = 'never';
18
+ }
19
+
20
+ if (token.name === 'color' && typeof token.value === 'string') {
21
+ color_mode = token.value;
22
+ }
23
+ }
24
+
25
+ return /** @type {CliColorMode} */ (color_mode);
26
+ }
27
+
28
+ /**
29
+ * @param {CliOptionToken[]} option_tokens
30
+ * @returns {string | null}
31
+ */
32
+ export function findInvalidColorMode(option_tokens) {
33
+ for (const token of option_tokens) {
34
+ if (
35
+ token.name === 'color' &&
36
+ typeof token.value === 'string' &&
37
+ !VALID_COLOR_MODES.has(token.value)
38
+ ) {
39
+ return 'Color must be one of "auto", "always", or "never".';
40
+ }
41
+ }
42
+
43
+ return null;
44
+ }
@@ -0,0 +1,49 @@
1
+ /**
2
+ * @typedef {import('./parse-cli-arguments-helpers.js').CliOptionToken} CliOptionToken
3
+ * @typedef {import('./parse-cli-arguments-helpers.js').CliOptionValues} CliOptionValues
4
+ */
5
+
6
+ /**
7
+ * @param {CliOptionValues} parsed_values
8
+ * @returns {{ query_limit?: number, query_offset?: number }}
9
+ */
10
+ export function buildQueryPagination(parsed_values) {
11
+ /** @type {{ query_limit?: number, query_offset?: number }} */
12
+ const query_pagination = {};
13
+
14
+ if (parsed_values.limit !== undefined) {
15
+ query_pagination.query_limit = Number(parsed_values.limit);
16
+ }
17
+
18
+ if (parsed_values.offset !== undefined) {
19
+ query_pagination.query_offset = Number(parsed_values.offset);
20
+ }
21
+
22
+ return query_pagination;
23
+ }
24
+
25
+ /**
26
+ * @param {CliOptionToken[]} option_tokens
27
+ * @returns {string | null}
28
+ */
29
+ export function findInvalidQueryPagination(option_tokens) {
30
+ for (const token of option_tokens) {
31
+ if (
32
+ token.name === 'offset' &&
33
+ typeof token.value === 'string' &&
34
+ !/^\d+$/du.test(token.value)
35
+ ) {
36
+ return 'Offset must be a non-negative integer.';
37
+ }
38
+
39
+ if (
40
+ token.name === 'limit' &&
41
+ typeof token.value === 'string' &&
42
+ !/^\d+$/du.test(token.value)
43
+ ) {
44
+ return 'Limit must be a non-negative integer.';
45
+ }
46
+ }
47
+
48
+ return null;
49
+ }
@@ -0,0 +1,184 @@
1
+ /**
2
+ * Collect JSDoc blocks and their activated `@patram` markers.
3
+ *
4
+ * @param {string} source_text
5
+ * @returns {Array<{ activation_column: number | null, activation_line: number | null, lines: Array<{ column: number, content: string, line: number }> }>}
6
+ */
7
+ export function collectJsdocBlocks(source_text) {
8
+ const source_lines = source_text.split('\n');
9
+ /** @type {Array<{ activation_column: number | null, activation_line: number | null, lines: Array<{ column: number, content: string, line: number }> }>} */
10
+ const jsdoc_blocks = [];
11
+
12
+ for (let line_index = 0; line_index < source_lines.length; line_index += 1) {
13
+ if (!source_lines[line_index].includes('/**')) {
14
+ continue;
15
+ }
16
+
17
+ const closing_line_index = findJsdocClosingLineIndex(
18
+ source_lines,
19
+ line_index,
20
+ );
21
+
22
+ if (closing_line_index < 0) {
23
+ break;
24
+ }
25
+
26
+ const block_lines = source_lines
27
+ .slice(line_index, closing_line_index + 1)
28
+ .map((raw_line, block_line_index) =>
29
+ createJsdocBlockLine(
30
+ raw_line,
31
+ line_index + block_line_index + 1,
32
+ block_line_index === 0,
33
+ line_index + block_line_index === closing_line_index,
34
+ ),
35
+ );
36
+ const activation_line = block_lines.find((block_line) =>
37
+ /^@patram(?:\s|$)/du.test(block_line.content),
38
+ );
39
+
40
+ jsdoc_blocks.push({
41
+ activation_column: activation_line?.column ?? null,
42
+ activation_line: activation_line?.line ?? null,
43
+ lines: block_lines,
44
+ });
45
+ line_index = closing_line_index;
46
+ }
47
+
48
+ return jsdoc_blocks;
49
+ }
50
+
51
+ /**
52
+ * @param {string[]} source_lines
53
+ * @param {number} start_line_index
54
+ * @returns {number}
55
+ */
56
+ function findJsdocClosingLineIndex(source_lines, start_line_index) {
57
+ for (
58
+ let line_index = start_line_index;
59
+ line_index < source_lines.length;
60
+ line_index += 1
61
+ ) {
62
+ const source_line = source_lines[line_index];
63
+ const search_start =
64
+ line_index === start_line_index ? source_line.indexOf('/**') + 3 : 0;
65
+
66
+ if (source_line.indexOf('*/', search_start) >= 0) {
67
+ return line_index;
68
+ }
69
+ }
70
+
71
+ return -1;
72
+ }
73
+
74
+ /**
75
+ * @param {string} raw_line
76
+ * @param {number} line_number
77
+ * @param {boolean} is_first_line
78
+ * @param {boolean} is_last_line
79
+ * @returns {{ column: number, content: string, line: number }}
80
+ */
81
+ function createJsdocBlockLine(
82
+ raw_line,
83
+ line_number,
84
+ is_first_line,
85
+ is_last_line,
86
+ ) {
87
+ if (isLastClosingLine(raw_line, is_first_line, is_last_line)) {
88
+ return {
89
+ column: raw_line.indexOf('*/') + 1,
90
+ content: '',
91
+ line: line_number,
92
+ };
93
+ }
94
+
95
+ const line_parts = is_first_line
96
+ ? extractFirstJsdocLineContent(raw_line, is_last_line)
97
+ : extractFollowingJsdocLineContent(raw_line, is_last_line);
98
+
99
+ return {
100
+ column: line_parts.column,
101
+ content: line_parts.content.trim(),
102
+ line: line_number,
103
+ };
104
+ }
105
+
106
+ /**
107
+ * @param {string} raw_line
108
+ * @param {boolean} is_first_line
109
+ * @param {boolean} is_last_line
110
+ * @returns {boolean}
111
+ */
112
+ function isLastClosingLine(raw_line, is_first_line, is_last_line) {
113
+ return is_last_line && !is_first_line && /^\s*\*\/\s*$/du.test(raw_line);
114
+ }
115
+
116
+ /**
117
+ * @param {string} raw_line
118
+ * @param {boolean} is_last_line
119
+ * @returns {{ column: number, content: string }}
120
+ */
121
+ function extractFirstJsdocLineContent(raw_line, is_last_line) {
122
+ const start_index = raw_line.indexOf('/**');
123
+ let line_content = raw_line.slice(start_index + 3);
124
+ let column = start_index + 4;
125
+
126
+ if (line_content.startsWith(' ')) {
127
+ line_content = line_content.slice(1);
128
+ column += 1;
129
+ }
130
+
131
+ return finalizeJsdocLineContent(line_content, column, is_last_line);
132
+ }
133
+
134
+ /**
135
+ * @param {string} raw_line
136
+ * @param {boolean} is_last_line
137
+ * @returns {{ column: number, content: string }}
138
+ */
139
+ function extractFollowingJsdocLineContent(raw_line, is_last_line) {
140
+ const prefix_match = raw_line.match(/^\s*\*\s?/du);
141
+ const prefix_length = prefix_match ? prefix_match[0].length : 0;
142
+ const line_content = raw_line.slice(prefix_length);
143
+
144
+ return finalizeJsdocLineContent(
145
+ line_content,
146
+ prefix_length + 1,
147
+ is_last_line,
148
+ );
149
+ }
150
+
151
+ /**
152
+ * @param {string} line_content
153
+ * @param {number} column
154
+ * @param {boolean} is_last_line
155
+ * @returns {{ column: number, content: string }}
156
+ */
157
+ function finalizeJsdocLineContent(line_content, column, is_last_line) {
158
+ const trimmed_line_content = is_last_line
159
+ ? removeJsdocClosingDelimiter(line_content)
160
+ : line_content;
161
+ const leading_whitespace_match = trimmed_line_content.match(/^\s*/du);
162
+ const leading_whitespace_length = leading_whitespace_match
163
+ ? leading_whitespace_match[0].length
164
+ : 0;
165
+
166
+ return {
167
+ column: column + leading_whitespace_length,
168
+ content: trimmed_line_content,
169
+ };
170
+ }
171
+
172
+ /**
173
+ * @param {string} line_content
174
+ * @returns {string}
175
+ */
176
+ function removeJsdocClosingDelimiter(line_content) {
177
+ const closing_index = line_content.indexOf('*/');
178
+
179
+ if (closing_index < 0) {
180
+ return line_content;
181
+ }
182
+
183
+ return line_content.slice(0, closing_index);
184
+ }
@@ -0,0 +1,280 @@
1
+ /** @import * as $k$$l$load$j$patram$j$config$k$types$k$ts from './load-patram-config.types.ts'; */
2
+ /**
3
+ * @import { ParseClaimsInput, ParseSourceFileResult, PatramClaim, PatramClaimFields } from './parse-claims.types.ts';
4
+ */
5
+
6
+ import {
7
+ createClaim,
8
+ getFileExtension,
9
+ isPathLikeTarget,
10
+ } from './claim-helpers.js';
11
+ import { normalizeDirectiveName } from './parse-markdown-directives.js';
12
+ import { collectJsdocBlocks } from './parse-jsdoc-blocks.js';
13
+ import {
14
+ createJsdocProseClaimEntries,
15
+ pushJsdocParagraph,
16
+ } from './parse-jsdoc-prose.js';
17
+ import { JSDOC_SOURCE_FILE_EXTENSIONS } from './source-file-defaults.js';
18
+
19
+ /**
20
+ * JSDoc @patram claim parsing.
21
+ *
22
+ * Activates one source anchor block, extracts directives and links, and
23
+ * derives title and description claims from prose.
24
+ *
25
+ * Kind: parse
26
+ * Status: active
27
+ * Tracked in: ../docs/plans/v0/source-anchor-dogfooding.md
28
+ * Decided by: ../docs/decisions/jsdoc-metadata-directive-syntax.md
29
+ * @patram
30
+ * @see {@link ./parse-claims.js}
31
+ * @see {@link ../docs/decisions/jsdoc-metadata-directive-syntax.md}
32
+ */
33
+
34
+ const JSDOC_EXTENSIONS = new Set(JSDOC_SOURCE_FILE_EXTENSIONS);
35
+ const JSDOC_LINK_TAG_PATTERN = /^@(link|see)\s+(.+)$/du;
36
+ const JSDOC_TAG_PATTERN = /^@[A-Za-z]+(?:\s|$)/du;
37
+ const JSDOC_VISIBLE_DIRECTIVE_PATTERN = /^([A-Z][A-Za-z _-]*):\s+(.+)$/du;
38
+
39
+ /**
40
+ * Parse JSDoc metadata claims from one source file.
41
+ *
42
+ * @param {ParseClaimsInput} parse_input
43
+ * @returns {ParseSourceFileResult}
44
+ */
45
+ export function parseJsdocClaims(parse_input) {
46
+ if (!JSDOC_EXTENSIONS.has(getFileExtension(parse_input.path))) {
47
+ return {
48
+ claims: [],
49
+ diagnostics: [],
50
+ };
51
+ }
52
+
53
+ const jsdoc_blocks = collectJsdocBlocks(parse_input.source);
54
+ const active_blocks = jsdoc_blocks.filter(
55
+ (jsdoc_block) => jsdoc_block.activation_line !== null,
56
+ );
57
+
58
+ if (active_blocks.length === 0) {
59
+ return {
60
+ claims: [],
61
+ diagnostics: [],
62
+ };
63
+ }
64
+
65
+ const claim_entries = collectJsdocClaimEntries(
66
+ parse_input.path,
67
+ active_blocks[0],
68
+ ).sort(compareClaimEntries);
69
+
70
+ return {
71
+ claims: claim_entries.map((claim_entry, claim_index) =>
72
+ createClaim(
73
+ parse_input.path,
74
+ claim_index + 1,
75
+ claim_entry.claim_type,
76
+ claim_entry.claim_fields,
77
+ ),
78
+ ),
79
+ diagnostics: active_blocks
80
+ .slice(1)
81
+ .map((active_block) =>
82
+ createMultiplePatramBlocksDiagnostic(parse_input.path, active_block),
83
+ ),
84
+ };
85
+ }
86
+
87
+ /**
88
+ * @param {string} file_path
89
+ * @param {{ lines: Array<{ column: number, content: string, line: number }> }} jsdoc_block
90
+ * @returns {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>}
91
+ */
92
+ function collectJsdocClaimEntries(file_path, jsdoc_block) {
93
+ /** @type {Array<{ claim_fields: PatramClaimFields, claim_type: string, order: number }>} */
94
+ const claim_entries = [];
95
+ /** @type {Array<Array<{ column: number, content: string, line: number }>>} */
96
+ const prose_paragraphs = [];
97
+ /** @type {Array<{ column: number, content: string, line: number }>} */
98
+ let current_paragraph_lines = [];
99
+
100
+ for (const block_line of jsdoc_block.lines) {
101
+ if (block_line.content.length === 0) {
102
+ pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
103
+ current_paragraph_lines = [];
104
+ continue;
105
+ }
106
+
107
+ const directive_fields = matchJsdocDirectiveFields(file_path, block_line);
108
+
109
+ if (directive_fields) {
110
+ pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
111
+ current_paragraph_lines = [];
112
+ claim_entries.push({
113
+ claim_fields: directive_fields,
114
+ claim_type: 'directive',
115
+ order: 0,
116
+ });
117
+ continue;
118
+ }
119
+
120
+ const jsdoc_link_claim_entry = createJsdocLinkClaimEntry(
121
+ file_path,
122
+ block_line,
123
+ );
124
+
125
+ if (jsdoc_link_claim_entry) {
126
+ pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
127
+ current_paragraph_lines = [];
128
+ claim_entries.push(jsdoc_link_claim_entry);
129
+ continue;
130
+ }
131
+
132
+ if (JSDOC_TAG_PATTERN.test(block_line.content)) {
133
+ pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
134
+ current_paragraph_lines = [];
135
+ continue;
136
+ }
137
+
138
+ current_paragraph_lines.push(block_line);
139
+ }
140
+
141
+ pushJsdocParagraph(prose_paragraphs, current_paragraph_lines);
142
+ claim_entries.push(
143
+ ...createJsdocProseClaimEntries(file_path, prose_paragraphs),
144
+ );
145
+
146
+ return claim_entries;
147
+ }
148
+
149
+ /**
150
+ * @param {string} file_path
151
+ * @param {{ column: number, content: string, line: number }} block_line
152
+ * @returns {PatramClaimFields | null}
153
+ */
154
+ function matchJsdocDirectiveFields(file_path, block_line) {
155
+ const directive_match = block_line.content.match(
156
+ JSDOC_VISIBLE_DIRECTIVE_PATTERN,
157
+ );
158
+
159
+ if (!directive_match) {
160
+ return null;
161
+ }
162
+
163
+ return {
164
+ name: normalizeDirectiveName(directive_match[1]),
165
+ origin: {
166
+ column: block_line.column,
167
+ line: block_line.line,
168
+ path: file_path,
169
+ },
170
+ parser: 'jsdoc',
171
+ value: directive_match[2].trim(),
172
+ };
173
+ }
174
+
175
+ /**
176
+ * @param {string} file_path
177
+ * @param {{ column: number, content: string, line: number }} block_line
178
+ * @returns {{ claim_fields: PatramClaimFields, claim_type: string, order: number } | null}
179
+ */
180
+ function createJsdocLinkClaimEntry(file_path, block_line) {
181
+ const link_match = block_line.content.match(JSDOC_LINK_TAG_PATTERN);
182
+
183
+ if (!link_match) {
184
+ return null;
185
+ }
186
+
187
+ const link_value = parseJsdocLinkValue(link_match[2].trim());
188
+
189
+ if (!link_value) {
190
+ return null;
191
+ }
192
+
193
+ return {
194
+ claim_fields: {
195
+ origin: {
196
+ column: block_line.column,
197
+ line: block_line.line,
198
+ path: file_path,
199
+ },
200
+ value: link_value,
201
+ },
202
+ claim_type: 'jsdoc.link',
203
+ order: 0,
204
+ };
205
+ }
206
+
207
+ /**
208
+ * @param {string} raw_value
209
+ * @returns {{ target: string, text: string } | null}
210
+ */
211
+ function parseJsdocLinkValue(raw_value) {
212
+ const inline_link_match = raw_value.match(
213
+ /^\{@link\s+([^}\s]+)(?:\s+([^}]+))?\}$/du,
214
+ );
215
+
216
+ if (inline_link_match) {
217
+ const target_value = inline_link_match[1];
218
+ const label_value = inline_link_match[2]?.trim();
219
+
220
+ if (!isPathLikeTarget(target_value)) {
221
+ return null;
222
+ }
223
+
224
+ return {
225
+ target: target_value,
226
+ text: label_value && label_value.length > 0 ? label_value : target_value,
227
+ };
228
+ }
229
+
230
+ const [target_value, ...label_parts] = raw_value.split(/\s+/du);
231
+
232
+ if (!target_value || !isPathLikeTarget(target_value)) {
233
+ return null;
234
+ }
235
+
236
+ return {
237
+ target: target_value,
238
+ text: label_parts.length > 0 ? label_parts.join(' ') : target_value,
239
+ };
240
+ }
241
+
242
+ /**
243
+ * @param {{ claim_fields: PatramClaimFields, claim_type: string, order: number }} left_entry
244
+ * @param {{ claim_fields: PatramClaimFields, claim_type: string, order: number }} right_entry
245
+ * @returns {number}
246
+ */
247
+ function compareClaimEntries(left_entry, right_entry) {
248
+ const left_origin = left_entry.claim_fields.origin;
249
+ const right_origin = right_entry.claim_fields.origin;
250
+
251
+ if (!left_origin || !right_origin) {
252
+ return left_entry.order - right_entry.order;
253
+ }
254
+
255
+ if (left_origin.line !== right_origin.line) {
256
+ return left_origin.line - right_origin.line;
257
+ }
258
+
259
+ if (left_origin.column !== right_origin.column) {
260
+ return left_origin.column - right_origin.column;
261
+ }
262
+
263
+ return left_entry.order - right_entry.order;
264
+ }
265
+
266
+ /**
267
+ * @param {string} file_path
268
+ * @param {{ activation_column: number | null, activation_line: number | null }} jsdoc_block
269
+ * @returns {$k$$l$load$j$patram$j$config$k$types$k$ts.PatramDiagnostic}
270
+ */
271
+ function createMultiplePatramBlocksDiagnostic(file_path, jsdoc_block) {
272
+ return {
273
+ code: 'jsdoc.multiple_patram_blocks',
274
+ column: jsdoc_block.activation_column ?? 1,
275
+ level: 'error',
276
+ line: jsdoc_block.activation_line ?? 1,
277
+ message: `File "${file_path}" contains multiple JSDoc blocks with "@patram".`,
278
+ path: file_path,
279
+ };
280
+ }