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,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
|
+
}
|