@shepherdjerred/helm-types 1.1.0 → 1.2.0-dev.893
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/README.md +11 -2
- package/dist/cli.js +4570 -3203
- package/dist/index.js +8 -20585
- package/package.json +5 -1
- package/src/chart-fetcher.ts +37 -16
- package/src/chart-info-parser.ts +54 -34
- package/src/cli.ts +76 -58
- package/src/code-generator.ts +57 -22
- package/src/comment-parser.ts +79 -55
- package/src/config.ts +12 -5
- package/src/helm-types.ts +16 -46
- package/src/index.ts +14 -1
- package/src/interface-generator.ts +58 -23
- package/src/schemas.ts +3 -1
- package/src/type-converter-helpers.ts +180 -0
- package/src/type-converter.ts +273 -300
- package/src/type-inference.ts +302 -194
- package/src/utils.ts +2 -2
- package/src/yaml-comment-filters.ts +103 -0
- package/src/yaml-comment-regex-parser.ts +150 -0
- package/src/yaml-comments.ts +216 -508
- package/src/yaml-preprocess.ts +235 -0
package/src/yaml-comments.ts
CHANGED
|
@@ -1,5 +1,8 @@
|
|
|
1
1
|
import { parseDocument } from "yaml";
|
|
2
2
|
import { z } from "zod";
|
|
3
|
+
import { filterCommentedOutYAML } from "./yaml-comment-filters.ts";
|
|
4
|
+
import { parseCommentsWithRegex } from "./yaml-comment-regex-parser.ts";
|
|
5
|
+
import { preprocessYAMLComments } from "./yaml-preprocess.ts";
|
|
3
6
|
|
|
4
7
|
/**
|
|
5
8
|
* Metadata about how a comment was extracted
|
|
@@ -24,7 +27,7 @@ export type CommentWithMetadata = {
|
|
|
24
27
|
* Exported for testing purposes
|
|
25
28
|
*/
|
|
26
29
|
export function isYAMLKey(line: string): boolean {
|
|
27
|
-
return /^[\w.-]+:\s*(
|
|
30
|
+
return /^[\w.-]+:\s*(?:\||$)/.test(line);
|
|
28
31
|
}
|
|
29
32
|
|
|
30
33
|
/**
|
|
@@ -34,30 +37,41 @@ export function isYAMLKey(line: string): boolean {
|
|
|
34
37
|
export function isSimpleYAMLValue(line: string): boolean {
|
|
35
38
|
const hasURL = line.includes("http://") || line.includes("https://");
|
|
36
39
|
const isRef = /^ref:/i.test(line);
|
|
37
|
-
return /^[\w.-]+:\s
|
|
40
|
+
return /^[\w.-]+:\s[^:]+$/.test(line) && !hasURL && !isRef;
|
|
38
41
|
}
|
|
39
42
|
|
|
40
43
|
/**
|
|
41
44
|
* Helper: Check if a line is a section header (short line followed by YAML config)
|
|
42
45
|
* Exported for testing purposes
|
|
43
46
|
*/
|
|
44
|
-
export function isSectionHeader(line: string, nextLine
|
|
45
|
-
if (
|
|
47
|
+
export function isSectionHeader(line: string, nextLine?: string): boolean {
|
|
48
|
+
if (nextLine == null || nextLine === "") {
|
|
49
|
+
return false;
|
|
50
|
+
}
|
|
46
51
|
|
|
47
52
|
const isFollowedByYAMLKey =
|
|
48
|
-
/^[\w.-]+:\s*\|/.test(nextLine) ||
|
|
53
|
+
/^[\w.-]+:\s*\|/.test(nextLine) ||
|
|
54
|
+
/^[\w.-]+:\s*$/.test(nextLine) ||
|
|
55
|
+
/^[\w.-]+:\s+/.test(nextLine);
|
|
49
56
|
|
|
50
|
-
if (!isFollowedByYAMLKey)
|
|
57
|
+
if (!isFollowedByYAMLKey) {
|
|
58
|
+
return false;
|
|
59
|
+
}
|
|
51
60
|
|
|
52
61
|
const wordCount = line.split(/\s+/).length;
|
|
53
|
-
const hasConfigKeywords =
|
|
62
|
+
const hasConfigKeywords =
|
|
63
|
+
/\b(?:configuration|config|example|setup|settings?|options?|alternative)\b/i.test(
|
|
64
|
+
line,
|
|
65
|
+
);
|
|
54
66
|
const endsWithPunctuation = /[.!?]$/.test(line);
|
|
55
67
|
const hasURL = line.includes("http://") || line.includes("https://");
|
|
56
|
-
const startsWithArticle = /^(This|The|A|An)\s/i.test(line);
|
|
57
|
-
const startsWithCommonWord =
|
|
68
|
+
const startsWithArticle = /^(?:This|The|A|An)\s/i.test(line);
|
|
69
|
+
const startsWithCommonWord =
|
|
70
|
+
/^(?:This|The|A|An|It|For|To|If|When|You|We|Use|Configure)\s/i.test(line);
|
|
58
71
|
|
|
59
72
|
return (
|
|
60
|
-
((wordCount === 2 && !startsWithCommonWord) ||
|
|
73
|
+
((wordCount === 2 && !startsWithCommonWord) ||
|
|
74
|
+
(hasConfigKeywords && !startsWithCommonWord)) &&
|
|
61
75
|
!endsWithPunctuation &&
|
|
62
76
|
!hasURL &&
|
|
63
77
|
!/^ref:/i.test(line) &&
|
|
@@ -72,12 +86,18 @@ export function isSectionHeader(line: string, nextLine: string | undefined): boo
|
|
|
72
86
|
export function isCodeExample(line: string, wordCount: number): boolean {
|
|
73
87
|
const looksLikeYAMLKey = isYAMLKey(line);
|
|
74
88
|
const looksLikeSimpleYAMLValue = isSimpleYAMLValue(line);
|
|
75
|
-
const looksLikeYAMLList =
|
|
89
|
+
const looksLikeYAMLList =
|
|
90
|
+
line.startsWith("-") && (line.includes(":") || /^-\s+\|/.test(line));
|
|
76
91
|
const looksLikePolicyRule = /^[pg],\s*/.test(line);
|
|
77
92
|
const hasIndentation = /^\s{2,}/.test(line);
|
|
78
|
-
const looksLikeCommand =
|
|
93
|
+
const looksLikeCommand =
|
|
94
|
+
/^echo\s+/.test(line) ||
|
|
95
|
+
line.includes("$ARGOCD_") ||
|
|
96
|
+
line.includes("$KUBE_");
|
|
79
97
|
const isSeparator =
|
|
80
|
-
/^-{3,}/.test(line) ||
|
|
98
|
+
/^-{3,}/.test(line) ||
|
|
99
|
+
/^BEGIN .*(?:KEY|CERTIFICATE)/.test(line) ||
|
|
100
|
+
/^END .*(?:KEY|CERTIFICATE)/.test(line);
|
|
81
101
|
|
|
82
102
|
return (
|
|
83
103
|
isSeparator ||
|
|
@@ -102,14 +122,18 @@ export function looksLikeProse(line: string, wordCount: number): boolean {
|
|
|
102
122
|
const notYamlKey = !(isYAMLKey(line) && !hasURL && !/^ref:/i.test(line));
|
|
103
123
|
const reasonableLength = line.length > 10;
|
|
104
124
|
const hasMultipleWords = wordCount >= 3;
|
|
105
|
-
const startsWithArticle = /^(This|The|A|An)\s/i.test(line);
|
|
125
|
+
const startsWithArticle = /^(?:This|The|A|An)\s/i.test(line);
|
|
106
126
|
|
|
107
127
|
// Lines starting with markers like ^, ->, etc. are documentation references
|
|
108
|
-
const isReferenceMarker = /^(
|
|
128
|
+
const isReferenceMarker = /^(?:\^|->|→)\s/.test(line);
|
|
109
129
|
|
|
110
130
|
return (
|
|
111
131
|
(startsWithCapital || isReferenceMarker) &&
|
|
112
|
-
(hasEndPunctuation ||
|
|
132
|
+
(hasEndPunctuation ||
|
|
133
|
+
hasURL ||
|
|
134
|
+
startsWithArticle ||
|
|
135
|
+
hasMultipleWords ||
|
|
136
|
+
isReferenceMarker) &&
|
|
113
137
|
notYamlKey &&
|
|
114
138
|
reasonableLength
|
|
115
139
|
);
|
|
@@ -133,10 +157,12 @@ export function normalizeCommentLine(line: string): string {
|
|
|
133
157
|
* Removes Helm-specific markers and filters out code examples and section headers
|
|
134
158
|
*/
|
|
135
159
|
export function cleanYAMLComment(comment: string): string {
|
|
136
|
-
if (!comment)
|
|
160
|
+
if (!comment) {
|
|
161
|
+
return "";
|
|
162
|
+
}
|
|
137
163
|
|
|
138
164
|
// Normalize all lines
|
|
139
|
-
const lines = comment.split("\n").map(normalizeCommentLine);
|
|
165
|
+
const lines = comment.split("\n").map((line) => normalizeCommentLine(line));
|
|
140
166
|
|
|
141
167
|
// Filter out code examples and section headers, keep documentation
|
|
142
168
|
const cleaned: string[] = [];
|
|
@@ -148,12 +174,16 @@ export function cleanYAMLComment(comment: string): string {
|
|
|
148
174
|
|
|
149
175
|
// Empty lines end code blocks (but not examples)
|
|
150
176
|
if (!line) {
|
|
151
|
-
if (inCodeBlock && !inExample)
|
|
177
|
+
if (inCodeBlock && !inExample) {
|
|
178
|
+
inCodeBlock = false;
|
|
179
|
+
}
|
|
152
180
|
continue;
|
|
153
181
|
}
|
|
154
182
|
|
|
155
183
|
// Skip @default lines (we'll generate our own)
|
|
156
|
-
if (line.startsWith("@default"))
|
|
184
|
+
if (line.startsWith("@default")) {
|
|
185
|
+
continue;
|
|
186
|
+
}
|
|
157
187
|
|
|
158
188
|
// Check if this line starts an Example section (preserve these!)
|
|
159
189
|
if (/^Example:?$/i.test(line.trim())) {
|
|
@@ -203,239 +233,6 @@ export function cleanYAMLComment(comment: string): string {
|
|
|
203
233
|
return cleaned.join("\n").trim();
|
|
204
234
|
}
|
|
205
235
|
|
|
206
|
-
/**
|
|
207
|
-
* Helper: Check if a line is part of a commented YAML block
|
|
208
|
-
*/
|
|
209
|
-
function isPartOfYAMLBlock(line: string, trimmed: string): boolean {
|
|
210
|
-
return line.startsWith(" ") || line.startsWith("\t") || trimmed.startsWith("-") || trimmed.startsWith("#");
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
/**
|
|
214
|
-
* Helper: Check if this is a section header followed by a YAML key
|
|
215
|
-
*/
|
|
216
|
-
function isSectionHeaderForCommentedBlock(nextLine: string | undefined): boolean {
|
|
217
|
-
if (!nextLine) return false;
|
|
218
|
-
const nextTrimmed = nextLine.trim();
|
|
219
|
-
return /^[\w.-]+:\s*(\||$)/.test(nextTrimmed);
|
|
220
|
-
}
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
* Helper: Check if line indicates start of real documentation
|
|
224
|
-
*/
|
|
225
|
-
function isRealDocumentation(trimmed: string): boolean {
|
|
226
|
-
return trimmed.startsWith("--") || trimmed.startsWith("#");
|
|
227
|
-
}
|
|
228
|
-
|
|
229
|
-
/**
|
|
230
|
-
* Filter out commented-out YAML blocks from the START of a comment string
|
|
231
|
-
* The YAML AST gives us ALL comments, including commented-out config sections
|
|
232
|
-
* We only remove these if they appear BEFORE the real documentation starts
|
|
233
|
-
*/
|
|
234
|
-
function filterCommentedOutYAML(comment: string): string {
|
|
235
|
-
const lines = comment.split("\n");
|
|
236
|
-
let startIndex = 0;
|
|
237
|
-
let inCommentedBlock = false;
|
|
238
|
-
let hasSeenRealDoc = false;
|
|
239
|
-
|
|
240
|
-
// First pass: find where real documentation starts
|
|
241
|
-
for (let i = 0; i < lines.length; i++) {
|
|
242
|
-
const line = lines[i] ?? "";
|
|
243
|
-
const trimmed = line.trim();
|
|
244
|
-
|
|
245
|
-
// Blank line could be end of commented block
|
|
246
|
-
if (trimmed === "") {
|
|
247
|
-
if (inCommentedBlock) {
|
|
248
|
-
inCommentedBlock = false;
|
|
249
|
-
}
|
|
250
|
-
continue;
|
|
251
|
-
}
|
|
252
|
-
|
|
253
|
-
// Check if this looks like a commented-out YAML key
|
|
254
|
-
const looksLikeYAMLKey = isYAMLKey(trimmed);
|
|
255
|
-
|
|
256
|
-
if (looksLikeYAMLKey && !hasSeenRealDoc) {
|
|
257
|
-
// This starts a commented-out YAML block
|
|
258
|
-
inCommentedBlock = true;
|
|
259
|
-
continue;
|
|
260
|
-
}
|
|
261
|
-
|
|
262
|
-
// If we're in a commented block, check if this line is part of it
|
|
263
|
-
if (inCommentedBlock) {
|
|
264
|
-
if (isPartOfYAMLBlock(line, trimmed)) {
|
|
265
|
-
continue;
|
|
266
|
-
} else {
|
|
267
|
-
// This line doesn't look like YAML content, we're out of the block
|
|
268
|
-
inCommentedBlock = false;
|
|
269
|
-
hasSeenRealDoc = true;
|
|
270
|
-
startIndex = i;
|
|
271
|
-
}
|
|
272
|
-
} else if (!hasSeenRealDoc) {
|
|
273
|
-
const nextLine = lines[i + 1];
|
|
274
|
-
|
|
275
|
-
// Check if this is a section header for a commented-out block
|
|
276
|
-
if (isSectionHeaderForCommentedBlock(nextLine)) {
|
|
277
|
-
continue;
|
|
278
|
-
}
|
|
279
|
-
|
|
280
|
-
// Check if this line indicates real documentation
|
|
281
|
-
if (isRealDocumentation(trimmed)) {
|
|
282
|
-
hasSeenRealDoc = true;
|
|
283
|
-
startIndex = i;
|
|
284
|
-
} else {
|
|
285
|
-
// First real prose line
|
|
286
|
-
hasSeenRealDoc = true;
|
|
287
|
-
startIndex = i;
|
|
288
|
-
}
|
|
289
|
-
}
|
|
290
|
-
}
|
|
291
|
-
|
|
292
|
-
// Return everything from where real documentation starts
|
|
293
|
-
return lines
|
|
294
|
-
.slice(startIndex)
|
|
295
|
-
.map((l) => l.trim())
|
|
296
|
-
.join("\n");
|
|
297
|
-
}
|
|
298
|
-
|
|
299
|
-
/**
|
|
300
|
-
* Pre-process YAML to uncomment commented-out keys
|
|
301
|
-
* In Helm charts, commented-out keys are documentation of available options
|
|
302
|
-
* e.g., "## key: value" or "# key: value"
|
|
303
|
-
*
|
|
304
|
-
* This allows us to parse them as real keys and associate their comments
|
|
305
|
-
*
|
|
306
|
-
* Only uncomments keys that are:
|
|
307
|
-
* - At root level or similar indentation to real keys
|
|
308
|
-
* - Not part of "Example:" blocks
|
|
309
|
-
* - Not part of documentation prose (have their own dedicated comment block)
|
|
310
|
-
* - Not deeply nested (which would indicate example YAML)
|
|
311
|
-
*
|
|
312
|
-
* Exported for testing purposes
|
|
313
|
-
*/
|
|
314
|
-
export function preprocessYAMLComments(yamlContent: string): string {
|
|
315
|
-
const lines = yamlContent.split("\n");
|
|
316
|
-
const processedLines: string[] = [];
|
|
317
|
-
let inExampleBlock = false;
|
|
318
|
-
let inBlockScalar = false; // Track if we're in a block scalar (| or >)
|
|
319
|
-
let lastRealKeyIndent = -1;
|
|
320
|
-
let consecutiveCommentedKeys = 0;
|
|
321
|
-
|
|
322
|
-
for (let i = 0; i < lines.length; i++) {
|
|
323
|
-
const line = lines[i] ?? "";
|
|
324
|
-
const trimmed = line.trim();
|
|
325
|
-
|
|
326
|
-
// Detect "Example:" markers (case-insensitive)
|
|
327
|
-
if (/^##?\s*Example:?$/i.test(trimmed)) {
|
|
328
|
-
inExampleBlock = true;
|
|
329
|
-
processedLines.push(line);
|
|
330
|
-
continue;
|
|
331
|
-
}
|
|
332
|
-
|
|
333
|
-
// Exit example block on blank line or "For more information"
|
|
334
|
-
if (inExampleBlock && (!trimmed || trimmed.startsWith("For more information") || trimmed.startsWith("Ref:"))) {
|
|
335
|
-
inExampleBlock = false;
|
|
336
|
-
}
|
|
337
|
-
|
|
338
|
-
// Detect if previous commented line had a block scalar indicator (| or >)
|
|
339
|
-
if (i > 0) {
|
|
340
|
-
const prevLine = lines[i - 1];
|
|
341
|
-
const prevTrimmed = prevLine?.trim() ?? "";
|
|
342
|
-
// Check if previous line is a commented key with block scalar
|
|
343
|
-
if (/^#+\s*[\w.-]+:\s*[|>]\s*$/.test(prevTrimmed)) {
|
|
344
|
-
inBlockScalar = true;
|
|
345
|
-
}
|
|
346
|
-
}
|
|
347
|
-
|
|
348
|
-
// Exit block scalar on non-indented line or blank line
|
|
349
|
-
if (inBlockScalar) {
|
|
350
|
-
const isIndented = trimmed && (line.startsWith(" ") || line.startsWith("\t") || /^#\s{2,}/.test(line));
|
|
351
|
-
if (!trimmed || !isIndented) {
|
|
352
|
-
inBlockScalar = false;
|
|
353
|
-
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
// Track indentation of real (uncommented) keys
|
|
357
|
-
if (!trimmed.startsWith("#") && /^[\w.-]+:/.test(trimmed)) {
|
|
358
|
-
lastRealKeyIndent = line.search(/\S/);
|
|
359
|
-
consecutiveCommentedKeys = 0;
|
|
360
|
-
}
|
|
361
|
-
|
|
362
|
-
// Don't uncomment if we're in an example block or block scalar
|
|
363
|
-
if (inExampleBlock || inBlockScalar) {
|
|
364
|
-
processedLines.push(line);
|
|
365
|
-
consecutiveCommentedKeys = 0;
|
|
366
|
-
continue;
|
|
367
|
-
}
|
|
368
|
-
|
|
369
|
-
// Check if this is a commented-out YAML key
|
|
370
|
-
// Pattern: one or more # followed by optional whitespace, then key: value
|
|
371
|
-
const commentedKeyMatch = /^([ \t]*)(#+)\s*([\w.-]+:\s*.*)$/.exec(line);
|
|
372
|
-
|
|
373
|
-
if (commentedKeyMatch) {
|
|
374
|
-
const [, indent, , keyValue] = commentedKeyMatch;
|
|
375
|
-
|
|
376
|
-
if (!keyValue || !indent) {
|
|
377
|
-
processedLines.push(line);
|
|
378
|
-
continue;
|
|
379
|
-
}
|
|
380
|
-
|
|
381
|
-
// Check if the key part looks like a valid YAML key (not prose)
|
|
382
|
-
const keyPart = keyValue.split(":")[0]?.trim() ?? "";
|
|
383
|
-
const isValidKey = /^[\w.-]+$/.test(keyPart);
|
|
384
|
-
|
|
385
|
-
// Don't uncomment documentation references like "ref: https://..."
|
|
386
|
-
const isDocReference = /^ref:/i.test(keyValue) && (keyValue.includes("http://") || keyValue.includes("https://"));
|
|
387
|
-
|
|
388
|
-
// Don't uncomment URLs (they might be continuation lines in multi-line Ref comments)
|
|
389
|
-
const isURL = keyValue.trim().startsWith("http://") || keyValue.trim().startsWith("https://");
|
|
390
|
-
|
|
391
|
-
if (isValidKey && !isDocReference && !isURL) {
|
|
392
|
-
const keyIndent = indent.length;
|
|
393
|
-
|
|
394
|
-
// Check the context: look at previous lines to see if this is part of prose
|
|
395
|
-
// If the previous line is prose (not a commented key or blank), this is likely an example
|
|
396
|
-
const prevLine = i > 0 ? lines[i - 1] : "";
|
|
397
|
-
const prevTrimmed = prevLine?.trim() ?? "";
|
|
398
|
-
const prevIsCommentedKey = /^#+\s*[\w.-]+:\s/.test(prevTrimmed);
|
|
399
|
-
const prevIsBlank = !prevTrimmed;
|
|
400
|
-
const prevIsListItem = prevTrimmed.startsWith("#") && prevTrimmed.substring(1).trim().startsWith("-");
|
|
401
|
-
|
|
402
|
-
// If previous line is prose or a list item, this is likely a YAML example
|
|
403
|
-
const likelyExample = (!prevIsBlank && !prevIsCommentedKey && prevTrimmed.startsWith("#")) || prevIsListItem;
|
|
404
|
-
|
|
405
|
-
// Also check if we're in a sequence of commented keys (good sign of commented-out config)
|
|
406
|
-
if (prevIsCommentedKey) {
|
|
407
|
-
consecutiveCommentedKeys++;
|
|
408
|
-
} else if (!prevIsBlank) {
|
|
409
|
-
consecutiveCommentedKeys = 0;
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Only uncomment if:
|
|
413
|
-
// 1. It's not likely an example (based on context)
|
|
414
|
-
// 2. AND (we haven't seen any real keys yet OR this key is at similar indent level)
|
|
415
|
-
// 3. OR we've seen multiple consecutive commented keys (likely a commented-out config block)
|
|
416
|
-
const shouldUncomment =
|
|
417
|
-
!likelyExample &&
|
|
418
|
-
(lastRealKeyIndent === -1 || Math.abs(keyIndent - lastRealKeyIndent) <= 4 || consecutiveCommentedKeys >= 2);
|
|
419
|
-
|
|
420
|
-
if (shouldUncomment) {
|
|
421
|
-
processedLines.push(`${indent}${keyValue}`);
|
|
422
|
-
continue;
|
|
423
|
-
}
|
|
424
|
-
}
|
|
425
|
-
}
|
|
426
|
-
|
|
427
|
-
// Reset consecutive count if not a commented key
|
|
428
|
-
if (!commentedKeyMatch) {
|
|
429
|
-
consecutiveCommentedKeys = 0;
|
|
430
|
-
}
|
|
431
|
-
|
|
432
|
-
// Keep the line as-is
|
|
433
|
-
processedLines.push(line);
|
|
434
|
-
}
|
|
435
|
-
|
|
436
|
-
return processedLines.join("\n");
|
|
437
|
-
}
|
|
438
|
-
|
|
439
236
|
/**
|
|
440
237
|
* Parse Bitnami-style @param directives from a comment
|
|
441
238
|
* Format: @param key.path Description
|
|
@@ -456,10 +253,15 @@ export function parseBitnamiParams(comment: string): {
|
|
|
456
253
|
.replace(/^#+\s*/, "")
|
|
457
254
|
.replace(/^--\s*/, "");
|
|
458
255
|
|
|
459
|
-
const paramMatch = /^@param\s+([\w.-]+)\s+(
|
|
256
|
+
const paramMatch = /^@param\s+([\w.-]+)\s+(\S.*)$/.exec(trimmedLine);
|
|
460
257
|
if (paramMatch) {
|
|
461
258
|
const [, paramKey, description] = paramMatch;
|
|
462
|
-
if (
|
|
259
|
+
if (
|
|
260
|
+
paramKey != null &&
|
|
261
|
+
paramKey !== "" &&
|
|
262
|
+
description != null &&
|
|
263
|
+
description !== ""
|
|
264
|
+
) {
|
|
463
265
|
params.set(paramKey, description);
|
|
464
266
|
}
|
|
465
267
|
} else if (trimmedLine) {
|
|
@@ -471,281 +273,187 @@ export function parseBitnamiParams(comment: string): {
|
|
|
471
273
|
}
|
|
472
274
|
|
|
473
275
|
/**
|
|
474
|
-
*
|
|
475
|
-
*
|
|
476
|
-
*
|
|
276
|
+
* Parse YAML comments with metadata for debugging
|
|
277
|
+
* Returns comments with information about how they were extracted
|
|
278
|
+
* Exported for testing purposes
|
|
477
279
|
*/
|
|
478
|
-
function
|
|
280
|
+
export function parseYAMLCommentsWithMetadata(
|
|
281
|
+
yamlContent: string,
|
|
282
|
+
): Map<string, CommentWithMetadata> {
|
|
479
283
|
const comments = new Map<string, CommentWithMetadata>();
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
//
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
284
|
+
|
|
285
|
+
try {
|
|
286
|
+
// Pre-process to uncomment commented-out keys
|
|
287
|
+
// This allows Helm chart commented-out options to be parsed as real keys
|
|
288
|
+
const preprocessedYaml = preprocessYAMLComments(yamlContent);
|
|
289
|
+
const doc = parseDocument(preprocessedYaml);
|
|
290
|
+
|
|
291
|
+
// Build a regex-based fallback map for cases where YAML parser loses comments
|
|
292
|
+
// Use preprocessed YAML so commented-out keys are treated as real keys
|
|
293
|
+
const regexComments = parseCommentsWithRegex(preprocessedYaml);
|
|
294
|
+
|
|
295
|
+
// Zod schemas for YAML AST parsing, defined once
|
|
296
|
+
const MapNodeSchema = z.object({
|
|
297
|
+
items: z.array(z.unknown()),
|
|
298
|
+
commentBefore: z.unknown().optional(),
|
|
299
|
+
});
|
|
300
|
+
const PairSchema = z.object({ key: z.unknown(), value: z.unknown() });
|
|
301
|
+
const KeyValueSchema = z.object({ value: z.string() });
|
|
302
|
+
const CommentBeforeSchema = z.object({ commentBefore: z.unknown() });
|
|
303
|
+
const InlineCommentSchema = z.object({ comment: z.unknown() });
|
|
304
|
+
|
|
305
|
+
/**
|
|
306
|
+
* Extract the commentBefore string from a YAML node
|
|
307
|
+
*/
|
|
308
|
+
function extractCommentBefore(node: unknown): string {
|
|
309
|
+
const check = CommentBeforeSchema.safeParse(node);
|
|
310
|
+
if (!check.success) {
|
|
311
|
+
return "";
|
|
497
312
|
}
|
|
498
|
-
|
|
313
|
+
const strCheck = z.string().safeParse(check.data.commentBefore);
|
|
314
|
+
return strCheck.success ? strCheck.data : "";
|
|
499
315
|
}
|
|
500
316
|
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
commentText = commentText.trim();
|
|
509
|
-
|
|
510
|
-
// Skip commented-out YAML keys (these are not documentation)
|
|
511
|
-
// Match patterns like "key: value" or "key:" but NOT prose-like text
|
|
512
|
-
// This includes quoted values like: key: "value"
|
|
513
|
-
const looksLikeYAMLKey = /^[\w.-]+:\s*(\||$|[\w.-]+$|"[^"]*"$|'[^']*'$|\[|\{)/.test(commentText);
|
|
514
|
-
|
|
515
|
-
if (!looksLikeYAMLKey) {
|
|
516
|
-
const commentIndent = line.search(/\S/);
|
|
517
|
-
|
|
518
|
-
// If we just discarded comments (indent === -1), this is a fresh start
|
|
519
|
-
if (pendingCommentIndent === -1) {
|
|
520
|
-
pendingComment = [commentText];
|
|
521
|
-
pendingCommentIndent = commentIndent;
|
|
522
|
-
pendingDebugInfo = [
|
|
523
|
-
`Line ${String(lineNum)}: Fresh start (indent=${String(commentIndent)}): "${commentText}"`,
|
|
524
|
-
];
|
|
525
|
-
} else if (commentIndent === pendingCommentIndent || Math.abs(commentIndent - pendingCommentIndent) <= 2) {
|
|
526
|
-
// Same indent level, add to pending
|
|
527
|
-
pendingComment.push(commentText);
|
|
528
|
-
pendingDebugInfo.push(
|
|
529
|
-
`Line ${String(lineNum)}: Continuing (indent=${String(commentIndent)}): "${commentText}"`,
|
|
530
|
-
);
|
|
531
|
-
} else {
|
|
532
|
-
// Different indent, reset
|
|
533
|
-
pendingDebugInfo.push(
|
|
534
|
-
`Line ${String(lineNum)}: Different indent (${String(commentIndent)} vs ${String(pendingCommentIndent)}), resetting`,
|
|
535
|
-
);
|
|
536
|
-
pendingComment = [commentText];
|
|
537
|
-
pendingCommentIndent = commentIndent;
|
|
538
|
-
pendingDebugInfo.push(`Line ${String(lineNum)}: New start: "${commentText}"`);
|
|
539
|
-
}
|
|
540
|
-
} else {
|
|
541
|
-
// This is a commented-out YAML key, which means the pending comments
|
|
542
|
-
// were describing this commented-out section, not a future real key
|
|
543
|
-
// So we should discard them
|
|
544
|
-
pendingDebugInfo.push(
|
|
545
|
-
`Line ${String(lineNum)}: Commented-out YAML key detected: "${commentText}", discarding pending`,
|
|
546
|
-
);
|
|
547
|
-
pendingComment = [];
|
|
548
|
-
pendingCommentIndent = -1;
|
|
549
|
-
pendingDebugInfo = [];
|
|
317
|
+
/**
|
|
318
|
+
* Extract the inline comment string from a YAML value node
|
|
319
|
+
*/
|
|
320
|
+
function extractInlineComment(node: unknown): string {
|
|
321
|
+
const check = InlineCommentSchema.safeParse(node);
|
|
322
|
+
if (!check.success) {
|
|
323
|
+
return "";
|
|
550
324
|
}
|
|
551
|
-
|
|
325
|
+
const strCheck = z.string().safeParse(check.data.comment);
|
|
326
|
+
return strCheck.success ? strCheck.data : "";
|
|
552
327
|
}
|
|
553
328
|
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
329
|
+
/**
|
|
330
|
+
* Collect all comment sources for a YAML pair (key comment, pair comment, inline comment)
|
|
331
|
+
*/
|
|
332
|
+
function collectItemComment(
|
|
333
|
+
pairData: { keyNode: unknown; item: unknown; valueNode: unknown },
|
|
334
|
+
context: { index: number; mapComment: string },
|
|
335
|
+
): string {
|
|
336
|
+
let comment = extractCommentBefore(pairData.keyNode);
|
|
337
|
+
const pairComment = extractCommentBefore(pairData.item);
|
|
338
|
+
if (pairComment) {
|
|
339
|
+
comment = comment ? `${pairComment}\n${comment}` : pairComment;
|
|
340
|
+
}
|
|
341
|
+
const inlineComment = extractInlineComment(pairData.valueNode);
|
|
342
|
+
if (inlineComment) {
|
|
343
|
+
comment = comment ? `${comment}\n${inlineComment}` : inlineComment;
|
|
344
|
+
}
|
|
345
|
+
// First item inherits map comment if it has none
|
|
346
|
+
if (context.index === 0 && !comment && context.mapComment) {
|
|
347
|
+
comment = context.mapComment;
|
|
348
|
+
}
|
|
349
|
+
if (comment) {
|
|
350
|
+
comment = filterCommentedOutYAML(comment);
|
|
351
|
+
}
|
|
352
|
+
return comment;
|
|
353
|
+
}
|
|
567
354
|
|
|
568
|
-
|
|
569
|
-
|
|
355
|
+
/**
|
|
356
|
+
* Store a comment (with @param handling) into the comments map
|
|
357
|
+
*/
|
|
358
|
+
function storeComment(comment: string, fullKey: string): void {
|
|
359
|
+
const hasParamDirective = comment.includes("@param ");
|
|
360
|
+
if (hasParamDirective) {
|
|
361
|
+
const { params, remainingLines } = parseBitnamiParams(comment);
|
|
362
|
+
for (const [paramKey, description] of params.entries()) {
|
|
363
|
+
comments.set(paramKey, {
|
|
364
|
+
text: description,
|
|
570
365
|
metadata: {
|
|
571
|
-
source: "
|
|
572
|
-
rawComment:
|
|
573
|
-
|
|
574
|
-
|
|
366
|
+
source: "AST",
|
|
367
|
+
rawComment: comment,
|
|
368
|
+
debugInfo: `Bitnami @param directive for ${paramKey}`,
|
|
369
|
+
},
|
|
370
|
+
});
|
|
371
|
+
}
|
|
372
|
+
const remainingCleaned =
|
|
373
|
+
remainingLines.length > 0
|
|
374
|
+
? cleanYAMLComment(remainingLines.join("\n"))
|
|
375
|
+
: "";
|
|
376
|
+
if (remainingCleaned) {
|
|
377
|
+
comments.set(fullKey, {
|
|
378
|
+
text: remainingCleaned,
|
|
379
|
+
metadata: {
|
|
380
|
+
source: "AST",
|
|
381
|
+
rawComment: comment,
|
|
382
|
+
debugInfo: `AST comment after extracting @param directives`,
|
|
383
|
+
},
|
|
384
|
+
});
|
|
385
|
+
}
|
|
386
|
+
} else if (comment) {
|
|
387
|
+
const cleaned = cleanYAMLComment(comment);
|
|
388
|
+
if (cleaned) {
|
|
389
|
+
comments.set(fullKey, {
|
|
390
|
+
text: cleaned,
|
|
391
|
+
metadata: {
|
|
392
|
+
source: "AST",
|
|
393
|
+
rawComment: comment,
|
|
394
|
+
debugInfo: `Direct AST comment for ${fullKey}`,
|
|
575
395
|
},
|
|
576
396
|
});
|
|
577
|
-
} else {
|
|
578
|
-
pendingDebugInfo.push(
|
|
579
|
-
`Line ${String(lineNum)}: Skipping key "${key}" due to indent mismatch (${String(keyIndent)} vs ${String(pendingCommentIndent)})`,
|
|
580
|
-
);
|
|
581
397
|
}
|
|
582
|
-
|
|
583
|
-
// Reset for next key
|
|
584
|
-
pendingComment = [];
|
|
585
|
-
pendingCommentIndent = -1;
|
|
586
|
-
pendingDebugInfo = [];
|
|
587
|
-
}
|
|
588
|
-
} else if (!trimmed.startsWith("#")) {
|
|
589
|
-
// Non-comment, non-key line - reset pending comment
|
|
590
|
-
if (pendingComment.length > 0) {
|
|
591
|
-
pendingDebugInfo.push(`Line ${String(lineNum)}: Non-comment/non-key line, resetting pending`);
|
|
592
398
|
}
|
|
593
|
-
pendingComment = [];
|
|
594
|
-
pendingCommentIndent = -1;
|
|
595
|
-
pendingDebugInfo = [];
|
|
596
399
|
}
|
|
597
|
-
});
|
|
598
400
|
|
|
599
|
-
|
|
600
|
-
|
|
401
|
+
// Recursively walk the YAML AST and extract comments
|
|
402
|
+
function visitNode(
|
|
403
|
+
node: unknown,
|
|
404
|
+
keyPath: string[] = [],
|
|
405
|
+
inheritedComment = "",
|
|
406
|
+
): void {
|
|
407
|
+
if (node == null) {
|
|
408
|
+
return;
|
|
409
|
+
}
|
|
601
410
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
*/
|
|
607
|
-
export function parseYAMLCommentsWithMetadata(yamlContent: string): Map<string, CommentWithMetadata> {
|
|
608
|
-
const comments = new Map<string, CommentWithMetadata>();
|
|
411
|
+
const mapNodeCheck = MapNodeSchema.safeParse(node);
|
|
412
|
+
if (!mapNodeCheck.success) {
|
|
413
|
+
return;
|
|
414
|
+
}
|
|
609
415
|
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
613
|
-
|
|
614
|
-
|
|
416
|
+
// Extract the map's own comment (to be inherited by first child if needed)
|
|
417
|
+
let mapComment = inheritedComment;
|
|
418
|
+
const mapCommentCheck = z
|
|
419
|
+
.string()
|
|
420
|
+
.safeParse(mapNodeCheck.data.commentBefore);
|
|
421
|
+
if (mapCommentCheck.success) {
|
|
422
|
+
mapComment = mapCommentCheck.data;
|
|
423
|
+
}
|
|
615
424
|
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
425
|
+
for (let i = 0; i < mapNodeCheck.data.items.length; i++) {
|
|
426
|
+
const item = mapNodeCheck.data.items[i];
|
|
427
|
+
const itemCheck = PairSchema.safeParse(item);
|
|
428
|
+
if (!itemCheck.success) {
|
|
429
|
+
continue;
|
|
430
|
+
}
|
|
619
431
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
// Handle map/object nodes - check if node has items array
|
|
625
|
-
const mapNodeCheck = z
|
|
626
|
-
.object({ items: z.array(z.unknown()), commentBefore: z.unknown().optional() })
|
|
627
|
-
.safeParse(node);
|
|
628
|
-
if (mapNodeCheck.success) {
|
|
629
|
-
// Extract the map's own comment (to be inherited by first child if needed)
|
|
630
|
-
let mapComment = inheritedComment;
|
|
631
|
-
const mapCommentCheck = z.string().safeParse(mapNodeCheck.data.commentBefore);
|
|
632
|
-
if (mapCommentCheck.success) {
|
|
633
|
-
mapComment = mapCommentCheck.data;
|
|
432
|
+
const keyNodeCheck = KeyValueSchema.safeParse(itemCheck.data.key);
|
|
433
|
+
if (!keyNodeCheck.success) {
|
|
434
|
+
continue;
|
|
634
435
|
}
|
|
635
436
|
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
657
|
-
// Also check the pair itself for comments
|
|
658
|
-
const pairCommentCheck = z.object({ commentBefore: z.unknown() }).safeParse(item);
|
|
659
|
-
if (pairCommentCheck.success) {
|
|
660
|
-
const pairCommentValue = z.string().safeParse(pairCommentCheck.data.commentBefore);
|
|
661
|
-
const pairComment = pairCommentValue.success ? pairCommentValue.data : "";
|
|
662
|
-
if (pairComment) {
|
|
663
|
-
comment = comment ? `${pairComment}\n${comment}` : pairComment;
|
|
664
|
-
}
|
|
665
|
-
}
|
|
666
|
-
|
|
667
|
-
// Check for inline comments on the value
|
|
668
|
-
const valueCommentCheck = z.object({ comment: z.unknown() }).safeParse(itemCheck.data.value);
|
|
669
|
-
if (valueCommentCheck.success) {
|
|
670
|
-
const inlineComment = z.string().safeParse(valueCommentCheck.data.comment);
|
|
671
|
-
if (inlineComment.success && inlineComment.data) {
|
|
672
|
-
comment = comment ? `${comment}\n${inlineComment.data}` : inlineComment.data;
|
|
673
|
-
}
|
|
674
|
-
}
|
|
675
|
-
|
|
676
|
-
// If this is the first item and has no comment, inherit from map
|
|
677
|
-
if (i === 0 && !comment && mapComment) {
|
|
678
|
-
comment = mapComment;
|
|
679
|
-
}
|
|
680
|
-
|
|
681
|
-
// Filter out commented-out YAML blocks before cleaning
|
|
682
|
-
if (comment) {
|
|
683
|
-
comment = filterCommentedOutYAML(comment);
|
|
684
|
-
}
|
|
685
|
-
|
|
686
|
-
// Check if this is a Bitnami-style @param comment
|
|
687
|
-
// These need special handling before cleaning
|
|
688
|
-
const hasParamDirective = comment.includes("@param ");
|
|
689
|
-
if (hasParamDirective) {
|
|
690
|
-
const { params, remainingLines } = parseBitnamiParams(comment);
|
|
691
|
-
|
|
692
|
-
// Store each param comment with its specific key
|
|
693
|
-
for (const [paramKey, description] of params.entries()) {
|
|
694
|
-
comments.set(paramKey, {
|
|
695
|
-
text: description,
|
|
696
|
-
metadata: {
|
|
697
|
-
source: "AST",
|
|
698
|
-
rawComment: comment,
|
|
699
|
-
debugInfo: `Bitnami @param directive for ${paramKey}`,
|
|
700
|
-
},
|
|
701
|
-
});
|
|
702
|
-
}
|
|
703
|
-
|
|
704
|
-
// Store remaining non-param lines with the current key if any
|
|
705
|
-
if (remainingLines.length > 0) {
|
|
706
|
-
const cleaned = cleanYAMLComment(remainingLines.join("\n"));
|
|
707
|
-
if (cleaned) {
|
|
708
|
-
comments.set(fullKey, {
|
|
709
|
-
text: cleaned,
|
|
710
|
-
metadata: {
|
|
711
|
-
source: "AST",
|
|
712
|
-
rawComment: comment,
|
|
713
|
-
debugInfo: `AST comment after extracting @param directives`,
|
|
714
|
-
},
|
|
715
|
-
});
|
|
716
|
-
}
|
|
717
|
-
}
|
|
718
|
-
} else {
|
|
719
|
-
// Clean and store the comment normally
|
|
720
|
-
if (comment) {
|
|
721
|
-
const cleaned = cleanYAMLComment(comment);
|
|
722
|
-
if (cleaned) {
|
|
723
|
-
comments.set(fullKey, {
|
|
724
|
-
text: cleaned,
|
|
725
|
-
metadata: {
|
|
726
|
-
source: "AST",
|
|
727
|
-
rawComment: comment,
|
|
728
|
-
debugInfo: `Direct AST comment for ${fullKey}`,
|
|
729
|
-
},
|
|
730
|
-
});
|
|
731
|
-
}
|
|
732
|
-
}
|
|
733
|
-
}
|
|
734
|
-
|
|
735
|
-
// Check if the value has a commentBefore (for nested structures)
|
|
736
|
-
let valueInheritedComment = "";
|
|
737
|
-
const valueCommentBeforeCheck = z.object({ commentBefore: z.unknown() }).safeParse(itemCheck.data.value);
|
|
738
|
-
if (valueCommentBeforeCheck.success) {
|
|
739
|
-
const valueCommentBefore = z.string().safeParse(valueCommentBeforeCheck.data.commentBefore);
|
|
740
|
-
if (valueCommentBefore.success) {
|
|
741
|
-
valueInheritedComment = valueCommentBefore.data;
|
|
742
|
-
}
|
|
743
|
-
}
|
|
744
|
-
|
|
745
|
-
// Recurse into nested structures
|
|
746
|
-
if (itemCheck.data.value) {
|
|
747
|
-
visitNode(itemCheck.data.value, newPath, valueInheritedComment);
|
|
748
|
-
}
|
|
437
|
+
const key = keyNodeCheck.data.value;
|
|
438
|
+
const newPath = [...keyPath, key];
|
|
439
|
+
const fullKey = newPath.join(".");
|
|
440
|
+
|
|
441
|
+
const comment = collectItemComment(
|
|
442
|
+
{
|
|
443
|
+
keyNode: itemCheck.data.key,
|
|
444
|
+
item,
|
|
445
|
+
valueNode: itemCheck.data.value,
|
|
446
|
+
},
|
|
447
|
+
{ index: i, mapComment },
|
|
448
|
+
);
|
|
449
|
+
storeComment(comment, fullKey);
|
|
450
|
+
|
|
451
|
+
const valueInheritedComment = extractCommentBefore(
|
|
452
|
+
itemCheck.data.value,
|
|
453
|
+
);
|
|
454
|
+
|
|
455
|
+
if (itemCheck.data.value != null) {
|
|
456
|
+
visitNode(itemCheck.data.value, newPath, valueInheritedComment);
|
|
749
457
|
}
|
|
750
458
|
}
|
|
751
459
|
}
|