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.
- package/CHANGELOG.md +23 -0
- package/dist/docstrings.d.ts +148 -0
- package/dist/docstrings.d.ts.map +1 -0
- package/dist/docstrings.js +1278 -0
- package/dist/docstrings.js.map +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +195 -4
- package/dist/index.js.map +1 -1
- package/manifest.json +1 -1
- package/package.json +24 -10
|
@@ -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
|