@codometer/languages 0.0.1
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/LICENSE +21 -0
- package/README.md +964 -0
- package/dist/src/index.d.ts +1697 -0
- package/dist/src/index.js +1985 -0
- package/package.json +74 -0
|
@@ -0,0 +1,1697 @@
|
|
|
1
|
+
import { CodometerCommentLanguage } from '@codometer/configuration';
|
|
2
|
+
import { CodometerCommentMeasurement } from '@codometer/configuration';
|
|
3
|
+
import { CodometerSeverity } from '@codometer/core';
|
|
4
|
+
import { CodometerSymbolKind } from '@codometer/core';
|
|
5
|
+
import { CodometerSymbolModifier } from '@codometer/core';
|
|
6
|
+
import { ConsoleLogger } from '@nestjs/common';
|
|
7
|
+
import { default as default_2 } from 'typescript';
|
|
8
|
+
import pino from 'pino';
|
|
9
|
+
import { ResolvedCodometerConfiguration } from '@codometer/configuration';
|
|
10
|
+
import { SourceFile } from 'typescript';
|
|
11
|
+
|
|
12
|
+
/** Arguments accepted by the Jupyter analyzer. */
|
|
13
|
+
export declare interface AnalyzeJupyterArguments {
|
|
14
|
+
notebookFiles: string[];
|
|
15
|
+
pythonCommand: string;
|
|
16
|
+
workingDirectory: string;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/** Arguments accepted when running every language analyzer. */
|
|
20
|
+
export declare interface AnalyzeLanguagesArguments {
|
|
21
|
+
/**
|
|
22
|
+
* One `comment`-selector custom statistic's budget, per declared statistic.
|
|
23
|
+
*
|
|
24
|
+
* Every counter arrives in one list, and every measurer is handed all of
|
|
25
|
+
* them: a counter naming a `kind` is measured by the TypeScript walk, and
|
|
26
|
+
* every other one by the comment readers, each selecting for itself.
|
|
27
|
+
*/
|
|
28
|
+
commentCounters: CommentCounter[];
|
|
29
|
+
configuration: ResolvedCodometerConfiguration;
|
|
30
|
+
discoveredFiles: DiscoveredLanguageFiles;
|
|
31
|
+
/** Configured counters over declarations, tallied during the TypeScript walk. */
|
|
32
|
+
symbolCounters: TypescriptSymbolCounter[];
|
|
33
|
+
workingDirectory: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/** Arguments accepted by the Python analyzer. */
|
|
37
|
+
export declare interface AnalyzePythonArguments {
|
|
38
|
+
command: string;
|
|
39
|
+
pythonFiles: string[];
|
|
40
|
+
workingDirectory: string;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** Arguments accepted when analyzing Python source text without files. */
|
|
44
|
+
export declare interface AnalyzePythonContentsArguments {
|
|
45
|
+
command: string;
|
|
46
|
+
contents: string[];
|
|
47
|
+
workingDirectory: string;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Arguments for analyzing a single source file. */
|
|
51
|
+
export declare interface AnalyzeTypescriptFileArguments {
|
|
52
|
+
commentCounters: CommentCounter[];
|
|
53
|
+
counters: TypescriptSymbolCounter[];
|
|
54
|
+
filePath: string;
|
|
55
|
+
stats: TypescriptResult;
|
|
56
|
+
workingDirectory: string;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** A run of comment lines a reader takes as one thought. */
|
|
60
|
+
export declare interface CommentBlock {
|
|
61
|
+
tokens: CommentToken[];
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* How long one comment or JSDoc block may run, carried as an explicit
|
|
66
|
+
* argument rather than read off a resolved configuration object.
|
|
67
|
+
*
|
|
68
|
+
* Shaped to match the `comment` selector `@codometer/configuration` declares
|
|
69
|
+
* — a `language`, a `kind`, and this same set of optional maxima plus
|
|
70
|
+
* `severity` — so mapping one onto this is a direct field copy rather than a
|
|
71
|
+
* translation.
|
|
72
|
+
*/
|
|
73
|
+
export declare interface CommentBudget {
|
|
74
|
+
maximumCharacters: number | undefined;
|
|
75
|
+
maximumLines: number | undefined;
|
|
76
|
+
maximumWords: number | undefined;
|
|
77
|
+
severity: CodometerSeverity;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/**
|
|
81
|
+
* One `comment`-selector custom statistic's budget, and what it is over.
|
|
82
|
+
*
|
|
83
|
+
* Mirrors the selector field for field: `kind` names a documentable
|
|
84
|
+
* declaration whose JSDoc is measured, `language` names the language whose
|
|
85
|
+
* plain comment blocks are, and `undefined` on both means every language that
|
|
86
|
+
* has comments — exactly as `CodometerCommentSelector` documents. A counter
|
|
87
|
+
* naming both is measured as a `kind`: the whole list reaches both measurers,
|
|
88
|
+
* and each selects from it by reading `kind`.
|
|
89
|
+
*
|
|
90
|
+
* Kept apart per statistic rather than merged into one budget per language or
|
|
91
|
+
* kind, so two statistics naming the same one with different maxima each
|
|
92
|
+
* count only their own breaches back against their own label.
|
|
93
|
+
*/
|
|
94
|
+
export declare interface CommentCounter {
|
|
95
|
+
budget: CommentBudget;
|
|
96
|
+
kind: CodometerSymbolKind | undefined;
|
|
97
|
+
label: string;
|
|
98
|
+
language: CodometerCommentLanguage | undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** One comment block, measured against one declared maximum. */
|
|
102
|
+
export declare type CommentMeasurement = CodometerCommentMeasurement;
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* NestJS module that provides comment-length measurement.
|
|
106
|
+
*
|
|
107
|
+
* One measuring service and one reader per comment syntax, so a language that
|
|
108
|
+
* marks its comments differently is a new reader here rather than a second
|
|
109
|
+
* definition of what a word is.
|
|
110
|
+
*/
|
|
111
|
+
export declare class CommentsModule {
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* Measures comments against the maxima a configuration declares.
|
|
116
|
+
*
|
|
117
|
+
* Everything here is language-agnostic: it takes comments somebody else
|
|
118
|
+
* already found and says how long they are. What counts as a comment differs
|
|
119
|
+
* per language and lives with the reader that knows — `HashCommentsService`
|
|
120
|
+
* for the `#` languages, `YamlCommentsService` for YAML's tokenizer, and the
|
|
121
|
+
* TypeScript walk for a JSDoc block.
|
|
122
|
+
*
|
|
123
|
+
* That split is what keeps one definition of a word, a line, and a character
|
|
124
|
+
* across every language, rather than four analyzers each counting slightly
|
|
125
|
+
* differently.
|
|
126
|
+
*/
|
|
127
|
+
export declare class CommentsService {
|
|
128
|
+
constructor();
|
|
129
|
+
/**
|
|
130
|
+
* Counts the words in a comment's prose, markers already stripped.
|
|
131
|
+
*
|
|
132
|
+
* Splitting a trimmed string on whitespace runs never yields an empty
|
|
133
|
+
* token, so the empty case is the only one worth guarding — and guarding it
|
|
134
|
+
* rather than filtering keeps a callback frame off the deepest stack this
|
|
135
|
+
* package owns.
|
|
136
|
+
*/
|
|
137
|
+
private countWords;
|
|
138
|
+
/**
|
|
139
|
+
* Every declared maximum, paired with what this comment measured.
|
|
140
|
+
*
|
|
141
|
+
* Written as three guarded pushes rather than a table walked by `flatMap`,
|
|
142
|
+
* because the callback would be one more frame on the deepest stack this
|
|
143
|
+
* package owns — the JSDoc walk reaches here through eleven of them, and
|
|
144
|
+
* `callidescope.config.ts` gates that at what it measures.
|
|
145
|
+
*/
|
|
146
|
+
private declaredLimits;
|
|
147
|
+
/** The prose of a run of comment lines, markers already stripped. */
|
|
148
|
+
private readProse;
|
|
149
|
+
/** A run of comment lines exactly as the file carries them. */
|
|
150
|
+
private readSource;
|
|
151
|
+
/** Shortens a block's prose to something a breach line can carry. */
|
|
152
|
+
private toExcerpt;
|
|
153
|
+
/**
|
|
154
|
+
* Groups a file's comment lines into the blocks a reader perceives.
|
|
155
|
+
*
|
|
156
|
+
* A trailing comment never joins anything — it sits after a value and is
|
|
157
|
+
* read with that value, not with the prose above it — and neither does a
|
|
158
|
+
* comment separated from the previous one by a blank line, which is how a
|
|
159
|
+
* writer marks the end of a thought.
|
|
160
|
+
*/
|
|
161
|
+
groupIntoBlocks(tokens: readonly CommentToken[]): CommentBlock[];
|
|
162
|
+
/**
|
|
163
|
+
* Measures every block a file's comment tokens form, breached or not.
|
|
164
|
+
*
|
|
165
|
+
* Every block is reported rather than only the breaches, so a length is
|
|
166
|
+
* visible in the JSON report before it ever becomes a problem — the same
|
|
167
|
+
* bargain the TypeScript declaration-comment measurement makes. A block is
|
|
168
|
+
* reported once per declared maximum, because the maxima are not
|
|
169
|
+
* alternatives: one can hold while another breaks.
|
|
170
|
+
*/
|
|
171
|
+
measure(args: MeasureCommentsArguments): CommentMeasurement[];
|
|
172
|
+
/**
|
|
173
|
+
* Measures one comment's text against every maximum declared for it.
|
|
174
|
+
*
|
|
175
|
+
* The seam a JSDoc block enters through: it is one comment already, found by
|
|
176
|
+
* the TypeScript walk rather than grouped from lines, so it needs the
|
|
177
|
+
* counting and none of the grouping.
|
|
178
|
+
*/
|
|
179
|
+
measureText(args: MeasureCommentTextArguments): CommentMeasurement[];
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** One comment line, with where it sits and what it says. */
|
|
183
|
+
export declare interface CommentToken {
|
|
184
|
+
/** 1-indexed line the comment is written on. */
|
|
185
|
+
line: number;
|
|
186
|
+
/** Whether nothing but whitespace precedes it on its line. */
|
|
187
|
+
ownLine: boolean;
|
|
188
|
+
/** The comment's prose, with its marker already stripped. */
|
|
189
|
+
prose: string;
|
|
190
|
+
/** The comment exactly as the file carries it, marker and all. */
|
|
191
|
+
source: string;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Reads CSS's `/* ... *\/` comments from postcss's own parse.
|
|
196
|
+
*
|
|
197
|
+
* CSS has only the one comment syntax, and no line form at all, so a real
|
|
198
|
+
* parser costs nothing extra here: postcss already reads the repository's
|
|
199
|
+
* stylesheets for `CssService`, and its comment nodes carry a line and column
|
|
200
|
+
* a scanner would otherwise have to recompute by hand.
|
|
201
|
+
*/
|
|
202
|
+
export declare class CssCommentsService {
|
|
203
|
+
constructor();
|
|
204
|
+
/**
|
|
205
|
+
* A comment's text with the whitespace postcss split off restored.
|
|
206
|
+
*
|
|
207
|
+
* `raws.left`/`raws.right` are typed optional, but postcss's own parse
|
|
208
|
+
* always fills both in — with an empty string for an empty comment, never
|
|
209
|
+
* `undefined` — so the fallback is unreachable rather than untested.
|
|
210
|
+
*/
|
|
211
|
+
private toBody;
|
|
212
|
+
/**
|
|
213
|
+
* Whether only whitespace precedes a comment's opening marker on its line.
|
|
214
|
+
*
|
|
215
|
+
* `comment.source` is typed optional because postcss also allows building a
|
|
216
|
+
* node by hand with none, which never happens here: every comment measured
|
|
217
|
+
* came from parsing real content, which always carries its position.
|
|
218
|
+
*/
|
|
219
|
+
private toOwnLine;
|
|
220
|
+
/**
|
|
221
|
+
* Reads every comment postcss's parse finds, in document order.
|
|
222
|
+
*
|
|
223
|
+
* Empty for a stylesheet postcss cannot parse at all, the same way
|
|
224
|
+
* `CssService` skips a file it cannot parse rather than throwing out of the
|
|
225
|
+
* whole measurement run.
|
|
226
|
+
*/
|
|
227
|
+
read(content: string): CommentToken[];
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/** Input to the Css analysis step. */
|
|
231
|
+
export declare interface CssInput {
|
|
232
|
+
cssFiles: string[];
|
|
233
|
+
workingDirectory: string;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
/**
|
|
237
|
+
* NestJS module that provides Css source analysis.
|
|
238
|
+
*/
|
|
239
|
+
export declare class CssModule {
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/** Aggregated metrics collected from parsing Css sources. */
|
|
243
|
+
export declare interface CssResult {
|
|
244
|
+
atRules: number;
|
|
245
|
+
comments: number;
|
|
246
|
+
customProperties: number;
|
|
247
|
+
declarations: number;
|
|
248
|
+
files: number;
|
|
249
|
+
lines: number;
|
|
250
|
+
mediaQueries: number;
|
|
251
|
+
rules: number;
|
|
252
|
+
selectors: number;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Walks parsed stylesheets to collect structural metrics.
|
|
257
|
+
*
|
|
258
|
+
* Parsed with postcss, which the repository's stylelint already reads CSS
|
|
259
|
+
* through, so a selector split across lines counts once and a declaration
|
|
260
|
+
* inside a comment counts not at all.
|
|
261
|
+
*/
|
|
262
|
+
export declare class CssService {
|
|
263
|
+
private readonly logger;
|
|
264
|
+
constructor(logger: LoggerService);
|
|
265
|
+
/** Records one node against the running totals. */
|
|
266
|
+
private countNode;
|
|
267
|
+
/** Analyze the given stylesheets, resolved against the directory. */
|
|
268
|
+
analyze({ cssFiles, workingDirectory }: CssInput): CssResult;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/**
|
|
272
|
+
* Finds a documentable declaration's leading JSDoc comment and hands it to be
|
|
273
|
+
* measured.
|
|
274
|
+
*
|
|
275
|
+
* Only the finding is TypeScript's: which declarations can carry a limit,
|
|
276
|
+
* where the `/**` range sits, and what the declaration is called. How long the
|
|
277
|
+
* comment is comes from `CommentsService`, the same counting every other
|
|
278
|
+
* language's comments go through, so a word means one thing across the tool
|
|
279
|
+
* rather than one thing per analyzer.
|
|
280
|
+
*/
|
|
281
|
+
declare class DeclarationCommentsService {
|
|
282
|
+
private readonly comments;
|
|
283
|
+
constructor(comments: CommentsService);
|
|
284
|
+
/** Reads a declaration's own name, or `"(anonymous)"` when it has none. */
|
|
285
|
+
private getDeclarationName;
|
|
286
|
+
/** Finds the node's leading JSDoc comment range, the last one if several. */
|
|
287
|
+
private getJsDocRange;
|
|
288
|
+
/**
|
|
289
|
+
* Everything a measurement needs about one node, or `undefined` when there
|
|
290
|
+
* is nothing to measure.
|
|
291
|
+
*
|
|
292
|
+
* Split out of `measure` so that method stays inside this repository's
|
|
293
|
+
* statement budget without a helper on the measuring path itself — this one
|
|
294
|
+
* is called before the comment counting starts, so it adds no frame to the
|
|
295
|
+
* deepest stack the JSDoc walk reaches.
|
|
296
|
+
*/
|
|
297
|
+
private prepare;
|
|
298
|
+
/**
|
|
299
|
+
* The comment's prose, with its delimiters and each line's `*` stripped.
|
|
300
|
+
*
|
|
301
|
+
* Stripped for the word count only. A character count stays the raw slice —
|
|
302
|
+
* it is the one unit a reader can check against their editor's own column
|
|
303
|
+
* count, and a marker is very much a character even though it is not a word.
|
|
304
|
+
*/
|
|
305
|
+
private readProse;
|
|
306
|
+
/**
|
|
307
|
+
* Measures one declaration's leading JSDoc comment against every
|
|
308
|
+
* comment counter that names its kind, if it has one.
|
|
309
|
+
*
|
|
310
|
+
* Empty when the node's kind matches no configured counter, or when it
|
|
311
|
+
* carries no `/**` comment at all — neither is a measurement, and reporting
|
|
312
|
+
* one would name a declaration nothing documented. A declaration is
|
|
313
|
+
* measured once per counter that names its kind and once per maximum that
|
|
314
|
+
* counter declares, because neither is an alternative: one can hold while
|
|
315
|
+
* another breaks.
|
|
316
|
+
*/
|
|
317
|
+
measure(node: default_2.Node, context: TypescriptWalkContext): LabeledCommentMeasurement[];
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/** The categorized file lists every language analyzer reads from. */
|
|
321
|
+
export declare interface DiscoveredLanguageFiles {
|
|
322
|
+
cssFiles: string[];
|
|
323
|
+
hclFiles: string[];
|
|
324
|
+
jsonFiles: string[];
|
|
325
|
+
markdownFiles: string[];
|
|
326
|
+
notebookFiles: string[];
|
|
327
|
+
pyFiles: string[];
|
|
328
|
+
shellFiles: string[];
|
|
329
|
+
sourceFiles: string[];
|
|
330
|
+
sqlFiles: string[];
|
|
331
|
+
tomlFiles: string[];
|
|
332
|
+
yamlFiles: string[];
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
/**
|
|
336
|
+
* Reads the comments of a language whose comments start with `#`.
|
|
337
|
+
*
|
|
338
|
+
* Shell, TOML, and Python all mark a comment the same way and all three
|
|
339
|
+
* already scan their sources line by line, which is why one reader serves
|
|
340
|
+
* them.
|
|
341
|
+
*
|
|
342
|
+
* It is a line scanner, not a tokenizer, and that is a real limitation rather
|
|
343
|
+
* than an oversight: a `#` inside a string literal is read as a comment here,
|
|
344
|
+
* exactly as those three analyzers' own `comments` counters already read it.
|
|
345
|
+
* YAML is measured by `YamlCommentsService` instead, whose tokenizer knows the
|
|
346
|
+
* difference, because YAML's `#` sits next to quoted scalars constantly.
|
|
347
|
+
*/
|
|
348
|
+
export declare class HashCommentsService {
|
|
349
|
+
constructor();
|
|
350
|
+
/**
|
|
351
|
+
* Whether this is the interpreter line rather than a comment.
|
|
352
|
+
*
|
|
353
|
+
* `#!` on the first line is an instruction to the kernel, not prose. Left in,
|
|
354
|
+
* it would be grouped with whatever comment follows it — every shell script
|
|
355
|
+
* opening with a shebang and a comment would measure one block carrying
|
|
356
|
+
* `!/bin/sh` as its first word.
|
|
357
|
+
*/
|
|
358
|
+
private isShebang;
|
|
359
|
+
/** Reads every `#` comment in a file, with its line and its placement. */
|
|
360
|
+
read(content: string): CommentToken[];
|
|
361
|
+
}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Reads HCL's `#`, `//`, and `/* ... *\/` comments — the only language this
|
|
365
|
+
* tool measures that marks a comment three different ways.
|
|
366
|
+
*
|
|
367
|
+
* A line scanner for the two line forms, none of it string-aware:
|
|
368
|
+
* `HclService`'s own line-based counting already accepts the same
|
|
369
|
+
* limitation, checking only whether a trimmed line's first characters open a
|
|
370
|
+
* comment rather than parsing the language properly. The block form is found
|
|
371
|
+
* with `indexOf` rather than a regular expression: a pattern matching an
|
|
372
|
+
* unclosed `/*` through to end of input has to fail once per occurrence,
|
|
373
|
+
* which is quadratic on adversarial input, while two `indexOf` calls per
|
|
374
|
+
* comment never scan the same text twice.
|
|
375
|
+
*/
|
|
376
|
+
export declare class HclCommentsService {
|
|
377
|
+
constructor();
|
|
378
|
+
/** Every `/* *\/` comment's span, left to right and never overlapping. */
|
|
379
|
+
private findBlockComments;
|
|
380
|
+
/** Whether only whitespace precedes an offset on its own line. */
|
|
381
|
+
private isOwnLine;
|
|
382
|
+
/** The 1-indexed line an offset sits on. */
|
|
383
|
+
private lineOf;
|
|
384
|
+
/** Every block comment's span, as a positioned token. */
|
|
385
|
+
private readBlocks;
|
|
386
|
+
/** Every match of a comment pattern, as a positioned token. */
|
|
387
|
+
private readMatches;
|
|
388
|
+
/**
|
|
389
|
+
* Reads every `#`, `//`, and `/* *\/` comment, in the order they appear.
|
|
390
|
+
*
|
|
391
|
+
* Block spans are found first so a line marker found inside one — a `#`
|
|
392
|
+
* written as prose in a `/* *\/` block, say — can be dropped rather than
|
|
393
|
+
* measured a second time as a comment of its own.
|
|
394
|
+
*/
|
|
395
|
+
read(content: string): CommentToken[];
|
|
396
|
+
}
|
|
397
|
+
|
|
398
|
+
/** Input to the Hcl analysis step. */
|
|
399
|
+
export declare interface HclInput {
|
|
400
|
+
hclFiles: string[];
|
|
401
|
+
workingDirectory: string;
|
|
402
|
+
}
|
|
403
|
+
|
|
404
|
+
/**
|
|
405
|
+
* NestJS module that provides Hcl source analysis.
|
|
406
|
+
*/
|
|
407
|
+
export declare class HclModule {
|
|
408
|
+
}
|
|
409
|
+
|
|
410
|
+
/** Aggregated metrics collected from parsing Hcl sources. */
|
|
411
|
+
export declare interface HclResult {
|
|
412
|
+
attributes: number;
|
|
413
|
+
blocks: number;
|
|
414
|
+
comments: number;
|
|
415
|
+
files: number;
|
|
416
|
+
interpolations: number;
|
|
417
|
+
lines: number;
|
|
418
|
+
outputs: number;
|
|
419
|
+
resources: number;
|
|
420
|
+
variables: number;
|
|
421
|
+
}
|
|
422
|
+
|
|
423
|
+
/**
|
|
424
|
+
* Counts the blocks and attributes an HCL configuration declares.
|
|
425
|
+
*
|
|
426
|
+
* Blocks are recognized by the header that opens them, which also names what
|
|
427
|
+
* kind they are: a `resource` block and an `output` block are both blocks, and
|
|
428
|
+
* knowing how many of each is what makes the count worth reading.
|
|
429
|
+
*/
|
|
430
|
+
export declare class HclService {
|
|
431
|
+
private readonly logger;
|
|
432
|
+
constructor(logger: LoggerService);
|
|
433
|
+
/** Records the block a header opens, by the kind it names. */
|
|
434
|
+
private countBlock;
|
|
435
|
+
/** Records what one line of HCL declares. */
|
|
436
|
+
private countLine;
|
|
437
|
+
/** Analyze the given HCL files, resolved against the directory. */
|
|
438
|
+
analyze({ hclFiles, workingDirectory }: HclInput): HclResult;
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
/** State used while stripping JSONC comments from a document. */
|
|
442
|
+
export declare interface JsoncState {
|
|
443
|
+
isInBlockComment: boolean;
|
|
444
|
+
isInLineComment: boolean;
|
|
445
|
+
isInString: boolean;
|
|
446
|
+
sanitizedContent: string;
|
|
447
|
+
shouldAdvanceIndex: boolean;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
/** Input to the JSON analysis step. */
|
|
451
|
+
export declare interface JsonInput {
|
|
452
|
+
jsonFiles: string[];
|
|
453
|
+
workingDirectory: string;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
/**
|
|
457
|
+
* TODO: Document the measureJson module.
|
|
458
|
+
*/
|
|
459
|
+
export declare class JsonModule {
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Aggregated metrics collected from parsing JSON documents. */
|
|
463
|
+
export declare interface JsonResult {
|
|
464
|
+
arrays: number;
|
|
465
|
+
booleans: number;
|
|
466
|
+
files: number;
|
|
467
|
+
items: number;
|
|
468
|
+
lines: number;
|
|
469
|
+
maxDepth: number;
|
|
470
|
+
nulls: number;
|
|
471
|
+
numbers: number;
|
|
472
|
+
objects: number;
|
|
473
|
+
properties: number;
|
|
474
|
+
strings: number;
|
|
475
|
+
totalNodes: number;
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/** Walks parsed JSON values to collect structural metrics. */
|
|
479
|
+
export declare class JsonService {
|
|
480
|
+
private readonly logger;
|
|
481
|
+
constructor(logger: LoggerService);
|
|
482
|
+
/** Consume a character that is not inside a comment or string. */
|
|
483
|
+
private consumeCharacterOutsideComments;
|
|
484
|
+
/** Consume one JSONC character and update the parser state. */
|
|
485
|
+
private consumeJsoncCharacter;
|
|
486
|
+
/** Count array nodes and their child values. */
|
|
487
|
+
private countArrayNode;
|
|
488
|
+
/** Recursively count JSON containers, primitives, and nesting depth. */
|
|
489
|
+
private countNode;
|
|
490
|
+
/** Count scalar values and update primitive stats. */
|
|
491
|
+
private countPrimitiveNode;
|
|
492
|
+
/** Increment stats for a scalar JSON value. */
|
|
493
|
+
private countPrimitiveValue;
|
|
494
|
+
/** Count object nodes and their child values. */
|
|
495
|
+
private countRecordNode;
|
|
496
|
+
/** Handle block comments while parsing JSONC content. */
|
|
497
|
+
private handleBlockCommentState;
|
|
498
|
+
/** Update the JSONC parser when it is inside a line comment. */
|
|
499
|
+
private handleLineCommentState;
|
|
500
|
+
/** Update the JSONC parser when it is inside a string literal. */
|
|
501
|
+
private handleStringState;
|
|
502
|
+
/** Return true when a value is a JSON array. */
|
|
503
|
+
private isArrayNode;
|
|
504
|
+
/** Return true when a value is a JSON object. */
|
|
505
|
+
private isRecordNode;
|
|
506
|
+
/** Parse a file into one or more JSON documents depending on the extension. */
|
|
507
|
+
private parseDocuments;
|
|
508
|
+
/** Remove comments from JSONC content while preserving string literals. */
|
|
509
|
+
private stripJsoncComments;
|
|
510
|
+
/** Analyze JSON files and return structural metrics for their contents. */
|
|
511
|
+
analyze(input: JsonInput): JsonResult;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
/**
|
|
515
|
+
* NestJS module that measures notebooks through the JSON, Python, and
|
|
516
|
+
* markdown analyzers it composes.
|
|
517
|
+
*/
|
|
518
|
+
export declare class JupyterModule {
|
|
519
|
+
}
|
|
520
|
+
|
|
521
|
+
/** Aggregated metrics collected from Jupyter notebooks. */
|
|
522
|
+
export declare interface JupyterResult {
|
|
523
|
+
cells: number;
|
|
524
|
+
classes: number;
|
|
525
|
+
codeBlocks: number;
|
|
526
|
+
codeCells: number;
|
|
527
|
+
codeLines: number;
|
|
528
|
+
decorators: number;
|
|
529
|
+
executedCells: number;
|
|
530
|
+
files: number;
|
|
531
|
+
functions: number;
|
|
532
|
+
headings: number;
|
|
533
|
+
images: number;
|
|
534
|
+
imports: number;
|
|
535
|
+
links: number;
|
|
536
|
+
markdownCells: number;
|
|
537
|
+
markdownLines: number;
|
|
538
|
+
maxDepth: number;
|
|
539
|
+
outputs: number;
|
|
540
|
+
properties: number;
|
|
541
|
+
rawCells: number;
|
|
542
|
+
totalNodes: number;
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
/**
|
|
546
|
+
* Measures Jupyter notebooks by handing their parts to the other analyzers.
|
|
547
|
+
*
|
|
548
|
+
* A notebook is three languages in one file, and this service owns none of
|
|
549
|
+
* them: the document is JSON, its code cells are Python, and its markdown
|
|
550
|
+
* cells are prose, so each is counted by the analyzer that already knows how.
|
|
551
|
+
* What is left — cells, outputs, execution — belongs to the notebook itself
|
|
552
|
+
* and is counted here.
|
|
553
|
+
*/
|
|
554
|
+
export declare class JupyterService {
|
|
555
|
+
private readonly jsonService;
|
|
556
|
+
private readonly markdownService;
|
|
557
|
+
private readonly pythonService;
|
|
558
|
+
private readonly logger;
|
|
559
|
+
constructor(jsonService: JsonService, markdownService: MarkdownService, pythonService: PythonService, logger: LoggerService);
|
|
560
|
+
/** Record one cell against the running notebook totals. */
|
|
561
|
+
private collectCell;
|
|
562
|
+
/** Read every notebook, collecting cell counts and cell sources. */
|
|
563
|
+
private collectParts;
|
|
564
|
+
/** Sum every heading level the markdown analyzer reports. */
|
|
565
|
+
private countHeadings;
|
|
566
|
+
/** Read and validate one notebook, returning its cells. */
|
|
567
|
+
private readNotebook;
|
|
568
|
+
/** Join a cell's source, which nbformat writes as a string or line array. */
|
|
569
|
+
private readSource;
|
|
570
|
+
/** Analyze the given notebooks, resolved against the directory. */
|
|
571
|
+
analyze(args: AnalyzeJupyterArguments): JupyterResult;
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
/** One measurement, tagged with the custom statistic label that produced it. */
|
|
575
|
+
declare interface LabeledCommentMeasurement {
|
|
576
|
+
label: string;
|
|
577
|
+
measurement: CommentMeasurement;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
/**
|
|
581
|
+
* The file lists comment measurement reads, one per language that has a
|
|
582
|
+
* budget.
|
|
583
|
+
*
|
|
584
|
+
* Named here rather than taken from `DiscoveredLanguageFiles`, which would
|
|
585
|
+
* point this module back at the one that depends on it. `languages` depends on
|
|
586
|
+
* `comments` and never the reverse; a cycle between the two crashes the Nest
|
|
587
|
+
* container outright rather than failing anything readable. The discovery
|
|
588
|
+
* result satisfies this structurally, so callers pass it unchanged.
|
|
589
|
+
*/
|
|
590
|
+
declare interface LanguageCommentFiles {
|
|
591
|
+
cssFiles: string[];
|
|
592
|
+
hclFiles: string[];
|
|
593
|
+
shellFiles: string[];
|
|
594
|
+
/** TypeScript and JavaScript sources, in every dialect the workspace holds. */
|
|
595
|
+
sourceFiles: string[];
|
|
596
|
+
sqlFiles: string[];
|
|
597
|
+
tomlFiles: string[];
|
|
598
|
+
yamlFiles: string[];
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
/**
|
|
602
|
+
* Measures the comment blocks every `comment`-selector custom statistic asks
|
|
603
|
+
* for, one counter at a time.
|
|
604
|
+
*
|
|
605
|
+
* Files are read here rather than inside each language analyzer, which is
|
|
606
|
+
* what lets Python be measured at all: its analysis runs in a subprocess and
|
|
607
|
+
* returns zeros when the interpreter is unreachable, so a gate that lived
|
|
608
|
+
* there would stop gating on any machine without `uv` and say nothing about
|
|
609
|
+
* it. Reading the sources directly makes the budget independent of that.
|
|
610
|
+
*
|
|
611
|
+
* A counter is measured on its own rather than merged with every other one
|
|
612
|
+
* that shares a language: two custom statistics can watch the same language
|
|
613
|
+
* with different maxima, and keeping them apart is what lets each one's own
|
|
614
|
+
* breaches be counted back against its own label.
|
|
615
|
+
*/
|
|
616
|
+
export declare class LanguageCommentsService {
|
|
617
|
+
private readonly comments;
|
|
618
|
+
private readonly cssComments;
|
|
619
|
+
private readonly hashComments;
|
|
620
|
+
private readonly hclComments;
|
|
621
|
+
private readonly logger;
|
|
622
|
+
private readonly sqlComments;
|
|
623
|
+
private readonly typescriptComments;
|
|
624
|
+
private readonly yamlComments;
|
|
625
|
+
constructor(comments: CommentsService, cssComments: CssCommentsService, hashComments: HashCommentsService, hclComments: HclCommentsService, logger: LoggerService, sqlComments: SqlCommentsService, typescriptComments: TypescriptCommentsService, yamlComments: YamlCommentsService);
|
|
626
|
+
/** Reads and measures one language's files against one counter's budget. */
|
|
627
|
+
private measureLanguage;
|
|
628
|
+
/** Measures one counter's budget against one language's discovered files. */
|
|
629
|
+
private measureOneLanguage;
|
|
630
|
+
/**
|
|
631
|
+
* Measures Python's comments, which its own analyzer already found.
|
|
632
|
+
*
|
|
633
|
+
* The tokens arrive grouped by nothing, so they are split per file before
|
|
634
|
+
* measuring — a block never spans two files, and `groupIntoBlocks` compares
|
|
635
|
+
* line numbers that would otherwise run together.
|
|
636
|
+
*/
|
|
637
|
+
private measurePython;
|
|
638
|
+
/** Reads one file, or reports which one it gave up on. */
|
|
639
|
+
private readFile;
|
|
640
|
+
/**
|
|
641
|
+
* Measures every counter that names no declaration kind, keyed by the custom
|
|
642
|
+
* statistic's label.
|
|
643
|
+
*
|
|
644
|
+
* A counter naming no language measures every one of them; one naming a
|
|
645
|
+
* language measures only that one. Either way its results land under its
|
|
646
|
+
* own label, never merged with another counter's.
|
|
647
|
+
*/
|
|
648
|
+
measure(args: MeasureLanguageCommentsArguments): Record<string, CommentMeasurement[]>;
|
|
649
|
+
}
|
|
650
|
+
|
|
651
|
+
/** What every language analyzer reported, keyed by language. */
|
|
652
|
+
export declare interface LanguageResults {
|
|
653
|
+
/** One measurement list per configured comment counter, keyed by its label. */
|
|
654
|
+
commentCounts: Record<string, CommentMeasurement[]>;
|
|
655
|
+
css: CssResult;
|
|
656
|
+
hcl: HclResult;
|
|
657
|
+
json: JsonResult;
|
|
658
|
+
jupyter: JupyterResult;
|
|
659
|
+
markdown: MarkdownResult;
|
|
660
|
+
python: PythonResult;
|
|
661
|
+
shell: ShellResult;
|
|
662
|
+
sql: SqlResult;
|
|
663
|
+
toml: TomlResult;
|
|
664
|
+
typescript: TypescriptResult;
|
|
665
|
+
yaml: YamlResult;
|
|
666
|
+
}
|
|
667
|
+
|
|
668
|
+
/**
|
|
669
|
+
* NestJS module that gathers every language analyzer behind one service.
|
|
670
|
+
*/
|
|
671
|
+
export declare class LanguagesModule {
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
/**
|
|
675
|
+
* Runs every language analyzer over the discovered files.
|
|
676
|
+
*
|
|
677
|
+
* One collaborator for the measurement pipeline instead of eleven: which
|
|
678
|
+
* languages exist is this service's business, and adding a twelfth changes
|
|
679
|
+
* nothing above it.
|
|
680
|
+
*/
|
|
681
|
+
export declare class LanguagesService {
|
|
682
|
+
private readonly cssService;
|
|
683
|
+
private readonly languageComments;
|
|
684
|
+
private readonly hclService;
|
|
685
|
+
private readonly jsonService;
|
|
686
|
+
private readonly jupyterService;
|
|
687
|
+
private readonly markdownService;
|
|
688
|
+
private readonly pythonService;
|
|
689
|
+
private readonly shellService;
|
|
690
|
+
private readonly sqlService;
|
|
691
|
+
private readonly tomlService;
|
|
692
|
+
private readonly typescriptService;
|
|
693
|
+
private readonly yamlService;
|
|
694
|
+
constructor(cssService: CssService, languageComments: LanguageCommentsService, hclService: HclService, jsonService: JsonService, jupyterService: JupyterService, markdownService: MarkdownService, pythonService: PythonService, shellService: ShellService, sqlService: SqlService, tomlService: TomlService, typescriptService: TypescriptService, yamlService: YamlService);
|
|
695
|
+
/** Analyze every language present in the discovered files. */
|
|
696
|
+
analyze(args: AnalyzeLanguagesArguments): LanguageResults;
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
/** A comment token together with the file it was found in. */
|
|
700
|
+
declare interface LocatedCommentToken extends CommentToken {
|
|
701
|
+
file: string;
|
|
702
|
+
}
|
|
703
|
+
|
|
704
|
+
/**
|
|
705
|
+
* Structured values that belong beside a log line rather than inside it.
|
|
706
|
+
*
|
|
707
|
+
* Counts, percentages, and durations are the values that change on every
|
|
708
|
+
* occurrence, so they are carried as fields: the message stays constant and
|
|
709
|
+
* groupable in telemetry, and the numbers stay queryable instead of having to
|
|
710
|
+
* be parsed back out of prose.
|
|
711
|
+
*
|
|
712
|
+
* The named members are the recurring ones; the index signature keeps the
|
|
713
|
+
* argument open for whatever a given call site needs to attach.
|
|
714
|
+
*/
|
|
715
|
+
declare interface LogData {
|
|
716
|
+
[key: string]: unknown;
|
|
717
|
+
/** How many things the operation handled. */
|
|
718
|
+
count?: number;
|
|
719
|
+
/** Wall-clock milliseconds the operation took. */
|
|
720
|
+
durationMs?: number;
|
|
721
|
+
/** Completion between 0 and 100. */
|
|
722
|
+
percent?: number;
|
|
723
|
+
/** How many things the operation set out to handle. */
|
|
724
|
+
total?: number;
|
|
725
|
+
}
|
|
726
|
+
|
|
727
|
+
/**
|
|
728
|
+
* Transient-scoped logger so each injecting class gets its own instance.
|
|
729
|
+
* Each consumer calls `setContext(ClassName.name)` to tag every log line
|
|
730
|
+
* with the originating class. Backed by pino for structured JSON output in
|
|
731
|
+
* production and human-readable pretty-print in development.
|
|
732
|
+
*
|
|
733
|
+
* Messages follow one grammar: an emoji naming the subject, a verb in present
|
|
734
|
+
* progressive or past tense, then the object. Values that vary per call —
|
|
735
|
+
* counts, percentages, durations — go in the `data` argument rather than the
|
|
736
|
+
* message, so the message stays constant enough for telemetry to group on.
|
|
737
|
+
*
|
|
738
|
+
* ```ts
|
|
739
|
+
* this.logger.info("📥 Downloading CSEL sources", undefined, { total: 428 });
|
|
740
|
+
* this.logger.info("📥 Downloaded CSEL sources", undefined, { count: 412 });
|
|
741
|
+
* ```
|
|
742
|
+
*/
|
|
743
|
+
declare @Injectable({ scope: Scope.TRANSIENT })
|
|
744
|
+
class LoggerService extends ConsoleLogger {
|
|
745
|
+
// 🏗 Dependency Injection
|
|
746
|
+
|
|
747
|
+
constructor() {
|
|
748
|
+
super();
|
|
749
|
+
}
|
|
750
|
+
|
|
751
|
+
// 🔐 Private Fields
|
|
752
|
+
|
|
753
|
+
private static readonly isProduction =
|
|
754
|
+
process.env["NODE_ENV"] === "production";
|
|
755
|
+
|
|
756
|
+
/**
|
|
757
|
+
* Built on first use, not when this file is evaluated.
|
|
758
|
+
*
|
|
759
|
+
* A destination fixed at import time could only ever be chosen by this
|
|
760
|
+
* package, since every consumer's own code runs after its imports.
|
|
761
|
+
*/
|
|
762
|
+
private static rootLogger: pino.Logger | undefined;
|
|
763
|
+
|
|
764
|
+
/** Whether lines go to standard error instead of standard output. */
|
|
765
|
+
private static writesToStandardError = false;
|
|
766
|
+
|
|
767
|
+
private child: pino.Logger = LoggerService.root;
|
|
768
|
+
|
|
769
|
+
// 🔑 Public Fields
|
|
770
|
+
|
|
771
|
+
// 🔏 Private Methods
|
|
772
|
+
|
|
773
|
+
/** Build the pino instance for production or local development output. */
|
|
774
|
+
private static createRootLogger(): pino.Logger {
|
|
775
|
+
const level = process.env["LOG_LEVEL"] ?? "info";
|
|
776
|
+
|
|
777
|
+
if (LoggerService.isProduction) {
|
|
778
|
+
return LoggerService.writesToStandardError
|
|
779
|
+
? pino({ level }, pino.destination(STANDARD_ERROR_DESCRIPTOR))
|
|
780
|
+
: pino({ level });
|
|
781
|
+
}
|
|
782
|
+
|
|
783
|
+
return pino({
|
|
784
|
+
level,
|
|
785
|
+
transport: {
|
|
786
|
+
options: {
|
|
787
|
+
colorize: true,
|
|
788
|
+
destination: LoggerService.writesToStandardError
|
|
789
|
+
? STANDARD_ERROR_DESCRIPTOR
|
|
790
|
+
: STANDARD_OUTPUT_DESCRIPTOR,
|
|
791
|
+
// The emoji is a field, not part of the message, so the console can
|
|
792
|
+
// show it while telemetry stores unadorned prose. `ignore` then keeps
|
|
793
|
+
// it from being printed a second time in the trailing object.
|
|
794
|
+
ignore: "pid,hostname,emoji",
|
|
795
|
+
messageFormat: "{emoji} {msg}",
|
|
796
|
+
singleLine: true,
|
|
797
|
+
},
|
|
798
|
+
target: "pino-pretty",
|
|
799
|
+
},
|
|
800
|
+
});
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
/**
|
|
804
|
+
* Sends every subsequent line to standard error instead of standard output.
|
|
805
|
+
*
|
|
806
|
+
* For a command-line application whose standard output *is* its result. A log
|
|
807
|
+
* line sharing that stream is not a diagnostic beside the data, it is a
|
|
808
|
+
* corruption of it. Call it before anything logs — the first statement of the
|
|
809
|
+
* application's bootstrap.
|
|
810
|
+
*
|
|
811
|
+
* A call after the first line warns and changes nothing: the destination is
|
|
812
|
+
* fixed when the pino instance is built, and tearing down a transport
|
|
813
|
+
* somebody is writing through would be worse than refusing. The warning is
|
|
814
|
+
* the point — silently leaving the lines on standard output is how a caller
|
|
815
|
+
* would ship a corrupted pipe without ever being told.
|
|
816
|
+
*/
|
|
817
|
+
static logToStandardError(): void {
|
|
818
|
+
if (LoggerService.rootLogger !== undefined) {
|
|
819
|
+
process.emitWarning(
|
|
820
|
+
"LoggerService.logToStandardError() was called after the first log line, so log lines still go to standard output and anything piping that stream will read them as data. Call it as the first statement of the application's bootstrap.",
|
|
821
|
+
);
|
|
822
|
+
return;
|
|
823
|
+
}
|
|
824
|
+
|
|
825
|
+
LoggerService.writesToStandardError = true;
|
|
826
|
+
}
|
|
827
|
+
|
|
828
|
+
/**
|
|
829
|
+
* Fails a malformed message in development, and never in production.
|
|
830
|
+
*
|
|
831
|
+
* A logger that throws in production turns an observability call into an
|
|
832
|
+
* outage, so the check runs only where a developer is present to fix it.
|
|
833
|
+
*/
|
|
834
|
+
private assertConventionalMessage(args: {
|
|
835
|
+
context: string | undefined;
|
|
836
|
+
parsed: ParsedLogMessage;
|
|
837
|
+
}): void {
|
|
838
|
+
if (
|
|
839
|
+
LoggerService.isProduction ||
|
|
840
|
+
this.shouldSkipConventionalMessageValidation(args.context)
|
|
841
|
+
) {
|
|
842
|
+
return;
|
|
843
|
+
}
|
|
844
|
+
|
|
845
|
+
const violation = this.getConventionalMessageViolation(args.parsed);
|
|
846
|
+
|
|
847
|
+
if (violation !== undefined) {
|
|
848
|
+
throw new Error(violation);
|
|
849
|
+
}
|
|
850
|
+
}
|
|
851
|
+
|
|
852
|
+
/** Assembles the object pino merges into the line. */
|
|
853
|
+
private buildBindings(args: {
|
|
854
|
+
context: string | undefined;
|
|
855
|
+
data: LogData | undefined;
|
|
856
|
+
parsed: ParsedLogMessage;
|
|
857
|
+
}): Record<string, unknown> {
|
|
858
|
+
this.assertConventionalMessage({
|
|
859
|
+
context: args.context,
|
|
860
|
+
parsed: args.parsed,
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
return {
|
|
864
|
+
...args.data,
|
|
865
|
+
context: args.context,
|
|
866
|
+
// Telemetry gets prose; only the console-bound transport reads this.
|
|
867
|
+
...(LoggerService.isProduction ? {} : { emoji: args.parsed.emoji }),
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
/** Returns a human-readable explanation when the message format is invalid. */
|
|
872
|
+
private getConventionalMessageViolation(
|
|
873
|
+
parsed: ParsedLogMessage,
|
|
874
|
+
): string | undefined {
|
|
875
|
+
const emoji = parsed.emoji;
|
|
876
|
+
const text = parsed.text;
|
|
877
|
+
|
|
878
|
+
if (emoji === undefined) {
|
|
879
|
+
return `Log message must start with an emoji naming its subject, then a verb: "${text}"`;
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
const firstWord = FIRST_WORD_PATTERN.exec(text)?.[1];
|
|
883
|
+
|
|
884
|
+
if (firstWord === undefined || !this.isConventionalVerb(firstWord)) {
|
|
885
|
+
return `Log message must begin with a verb in present progressive or past tense, got "${firstWord ?? ""}": "${emoji} ${text}"`;
|
|
886
|
+
}
|
|
887
|
+
|
|
888
|
+
return undefined;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
/**
|
|
892
|
+
* Whether a word is a verb in one of the two tenses the convention allows.
|
|
893
|
+
*
|
|
894
|
+
* Present progressive means the operation is under way; past means it
|
|
895
|
+
* finished. Regular morphology covers both, so a new verb needs no
|
|
896
|
+
* registration anywhere — only irregular pasts are enumerated.
|
|
897
|
+
*/
|
|
898
|
+
private isConventionalVerb(word: string): boolean {
|
|
899
|
+
const lowercased = word.toLowerCase();
|
|
900
|
+
|
|
901
|
+
return (
|
|
902
|
+
lowercased.endsWith("ing") ||
|
|
903
|
+
lowercased.endsWith("ed") ||
|
|
904
|
+
IRREGULAR_PAST_VERBS.has(lowercased)
|
|
905
|
+
);
|
|
906
|
+
}
|
|
907
|
+
|
|
908
|
+
/** Splits a leading emoji off a message, leaving prose behind. */
|
|
909
|
+
private parseMessage(message: unknown): ParsedLogMessage {
|
|
910
|
+
const text = String(message);
|
|
911
|
+
const match = LEADING_EMOJI_PATTERN.exec(text);
|
|
912
|
+
const emoji = match?.[1];
|
|
913
|
+
|
|
914
|
+
return emoji === undefined
|
|
915
|
+
? { emoji: undefined, text }
|
|
916
|
+
: { emoji, text: text.slice(match?.[0].length) };
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
/** Whether a context is intentionally exempt from the validation rule. */
|
|
920
|
+
private shouldSkipConventionalMessageValidation(
|
|
921
|
+
context: string | undefined,
|
|
922
|
+
): boolean {
|
|
923
|
+
return context !== undefined && UNVALIDATED_LOG_CONTEXTS.has(context);
|
|
924
|
+
}
|
|
925
|
+
|
|
926
|
+
// 🌎 Public Methods
|
|
927
|
+
|
|
928
|
+
/** The pino instance every logger's child is taken from. */
|
|
929
|
+
private static get root(): pino.Logger {
|
|
930
|
+
LoggerService.rootLogger ??= LoggerService.createRootLogger();
|
|
931
|
+
|
|
932
|
+
return LoggerService.rootLogger;
|
|
933
|
+
}
|
|
934
|
+
|
|
935
|
+
/** Normalizes unknown errors into a stable message and timestamped log line. */
|
|
936
|
+
buildErrorLogEntry(
|
|
937
|
+
context: string,
|
|
938
|
+
error: unknown,
|
|
939
|
+
): { errorMessage: string; logLine: string } {
|
|
940
|
+
const errorMessage =
|
|
941
|
+
error instanceof Error ? error.stack || error.message : String(error);
|
|
942
|
+
|
|
943
|
+
return {
|
|
944
|
+
errorMessage,
|
|
945
|
+
logLine: `[${new Date().toISOString()}] ${context}: ${errorMessage}\n`,
|
|
946
|
+
};
|
|
947
|
+
}
|
|
948
|
+
|
|
949
|
+
/** Builds a timestamped output log file path and ensures the output directory exists. */
|
|
950
|
+
createTimestampedOutputLogFilePath(filePrefix: string): string {
|
|
951
|
+
const outputDirectory = path.join(process.cwd(), "output");
|
|
952
|
+
if (!existsSync(outputDirectory)) {
|
|
953
|
+
mkdirSync(outputDirectory, { recursive: true });
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
return path.join(
|
|
957
|
+
outputDirectory,
|
|
958
|
+
`${filePrefix}-${new Date().toISOString().replaceAll(/[:.]/g, "-")}.log`,
|
|
959
|
+
);
|
|
960
|
+
}
|
|
961
|
+
|
|
962
|
+
/** Logs a debug message at the `debug` level. */
|
|
963
|
+
override debug(message: unknown, context?: string, data?: LogData): void {
|
|
964
|
+
const parsed = this.parseMessage(message);
|
|
965
|
+
this.child.debug(
|
|
966
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
967
|
+
parsed.text,
|
|
968
|
+
);
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
/**
|
|
972
|
+
* Logs an error message at the `error` level, optionally including a stack trace.
|
|
973
|
+
*
|
|
974
|
+
* `ConsoleLogger.error` spends a third slot on a context string that the
|
|
975
|
+
* other levels do not have, so this one accepts either: a string keeps
|
|
976
|
+
* NestJS's meaning, an object is structured data like everywhere else.
|
|
977
|
+
*/
|
|
978
|
+
override error(
|
|
979
|
+
message: unknown,
|
|
980
|
+
stackOrContext?: string,
|
|
981
|
+
contextOrData?: LogData | string,
|
|
982
|
+
): void {
|
|
983
|
+
const parsed = this.parseMessage(message);
|
|
984
|
+
const data = typeof contextOrData === "object" ? contextOrData : undefined;
|
|
985
|
+
const context =
|
|
986
|
+
typeof contextOrData === "string" ? contextOrData : this.context;
|
|
987
|
+
|
|
988
|
+
this.child.error(
|
|
989
|
+
{
|
|
990
|
+
...this.buildBindings({ context, data, parsed }),
|
|
991
|
+
stack: stackOrContext,
|
|
992
|
+
},
|
|
993
|
+
parsed.text,
|
|
994
|
+
);
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
/** Logs an informational message at the `info` level. */
|
|
998
|
+
info(message: unknown, context?: string, data?: LogData): void {
|
|
999
|
+
const parsed = this.parseMessage(message);
|
|
1000
|
+
this.child.info(
|
|
1001
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
1002
|
+
parsed.text,
|
|
1003
|
+
);
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Logs an informational message at the `info` level.
|
|
1008
|
+
*
|
|
1009
|
+
* NestJS and `nest-commander` call this method directly as part of the
|
|
1010
|
+
* framework's own `LoggerService` contract, so it must keep working
|
|
1011
|
+
* exactly as before. Application code should call `info` instead — the
|
|
1012
|
+
* same behavior under a name that says what level it logs at.
|
|
1013
|
+
*/
|
|
1014
|
+
override log(message: unknown, context?: string, data?: LogData): void {
|
|
1015
|
+
this.info(message, context, data);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
/** Sets the context label included in every subsequent log line. */
|
|
1019
|
+
override setContext(context: string): void {
|
|
1020
|
+
super.setContext(context);
|
|
1021
|
+
this.child = LoggerService.root.child({ context });
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
/** Logs a verbose message at the `trace` level. */
|
|
1025
|
+
override verbose(message: unknown, context?: string, data?: LogData): void {
|
|
1026
|
+
const parsed = this.parseMessage(message);
|
|
1027
|
+
this.child.trace(
|
|
1028
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
1029
|
+
parsed.text,
|
|
1030
|
+
);
|
|
1031
|
+
}
|
|
1032
|
+
|
|
1033
|
+
/** Logs a warning message at the `warn` level. */
|
|
1034
|
+
override warn(message: unknown, context?: string, data?: LogData): void {
|
|
1035
|
+
const parsed = this.parseMessage(message);
|
|
1036
|
+
this.child.warn(
|
|
1037
|
+
this.buildBindings({ context: context ?? this.context, data, parsed }),
|
|
1038
|
+
parsed.text,
|
|
1039
|
+
);
|
|
1040
|
+
}
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
/** Input to the markdown analysis step. */
|
|
1044
|
+
export declare interface MarkdownInput {
|
|
1045
|
+
markdownFiles: string[];
|
|
1046
|
+
workingDirectory: string;
|
|
1047
|
+
}
|
|
1048
|
+
|
|
1049
|
+
/**
|
|
1050
|
+
* Provides structural measurement of markdown documents.
|
|
1051
|
+
*/
|
|
1052
|
+
export declare class MarkdownModule {
|
|
1053
|
+
}
|
|
1054
|
+
|
|
1055
|
+
/** Aggregated metrics collected from parsing markdown documents. */
|
|
1056
|
+
export declare interface MarkdownResult {
|
|
1057
|
+
blockQuotes: number;
|
|
1058
|
+
codeBlocks: number;
|
|
1059
|
+
files: number;
|
|
1060
|
+
headingLevel1: number;
|
|
1061
|
+
headingLevel2: number;
|
|
1062
|
+
headingLevel3: number;
|
|
1063
|
+
headingLevel4: number;
|
|
1064
|
+
headingLevel5: number;
|
|
1065
|
+
headingLevel6: number;
|
|
1066
|
+
images: number;
|
|
1067
|
+
inlineCode: number;
|
|
1068
|
+
lines: number;
|
|
1069
|
+
links: number;
|
|
1070
|
+
listItems: number;
|
|
1071
|
+
lists: number;
|
|
1072
|
+
paragraphs: number;
|
|
1073
|
+
tableRows: number;
|
|
1074
|
+
tables: number;
|
|
1075
|
+
taskListItems: number;
|
|
1076
|
+
thematicBreaks: number;
|
|
1077
|
+
}
|
|
1078
|
+
|
|
1079
|
+
/**
|
|
1080
|
+
* Walks parsed markdown documents to collect structural metrics.
|
|
1081
|
+
*
|
|
1082
|
+
* Comparison is structural rather than textual, so a heading only counts when
|
|
1083
|
+
* the parser agrees it is one. Frontmatter is parsed as its own node because
|
|
1084
|
+
* a `---` delimited block is otherwise read as a setext heading, which would
|
|
1085
|
+
* report a level-two heading at the top of every file that has frontmatter.
|
|
1086
|
+
* GFM is enabled so tables and task list items exist as nodes at all.
|
|
1087
|
+
*/
|
|
1088
|
+
export declare class MarkdownService {
|
|
1089
|
+
private readonly logger;
|
|
1090
|
+
constructor(logger: LoggerService);
|
|
1091
|
+
private readonly processor;
|
|
1092
|
+
/** Records a heading against the field for its depth. */
|
|
1093
|
+
private countHeading;
|
|
1094
|
+
/** Records a list item, separating GFM checkboxes from plain bullets. */
|
|
1095
|
+
private countListItem;
|
|
1096
|
+
/** Records a single node against the running totals. */
|
|
1097
|
+
private countNode;
|
|
1098
|
+
/** Walks every node in a parsed document. */
|
|
1099
|
+
private walk;
|
|
1100
|
+
/** Analyze the given markdown files, resolved against the directory. */
|
|
1101
|
+
analyze({ markdownFiles, workingDirectory }: MarkdownInput): MarkdownResult;
|
|
1102
|
+
/**
|
|
1103
|
+
* Analyze markdown source text that never came from a file of its own.
|
|
1104
|
+
*
|
|
1105
|
+
* The seam the jupyter analyzer reads through: a notebook's markdown cells
|
|
1106
|
+
* are documents without paths, and re-parsing them anywhere else would be a
|
|
1107
|
+
* second implementation of this counting.
|
|
1108
|
+
*/
|
|
1109
|
+
analyzeContents(contents: string[]): MarkdownResult;
|
|
1110
|
+
}
|
|
1111
|
+
|
|
1112
|
+
/** Arguments accepted when measuring one file's comment blocks. */
|
|
1113
|
+
declare interface MeasureCommentsArguments {
|
|
1114
|
+
comments: CommentBudget;
|
|
1115
|
+
filePath: string;
|
|
1116
|
+
tokens: CommentToken[];
|
|
1117
|
+
}
|
|
1118
|
+
|
|
1119
|
+
/** Arguments accepted when measuring one comment's text directly. */
|
|
1120
|
+
declare interface MeasureCommentTextArguments {
|
|
1121
|
+
comments: CommentBudget;
|
|
1122
|
+
declaration: string;
|
|
1123
|
+
filePath: string;
|
|
1124
|
+
kind: string;
|
|
1125
|
+
line: number;
|
|
1126
|
+
/** The comment's prose, with every marker stripped. */
|
|
1127
|
+
prose: string;
|
|
1128
|
+
/** The comment exactly as the file carries it, markers and all. */
|
|
1129
|
+
source: string;
|
|
1130
|
+
}
|
|
1131
|
+
|
|
1132
|
+
/** Arguments accepted when measuring every configured language's comments. */
|
|
1133
|
+
declare interface MeasureLanguageCommentsArguments {
|
|
1134
|
+
/** One `comment`-selector custom statistic's budget, per declared statistic. */
|
|
1135
|
+
counters: CommentCounter[];
|
|
1136
|
+
files: LanguageCommentFiles;
|
|
1137
|
+
/**
|
|
1138
|
+
* Python's comments, already found by its own analyzer.
|
|
1139
|
+
*
|
|
1140
|
+
* Python is read in Python: its analysis runs `tokenize` in a subprocess,
|
|
1141
|
+
* which knows a `#` inside a string literal from a real comment the way no
|
|
1142
|
+
* line scanner can. The tokens arrive here rather than the measurements, so
|
|
1143
|
+
* a word still means one thing across every language.
|
|
1144
|
+
*/
|
|
1145
|
+
pythonComments: readonly LocatedCommentToken[];
|
|
1146
|
+
workingDirectory: string;
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
/**
|
|
1150
|
+
* What the notebooks themselves report, before any language analyzer runs.
|
|
1151
|
+
*
|
|
1152
|
+
* The two source lists are what the notebook contributes to the Python and
|
|
1153
|
+
* markdown analyzers: cell bodies that exist in no file of their own.
|
|
1154
|
+
*/
|
|
1155
|
+
export declare interface NotebookParts {
|
|
1156
|
+
cells: number;
|
|
1157
|
+
codeCells: number;
|
|
1158
|
+
codeSources: string[];
|
|
1159
|
+
executedCells: number;
|
|
1160
|
+
files: number;
|
|
1161
|
+
markdownCells: number;
|
|
1162
|
+
markdownSources: string[];
|
|
1163
|
+
outputs: number;
|
|
1164
|
+
rawCells: number;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/** A message split into the emoji the console shows and the prose telemetry stores. */
|
|
1168
|
+
declare interface ParsedLogMessage {
|
|
1169
|
+
emoji: string | undefined;
|
|
1170
|
+
text: string;
|
|
1171
|
+
}
|
|
1172
|
+
|
|
1173
|
+
/**
|
|
1174
|
+
* NestJS module that provides Python code analysis.
|
|
1175
|
+
*/
|
|
1176
|
+
export declare class PythonModule {
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
/**
|
|
1180
|
+
* Aggregated statistics produced by the Python analyzer.
|
|
1181
|
+
*/
|
|
1182
|
+
export declare interface PythonResult {
|
|
1183
|
+
classes: number;
|
|
1184
|
+
commentLines: number;
|
|
1185
|
+
comments: number;
|
|
1186
|
+
/** Every comment `tokenize` found, with the file it was found in. */
|
|
1187
|
+
commentTokens: LocatedCommentToken[];
|
|
1188
|
+
constants: number;
|
|
1189
|
+
decorators: number;
|
|
1190
|
+
docstringLines: number;
|
|
1191
|
+
docstrings: number;
|
|
1192
|
+
files: number;
|
|
1193
|
+
functions: number;
|
|
1194
|
+
imports: number;
|
|
1195
|
+
lines: number;
|
|
1196
|
+
protocols: number;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
/**
|
|
1200
|
+
* Executes the Python analysis script and returns aggregated metrics.
|
|
1201
|
+
*/
|
|
1202
|
+
export declare class PythonService {
|
|
1203
|
+
private readonly logger;
|
|
1204
|
+
constructor(logger: LoggerService);
|
|
1205
|
+
private readonly scriptPath;
|
|
1206
|
+
/**
|
|
1207
|
+
* Analyze the given Python files, resolved relative to the directory.
|
|
1208
|
+
*
|
|
1209
|
+
* The interpreter comes from the configuration because reaching Python is a
|
|
1210
|
+
* property of the repository, not of the analysis: a managed environment is
|
|
1211
|
+
* entered with `uv run python` or `poetry run python`, and a plain one with
|
|
1212
|
+
* `python3`.
|
|
1213
|
+
*
|
|
1214
|
+
* The paths travel over stdin rather than argv so that a repository with
|
|
1215
|
+
* thousands of Python files cannot overflow the command-line length limit.
|
|
1216
|
+
*/
|
|
1217
|
+
analyze(args: AnalyzePythonArguments): PythonResult;
|
|
1218
|
+
/**
|
|
1219
|
+
* Analyze Python source text that never came from a file of its own.
|
|
1220
|
+
*
|
|
1221
|
+
* The seam the jupyter analyzer reads through. The interpreter reads files,
|
|
1222
|
+
* so the sources are staged in a temporary directory first — but the
|
|
1223
|
+
* analysis still runs from the measured directory, because a command like
|
|
1224
|
+
* `uv run python` resolves its environment from the working directory and
|
|
1225
|
+
* would find no project under the system temp directory.
|
|
1226
|
+
*/
|
|
1227
|
+
analyzeContents(args: AnalyzePythonContentsArguments): PythonResult;
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
/** Input to the Shell analysis step. */
|
|
1231
|
+
export declare interface ShellInput {
|
|
1232
|
+
shellFiles: string[];
|
|
1233
|
+
workingDirectory: string;
|
|
1234
|
+
}
|
|
1235
|
+
|
|
1236
|
+
/**
|
|
1237
|
+
* NestJS module that provides Shell source analysis.
|
|
1238
|
+
*/
|
|
1239
|
+
export declare class ShellModule {
|
|
1240
|
+
}
|
|
1241
|
+
|
|
1242
|
+
/** Aggregated metrics collected from parsing Shell sources. */
|
|
1243
|
+
export declare interface ShellResult {
|
|
1244
|
+
commentLines: number;
|
|
1245
|
+
comments: number;
|
|
1246
|
+
conditionals: number;
|
|
1247
|
+
exports: number;
|
|
1248
|
+
files: number;
|
|
1249
|
+
functions: number;
|
|
1250
|
+
lines: number;
|
|
1251
|
+
loops: number;
|
|
1252
|
+
pipelines: number;
|
|
1253
|
+
shebangs: number;
|
|
1254
|
+
variables: number;
|
|
1255
|
+
}
|
|
1256
|
+
|
|
1257
|
+
/**
|
|
1258
|
+
* Counts the constructs a shell script is built from.
|
|
1259
|
+
*
|
|
1260
|
+
* Pattern-based rather than parsed: shell has no syntax tree available without
|
|
1261
|
+
* a native dependency, and a heuristic that a reader would agree with is worth
|
|
1262
|
+
* more here than no measurement at all. Comment lines are recognized before
|
|
1263
|
+
* anything else so that a `#` opening a line never reads as code.
|
|
1264
|
+
*/
|
|
1265
|
+
export declare class ShellService {
|
|
1266
|
+
private readonly logger;
|
|
1267
|
+
constructor(logger: LoggerService);
|
|
1268
|
+
/** Records the constructs one line of shell holds. */
|
|
1269
|
+
private countLine;
|
|
1270
|
+
/** Records the constructs a line of shell code holds. */
|
|
1271
|
+
private countStatements;
|
|
1272
|
+
/** Analyze the given shell scripts, resolved against the directory. */
|
|
1273
|
+
analyze({ shellFiles, workingDirectory }: ShellInput): ShellResult;
|
|
1274
|
+
}
|
|
1275
|
+
|
|
1276
|
+
/**
|
|
1277
|
+
* Reads SQL's `--` and `/* ... *\/` comments.
|
|
1278
|
+
*
|
|
1279
|
+
* A line scanner rather than a tokenizer, so a `--` or `/*` inside a `'…'`
|
|
1280
|
+
* string literal is read as a comment here exactly as it already is when
|
|
1281
|
+
* `SqlService` counts keywords — the reading is the same, only the positions
|
|
1282
|
+
* are new. The block form is found with `indexOf` rather than a regular
|
|
1283
|
+
* expression: a pattern matching an unclosed `/*` through to end of input has
|
|
1284
|
+
* to fail once per occurrence, which is quadratic on adversarial input, while
|
|
1285
|
+
* two `indexOf` calls per comment never scan the same text twice.
|
|
1286
|
+
*/
|
|
1287
|
+
export declare class SqlCommentsService {
|
|
1288
|
+
constructor();
|
|
1289
|
+
/** Every `/* *\/` comment's span, left to right and never overlapping. */
|
|
1290
|
+
private findBlockComments;
|
|
1291
|
+
/** Whether only whitespace precedes an offset on its own line. */
|
|
1292
|
+
private isOwnLine;
|
|
1293
|
+
/** The 1-indexed line an offset sits on. */
|
|
1294
|
+
private lineOf;
|
|
1295
|
+
/** Every block comment's span, as a positioned token. */
|
|
1296
|
+
private readBlocks;
|
|
1297
|
+
/** Every match of a comment pattern, as a positioned token. */
|
|
1298
|
+
private readMatches;
|
|
1299
|
+
/**
|
|
1300
|
+
* Reads every `--` and `/* *\/` comment, in the order they appear.
|
|
1301
|
+
*
|
|
1302
|
+
* Block spans are found first so a `--` matched inside one can be
|
|
1303
|
+
* dropped — the block already spans the whole thing, and without dropping
|
|
1304
|
+
* the overlap a `--` written as prose inside a block would be measured a
|
|
1305
|
+
* second time as a comment of its own.
|
|
1306
|
+
*/
|
|
1307
|
+
read(content: string): CommentToken[];
|
|
1308
|
+
}
|
|
1309
|
+
|
|
1310
|
+
/** Input to the Sql analysis step. */
|
|
1311
|
+
export declare interface SqlInput {
|
|
1312
|
+
sqlFiles: string[];
|
|
1313
|
+
workingDirectory: string;
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
/**
|
|
1317
|
+
* NestJS module that provides Sql source analysis.
|
|
1318
|
+
*/
|
|
1319
|
+
export declare class SqlModule {
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
/** Aggregated metrics collected from parsing Sql sources. */
|
|
1323
|
+
export declare interface SqlResult {
|
|
1324
|
+
comments: number;
|
|
1325
|
+
commonTableExpressions: number;
|
|
1326
|
+
creates: number;
|
|
1327
|
+
deletes: number;
|
|
1328
|
+
files: number;
|
|
1329
|
+
inserts: number;
|
|
1330
|
+
joins: number;
|
|
1331
|
+
lines: number;
|
|
1332
|
+
selects: number;
|
|
1333
|
+
statements: number;
|
|
1334
|
+
updates: number;
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
/**
|
|
1338
|
+
* Counts the statements and clauses a SQL script is built from.
|
|
1339
|
+
*
|
|
1340
|
+
* Comments are stripped before anything is counted, so a `SELECT` inside a
|
|
1341
|
+
* `--` explanation is prose rather than a query. Statements are separated on
|
|
1342
|
+
* semicolons, which is what the dialect-agnostic reading of a script is.
|
|
1343
|
+
*/
|
|
1344
|
+
export declare class SqlService {
|
|
1345
|
+
private readonly logger;
|
|
1346
|
+
constructor(logger: LoggerService);
|
|
1347
|
+
/** Records every keyword occurrence in the comment-free source. */
|
|
1348
|
+
private countKeywords;
|
|
1349
|
+
/** Counts the comments in a script and returns the source without them. */
|
|
1350
|
+
private stripComments;
|
|
1351
|
+
/** Analyze the given SQL scripts, resolved against the directory. */
|
|
1352
|
+
analyze({ sqlFiles, workingDirectory }: SqlInput): SqlResult;
|
|
1353
|
+
}
|
|
1354
|
+
|
|
1355
|
+
/** Input to the Toml analysis step. */
|
|
1356
|
+
export declare interface TomlInput {
|
|
1357
|
+
tomlFiles: string[];
|
|
1358
|
+
workingDirectory: string;
|
|
1359
|
+
}
|
|
1360
|
+
|
|
1361
|
+
/**
|
|
1362
|
+
* NestJS module that provides Toml source analysis.
|
|
1363
|
+
*/
|
|
1364
|
+
export declare class TomlModule {
|
|
1365
|
+
}
|
|
1366
|
+
|
|
1367
|
+
/** Aggregated metrics collected from parsing Toml sources. */
|
|
1368
|
+
export declare interface TomlResult {
|
|
1369
|
+
arrays: number;
|
|
1370
|
+
arrayTables: number;
|
|
1371
|
+
comments: number;
|
|
1372
|
+
files: number;
|
|
1373
|
+
keys: number;
|
|
1374
|
+
lines: number;
|
|
1375
|
+
tables: number;
|
|
1376
|
+
}
|
|
1377
|
+
|
|
1378
|
+
/**
|
|
1379
|
+
* Counts the tables, keys, and arrays a TOML document declares.
|
|
1380
|
+
*
|
|
1381
|
+
* Read line by line while tracking whether a multi-line string is open, so a
|
|
1382
|
+
* `#` or a `[heading]` inside one of those strings is content rather than
|
|
1383
|
+
* syntax. That state is the whole reason this is not three regexes.
|
|
1384
|
+
*/
|
|
1385
|
+
export declare class TomlService {
|
|
1386
|
+
private readonly logger;
|
|
1387
|
+
constructor(logger: LoggerService);
|
|
1388
|
+
/** Records a key assignment and whether its value opens an array. */
|
|
1389
|
+
private countKey;
|
|
1390
|
+
/** Records the declaration one line of TOML holds. */
|
|
1391
|
+
private countLine;
|
|
1392
|
+
/**
|
|
1393
|
+
* Whether the line closes the multi-line string that was open, or opens one.
|
|
1394
|
+
*
|
|
1395
|
+
* An odd number of `"""` or `'''` delimiters flips the state; an even number
|
|
1396
|
+
* opens and closes within the same line and leaves it as it was.
|
|
1397
|
+
*/
|
|
1398
|
+
private isInsideMultilineString;
|
|
1399
|
+
/** Analyze the given TOML documents, resolved against the directory. */
|
|
1400
|
+
analyze({ tomlFiles, workingDirectory }: TomlInput): TomlResult;
|
|
1401
|
+
}
|
|
1402
|
+
|
|
1403
|
+
/**
|
|
1404
|
+
* Reads TypeScript and JavaScript's `//` and non-JSDoc `/* ... *\/` comments
|
|
1405
|
+
* from the compiler's real parse, rather than its bare scanner.
|
|
1406
|
+
*
|
|
1407
|
+
* A bare `ts.createScanner` cannot tell a `/` that divides from one that opens
|
|
1408
|
+
* a regular expression without the parser's context, and gets it wrong on real
|
|
1409
|
+
* source — swallowing a long run of genuine comments as trivia inside a
|
|
1410
|
+
* misread token. Parsing for real and walking every leaf token's leading and
|
|
1411
|
+
* trailing trivia is what a full parse resolves correctly, at the cost of one
|
|
1412
|
+
* parse per file instead of one scan.
|
|
1413
|
+
*
|
|
1414
|
+
* A JSDoc `/**` block is skipped here — `DeclarationCommentsService`
|
|
1415
|
+
* already measures those — the same check it uses to find one: the
|
|
1416
|
+
* delimiter's first three characters.
|
|
1417
|
+
*/
|
|
1418
|
+
export declare class TypescriptCommentsService {
|
|
1419
|
+
constructor();
|
|
1420
|
+
/**
|
|
1421
|
+
* Walks every leaf token, collecting the comments around it.
|
|
1422
|
+
*
|
|
1423
|
+
* Both sides of every leaf, because a comment sharing its line with the
|
|
1424
|
+
* token before it is that token's *trailing* trivia rather than the next
|
|
1425
|
+
* token's *leading* trivia — `getLeadingCommentRanges` at a token's start
|
|
1426
|
+
* never finds it, only `getTrailingCommentRanges` at the previous token's
|
|
1427
|
+
* end does.
|
|
1428
|
+
*/
|
|
1429
|
+
private collectComments;
|
|
1430
|
+
/** Choose the dialect a file is parsed as, from its extension. */
|
|
1431
|
+
private getScriptKind;
|
|
1432
|
+
/** Whether this range is a JSDoc block, measured elsewhere. */
|
|
1433
|
+
private isJsDoc;
|
|
1434
|
+
/** Whether only whitespace precedes an offset on its own line. */
|
|
1435
|
+
private isOwnLine;
|
|
1436
|
+
/** The 1-indexed line an offset sits on. */
|
|
1437
|
+
private lineOf;
|
|
1438
|
+
/** Strips a comment's delimiters, leaving its prose. */
|
|
1439
|
+
private toProse;
|
|
1440
|
+
/** Turns one comment range into a token, unless it is a JSDoc block. */
|
|
1441
|
+
private toToken;
|
|
1442
|
+
/** Reads every non-JSDoc comment the parse finds, in source order. */
|
|
1443
|
+
read(content: string, filePath: string): CommentToken[];
|
|
1444
|
+
}
|
|
1445
|
+
|
|
1446
|
+
/** Input to the TypeScript/JavaScript AST analysis step. */
|
|
1447
|
+
export declare interface TypescriptInput {
|
|
1448
|
+
/**
|
|
1449
|
+
* Every `comment`-selector custom statistic's budget, per declared statistic.
|
|
1450
|
+
*
|
|
1451
|
+
* The whole list, not only the counters this walk measures: a counter naming
|
|
1452
|
+
* a documentable `kind` is one of those and every other one is ignored here,
|
|
1453
|
+
* which is what leaves a configuration declaring no such statistic measuring
|
|
1454
|
+
* nothing rather than measuring against a limit nobody chose.
|
|
1455
|
+
*/
|
|
1456
|
+
commentCounters: CommentCounter[];
|
|
1457
|
+
sourceFiles: string[];
|
|
1458
|
+
/** Configured counters over declarations, tallied during the same walk. */
|
|
1459
|
+
symbolCounters: TypescriptSymbolCounter[];
|
|
1460
|
+
workingDirectory: string;
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
/**
|
|
1464
|
+
* NestJS module that provides TypeScript and JavaScript code analysis.
|
|
1465
|
+
*/
|
|
1466
|
+
export declare class TypescriptModule {
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
/** Aggregated metrics collected from walking TypeScript and JavaScript ASTs. */
|
|
1470
|
+
export declare interface TypescriptResult {
|
|
1471
|
+
asyncFunctions: number;
|
|
1472
|
+
blockComments: number;
|
|
1473
|
+
classes: number;
|
|
1474
|
+
commentLines: number;
|
|
1475
|
+
comments: number;
|
|
1476
|
+
constants: number;
|
|
1477
|
+
/** One measurement list per configured declaration-comment counter, keyed by its label. */
|
|
1478
|
+
declarationCommentCounts: Record<string, CommentMeasurement[]>;
|
|
1479
|
+
decorators: number;
|
|
1480
|
+
docComments: number;
|
|
1481
|
+
docTags: Record<string, number>;
|
|
1482
|
+
enums: number;
|
|
1483
|
+
exported: number;
|
|
1484
|
+
externalPackages: Set<string>;
|
|
1485
|
+
functions: number;
|
|
1486
|
+
genericDeclarations: number;
|
|
1487
|
+
imports: number;
|
|
1488
|
+
interfaces: number;
|
|
1489
|
+
jsFiles: number;
|
|
1490
|
+
lineComments: number;
|
|
1491
|
+
lines: number;
|
|
1492
|
+
methods: number;
|
|
1493
|
+
/** One tally per configured symbol counter, keyed by its label. */
|
|
1494
|
+
symbolCounts: Record<string, number>;
|
|
1495
|
+
syncFunctions: number;
|
|
1496
|
+
testFiles: number;
|
|
1497
|
+
todos: number;
|
|
1498
|
+
tsFiles: number;
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
/** Walks TypeScript and JavaScript ASTs to collect code metrics. */
|
|
1502
|
+
export declare class TypescriptService {
|
|
1503
|
+
private readonly declarationComments;
|
|
1504
|
+
/** Creates the TypescriptService. */
|
|
1505
|
+
constructor(declarationComments: DeclarationCommentsService);
|
|
1506
|
+
/**
|
|
1507
|
+
* What to count for each syntax kind the walk cares about.
|
|
1508
|
+
*
|
|
1509
|
+
* Read by `dispatchNode`, which is the only caller: a kind absent from the
|
|
1510
|
+
* table is a node this analyzer counts nothing for, so adding a statistic
|
|
1511
|
+
* means adding a row here rather than another branch inside the walk.
|
|
1512
|
+
*
|
|
1513
|
+
* `dispatchNode` reaches a row by computed member access, which no call
|
|
1514
|
+
* graph can follow — callidescope records the call as unfollowable and
|
|
1515
|
+
* every row here as an entry point nothing calls. So a traced stack stops
|
|
1516
|
+
* at `dispatchNode` and each `handle*` method appears again at the root of
|
|
1517
|
+
* a stack of its own. That is the price of a table over a `switch`, and
|
|
1518
|
+
* worth knowing before reading those roots as dead code.
|
|
1519
|
+
*/
|
|
1520
|
+
private readonly kindDispatch;
|
|
1521
|
+
/** Read one source file, count its lines and comments, and walk its AST. */
|
|
1522
|
+
private analyzeFile;
|
|
1523
|
+
/** Measure a documentable declaration's leading JSDoc comment, if it has one. */
|
|
1524
|
+
private collectDeclarationComments;
|
|
1525
|
+
/** Count a discovered comment and update the appropriate metrics. */
|
|
1526
|
+
private countComment;
|
|
1527
|
+
/**
|
|
1528
|
+
* Tally every configured counter that claims this declaration.
|
|
1529
|
+
*
|
|
1530
|
+
* A declaration is claimed when its kind is one the counter asked for and
|
|
1531
|
+
* it carries every modifier the counter requires; a counter naming no
|
|
1532
|
+
* modifiers asks for the kind alone.
|
|
1533
|
+
*/
|
|
1534
|
+
private countSymbols;
|
|
1535
|
+
/**
|
|
1536
|
+
* Build the zeroed result every file's counters accumulate into.
|
|
1537
|
+
*
|
|
1538
|
+
* Every configured counter is seeded, so one that matches nothing reports a
|
|
1539
|
+
* zero rather than going missing from the report entirely.
|
|
1540
|
+
*/
|
|
1541
|
+
private createEmptyResult;
|
|
1542
|
+
/** Dispatches non-class AST nodes to the appropriate metric-collection handler. */
|
|
1543
|
+
private dispatchNode;
|
|
1544
|
+
/** Narrow the configured counters to the ones that search this file. */
|
|
1545
|
+
private getCountersForFile;
|
|
1546
|
+
/** Choose the dialect a file is parsed as, from its extension. */
|
|
1547
|
+
private getScriptKind;
|
|
1548
|
+
/** Collect the modifier keywords a node carries, by configured name. */
|
|
1549
|
+
private getSymbolModifiers;
|
|
1550
|
+
/** Increments class, exported, and generic counts for a class node. */
|
|
1551
|
+
private handleClass;
|
|
1552
|
+
/** Increments enum and exported counts for an enum declaration node. */
|
|
1553
|
+
private handleEnum;
|
|
1554
|
+
/** Increments function, method, async, sync, exported, and generic counts for a function node. */
|
|
1555
|
+
private handleFunction;
|
|
1556
|
+
/** Increments import count and tracks the external package name if applicable. */
|
|
1557
|
+
private handleImport;
|
|
1558
|
+
/** Increments interface, exported, and generic counts for an interface declaration node. */
|
|
1559
|
+
private handleInterface;
|
|
1560
|
+
/** Increments method and async or sync counts for a method or accessor node. */
|
|
1561
|
+
private handleMethodOrAccessor;
|
|
1562
|
+
/** Increments exported and generic counts for a type alias declaration node. */
|
|
1563
|
+
private handleTypeAlias;
|
|
1564
|
+
/** Increments constant and exported counts for a const variable statement. */
|
|
1565
|
+
private handleVariable;
|
|
1566
|
+
/** Returns true when the node has an async modifier keyword. */
|
|
1567
|
+
private hasAsyncKeyword;
|
|
1568
|
+
/** Returns true when the node has an export modifier keyword. */
|
|
1569
|
+
private hasExportKeyword;
|
|
1570
|
+
/** Returns true when the node declares one or more type parameters. */
|
|
1571
|
+
private hasTypeParameters;
|
|
1572
|
+
/** Scan the provided source text and collect comment-based metrics. */
|
|
1573
|
+
private scanComments;
|
|
1574
|
+
/**
|
|
1575
|
+
* Seed one empty list per counter this walk is the measurer for.
|
|
1576
|
+
*
|
|
1577
|
+
* A counter naming a `kind` is one of those, and a counter naming only a
|
|
1578
|
+
* `language` is not: `kind` takes precedence, so a counter naming both is
|
|
1579
|
+
* measured here and its `language` never read, which is the same tie-break
|
|
1580
|
+
* `LanguageCommentsService` makes by skipping it. Written as a guarded loop
|
|
1581
|
+
* rather than a filtered `Object.fromEntries`, which would spend two of
|
|
1582
|
+
* `createEmptyResult`'s direct calls instead of one.
|
|
1583
|
+
*/
|
|
1584
|
+
private seedDeclarationCommentCounts;
|
|
1585
|
+
/** Recursively visits each AST node and dispatches to the appropriate handler. */
|
|
1586
|
+
private walkNode;
|
|
1587
|
+
/** Analyzes TypeScript and JavaScript source files and returns aggregated AST metrics. */
|
|
1588
|
+
analyze(input: TypescriptInput): TypescriptResult;
|
|
1589
|
+
}
|
|
1590
|
+
|
|
1591
|
+
/** One configured counter over declarations, resolved for the analyzer. */
|
|
1592
|
+
export declare interface TypescriptSymbolCounter {
|
|
1593
|
+
kinds: CodometerSymbolKind[];
|
|
1594
|
+
label: string;
|
|
1595
|
+
modifiers: CodometerSymbolModifier[];
|
|
1596
|
+
/** Globs narrowing which files are searched; empty searches all of them. */
|
|
1597
|
+
patterns: string[];
|
|
1598
|
+
}
|
|
1599
|
+
|
|
1600
|
+
/**
|
|
1601
|
+
* What one file's AST walk carries with it, node to node.
|
|
1602
|
+
*
|
|
1603
|
+
* Narrowed once per file rather than rebuilt per node: which counters search a
|
|
1604
|
+
* file depends on its path, and the path does not change as the walk descends.
|
|
1605
|
+
*/
|
|
1606
|
+
export declare interface TypescriptWalkContext {
|
|
1607
|
+
commentCounters: CommentCounter[];
|
|
1608
|
+
/**
|
|
1609
|
+
* The symbol counters that apply to the file being walked.
|
|
1610
|
+
*
|
|
1611
|
+
* Narrowed once per file rather than per node: which counters search a file
|
|
1612
|
+
* depends on its path, which does not change as the walk descends.
|
|
1613
|
+
*/
|
|
1614
|
+
counters: TypescriptSymbolCounter[];
|
|
1615
|
+
filePath: string;
|
|
1616
|
+
insideClass: boolean;
|
|
1617
|
+
sourceFile: SourceFile;
|
|
1618
|
+
stats: TypescriptResult;
|
|
1619
|
+
}
|
|
1620
|
+
|
|
1621
|
+
/**
|
|
1622
|
+
* Reads YAML's comments from the tokenizer rather than from the text.
|
|
1623
|
+
*
|
|
1624
|
+
* The CST is the only view carrying both facts a measurement needs: an offset,
|
|
1625
|
+
* so a breach can name the line it sits on, and the tokenizer's judgement of
|
|
1626
|
+
* what is a comment at all, so a `#` inside a quoted scalar stays a character
|
|
1627
|
+
* in a string. The composed document has the second and not the first — it
|
|
1628
|
+
* hangs a whole run of `#` lines on a node as one string with no position,
|
|
1629
|
+
* blank lines and all.
|
|
1630
|
+
*
|
|
1631
|
+
* This is why YAML does not use `HashCommentsService`: a line scanner would
|
|
1632
|
+
* read `key: "a # b"` as carrying a comment, and YAML puts quoted scalars next
|
|
1633
|
+
* to `#` constantly.
|
|
1634
|
+
*/
|
|
1635
|
+
export declare class YamlCommentsService {
|
|
1636
|
+
constructor();
|
|
1637
|
+
/** Walks one parsed token, recording every comment beneath it. */
|
|
1638
|
+
private collectComments;
|
|
1639
|
+
/** Whether a parsed token is a comment carrying an offset. */
|
|
1640
|
+
private isCommentToken;
|
|
1641
|
+
/** Turns one tokenizer offset into a positioned, placed comment line. */
|
|
1642
|
+
private toToken;
|
|
1643
|
+
/** Reads every comment the tokenizer found, in the order they appear. */
|
|
1644
|
+
read(content: string): CommentToken[];
|
|
1645
|
+
}
|
|
1646
|
+
|
|
1647
|
+
/** Input to the YAML analysis step. */
|
|
1648
|
+
export declare interface YamlInput {
|
|
1649
|
+
workingDirectory: string;
|
|
1650
|
+
yamlFiles: string[];
|
|
1651
|
+
}
|
|
1652
|
+
|
|
1653
|
+
/**
|
|
1654
|
+
* NestJS module that provides YAML document analysis.
|
|
1655
|
+
*/
|
|
1656
|
+
export declare class YamlModule {
|
|
1657
|
+
}
|
|
1658
|
+
|
|
1659
|
+
/** Aggregated metrics collected from parsing YAML documents. */
|
|
1660
|
+
export declare interface YamlResult {
|
|
1661
|
+
aliases: number;
|
|
1662
|
+
anchors: number;
|
|
1663
|
+
comments: number;
|
|
1664
|
+
documents: number;
|
|
1665
|
+
files: number;
|
|
1666
|
+
keys: number;
|
|
1667
|
+
lines: number;
|
|
1668
|
+
mappings: number;
|
|
1669
|
+
maxDepth: number;
|
|
1670
|
+
scalars: number;
|
|
1671
|
+
sequences: number;
|
|
1672
|
+
}
|
|
1673
|
+
|
|
1674
|
+
/**
|
|
1675
|
+
* Walks parsed YAML documents to collect structural metrics.
|
|
1676
|
+
*
|
|
1677
|
+
* Counted from the parse tree rather than from the text, so a `#` inside a
|
|
1678
|
+
* quoted scalar stays a character in a string and an indented block reports
|
|
1679
|
+
* its real nesting instead of a column number. The `yaml` package keeps
|
|
1680
|
+
* comments and anchors on the nodes, which is what makes both countable at all.
|
|
1681
|
+
*/
|
|
1682
|
+
export declare class YamlService {
|
|
1683
|
+
private readonly logger;
|
|
1684
|
+
constructor(logger: LoggerService);
|
|
1685
|
+
/** Walks a mapping's pairs or a sequence's items. */
|
|
1686
|
+
private countCollection;
|
|
1687
|
+
/** Records the comments attached to one node or document. */
|
|
1688
|
+
private countComments;
|
|
1689
|
+
/** Records one document and everything under it. */
|
|
1690
|
+
private countDocument;
|
|
1691
|
+
/** Records one node against the running totals, then walks its children. */
|
|
1692
|
+
private countNode;
|
|
1693
|
+
/** Analyze the given YAML files, resolved against the directory. */
|
|
1694
|
+
analyze({ workingDirectory, yamlFiles }: YamlInput): YamlResult;
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
export { }
|