pm-ops 2026.7.31 → 2026.8.3

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.
@@ -0,0 +1,1278 @@
1
+ /**
2
+ * @file Lexer-backed docstring coverage analyzer.
3
+ *
4
+ * The fleet mandate requires every source declaration to carry a real
5
+ * docstring, enforced by a gate that cannot be cheated. A naive implementation
6
+ * is worse than none: a green check that proves nothing is active cover for
7
+ * missing documentation. This module is the replacement for a prior regex-based
8
+ * attempt, rebuilt on the one source of truth that distinguishes real syntax
9
+ * from text that merely looks syntactic: the TypeScript token stream.
10
+ *
11
+ * ## Why a lexer, not a regex or the compiler
12
+ *
13
+ * TypeScript 7 removed the stable compiler API (`createSourceFile`,
14
+ * `forEachChild`, …), so there is no AST to walk: `import ts from "typescript"`
15
+ * yields only `{ version }` at run time. What the `typescript/unstable/ast`
16
+ * surface still exposes is the lexer — `createScanner` — which emits exactly
17
+ * the punctuation and keywords that exist in the file. A brace inside a string,
18
+ * template literal, regex, or comment is never a `OpenBraceToken`; a declaration
19
+ * written inside a comment produces no tokens at all. That is precisely the
20
+ * property a brace-counting regex lacks, and it is what makes the rules below
21
+ * trustworthy.
22
+ *
23
+ * The scanner needs two corrections the unstable surface does not apply
24
+ * automatically, both handled in {@link tokenize}:
25
+ *
26
+ * 1. A template substitution `${ … }` emits a `CloseBraceToken` for the `}`
27
+ * without a matching `OpenBraceToken` (the `${` is folded into the
28
+ * `TemplateHead`). Plain brace counting is corrupted by it, exactly as the
29
+ * regex version was corrupted by braces in strings. The fix is to track a
30
+ * frame stack and, only when a `}` closes a substitution frame, call
31
+ * `reScanTemplateToken` so it becomes a `TemplateMiddle` / `TemplateTail`
32
+ * and never reaches the brace counter.
33
+ *
34
+ * 2. The scanner does not decide regex versus division on its own: a `/` is
35
+ * always a `SlashToken`, and `reScanSlashToken` scans a regex
36
+ * unconditionally — which swallows a division operator and everything after
37
+ * it as one unterminated regular expression. The fix is to rescan `/` as a
38
+ * regex only when the previous significant token cannot end an expression,
39
+ * matching the heuristic every JavaScript lexer uses.
40
+ *
41
+ * After that, the token array contains only real, balanced braces, and group
42
+ * matching over it is exact.
43
+ *
44
+ * ## Docstring recovery
45
+ *
46
+ * `getLeadingCommentRanges(text, tokenFullStart)` returns only the comments in
47
+ * the contiguous trivia immediately preceding a token, so a JSDoc block that
48
+ * lives inside a string literal, a template, or a commented-out line is
49
+ * invisible to it — the four classic cheats. JSDoc is a block comment (`kind`
50
+ * `3`) beginning with `/**`; a `//` line comment is `kind` `2` and does not
51
+ * count.
52
+ *
53
+ * ## Rules
54
+ *
55
+ * The documented surface — every symbol {@link judge} evaluates — is:
56
+ *
57
+ * - every exported declaration (`function`, `class`, `interface`, `type`,
58
+ * `const` / `let` / `var`, including `export default function|class`);
59
+ * - every non-`private`, non-`protected`, non-`#` member of an exported class
60
+ * (methods, properties, and `get` / `set` accessors);
61
+ * - every non-exported `function` whose body spans more than
62
+ * {@link INTERNAL_BODY_LINES} lines, implemented (unlike the prior attempt's
63
+ * declared-but-never-read constant).
64
+ *
65
+ * A passing docstring must carry at least {@link MIN_DOC_WORDS} meaningful words
66
+ * after filler removal and contribute at least {@link MIN_NOVEL_WORDS} terms the
67
+ * identifier did not already carry, so a `Gets the name.` JSDoc on `getName`
68
+ * fails.
69
+ *
70
+ * ## Fail-closed
71
+ *
72
+ * A declaration keyword the finder cannot classify is a violation with reason
73
+ * `unrecognized declaration form`, never a silent skip. The silent skip is the
74
+ * cheat this gate exists to prevent.
75
+ *
76
+ * ## Out of scope (structural, not configurable)
77
+ *
78
+ * `.d.ts` ambient declarations, `test/`, `dist/`, and `node_modules` are skipped
79
+ * by hard-coded directory rules; imports and re-export statements (`export { … }
80
+ * from`, `export * from`) declare nothing; `export default <expression>`,
81
+ * overload signatures, constructors, index signatures, computed-name members,
82
+ * `private` / `protected` / `#` members, and anonymous function/arrow
83
+ * expressions carry no stable identifier and are therefore not in the documented
84
+ * surface. There is deliberately no ignore list: an exemption must be a
85
+ * documented rule with a reason, not a configuration entry.
86
+ */
87
+ import { createScanner, getLeadingCommentRanges } from "typescript/unstable/ast/scanner";
88
+ import { SyntaxKind, LanguageVariant } from "typescript/unstable/ast";
89
+ import { readFileSync, readdirSync, realpathSync, statSync } from "node:fs";
90
+ import { dirname, join, relative } from "node:path";
91
+ /**
92
+ * Minimum number of meaningful words a docstring must contain after filler
93
+ * removal. Below this the comment is too thin to document anything.
94
+ */
95
+ export const MIN_DOC_WORDS = 4;
96
+ /**
97
+ * Minimum number of meaningful words a docstring must contribute that the
98
+ * identifier itself does not already carry. This is what rejects a comment that
99
+ * merely restates the name.
100
+ */
101
+ export const MIN_NOVEL_WORDS = 2;
102
+ /**
103
+ * Number of source lines a non-exported function body must span before it joins
104
+ * the documented surface. Short private helpers stay undocumented; substantial
105
+ * ones must explain themselves.
106
+ */
107
+ export const INTERNAL_BODY_LINES = 4;
108
+ /**
109
+ * Filler words stripped before a docstring or identifier is reduced to its
110
+ * content terms. These carry no domain meaning, so counting them would let a
111
+ * restating comment pass the word-count rule.
112
+ */
113
+ const FILLER = new Set([
114
+ "a", "an", "the", "of", "for", "to", "from", "in", "on", "at", "by", "with",
115
+ "and", "or", "is", "are", "was", "be", "been", "this", "that", "these", "those",
116
+ "it", "its", "as", "get", "gets", "set", "sets", "return", "returns", "returned",
117
+ "value", "values", "given", "used", "use", "uses", "when", "if", "then", "into",
118
+ "via", "per", "not", "may", "can", "will", "has", "have", "had", "does", "do",
119
+ "making", "make", "called", "call", "calls",
120
+ "whether", "while", "which", "what", "where", "how", "why",
121
+ "both", "each", "all", "any", "some", "no", "nor", "none", "either", "neither",
122
+ "also", "always", "never", "often", "usually", "typically", "generally",
123
+ "respectively", "eg", "ie",
124
+ ]);
125
+ /**
126
+ * Directory entries never descended into while collecting source files. Each is
127
+ * a structural exclusion with a reason, not a configurable ignore list:
128
+ *
129
+ * - `node_modules` / `dist` / `dist-test` / `coverage` are installed or built
130
+ * output, not authored source;
131
+ * - `test` holds the gate's own fixtures, which intentionally lack docstrings;
132
+ * - `.git` is version-control metadata.
133
+ */
134
+ const SKIP_DIRS = new Set(["node_modules", "dist", "dist-test", "coverage", "test", ".git"]);
135
+ /**
136
+ * Tokens after which a `/` is a regex rather than division, per the heuristic
137
+ * every JavaScript lexer applies: a `/` is division only when the previous
138
+ * significant token can end an expression. Everything else (operators, `=`, `,`,
139
+ * `(`, `[`, `{`, `;`, `:`, `?`, `return`, `typeof`, …) starts a new expression
140
+ * and so begins a regex.
141
+ */
142
+ const DIVISION_PREVIOUS = new Set([
143
+ SyntaxKind.Identifier,
144
+ SyntaxKind.NumericLiteral,
145
+ SyntaxKind.StringLiteral,
146
+ SyntaxKind.NoSubstitutionTemplateLiteral,
147
+ SyntaxKind.TemplateTail,
148
+ SyntaxKind.RegularExpressionLiteral,
149
+ SyntaxKind.BigIntLiteral,
150
+ SyntaxKind.TrueKeyword,
151
+ SyntaxKind.FalseKeyword,
152
+ SyntaxKind.NullKeyword,
153
+ SyntaxKind.ThisKeyword,
154
+ SyntaxKind.SuperKeyword,
155
+ SyntaxKind.CloseParenToken,
156
+ SyntaxKind.CloseBracketToken,
157
+ SyntaxKind.CloseBraceToken,
158
+ ]);
159
+ /** Tokens that can begin a class member after automatic semicolon insertion. */
160
+ const MEMBER_STARTS = new Set([
161
+ SyntaxKind.Identifier,
162
+ SyntaxKind.PrivateIdentifier,
163
+ SyntaxKind.StringLiteral,
164
+ SyntaxKind.NumericLiteral,
165
+ SyntaxKind.AtToken,
166
+ SyntaxKind.OpenBracketToken,
167
+ SyntaxKind.AsteriskToken,
168
+ SyntaxKind.PublicKeyword,
169
+ SyntaxKind.PrivateKeyword,
170
+ SyntaxKind.ProtectedKeyword,
171
+ SyntaxKind.StaticKeyword,
172
+ SyntaxKind.ReadonlyKeyword,
173
+ SyntaxKind.AbstractKeyword,
174
+ SyntaxKind.OverrideKeyword,
175
+ SyntaxKind.DeclareKeyword,
176
+ SyntaxKind.AsyncKeyword,
177
+ SyntaxKind.GetKeyword,
178
+ SyntaxKind.SetKeyword,
179
+ SyntaxKind.ConstructorKeyword,
180
+ ]);
181
+ /** Tokens that can end an expression immediately before an ASI boundary. */
182
+ const STATEMENT_ENDS = new Set([
183
+ ...DIVISION_PREVIOUS,
184
+ SyntaxKind.PlusPlusToken,
185
+ SyntaxKind.MinusMinusToken,
186
+ SyntaxKind.ExclamationToken,
187
+ ]);
188
+ /** Tokens that can begin a declaration after an ASI-terminated variable statement. */
189
+ const DECLARATION_STARTS = new Set([
190
+ SyntaxKind.ExportKeyword,
191
+ SyntaxKind.ImportKeyword,
192
+ SyntaxKind.FunctionKeyword,
193
+ SyntaxKind.ClassKeyword,
194
+ SyntaxKind.InterfaceKeyword,
195
+ SyntaxKind.TypeKeyword,
196
+ SyntaxKind.EnumKeyword,
197
+ SyntaxKind.NamespaceKeyword,
198
+ SyntaxKind.ModuleKeyword,
199
+ SyntaxKind.ConstKeyword,
200
+ SyntaxKind.LetKeyword,
201
+ SyntaxKind.VarKeyword,
202
+ SyntaxKind.DeclareKeyword,
203
+ ]);
204
+ /**
205
+ * Tokens after which a fresh primary type may begin, so a following `{` is an
206
+ * object/type literal rather than a function body. Used by the return-type
207
+ * skipper to find where a function body actually starts.
208
+ */
209
+ const TYPE_CONTINUES = new Set([
210
+ SyntaxKind.ColonToken,
211
+ SyntaxKind.BarToken,
212
+ SyntaxKind.AmpersandToken,
213
+ SyntaxKind.EqualsGreaterThanToken,
214
+ SyntaxKind.CommaToken,
215
+ SyntaxKind.QuestionToken,
216
+ SyntaxKind.DotToken,
217
+ SyntaxKind.ExtendsKeyword,
218
+ SyntaxKind.InKeyword,
219
+ SyntaxKind.KeyOfKeyword,
220
+ SyntaxKind.TypeOfKeyword,
221
+ SyntaxKind.InferKeyword,
222
+ SyntaxKind.ReadonlyKeyword,
223
+ SyntaxKind.NewKeyword,
224
+ SyntaxKind.AsKeyword,
225
+ SyntaxKind.SatisfiesKeyword,
226
+ SyntaxKind.IsKeyword,
227
+ ]);
228
+ /** Sentinel returned when the cursor runs past the last token, so every loop can test for end of input uniformly. */
229
+ const EOF_TOKEN = { kind: SyntaxKind.EndOfFile, start: Number.MAX_SAFE_INTEGER, fullStart: Number.MAX_SAFE_INTEGER, text: "" };
230
+ /** `true` for a `SyntaxKind` in the keyword range (used for member-name checks). */
231
+ function isKeyword(kind) {
232
+ return kind >= SyntaxKind.FirstKeyword && kind <= SyntaxKind.LastKeyword;
233
+ }
234
+ /**
235
+ * Convert a token stream into a clean array of braces. Runs the unstable
236
+ * scanner with trivia skipped and applies the two corrections documented at the
237
+ * top of this file: template-substitution `}` is rescanned into a template
238
+ * continuation, and `/` is rescanned as a regex only in regex context. The
239
+ * returned array never contains a brace that does not correspond to a real
240
+ * `{` / `}` in the source.
241
+ */
242
+ function tokenize(text) {
243
+ const scanner = createScanner(true, LanguageVariant.Standard, text);
244
+ const tokens = [];
245
+ const group = [];
246
+ let prev;
247
+ for (;;) {
248
+ let kind = scanner.scan();
249
+ if (kind === SyntaxKind.EndOfFile)
250
+ break;
251
+ if (kind === SyntaxKind.SlashToken || kind === SyntaxKind.SlashEqualsToken) {
252
+ if (prev === undefined || !DIVISION_PREVIOUS.has(prev)) {
253
+ kind = scanner.reScanSlashToken();
254
+ }
255
+ }
256
+ if (kind === SyntaxKind.TemplateHead || kind === SyntaxKind.TemplateMiddle) {
257
+ group.push("sub");
258
+ }
259
+ else if (kind === SyntaxKind.OpenBraceToken) {
260
+ group.push("brace");
261
+ }
262
+ else if (kind === SyntaxKind.OpenParenToken) {
263
+ group.push("paren");
264
+ }
265
+ else if (kind === SyntaxKind.OpenBracketToken) {
266
+ group.push("bracket");
267
+ }
268
+ else if (kind === SyntaxKind.CloseBraceToken) {
269
+ const top = group[group.length - 1];
270
+ if (top === "sub") {
271
+ kind = scanner.reScanTemplateToken(false);
272
+ group.pop();
273
+ if (kind === SyntaxKind.TemplateMiddle)
274
+ group.push("sub");
275
+ }
276
+ else if (top === "brace") {
277
+ group.pop();
278
+ }
279
+ }
280
+ else if (kind === SyntaxKind.CloseParenToken) {
281
+ if (group[group.length - 1] === "paren")
282
+ group.pop();
283
+ }
284
+ else if (kind === SyntaxKind.CloseBracketToken) {
285
+ if (group[group.length - 1] === "bracket")
286
+ group.pop();
287
+ }
288
+ tokens.push({ kind, start: scanner.getTokenStart(), fullStart: scanner.getTokenFullStart(), text: scanner.getTokenText() });
289
+ prev = kind;
290
+ }
291
+ return tokens;
292
+ }
293
+ /**
294
+ * Build a direct offset-to-1-based-line map so any token offset resolves to its
295
+ * line in O(1) without re-scanning the text per lookup.
296
+ */
297
+ function buildLineMap(text) {
298
+ const lines = new Uint32Array(text.length + 1);
299
+ let line = 1;
300
+ for (let i = 0; i < text.length; i++) {
301
+ lines[i] = line;
302
+ if (text.charCodeAt(i) === 10)
303
+ line++;
304
+ }
305
+ lines[text.length] = line;
306
+ return lines;
307
+ }
308
+ /**
309
+ * Reduce a comment or identifier to its lowercased, content-bearing words by
310
+ * splitting camelCase and identifier boundaries and dropping short tokens and
311
+ * {@link FILLER}.
312
+ */
313
+ function toWords(text) {
314
+ return text
315
+ .replace(/([a-z0-9])([A-Z])/g, "$1 $2")
316
+ .replace(/([A-Z]+)([A-Z][a-z])/g, "$1 $2")
317
+ .split(/[^A-Za-z0-9]+/)
318
+ .map((word) => word.toLowerCase())
319
+ .filter((word) => word.length > 1 && !FILLER.has(word));
320
+ }
321
+ /**
322
+ * Strip the `/** … *\/` delimiters and per-line leading asterisks from a raw
323
+ * comment range, then drop fenced code blocks and `@tag` markers so the
324
+ * remaining text is the prose a docstring is judged on.
325
+ */
326
+ function cleanComment(raw) {
327
+ const body = raw.replace(/^\/\*\*?/, "").replace(/\*\/$/, "");
328
+ const stripped = body.split("\n").map((line) => line.replace(/^\s*\* ?/, "").trim());
329
+ let text = stripped.join("\n");
330
+ text = text.replace(/```[^`]*?```/g, " ");
331
+ text = text.replace(/@[A-Za-z][A-Za-z0-9_-]*/g, " ");
332
+ return text.replace(/\s+/g, " ").trim();
333
+ }
334
+ /**
335
+ * Recursively collect every authored `.ts` file (excluding `.d.ts`) beneath the
336
+ * given roots, skipping the structural non-source directories in
337
+ * {@link SKIP_DIRS}. A root may itself be a single file.
338
+ */
339
+ function collectSourceFiles(roots) {
340
+ const out = [];
341
+ const visited = new Set();
342
+ const walk = (dir) => {
343
+ let canonical;
344
+ try {
345
+ canonical = realpathSync(dir);
346
+ }
347
+ catch (error) {
348
+ throw new Error(`docstring coverage: cannot resolve directory ${dir}: ${error instanceof Error ? error.message : String(error)}`);
349
+ }
350
+ if (visited.has(canonical))
351
+ return;
352
+ visited.add(canonical);
353
+ let entries;
354
+ try {
355
+ entries = readdirSync(dir);
356
+ }
357
+ catch (error) {
358
+ throw new Error(`docstring coverage: cannot read directory ${dir}: ${error instanceof Error ? error.message : String(error)}`);
359
+ }
360
+ for (const name of entries) {
361
+ const full = join(dir, name);
362
+ let info;
363
+ try {
364
+ info = statSync(full);
365
+ }
366
+ catch (error) {
367
+ throw new Error(`docstring coverage: cannot stat ${full}: ${error instanceof Error ? error.message : String(error)}`);
368
+ }
369
+ if (info.isDirectory()) {
370
+ if (!SKIP_DIRS.has(name))
371
+ walk(full);
372
+ }
373
+ else if (info.isFile() && name.endsWith(".ts") && !name.endsWith(".d.ts")) {
374
+ out.push(full);
375
+ }
376
+ }
377
+ };
378
+ for (const root of roots) {
379
+ let info;
380
+ try {
381
+ info = statSync(root);
382
+ }
383
+ catch (error) {
384
+ if (typeof error === "object" && error !== null && "code" in error && error.code === "ENOENT")
385
+ continue;
386
+ throw new Error(`docstring coverage: cannot stat root ${root}: ${error instanceof Error ? error.message : String(error)}`);
387
+ }
388
+ if (info.isDirectory()) {
389
+ walk(root);
390
+ }
391
+ else if (info.isFile() && root.endsWith(".ts") && !root.endsWith(".d.ts")) {
392
+ out.push(root);
393
+ }
394
+ }
395
+ return out;
396
+ }
397
+ /**
398
+ * Recursive-descent declaration finder over a clean token array. Non-exported by
399
+ * design: its members are implementation detail, not documented surface, and
400
+ * the public entry points {@link analyzeSource} / {@link analyzeDocstringCoverage}
401
+ * are what callers (and the gate) depend on.
402
+ */
403
+ class SourceAnalyzer {
404
+ violations = [];
405
+ declarationsChecked = 0;
406
+ i = 0;
407
+ text;
408
+ file;
409
+ tokens;
410
+ lineOf;
411
+ constructor(text, file, tokens, lineOf) {
412
+ this.text = text;
413
+ this.file = file;
414
+ this.tokens = tokens;
415
+ this.lineOf = lineOf;
416
+ }
417
+ /** Parse the whole source as a module body. */
418
+ run() {
419
+ this.parseStatements();
420
+ }
421
+ cur() {
422
+ return this.tokens[this.i] ?? EOF_TOKEN;
423
+ }
424
+ peekKind(n) {
425
+ return this.tokens[this.i + n]?.kind;
426
+ }
427
+ at(kind) {
428
+ return this.cur().kind === kind;
429
+ }
430
+ /** Index of the closer matching the opener at `openIdx`, balancing over the clean array. */
431
+ matchingClose(openIdx) {
432
+ let depth = 0;
433
+ for (let j = openIdx; j < this.tokens.length; j++) {
434
+ const k = this.tokens[j].kind;
435
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken) {
436
+ depth++;
437
+ }
438
+ else if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.CloseParenToken || k === SyntaxKind.CloseBracketToken) {
439
+ depth--;
440
+ if (depth === 0)
441
+ return j;
442
+ }
443
+ }
444
+ return this.tokens.length - 1;
445
+ }
446
+ /** Advance past the balanced group whose opener is the current token. */
447
+ skipGroup() {
448
+ this.i = this.matchingClose(this.i) + 1;
449
+ }
450
+ /** Parse statements inside the brace block opened at `openIdx`, then consume its closer. */
451
+ descendBlock(openIdx) {
452
+ this.i = openIdx + 1;
453
+ this.parseStatements();
454
+ if (this.i < this.tokens.length && this.at(SyntaxKind.CloseBraceToken))
455
+ this.i++;
456
+ }
457
+ /** Parse statements until a closing brace or end of input. */
458
+ parseStatements() {
459
+ while (this.i < this.tokens.length) {
460
+ const k = this.cur().kind;
461
+ if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile)
462
+ return;
463
+ this.parseStatement();
464
+ }
465
+ }
466
+ /** Parse one statement in any statement context (module, block, body). */
467
+ parseStatement() {
468
+ const head = this.cur();
469
+ const k = head.kind;
470
+ switch (k) {
471
+ case SyntaxKind.ExportKeyword:
472
+ this.parseExport();
473
+ return;
474
+ case SyntaxKind.ImportKeyword:
475
+ this.skipUntilSemicolon();
476
+ return;
477
+ case SyntaxKind.FunctionKeyword:
478
+ this.parseFunction({ exported: false, head });
479
+ return;
480
+ case SyntaxKind.ClassKeyword:
481
+ this.parseClass({ exported: false, head });
482
+ return;
483
+ case SyntaxKind.AsyncKeyword:
484
+ if (this.peekKind(1) === SyntaxKind.FunctionKeyword) {
485
+ this.i++;
486
+ this.parseFunction({ exported: false, head });
487
+ }
488
+ else {
489
+ this.skipExpressionStatement();
490
+ }
491
+ return;
492
+ case SyntaxKind.AbstractKeyword:
493
+ if (this.peekKind(1) === SyntaxKind.ClassKeyword) {
494
+ this.i++;
495
+ this.parseClass({ exported: false, head });
496
+ }
497
+ else {
498
+ this.skipExpressionStatement();
499
+ }
500
+ return;
501
+ case SyntaxKind.DeclareKeyword:
502
+ this.skipUntilSemicolon();
503
+ return;
504
+ case SyntaxKind.InterfaceKeyword:
505
+ this.skipNonExportedInterface();
506
+ return;
507
+ case SyntaxKind.TypeKeyword:
508
+ this.skipNonExportedTypeAlias();
509
+ return;
510
+ case SyntaxKind.ConstKeyword:
511
+ case SyntaxKind.LetKeyword:
512
+ case SyntaxKind.VarKeyword:
513
+ this.i++;
514
+ this.skipExpressionStatement();
515
+ return;
516
+ case SyntaxKind.EnumKeyword:
517
+ case SyntaxKind.NamespaceKeyword:
518
+ this.failClosed(head, "unrecognized declaration form");
519
+ this.skipUntilSemicolon();
520
+ return;
521
+ case SyntaxKind.OpenBraceToken:
522
+ this.descendBlock(this.i);
523
+ return;
524
+ case SyntaxKind.SemicolonToken:
525
+ this.i++;
526
+ return;
527
+ case SyntaxKind.AtToken:
528
+ this.skipDecorators();
529
+ this.parseStatement();
530
+ return;
531
+ case SyntaxKind.IfKeyword:
532
+ this.parseIf();
533
+ return;
534
+ case SyntaxKind.ForKeyword:
535
+ this.parseFor();
536
+ return;
537
+ case SyntaxKind.WhileKeyword:
538
+ this.i++;
539
+ this.skipHeaderParen();
540
+ this.parseBlockOrStatement();
541
+ return;
542
+ case SyntaxKind.DoKeyword:
543
+ this.parseDoWhile();
544
+ return;
545
+ case SyntaxKind.SwitchKeyword:
546
+ this.i++;
547
+ this.skipHeaderParen();
548
+ if (this.at(SyntaxKind.OpenBraceToken))
549
+ this.descendBlock(this.i);
550
+ return;
551
+ case SyntaxKind.TryKeyword:
552
+ this.parseTry();
553
+ return;
554
+ case SyntaxKind.CaseKeyword:
555
+ this.i++;
556
+ this.skipUntilColon();
557
+ return;
558
+ case SyntaxKind.DefaultKeyword:
559
+ this.i++;
560
+ if (this.at(SyntaxKind.ColonToken))
561
+ this.i++;
562
+ return;
563
+ case SyntaxKind.ReturnKeyword:
564
+ case SyntaxKind.ThrowKeyword:
565
+ case SyntaxKind.BreakKeyword:
566
+ case SyntaxKind.ContinueKeyword:
567
+ this.skipExpressionStatement();
568
+ return;
569
+ default:
570
+ if (k === SyntaxKind.Identifier && this.peekKind(1) === SyntaxKind.ColonToken) {
571
+ this.i += 2;
572
+ this.parseStatement();
573
+ return;
574
+ }
575
+ this.skipExpressionStatement();
576
+ return;
577
+ }
578
+ }
579
+ /** Parse an `export` form, classifying the declaration or skipping a re-export. */
580
+ parseExport() {
581
+ const exportToken = this.cur();
582
+ this.i++; // export
583
+ if (this.at(SyntaxKind.DefaultKeyword)) {
584
+ this.i++; // default
585
+ if (this.at(SyntaxKind.AsyncKeyword) && this.peekKind(1) === SyntaxKind.FunctionKeyword) {
586
+ this.i++;
587
+ this.parseFunction({ exported: true, defaultExport: true, head: exportToken });
588
+ return;
589
+ }
590
+ if (this.at(SyntaxKind.FunctionKeyword)) {
591
+ this.parseFunction({ exported: true, defaultExport: true, head: exportToken });
592
+ return;
593
+ }
594
+ if (this.at(SyntaxKind.AbstractKeyword) && this.peekKind(1) === SyntaxKind.ClassKeyword) {
595
+ this.i++;
596
+ this.parseClass({ exported: true, defaultExport: true, head: exportToken });
597
+ return;
598
+ }
599
+ if (this.at(SyntaxKind.ClassKeyword)) {
600
+ this.parseClass({ exported: true, defaultExport: true, head: exportToken });
601
+ return;
602
+ }
603
+ // export default <expression> declares nothing; skip it.
604
+ this.skipExpressionStatement();
605
+ return;
606
+ }
607
+ if (this.at(SyntaxKind.OpenBraceToken)) {
608
+ this.skipGroup();
609
+ this.skipUntilSemicolon();
610
+ return;
611
+ }
612
+ if (this.at(SyntaxKind.AsteriskToken)) {
613
+ this.skipUntilSemicolon();
614
+ return;
615
+ }
616
+ if (this.at(SyntaxKind.TypeKeyword) && this.peekKind(1) === SyntaxKind.OpenBraceToken) {
617
+ this.i++;
618
+ this.skipGroup();
619
+ this.skipUntilSemicolon();
620
+ return;
621
+ }
622
+ if (this.at(SyntaxKind.TypeKeyword)) {
623
+ this.i++;
624
+ this.parseTypeLike({ exported: true, head: exportToken });
625
+ return;
626
+ }
627
+ if (this.at(SyntaxKind.AsyncKeyword) && this.peekKind(1) === SyntaxKind.FunctionKeyword) {
628
+ this.i++;
629
+ this.parseFunction({ exported: true, head: exportToken });
630
+ return;
631
+ }
632
+ if (this.at(SyntaxKind.FunctionKeyword)) {
633
+ this.parseFunction({ exported: true, head: exportToken });
634
+ return;
635
+ }
636
+ if (this.at(SyntaxKind.AbstractKeyword) && this.peekKind(1) === SyntaxKind.ClassKeyword) {
637
+ this.i++;
638
+ this.parseClass({ exported: true, head: exportToken });
639
+ return;
640
+ }
641
+ if (this.at(SyntaxKind.ClassKeyword)) {
642
+ this.parseClass({ exported: true, head: exportToken });
643
+ return;
644
+ }
645
+ if (this.at(SyntaxKind.InterfaceKeyword)) {
646
+ this.i++;
647
+ this.parseTypeLike({ exported: true, head: exportToken });
648
+ return;
649
+ }
650
+ if (this.at(SyntaxKind.ConstKeyword) || this.at(SyntaxKind.LetKeyword) || this.at(SyntaxKind.VarKeyword)) {
651
+ this.i++;
652
+ this.parseExportedVar({ head: exportToken });
653
+ return;
654
+ }
655
+ this.failClosed(this.cur(), "unrecognized declaration form");
656
+ this.skipUntilSemicolon();
657
+ }
658
+ /** Parse a `function` declaration or expression at the current token. */
659
+ parseFunction(opts) {
660
+ this.i++; // function
661
+ if (this.at(SyntaxKind.AsteriskToken))
662
+ this.i++; // generator
663
+ let name;
664
+ if (this.at(SyntaxKind.Identifier)) {
665
+ name = this.cur().text;
666
+ this.i++;
667
+ }
668
+ if (this.at(SyntaxKind.LessThanToken))
669
+ this.skipTypeParams();
670
+ if (this.at(SyntaxKind.OpenParenToken))
671
+ this.skipGroup();
672
+ const body = this.findFunctionBody();
673
+ if (body.kind === "body") {
674
+ const open = this.tokens[body.openIdx];
675
+ const closeIdx = this.matchingClose(body.openIdx);
676
+ const bodyLines = this.lineOf[this.tokens[closeIdx].start] - this.lineOf[open.start];
677
+ if (opts.exported || bodyLines > INTERNAL_BODY_LINES) {
678
+ this.judge(opts.head, name ?? (opts.defaultExport ? "default" : "(anonymous)"));
679
+ }
680
+ this.descendBlock(body.openIdx);
681
+ }
682
+ // A signature with no body is an overload or ambient declaration: out of scope.
683
+ }
684
+ /** Parse a `class` declaration or expression at the current token. */
685
+ parseClass(opts) {
686
+ this.i++; // class
687
+ let name;
688
+ if (this.at(SyntaxKind.Identifier)) {
689
+ name = this.cur().text;
690
+ this.i++;
691
+ }
692
+ if (this.at(SyntaxKind.LessThanToken))
693
+ this.skipTypeParams();
694
+ const bodyIdx = this.findClassBody();
695
+ if (opts.exported)
696
+ this.judge(opts.head, name ?? "default");
697
+ if (bodyIdx >= 0) {
698
+ this.i = bodyIdx + 1;
699
+ this.parseMembers(name ?? "default", opts.exported);
700
+ if (this.at(SyntaxKind.CloseBraceToken))
701
+ this.i++;
702
+ }
703
+ else if (this.at(SyntaxKind.SemicolonToken)) {
704
+ this.i++;
705
+ }
706
+ }
707
+ /** Parse members of a class body until its closing brace. */
708
+ parseMembers(className, classExported) {
709
+ while (this.i < this.tokens.length) {
710
+ const k = this.cur().kind;
711
+ if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile)
712
+ return;
713
+ this.parseMember(className, classExported);
714
+ }
715
+ }
716
+ /** Parse one class member, enforcing the documented-surface rule when in scope. */
717
+ parseMember(className, classExported) {
718
+ const head = this.cur();
719
+ while (this.at(SyntaxKind.AtToken))
720
+ this.skipDecorator();
721
+ let isPrivate = false;
722
+ let accessor = "";
723
+ for (;;) {
724
+ const k = this.cur().kind;
725
+ if (k === SyntaxKind.PrivateKeyword || k === SyntaxKind.ProtectedKeyword) {
726
+ isPrivate = true;
727
+ this.i++;
728
+ continue;
729
+ }
730
+ if (k === SyntaxKind.PublicKeyword ||
731
+ k === SyntaxKind.StaticKeyword ||
732
+ k === SyntaxKind.ReadonlyKeyword ||
733
+ k === SyntaxKind.AbstractKeyword ||
734
+ k === SyntaxKind.OverrideKeyword ||
735
+ k === SyntaxKind.DeclareKeyword ||
736
+ k === SyntaxKind.AsyncKeyword) {
737
+ this.i++;
738
+ continue;
739
+ }
740
+ if ((k === SyntaxKind.GetKeyword || k === SyntaxKind.SetKeyword) && this.peekKind(1) === SyntaxKind.Identifier) {
741
+ accessor = k === SyntaxKind.GetKeyword ? "get" : "set";
742
+ this.i++;
743
+ continue;
744
+ }
745
+ break;
746
+ }
747
+ const k = this.cur().kind;
748
+ if (k === SyntaxKind.HashToken || k === SyntaxKind.PrivateIdentifier) {
749
+ this.i++;
750
+ if (this.at(SyntaxKind.OpenParenToken) || this.at(SyntaxKind.LessThanToken))
751
+ this.skipCallable();
752
+ else
753
+ this.skipMemberRest();
754
+ return;
755
+ }
756
+ if (k === SyntaxKind.SemicolonToken) {
757
+ this.i++;
758
+ return;
759
+ }
760
+ if (k === SyntaxKind.OpenBracketToken) {
761
+ this.skipGroup();
762
+ this.skipMemberRest();
763
+ return;
764
+ }
765
+ if (k === SyntaxKind.OpenBraceToken) {
766
+ this.descendBlock(this.i);
767
+ return;
768
+ }
769
+ if (k === SyntaxKind.AsteriskToken)
770
+ this.i++;
771
+ const nameKind = this.cur().kind;
772
+ if (nameKind === SyntaxKind.ConstructorKeyword) {
773
+ this.i++;
774
+ this.skipCallable();
775
+ return;
776
+ }
777
+ if (nameKind === SyntaxKind.Identifier || isKeyword(nameKind)) {
778
+ const name = this.cur().text;
779
+ this.i++;
780
+ const memberName = accessor ? `${accessor} ${name}` : name;
781
+ const next = this.cur().kind;
782
+ if (next === SyntaxKind.OpenParenToken || next === SyntaxKind.LessThanToken) {
783
+ if (next === SyntaxKind.LessThanToken)
784
+ this.skipTypeParams();
785
+ if (this.at(SyntaxKind.OpenParenToken))
786
+ this.skipGroup();
787
+ const body = this.findFunctionBody();
788
+ if (body.kind === "body") {
789
+ if (classExported && !isPrivate)
790
+ this.judge(head, `${className}.${memberName}`);
791
+ this.descendBlock(body.openIdx);
792
+ }
793
+ }
794
+ else {
795
+ if (classExported && !isPrivate)
796
+ this.judge(head, `${className}.${name}`);
797
+ this.skipMemberRest();
798
+ }
799
+ return;
800
+ }
801
+ this.i++;
802
+ }
803
+ /** Skip a decorator (`@expr`) and any chained decorators. */
804
+ skipDecorators() {
805
+ while (this.at(SyntaxKind.AtToken))
806
+ this.skipDecorator();
807
+ }
808
+ /** Skip one decorator: the `@` and the following dotted-name and optional call arguments. */
809
+ skipDecorator() {
810
+ this.i++; // @
811
+ if (this.cur().kind === SyntaxKind.Identifier) {
812
+ this.i++;
813
+ while (this.at(SyntaxKind.DotToken)) {
814
+ this.i++;
815
+ if (this.cur().kind === SyntaxKind.Identifier || isKeyword(this.cur().kind))
816
+ this.i++;
817
+ }
818
+ }
819
+ if (this.at(SyntaxKind.OpenParenToken))
820
+ this.skipGroup();
821
+ }
822
+ /** Skip type parameters `<…>` starting at the current `<`. */
823
+ skipTypeParams() {
824
+ let depth = 0;
825
+ while (this.i < this.tokens.length) {
826
+ const k = this.cur().kind;
827
+ if (k === SyntaxKind.LessThanToken) {
828
+ depth++;
829
+ this.i++;
830
+ continue;
831
+ }
832
+ if (k === SyntaxKind.GreaterThanToken) {
833
+ depth--;
834
+ this.i++;
835
+ if (depth <= 0)
836
+ return;
837
+ continue;
838
+ }
839
+ if (k === SyntaxKind.GreaterThanGreaterThanToken) {
840
+ depth -= 2;
841
+ this.i++;
842
+ if (depth <= 0)
843
+ return;
844
+ continue;
845
+ }
846
+ if (k === SyntaxKind.GreaterThanGreaterThanGreaterThanToken) {
847
+ depth -= 3;
848
+ this.i++;
849
+ if (depth <= 0)
850
+ return;
851
+ continue;
852
+ }
853
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken || k === SyntaxKind.SemicolonToken)
854
+ return;
855
+ this.i++;
856
+ }
857
+ }
858
+ /**
859
+ * Starting just after a function's parameter list, skip the optional return
860
+ * type and locate the body brace (or the `;` of an overload signature). A `{`
861
+ * is the body only when the type so far is complete (no primary can follow),
862
+ * so an object-literal return type `{ a: number }` is consumed as a type
863
+ * rather than mistaken for the body.
864
+ */
865
+ findFunctionBody() {
866
+ let depth = 0;
867
+ let typeStart = false;
868
+ while (this.i < this.tokens.length) {
869
+ const k = this.cur().kind;
870
+ if (k === SyntaxKind.OpenBraceToken && depth === 0 && !typeStart)
871
+ return { kind: "body", openIdx: this.i };
872
+ if (k === SyntaxKind.SemicolonToken && depth === 0) {
873
+ this.i++;
874
+ return { kind: "signature" };
875
+ }
876
+ if ((k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile) && depth === 0)
877
+ return { kind: "none" };
878
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken || k === SyntaxKind.LessThanToken) {
879
+ depth++;
880
+ typeStart = false;
881
+ }
882
+ else if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.CloseParenToken || k === SyntaxKind.CloseBracketToken || k === SyntaxKind.GreaterThanToken) {
883
+ depth = Math.max(0, depth - 1);
884
+ typeStart = false;
885
+ }
886
+ else if (k === SyntaxKind.GreaterThanGreaterThanToken) {
887
+ depth = Math.max(0, depth - 2);
888
+ typeStart = false;
889
+ }
890
+ else if (k === SyntaxKind.GreaterThanGreaterThanGreaterThanToken) {
891
+ depth = Math.max(0, depth - 3);
892
+ typeStart = false;
893
+ }
894
+ else if (TYPE_CONTINUES.has(k)) {
895
+ typeStart = true;
896
+ }
897
+ else {
898
+ typeStart = false;
899
+ }
900
+ this.i++;
901
+ }
902
+ return { kind: "none" };
903
+ }
904
+ /** Starting after a class name, skip type parameters and heritage to the body brace (or `-1`). */
905
+ findClassBody() {
906
+ let depth = 0;
907
+ while (this.i < this.tokens.length) {
908
+ const k = this.cur().kind;
909
+ if (k === SyntaxKind.OpenBraceToken && depth === 0)
910
+ return this.i;
911
+ if (k === SyntaxKind.SemicolonToken && depth === 0) {
912
+ this.i++;
913
+ return -1;
914
+ }
915
+ if (k === SyntaxKind.EndOfFile)
916
+ return -1;
917
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken || k === SyntaxKind.LessThanToken) {
918
+ depth++;
919
+ }
920
+ else if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.CloseParenToken || k === SyntaxKind.CloseBracketToken || k === SyntaxKind.GreaterThanToken) {
921
+ depth = Math.max(0, depth - 1);
922
+ }
923
+ else if (k === SyntaxKind.GreaterThanGreaterThanToken) {
924
+ depth = Math.max(0, depth - 2);
925
+ }
926
+ else if (k === SyntaxKind.GreaterThanGreaterThanGreaterThanToken) {
927
+ depth = Math.max(0, depth - 3);
928
+ }
929
+ this.i++;
930
+ }
931
+ return -1;
932
+ }
933
+ /** Skip a non-exported `interface` (its body declares nothing in scope). */
934
+ skipNonExportedInterface() {
935
+ this.i++; // interface
936
+ if (this.at(SyntaxKind.Identifier))
937
+ this.i++;
938
+ if (this.at(SyntaxKind.LessThanToken))
939
+ this.skipTypeParams();
940
+ const bodyIdx = this.findClassBody();
941
+ if (bodyIdx >= 0)
942
+ this.i = this.matchingClose(bodyIdx) + 1;
943
+ if (this.at(SyntaxKind.SemicolonToken))
944
+ this.i++;
945
+ }
946
+ /** Skip a non-exported `type` alias to its terminating `;`. */
947
+ skipNonExportedTypeAlias() {
948
+ this.i++; // type
949
+ if (this.at(SyntaxKind.Identifier))
950
+ this.i++;
951
+ if (this.at(SyntaxKind.LessThanToken))
952
+ this.skipTypeParams();
953
+ this.skipUntilSemicolon();
954
+ }
955
+ /** Judge an exported `interface` or `type` alias, then skip its body. */
956
+ parseTypeLike(opts) {
957
+ if (opts.exported) {
958
+ if (this.at(SyntaxKind.Identifier)) {
959
+ this.judge(opts.head, this.cur().text);
960
+ }
961
+ else {
962
+ this.judge(opts.head, "(anonymous)");
963
+ }
964
+ }
965
+ if (this.at(SyntaxKind.Identifier))
966
+ this.i++;
967
+ if (this.at(SyntaxKind.LessThanToken))
968
+ this.skipTypeParams();
969
+ this.skipUntilSemicolon();
970
+ }
971
+ /** Judge every binding in an exported `const` / `let` / `var` declaration. */
972
+ parseExportedVar(opts) {
973
+ for (;;) {
974
+ if (this.at(SyntaxKind.Identifier)) {
975
+ this.judge(opts.head, this.cur().text);
976
+ this.i++;
977
+ }
978
+ else if (this.at(SyntaxKind.OpenBraceToken) || this.at(SyntaxKind.OpenBracketToken)) {
979
+ const openIdx = this.i;
980
+ const closeIdx = this.matchingClose(openIdx);
981
+ const open = this.tokens[openIdx];
982
+ const close = this.tokens[closeIdx];
983
+ this.judge(opts.head, this.text.slice(open.start, close.start + close.text.length).replace(/\s+/g, " "));
984
+ this.i = closeIdx + 1;
985
+ }
986
+ else {
987
+ this.failClosed(this.cur(), "unrecognized declaration form");
988
+ this.skipExpressionStatement();
989
+ return;
990
+ }
991
+ let hasInitializer = false;
992
+ let hasNextDeclarator = false;
993
+ while (this.i < this.tokens.length) {
994
+ const k = this.cur().kind;
995
+ if (k === SyntaxKind.SemicolonToken) {
996
+ this.i++;
997
+ return;
998
+ }
999
+ if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile)
1000
+ return;
1001
+ if (k === SyntaxKind.CommaToken) {
1002
+ this.i++;
1003
+ hasNextDeclarator = true;
1004
+ break;
1005
+ }
1006
+ if (k === SyntaxKind.EqualsToken) {
1007
+ hasInitializer = true;
1008
+ this.i++;
1009
+ continue;
1010
+ }
1011
+ const previous = this.tokens[this.i - 1];
1012
+ if (previous &&
1013
+ this.lineOf[this.cur().start] > this.lineOf[previous.start] &&
1014
+ STATEMENT_ENDS.has(previous.kind) &&
1015
+ DECLARATION_STARTS.has(k)) {
1016
+ return;
1017
+ }
1018
+ if (!hasInitializer && k === SyntaxKind.LessThanToken) {
1019
+ this.skipTypeParams();
1020
+ continue;
1021
+ }
1022
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken) {
1023
+ this.skipGroup();
1024
+ continue;
1025
+ }
1026
+ this.i++;
1027
+ }
1028
+ if (!hasNextDeclarator)
1029
+ return;
1030
+ }
1031
+ }
1032
+ /** Skip a callable's optional type parameters, parameter list, and body. */
1033
+ skipCallable() {
1034
+ if (this.at(SyntaxKind.LessThanToken))
1035
+ this.skipTypeParams();
1036
+ if (this.at(SyntaxKind.OpenParenToken))
1037
+ this.skipGroup();
1038
+ const body = this.findFunctionBody();
1039
+ if (body.kind === "body")
1040
+ this.descendBlock(body.openIdx);
1041
+ }
1042
+ /** Skip a member's trailing type or initializer to its terminating `;`. */
1043
+ skipMemberRest() {
1044
+ while (this.i < this.tokens.length) {
1045
+ const k = this.cur().kind;
1046
+ if (k === SyntaxKind.SemicolonToken) {
1047
+ this.i++;
1048
+ return;
1049
+ }
1050
+ if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile)
1051
+ return;
1052
+ const previous = this.tokens[this.i - 1];
1053
+ if (previous &&
1054
+ this.lineOf[this.cur().start] > this.lineOf[previous.start] &&
1055
+ DIVISION_PREVIOUS.has(previous.kind) &&
1056
+ MEMBER_STARTS.has(k)) {
1057
+ return;
1058
+ }
1059
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken) {
1060
+ this.skipGroup();
1061
+ continue;
1062
+ }
1063
+ this.i++;
1064
+ }
1065
+ }
1066
+ /** Skip tokens to the next `;` at group-depth zero, balancing nested groups. */
1067
+ skipUntilSemicolon() {
1068
+ while (this.i < this.tokens.length) {
1069
+ const k = this.cur().kind;
1070
+ if (k === SyntaxKind.SemicolonToken) {
1071
+ this.i++;
1072
+ return;
1073
+ }
1074
+ if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile)
1075
+ return;
1076
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken) {
1077
+ this.skipGroup();
1078
+ continue;
1079
+ }
1080
+ this.i++;
1081
+ }
1082
+ }
1083
+ /** Skip tokens to the next `:` at group-depth zero (a `case` label). */
1084
+ skipUntilColon() {
1085
+ while (this.i < this.tokens.length) {
1086
+ const k = this.cur().kind;
1087
+ if (k === SyntaxKind.ColonToken) {
1088
+ this.i++;
1089
+ return;
1090
+ }
1091
+ if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile)
1092
+ return;
1093
+ if (k === SyntaxKind.OpenBraceToken || k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken) {
1094
+ this.skipGroup();
1095
+ continue;
1096
+ }
1097
+ this.i++;
1098
+ }
1099
+ }
1100
+ /** Skip a control-flow header's `(...)` when present. */
1101
+ skipHeaderParen() {
1102
+ if (this.at(SyntaxKind.OpenParenToken))
1103
+ this.skipGroup();
1104
+ }
1105
+ /** Parse an `if (…) body else body`. */
1106
+ parseIf() {
1107
+ this.i++;
1108
+ this.skipHeaderParen();
1109
+ this.parseBlockOrStatement();
1110
+ if (this.at(SyntaxKind.ElseKeyword)) {
1111
+ this.i++;
1112
+ this.parseBlockOrStatement();
1113
+ }
1114
+ }
1115
+ /** Parse a `for (…) body` (and `for await`). */
1116
+ parseFor() {
1117
+ this.i++;
1118
+ if (this.at(SyntaxKind.AwaitKeyword))
1119
+ this.i++;
1120
+ this.skipHeaderParen();
1121
+ this.parseBlockOrStatement();
1122
+ }
1123
+ /** Parse a `do body while (…);`. */
1124
+ parseDoWhile() {
1125
+ this.i++;
1126
+ this.parseBlockOrStatement();
1127
+ if (this.at(SyntaxKind.WhileKeyword)) {
1128
+ this.i++;
1129
+ this.skipHeaderParen();
1130
+ if (this.at(SyntaxKind.SemicolonToken))
1131
+ this.i++;
1132
+ }
1133
+ }
1134
+ /** Parse a `try { } catch (…) { } finally { }`. */
1135
+ parseTry() {
1136
+ this.i++;
1137
+ if (this.at(SyntaxKind.OpenBraceToken))
1138
+ this.descendBlock(this.i);
1139
+ if (this.at(SyntaxKind.CatchKeyword)) {
1140
+ this.i++;
1141
+ if (this.at(SyntaxKind.OpenParenToken))
1142
+ this.skipGroup();
1143
+ if (this.at(SyntaxKind.OpenBraceToken))
1144
+ this.descendBlock(this.i);
1145
+ }
1146
+ if (this.at(SyntaxKind.FinallyKeyword)) {
1147
+ this.i++;
1148
+ if (this.at(SyntaxKind.OpenBraceToken))
1149
+ this.descendBlock(this.i);
1150
+ }
1151
+ }
1152
+ /** Parse a statement body that is either a brace block or a single statement. */
1153
+ parseBlockOrStatement() {
1154
+ if (this.at(SyntaxKind.OpenBraceToken))
1155
+ this.descendBlock(this.i);
1156
+ else
1157
+ this.parseStatement();
1158
+ }
1159
+ /**
1160
+ * Skip an expression statement to its terminating `;` (or a block/EOF under
1161
+ * ASI), descending into arrow/function brace bodies so nested declarations
1162
+ * are still found, and skipping object literals and other groups opaquely.
1163
+ */
1164
+ skipExpressionStatement() {
1165
+ while (this.i < this.tokens.length) {
1166
+ const k = this.cur().kind;
1167
+ if (k === SyntaxKind.SemicolonToken) {
1168
+ this.i++;
1169
+ return;
1170
+ }
1171
+ if (k === SyntaxKind.CloseBraceToken || k === SyntaxKind.EndOfFile)
1172
+ return;
1173
+ if (k === SyntaxKind.OpenBraceToken) {
1174
+ const prev = this.tokens[this.i - 1]?.kind;
1175
+ if (prev === SyntaxKind.CloseParenToken || prev === SyntaxKind.EqualsGreaterThanToken) {
1176
+ this.descendBlock(this.i);
1177
+ }
1178
+ else {
1179
+ this.skipGroup();
1180
+ }
1181
+ continue;
1182
+ }
1183
+ if (k === SyntaxKind.OpenParenToken || k === SyntaxKind.OpenBracketToken) {
1184
+ this.skipGroup();
1185
+ continue;
1186
+ }
1187
+ this.i++;
1188
+ }
1189
+ }
1190
+ /** Record a fail-closed violation for a declaration the finder cannot classify. */
1191
+ failClosed(token, reason) {
1192
+ this.declarationsChecked++;
1193
+ this.violations.push({ file: this.file, line: this.lineOf[token.start], symbol: token.text, reason });
1194
+ }
1195
+ /**
1196
+ * Evaluate one documented-surface declaration: recover its leading JSDoc and
1197
+ * check it has enough meaningful, non-restating words, recording a violation
1198
+ * with a specific reason otherwise.
1199
+ */
1200
+ judge(head, symbol) {
1201
+ this.declarationsChecked++;
1202
+ const doc = this.extractJsdoc(head);
1203
+ const line = this.lineOf[head.start];
1204
+ if (!doc) {
1205
+ this.violations.push({ file: this.file, line, symbol, reason: "no docstring" });
1206
+ return;
1207
+ }
1208
+ const words = toWords(doc);
1209
+ if (words.length < MIN_DOC_WORDS) {
1210
+ this.violations.push({ file: this.file, line, symbol, reason: `docstring has fewer than ${MIN_DOC_WORDS} meaningful words (got ${words.length})` });
1211
+ return;
1212
+ }
1213
+ const nameWords = new Set(toWords(symbol));
1214
+ const novel = words.filter((word) => !nameWords.has(word));
1215
+ if (novel.length < MIN_NOVEL_WORDS) {
1216
+ this.violations.push({ file: this.file, line, symbol, reason: `docstring restates the identifier (needs ${MIN_NOVEL_WORDS}+ terms not in the name)` });
1217
+ }
1218
+ }
1219
+ /**
1220
+ * Recover the JSDoc text immediately preceding a token, or `""` when none.
1221
+ * Only a block comment (`kind` {@link SyntaxKind.MultiLineCommentTrivia}) beginning with `/**` counts; a `//` line
1222
+ * comment, or no comment at all, yields `""`.
1223
+ */
1224
+ extractJsdoc(head) {
1225
+ const ranges = getLeadingCommentRanges(this.text, head.fullStart);
1226
+ if (!ranges || ranges.length === 0)
1227
+ return "";
1228
+ let best = -1;
1229
+ for (let index = 0; index < ranges.length; index++) {
1230
+ const range = ranges[index];
1231
+ if (range.kind === SyntaxKind.MultiLineCommentTrivia &&
1232
+ this.text.charCodeAt(range.pos) === 0x2f &&
1233
+ this.text.charCodeAt(range.pos + 1) === 0x2a &&
1234
+ this.text.charCodeAt(range.pos + 2) === 0x2a) {
1235
+ best = index;
1236
+ }
1237
+ }
1238
+ if (best < 0)
1239
+ return "";
1240
+ const range = ranges[best];
1241
+ return cleanComment(this.text.slice(range.pos, range.end));
1242
+ }
1243
+ }
1244
+ /**
1245
+ * Analyze one already-read TypeScript source text and report the docstring
1246
+ * violations it contains plus how many declarations were evaluated.
1247
+ */
1248
+ export function analyzeSource(text, file) {
1249
+ const tokens = tokenize(text);
1250
+ const analyzer = new SourceAnalyzer(text, file, tokens, buildLineMap(text));
1251
+ analyzer.run();
1252
+ return { violations: analyzer.violations, declarations: analyzer.declarationsChecked };
1253
+ }
1254
+ /**
1255
+ * Walk a directory tree and analyze every authored `.ts` source beneath it,
1256
+ * skipping `.d.ts` files and the structural non-source directories. Scanning
1257
+ * zero files fails by throwing rather than passing vacuously.
1258
+ */
1259
+ export function analyzeDocstringCoverage(options) {
1260
+ const roots = options.sourceDirs && options.sourceDirs.length > 0
1261
+ ? options.sourceDirs.map((dir) => join(options.root, dir))
1262
+ : [options.root];
1263
+ const files = collectSourceFiles(roots).sort();
1264
+ if (files.length === 0) {
1265
+ throw new Error(`docstring coverage: no TypeScript source files found under ${options.root} (scanning zero files cannot pass vacuously)`);
1266
+ }
1267
+ const reportBase = files.length === 1 && files[0] === options.root ? dirname(options.root) : options.root;
1268
+ let declarations = 0;
1269
+ const violations = [];
1270
+ for (const file of files) {
1271
+ const text = readFileSync(file, "utf8");
1272
+ const result = analyzeSource(text, relative(reportBase, file));
1273
+ declarations += result.declarations;
1274
+ violations.push(...result.violations);
1275
+ }
1276
+ return { files_scanned: files.length, declarations_checked: declarations, violations };
1277
+ }
1278
+ //# sourceMappingURL=docstrings.js.map