code-gauge 10.2.4 → 10.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +46 -4
- package/dist/allowDirectives.cjs +2 -0
- package/dist/allowDirectives.cjs.map +1 -0
- package/dist/allowDirectives.d.ts +45 -0
- package/dist/allowDirectives.js +2 -0
- package/dist/allowDirectives.js.map +1 -0
- package/dist/checkCommand.cjs +3 -3
- package/dist/checkCommand.cjs.map +1 -1
- package/dist/checkCommand.d.ts +1 -1
- package/dist/checkCommand.js +3 -3
- package/dist/checkCommand.js.map +1 -1
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/crossFileNearMiss.cjs +1 -1
- package/dist/crossFileNearMiss.cjs.map +1 -1
- package/dist/crossFileNearMiss.js +1 -1
- package/dist/crossFileNearMiss.js.map +1 -1
- package/dist/duplicateSelection.cjs.map +1 -1
- package/dist/duplicateSelection.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/exclusion.cjs.map +1 -1
- package/dist/exclusion.js.map +1 -1
- package/dist/git.cjs +2 -1
- package/dist/git.cjs.map +1 -1
- package/dist/git.d.ts +7 -0
- package/dist/git.js +2 -1
- package/dist/git.js.map +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/scan.cjs +1 -1
- package/dist/scan.cjs.map +1 -1
- package/dist/scan.js +1 -1
- package/dist/scan.js.map +1 -1
- package/dist/thresholdCheck.cjs +1 -1
- package/dist/thresholdCheck.cjs.map +1 -1
- package/dist/thresholdCheck.d.ts +15 -1
- package/dist/thresholdCheck.js +1 -1
- package/dist/thresholdCheck.js.map +1 -1
- package/dist/thresholds.cjs.map +1 -1
- package/dist/thresholds.js.map +1 -1
- package/dist/wasmBinding.cjs.map +1 -1
- package/dist/wasmBinding.js.map +1 -1
- package/dist/worker.cjs.map +1 -1
- package/dist/worker.js.map +1 -1
- package/package.json +39 -43
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
[](https://github.com/WillBooster/code-gauge/actions/workflows/test.yml)
|
|
6
6
|
[](https://github.com/WillBooster/code-gauge/actions/workflows/test-rust.yml)
|
|
7
7
|
[](https://github.com/semantic-release/semantic-release)
|
|
8
|
-
[](https://github.com/WillBooster/shared/tree/main/packages/wbfy)
|
|
9
9
|
|
|
10
10
|
A command-line tool that ranks the files of a project by refactoring priority and checks code
|
|
11
11
|
against metric thresholds, built for AI-agent workflows: an agent asked to "refactor this
|
|
@@ -156,7 +156,11 @@ With `--base <ref>`, the check covers only what the working tree changed since t
|
|
|
156
156
|
tail;
|
|
157
157
|
- a file-level threshold, when the change adds or deletes lines of the file (a rename or a mode
|
|
158
158
|
change alone does not count);
|
|
159
|
-
- a duplicated block, when it overlaps lines the change added
|
|
159
|
+
- a duplicated block, when it overlaps lines the change added;
|
|
160
|
+
- the whole file, when the change touches a [directive](#allowing-a-warning-in-the-code): it edits
|
|
161
|
+
a directive's line or the lines between it and its function (through the next code line for a
|
|
162
|
+
directive attached to no function), or deletes a line holding one. Editing the function itself
|
|
163
|
+
only touches the function.
|
|
160
164
|
|
|
161
165
|
`--base` only filters the report: the files measured and the duplication found are those of the
|
|
162
166
|
check without it, so code pasted from an unchanged file is reported where it was pasted. A
|
|
@@ -206,6 +210,39 @@ repository are warnings on stderr.
|
|
|
206
210
|
`check` accepts `--config`, `--include-tests`, `--json`, and the `--duplication-*` options of the
|
|
207
211
|
ranking command.
|
|
208
212
|
|
|
213
|
+
### Allowing a warning in the code
|
|
214
|
+
|
|
215
|
+
When code exceeding a warning limit is deliberately kept, a comment on a line of its own allows the
|
|
216
|
+
value it has, with the reason:
|
|
217
|
+
|
|
218
|
+
```ts
|
|
219
|
+
// code-gauge-allow functionParameterCount 8 -- mirrors the positional API of the vendor SDK
|
|
220
|
+
export function createClient(host, port, user, password, database, timeout, retries, logger) {
|
|
221
|
+
```
|
|
222
|
+
|
|
223
|
+
```py
|
|
224
|
+
# code-gauge-allow-file functionParameterCount 9 -- each benchmark passes its whole setup
|
|
225
|
+
```
|
|
226
|
+
|
|
227
|
+
- `code-gauge-allow <metric> <max> -- <reason>` covers the function starting on the next code
|
|
228
|
+
line, the functions nested in it, and the duplicated blocks starting in it. Only comment lines
|
|
229
|
+
may stand between the directive and that line. Where a function's span starts below its
|
|
230
|
+
decorators or attributes (Python, TypeScript, and Rust), the directive goes below them; where the
|
|
231
|
+
span includes them (Java, Kotlin, and C#), above them. A directive after code on the same line is
|
|
232
|
+
not read.
|
|
233
|
+
- `code-gauge-allow-file <metric> <max> -- <reason>` covers the whole file, and is the only way to
|
|
234
|
+
allow `fileNcss`.
|
|
235
|
+
- `<metric>` is a metric name of the [JSON report](#json-report) (`functionCognitiveComplexity`,
|
|
236
|
+
`duplicateLines`, ...), `<max>` the largest value allowed, and the reason is required.
|
|
237
|
+
|
|
238
|
+
A directive allows warnings only: a value above `<max>` or above an error limit is reported as
|
|
239
|
+
usual, so code that grows worse is reported again, and an error is fixed or its limit changed in the
|
|
240
|
+
[config](#configuration). A copy of a duplicated block is reported in its own place, so each copy
|
|
241
|
+
needs a directive of its own. The report counts the limits directives allowed, and lists as
|
|
242
|
+
warnings, which never fail the check, each directive that is malformed, misplaced, allows an
|
|
243
|
+
error-level value, is outgrown by the value, or allows nothing; with `--base`, only those whose
|
|
244
|
+
line or covered code the change touches.
|
|
245
|
+
|
|
209
246
|
### JSON report
|
|
210
247
|
|
|
211
248
|
`code-gauge check --json` prints:
|
|
@@ -223,7 +260,8 @@ ranking command.
|
|
|
223
260
|
"fileViolationCount": 0,
|
|
224
261
|
"duplicationViolationCount": 1,
|
|
225
262
|
"checkedFileCount": 2,
|
|
226
|
-
"checkedFunctionCount": 2
|
|
263
|
+
"checkedFunctionCount": 2,
|
|
264
|
+
"allowedLimitCount": 0
|
|
227
265
|
},
|
|
228
266
|
"violations": [
|
|
229
267
|
{
|
|
@@ -267,6 +305,7 @@ ranking command.
|
|
|
267
305
|
"partners": [{ "file": "src/report.ts", "startLine": 1, "endLine": 18 }]
|
|
268
306
|
}
|
|
269
307
|
],
|
|
308
|
+
"directiveProblems": [],
|
|
270
309
|
"errors": [],
|
|
271
310
|
"warnings": []
|
|
272
311
|
}
|
|
@@ -276,7 +315,8 @@ ranking command.
|
|
|
276
315
|
- `base`, `mergeBase`: the `--base` ref and the commit compared against; absent without `--base`.
|
|
277
316
|
- `summary`: the violation counts in total, per level, and per kind, and the number of files and
|
|
278
317
|
functions checked: those under the target or, with `--base`, the changed files under it and the
|
|
279
|
-
functions the change touches
|
|
318
|
+
functions the change touches, and `allowedLimitCount`, the exceeded limits that
|
|
319
|
+
[directives](#allowing-a-warning-in-the-code) allowed, which `violations` leaves out.
|
|
280
320
|
- `violations`: the errors first, each level ordered by `file`, then `startLine`. Each has
|
|
281
321
|
- `kind`: `function`, `file`, or `duplication`;
|
|
282
322
|
- `level`: `error` when any of its `exceeded` limits is an error, otherwise `warning`;
|
|
@@ -297,6 +337,8 @@ ranking command.
|
|
|
297
337
|
place repeating only a part of the block (less than `minSimilarityPercent` of its tokens) is
|
|
298
338
|
not a copy of it, and is listed only for a block that no place copies as a whole, its parts
|
|
299
339
|
being repeated in different places.
|
|
340
|
+
- `directiveProblems`: the directives the text report lists as warnings, each with its `file`,
|
|
341
|
+
`line`, and `message`, ordered by `file`, then `line`.
|
|
300
342
|
- `errors`: the files the check covers that could not be measured (exit code 2); unrelated to the
|
|
301
343
|
`error` level of a violation.
|
|
302
344
|
- `warnings`: files measured without cross-file duplication data, and unmeasured files of the
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
"use strict";const e=require("./thresholds.cjs"),t=`code-gauge-allow`,n=`code-gauge-allow-file`,r=`\`${t} <metric> <max> -- <reason>\``,i=/^[\s!#*/]*(code-gauge-allow(?:-file)?)(?![\w-])(.*)$/u,a=/^(\w+)\s+(\d+(?:\.\d+)?)\s+--\s+\S/u,o=new Set(e.fileThresholds.map(t=>e.metricNameOf(t))),s=new Set([...e.functionThresholds,e.duplicationThreshold].map(t=>e.metricNameOf(t)));function c(e,n){let r={directives:[],problems:[],attachmentSpans:[]};if(!e.includes(t))return r;let a=e.split(/\r\n|\n|\r/u),o=new Set(n.codeLineNumbers);for(let e=1;e<=a.length;e++){let t=o.has(e)?void 0:i.exec(a[e-1]??``);if(!t)continue;let[,s=``,c=``]=t,{directive:l,span:d}=u(e,s,c,a,n);r.attachmentSpans.push(d),typeof l==`string`?r.problems.push({line:e,message:l}):r.directives.push({line:e,...l})}return r}function l(e){return i.test(e)}function u(e,t,r,i,a){let o=d(t,r.replace(/\*\/\s*$/u,``).trim());if(typeof o==`string`||t===n)return{directive:o,span:{startLine:e,endLine:e}};let s=a.codeLineNumbers.find(t=>t>e),c=f(o,i.slice(e,(s??i.length+1)-1).some(e=>e.trim()===``)?void 0:s,a.functions);return{directive:c,span:typeof c==`string`?{startLine:e,endLine:s??i.length}:{startLine:e,endLine:c.target.startLine,endsAtFunction:!0}}}function d(e,i){let c=a.exec(i);if(!c)return`malformed directive; expected ${r.replace(t,e)} with a reason`;let[,l=``,u=``]=c,d=e===n?/* @__PURE__ */ new Set([...s,...o]):s;return d.has(l)?{metric:l,max:Number(u)}:o.has(l)?`${l} is measured per file; allow it with ${n}`:`unknown metric ${l}; expected one of ${[...d].join(`, `)}`}function f(e,r,i){let[a]=i.filter(e=>e.startLine===r).toSorted((e,t)=>e.startColumn-t.startColumn);return a?{...e,target:a}:r===void 0?`${t} must directly precede a function; use ${n} for the whole file`:`no function starts on line ${r}, the code after ${t}; put it right above the line a function starts on (below decorators the function's span leaves out), or use ${n}`}exports.isDirectiveLine=l,exports.parseAllowDirectives=c;
|
|
2
|
+
//# sourceMappingURL=allowDirectives.cjs.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowDirectives.cjs","names":["fileThresholds","metricNameOf","functionThresholds","duplicationThreshold","require_thresholds","allowKeyword","allowFileKeyword","syntax","directivePattern","argumentsPattern","fileMetrics","threshold","functionMetrics","parseAllowDirectives","result","content","lines","codeLines","metrics","line","match","keyword","rest","readDirective","span","directive","isDirectiveLine","text","parsed","parseDirective","following","codeLine","attachToFunction","metric","max","allowed","target","functions","fn","next","left","right"],"sources":["../src/allowDirectives.ts"],"sourcesContent":["import { duplicationThreshold, fileThresholds, functionThresholds, metricNameOf } from './thresholds.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** A `code-gauge-allow` or `code-gauge-allow-file` comment: the warnings of a metric it allows. */\nexport interface AllowDirective {\n line: number;\n metric: string;\n /** The largest value of the metric allowed at the warning level. */\n max: number;\n /** The function a `code-gauge-allow` precedes; absent for `code-gauge-allow-file`, which covers the file. */\n target?: FunctionMetrics;\n}\n\n/** A directive that cannot be applied as written, or that allows nothing. */\nexport interface DirectiveProblem {\n line: number;\n message: string;\n}\n\nexport interface AttachmentSpan {\n startLine: number;\n endLine: number;\n endsAtFunction?: true;\n}\n\nexport interface AllowDirectives {\n directives: AllowDirective[];\n problems: DirectiveProblem[];\n /**\n * The lines whose change may change what a directive allows: those from the directive to the\n * first line of the function it attaches to, which `endsAtFunction` marks as the function's own,\n * or, when it attaches to none, through the next code line; a `code-gauge-allow-file` or\n * malformed directive's own line.\n */\n attachmentSpans: AttachmentSpan[];\n}\n\nconst allowKeyword = 'code-gauge-allow';\nconst allowFileKeyword = 'code-gauge-allow-file';\nconst syntax = `\\`${allowKeyword} <metric> <max> -- <reason>\\``;\n\n// The comment delimiters of every supported language (`//`, `///`, `/*`, `*`, `#`) may precede the\n// keyword; any other text before it makes the comment prose mentioning a directive.\nconst directivePattern = /^[\\s!#*/]*(code-gauge-allow(?:-file)?)(?![\\w-])(.*)$/u;\nconst argumentsPattern = /^(\\w+)\\s+(\\d+(?:\\.\\d+)?)\\s+--\\s+\\S/u;\n\nconst fileMetrics = new Set(fileThresholds.map((threshold) => metricNameOf(threshold)));\nconst functionMetrics = new Set(\n [...functionThresholds, duplicationThreshold].map((threshold) => metricNameOf(threshold))\n);\n\n/**\n * The directives of a file, each on a comment-only line: `code-gauge-allow` attaches to the\n * function starting on the next code line, with only comment lines between, and\n * `code-gauge-allow-file` to the whole file. Decorators and attributes are not skipped, since\n * telling where one ends takes a parser; a directive for a function whose span starts after them\n * goes below them. A directive after code on a line is not read, since the line may hold it in a\n * string.\n */\nexport function parseAllowDirectives(content: string, metrics: CodeMetrics): AllowDirectives {\n const result: AllowDirectives = { directives: [], problems: [], attachmentSpans: [] };\n if (!content.includes(allowKeyword)) {\n return result;\n }\n const lines = content.split(/\\r\\n|\\n|\\r/u);\n const codeLines = new Set(metrics.codeLineNumbers);\n for (let line = 1; line <= lines.length; line++) {\n const match = codeLines.has(line) ? undefined : directivePattern.exec(lines[line - 1] ?? '');\n if (!match) continue;\n const [, keyword = '', rest = ''] = match;\n const { directive, span } = readDirective(line, keyword, rest, lines, metrics);\n result.attachmentSpans.push(span);\n if (typeof directive === 'string') {\n result.problems.push({ line, message: directive });\n } else {\n result.directives.push({ line, ...directive });\n }\n }\n return result;\n}\n\n/**\n * Whether the line reads as a directive when comment-only, for a line whose measurement is not at\n * hand, such as one a change deleted.\n */\nexport function isDirectiveLine(text: string): boolean {\n return directivePattern.test(text);\n}\n\n/** The directive on the line or what is wrong with it, and the end of its attachment span. */\nfunction readDirective(\n line: number,\n keyword: string,\n rest: string,\n lines: readonly string[],\n metrics: CodeMetrics\n): { directive: Omit<AllowDirective, 'line'> | string; span: AttachmentSpan } {\n const parsed = parseDirective(keyword, rest.replace(/\\*\\/\\s*$/u, '').trim());\n if (typeof parsed === 'string' || keyword === allowFileKeyword) {\n return { directive: parsed, span: { startLine: line, endLine: line } };\n }\n const following = metrics.codeLineNumbers.find((codeLine) => codeLine > line);\n const isBlankBetween = lines.slice(line, (following ?? lines.length + 1) - 1).some((text) => text.trim() === '');\n const directive = attachToFunction(parsed, isBlankBetween ? undefined : following, metrics.functions);\n // An edit of the code line a directive fails to attach to may be what detached it.\n const span: AttachmentSpan =\n typeof directive === 'string'\n ? { startLine: line, endLine: following ?? lines.length }\n : { startLine: line, endLine: directive.target.startLine, endsAtFunction: true };\n return { directive, span };\n}\n\n/** The metric and max of a directive, or what is wrong with it. */\nfunction parseDirective(keyword: string, rest: string): { metric: string; max: number } | string {\n const match = argumentsPattern.exec(rest);\n if (!match) {\n return `malformed directive; expected ${syntax.replace(allowKeyword, keyword)} with a reason`;\n }\n const [, metric = '', max = ''] = match;\n const allowed = keyword === allowFileKeyword ? new Set([...functionMetrics, ...fileMetrics]) : functionMetrics;\n if (allowed.has(metric)) {\n return { metric, max: Number(max) };\n }\n if (fileMetrics.has(metric)) {\n return `${metric} is measured per file; allow it with ${allowFileKeyword}`;\n }\n return `unknown metric ${metric}; expected one of ${[...allowed].join(', ')}`;\n}\n\n/** The parsed directive with the outermost function starting on `next`, or what is wrong. */\nfunction attachToFunction(\n parsed: { metric: string; max: number },\n next: number | undefined,\n functions: readonly FunctionMetrics[]\n): { metric: string; max: number; target: FunctionMetrics } | string {\n const [target] = functions\n .filter((fn) => fn.startLine === next)\n .toSorted((left, right) => left.startColumn - right.startColumn);\n if (target) {\n return { ...parsed, target };\n }\n return next === undefined\n ? `${allowKeyword} must directly precede a function; use ${allowFileKeyword} for the whole file`\n : `no function starts on line ${next}, the code after ${allowKeyword}; put it right above the line a function starts on (below decorators the function's span leaves out), or use ${allowFileKeyword}`;\n}\n"],"mappings":"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"}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import type { CodeMetrics, FunctionMetrics } from './types.js';
|
|
2
|
+
/** A `code-gauge-allow` or `code-gauge-allow-file` comment: the warnings of a metric it allows. */
|
|
3
|
+
export interface AllowDirective {
|
|
4
|
+
line: number;
|
|
5
|
+
metric: string;
|
|
6
|
+
/** The largest value of the metric allowed at the warning level. */
|
|
7
|
+
max: number;
|
|
8
|
+
/** The function a `code-gauge-allow` precedes; absent for `code-gauge-allow-file`, which covers the file. */
|
|
9
|
+
target?: FunctionMetrics;
|
|
10
|
+
}
|
|
11
|
+
/** A directive that cannot be applied as written, or that allows nothing. */
|
|
12
|
+
export interface DirectiveProblem {
|
|
13
|
+
line: number;
|
|
14
|
+
message: string;
|
|
15
|
+
}
|
|
16
|
+
export interface AttachmentSpan {
|
|
17
|
+
startLine: number;
|
|
18
|
+
endLine: number;
|
|
19
|
+
endsAtFunction?: true;
|
|
20
|
+
}
|
|
21
|
+
export interface AllowDirectives {
|
|
22
|
+
directives: AllowDirective[];
|
|
23
|
+
problems: DirectiveProblem[];
|
|
24
|
+
/**
|
|
25
|
+
* The lines whose change may change what a directive allows: those from the directive to the
|
|
26
|
+
* first line of the function it attaches to, which `endsAtFunction` marks as the function's own,
|
|
27
|
+
* or, when it attaches to none, through the next code line; a `code-gauge-allow-file` or
|
|
28
|
+
* malformed directive's own line.
|
|
29
|
+
*/
|
|
30
|
+
attachmentSpans: AttachmentSpan[];
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* The directives of a file, each on a comment-only line: `code-gauge-allow` attaches to the
|
|
34
|
+
* function starting on the next code line, with only comment lines between, and
|
|
35
|
+
* `code-gauge-allow-file` to the whole file. Decorators and attributes are not skipped, since
|
|
36
|
+
* telling where one ends takes a parser; a directive for a function whose span starts after them
|
|
37
|
+
* goes below them. A directive after code on a line is not read, since the line may hold it in a
|
|
38
|
+
* string.
|
|
39
|
+
*/
|
|
40
|
+
export declare function parseAllowDirectives(content: string, metrics: CodeMetrics): AllowDirectives;
|
|
41
|
+
/**
|
|
42
|
+
* Whether the line reads as a directive when comment-only, for a line whose measurement is not at
|
|
43
|
+
* hand, such as one a change deleted.
|
|
44
|
+
*/
|
|
45
|
+
export declare function isDirectiveLine(text: string): boolean;
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{duplicationThreshold as e,fileThresholds as t,functionThresholds as n,metricNameOf as r}from"./thresholds.js";const i=`code-gauge-allow`,a=`code-gauge-allow-file`,o=`\`${i} <metric> <max> -- <reason>\``,s=/^[\s!#*/]*(code-gauge-allow(?:-file)?)(?![\w-])(.*)$/u,c=/^(\w+)\s+(\d+(?:\.\d+)?)\s+--\s+\S/u,l=new Set(t.map(e=>r(e))),u=new Set([...n,e].map(e=>r(e)));function d(e,t){let n={directives:[],problems:[],attachmentSpans:[]};if(!e.includes(i))return n;let r=e.split(/\r\n|\n|\r/u),a=new Set(t.codeLineNumbers);for(let e=1;e<=r.length;e++){let i=a.has(e)?void 0:s.exec(r[e-1]??``);if(!i)continue;let[,o=``,c=``]=i,{directive:l,span:u}=p(e,o,c,r,t);n.attachmentSpans.push(u),typeof l==`string`?n.problems.push({line:e,message:l}):n.directives.push({line:e,...l})}return n}function f(e){return s.test(e)}function p(e,t,n,r,i){let o=m(t,n.replace(/\*\/\s*$/u,``).trim());if(typeof o==`string`||t===a)return{directive:o,span:{startLine:e,endLine:e}};let s=i.codeLineNumbers.find(t=>t>e),c=h(o,r.slice(e,(s??r.length+1)-1).some(e=>e.trim()===``)?void 0:s,i.functions);return{directive:c,span:typeof c==`string`?{startLine:e,endLine:s??r.length}:{startLine:e,endLine:c.target.startLine,endsAtFunction:!0}}}function m(e,t){let n=c.exec(t);if(!n)return`malformed directive; expected ${o.replace(i,e)} with a reason`;let[,r=``,s=``]=n,d=e===a?/* @__PURE__ */ new Set([...u,...l]):u;return d.has(r)?{metric:r,max:Number(s)}:l.has(r)?`${r} is measured per file; allow it with ${a}`:`unknown metric ${r}; expected one of ${[...d].join(`, `)}`}function h(e,t,n){let[r]=n.filter(e=>e.startLine===t).toSorted((e,t)=>e.startColumn-t.startColumn);return r?{...e,target:r}:t===void 0?`${i} must directly precede a function; use ${a} for the whole file`:`no function starts on line ${t}, the code after ${i}; put it right above the line a function starts on (below decorators the function's span leaves out), or use ${a}`}export{f as isDirectiveLine,d as parseAllowDirectives};
|
|
2
|
+
//# sourceMappingURL=allowDirectives.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"allowDirectives.js","names":["allowKeyword","allowFileKeyword","syntax","directivePattern","argumentsPattern","fileMetrics","fileThresholds","metricNameOf","threshold","functionMetrics","functionThresholds","duplicationThreshold","parseAllowDirectives","result","content","lines","codeLines","metrics","line","match","keyword","rest","readDirective","span","directive","isDirectiveLine","text","parsed","parseDirective","following","codeLine","attachToFunction","metric","max","allowed","target","functions","fn","next","left","right"],"sources":["../src/allowDirectives.ts"],"sourcesContent":["import { duplicationThreshold, fileThresholds, functionThresholds, metricNameOf } from './thresholds.js';\nimport type { CodeMetrics, FunctionMetrics } from './types.js';\n\n/** A `code-gauge-allow` or `code-gauge-allow-file` comment: the warnings of a metric it allows. */\nexport interface AllowDirective {\n line: number;\n metric: string;\n /** The largest value of the metric allowed at the warning level. */\n max: number;\n /** The function a `code-gauge-allow` precedes; absent for `code-gauge-allow-file`, which covers the file. */\n target?: FunctionMetrics;\n}\n\n/** A directive that cannot be applied as written, or that allows nothing. */\nexport interface DirectiveProblem {\n line: number;\n message: string;\n}\n\nexport interface AttachmentSpan {\n startLine: number;\n endLine: number;\n endsAtFunction?: true;\n}\n\nexport interface AllowDirectives {\n directives: AllowDirective[];\n problems: DirectiveProblem[];\n /**\n * The lines whose change may change what a directive allows: those from the directive to the\n * first line of the function it attaches to, which `endsAtFunction` marks as the function's own,\n * or, when it attaches to none, through the next code line; a `code-gauge-allow-file` or\n * malformed directive's own line.\n */\n attachmentSpans: AttachmentSpan[];\n}\n\nconst allowKeyword = 'code-gauge-allow';\nconst allowFileKeyword = 'code-gauge-allow-file';\nconst syntax = `\\`${allowKeyword} <metric> <max> -- <reason>\\``;\n\n// The comment delimiters of every supported language (`//`, `///`, `/*`, `*`, `#`) may precede the\n// keyword; any other text before it makes the comment prose mentioning a directive.\nconst directivePattern = /^[\\s!#*/]*(code-gauge-allow(?:-file)?)(?![\\w-])(.*)$/u;\nconst argumentsPattern = /^(\\w+)\\s+(\\d+(?:\\.\\d+)?)\\s+--\\s+\\S/u;\n\nconst fileMetrics = new Set(fileThresholds.map((threshold) => metricNameOf(threshold)));\nconst functionMetrics = new Set(\n [...functionThresholds, duplicationThreshold].map((threshold) => metricNameOf(threshold))\n);\n\n/**\n * The directives of a file, each on a comment-only line: `code-gauge-allow` attaches to the\n * function starting on the next code line, with only comment lines between, and\n * `code-gauge-allow-file` to the whole file. Decorators and attributes are not skipped, since\n * telling where one ends takes a parser; a directive for a function whose span starts after them\n * goes below them. A directive after code on a line is not read, since the line may hold it in a\n * string.\n */\nexport function parseAllowDirectives(content: string, metrics: CodeMetrics): AllowDirectives {\n const result: AllowDirectives = { directives: [], problems: [], attachmentSpans: [] };\n if (!content.includes(allowKeyword)) {\n return result;\n }\n const lines = content.split(/\\r\\n|\\n|\\r/u);\n const codeLines = new Set(metrics.codeLineNumbers);\n for (let line = 1; line <= lines.length; line++) {\n const match = codeLines.has(line) ? undefined : directivePattern.exec(lines[line - 1] ?? '');\n if (!match) continue;\n const [, keyword = '', rest = ''] = match;\n const { directive, span } = readDirective(line, keyword, rest, lines, metrics);\n result.attachmentSpans.push(span);\n if (typeof directive === 'string') {\n result.problems.push({ line, message: directive });\n } else {\n result.directives.push({ line, ...directive });\n }\n }\n return result;\n}\n\n/**\n * Whether the line reads as a directive when comment-only, for a line whose measurement is not at\n * hand, such as one a change deleted.\n */\nexport function isDirectiveLine(text: string): boolean {\n return directivePattern.test(text);\n}\n\n/** The directive on the line or what is wrong with it, and the end of its attachment span. */\nfunction readDirective(\n line: number,\n keyword: string,\n rest: string,\n lines: readonly string[],\n metrics: CodeMetrics\n): { directive: Omit<AllowDirective, 'line'> | string; span: AttachmentSpan } {\n const parsed = parseDirective(keyword, rest.replace(/\\*\\/\\s*$/u, '').trim());\n if (typeof parsed === 'string' || keyword === allowFileKeyword) {\n return { directive: parsed, span: { startLine: line, endLine: line } };\n }\n const following = metrics.codeLineNumbers.find((codeLine) => codeLine > line);\n const isBlankBetween = lines.slice(line, (following ?? lines.length + 1) - 1).some((text) => text.trim() === '');\n const directive = attachToFunction(parsed, isBlankBetween ? undefined : following, metrics.functions);\n // An edit of the code line a directive fails to attach to may be what detached it.\n const span: AttachmentSpan =\n typeof directive === 'string'\n ? { startLine: line, endLine: following ?? lines.length }\n : { startLine: line, endLine: directive.target.startLine, endsAtFunction: true };\n return { directive, span };\n}\n\n/** The metric and max of a directive, or what is wrong with it. */\nfunction parseDirective(keyword: string, rest: string): { metric: string; max: number } | string {\n const match = argumentsPattern.exec(rest);\n if (!match) {\n return `malformed directive; expected ${syntax.replace(allowKeyword, keyword)} with a reason`;\n }\n const [, metric = '', max = ''] = match;\n const allowed = keyword === allowFileKeyword ? new Set([...functionMetrics, ...fileMetrics]) : functionMetrics;\n if (allowed.has(metric)) {\n return { metric, max: Number(max) };\n }\n if (fileMetrics.has(metric)) {\n return `${metric} is measured per file; allow it with ${allowFileKeyword}`;\n }\n return `unknown metric ${metric}; expected one of ${[...allowed].join(', ')}`;\n}\n\n/** The parsed directive with the outermost function starting on `next`, or what is wrong. */\nfunction attachToFunction(\n parsed: { metric: string; max: number },\n next: number | undefined,\n functions: readonly FunctionMetrics[]\n): { metric: string; max: number; target: FunctionMetrics } | string {\n const [target] = functions\n .filter((fn) => fn.startLine === next)\n .toSorted((left, right) => left.startColumn - right.startColumn);\n if (target) {\n return { ...parsed, target };\n }\n return next === undefined\n ? `${allowKeyword} must directly precede a function; use ${allowFileKeyword} for the whole file`\n : `no function starts on line ${next}, the code after ${allowKeyword}; put it right above the line a function starts on (below decorators the function's span leaves out), or use ${allowFileKeyword}`;\n}\n"],"mappings":"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"}
|
package/dist/checkCommand.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./
|
|
1
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./metrics.cjs"),n=require("./thresholds.cjs"),r=require("./allowDirectives.cjs"),i=require("./cliConfig.cjs"),a=require("./git.cjs"),o=require("./exclusion.cjs"),s=require("./scan.cjs"),c=require("./thresholdCheck.cjs");let l=require("@willbooster/shared-lib"),u=require("node:fs/promises"),d=require("node:os");d=e.__toESM(d,1);let f=require("node:path");f=e.__toESM(f,1);async function p(e,t,r){try{let a=s.resolveTarget(e),o=await i.loadConfig(t.config,await s.configSearchDirectory(a)),l=i.resolveOptions(t,o),u=await _(a,t.base,l),d=await m(u.files,u.root),f=c.checkThresholds(d,u.duplication,n.resolveLimits(r,o.config.thresholds));await g(f.violations,u.root),l.json?I(t,u,f):E(t,u,f),u.errors.length>0?process.exitCode=2:h(f.violations)&&(process.exitCode=1)}catch(e){s.writeStderr(`Error: ${(0,l.getErrorMessage)(e)}\n`),process.exitCode=2}}async function m(e,t){return await(0,l.mapConcurrently)(e,d.default.availableParallelism()*2,async e=>({...e,allowDirectives:r.parseAllowDirectives(await(0,u.readFile)(f.default.join(t,e.file),`utf8`),e.metrics)}))}function h(e){return e.some(({level:e})=>e===`error`)}async function g(e,t){for(let n of e.filter(({kind:e})=>e===`file`)){let e=await(0,u.readFile)(f.default.join(t,n.file),`utf8`);/[\n\r]$/u.test(e)&&(n.endLine=Math.max(n.endLine-1,1))}}async function _(e,t,n){let r=await(0,u.realpath)(e),i=(await(0,u.stat)(r)).isFile()?r:void 0;if(i&&!s.getLanguage(i,n,!0))throw Error(`${f.default.basename(i)}: unsupported file type`);let o;try{o=await(0,u.realpath)(await a.resolveRepoRoot(i?f.default.dirname(r):r))}catch(e){if(t!==void 0)throw e;return await v(r,n)}return await y({repoRoot:o,canonicalTarget:r,targetFile:i},t,n)}async function v(e,t){let n=await s.configSearchDirectory(e),r=await s.scanTarget(e,{...t,loadExclusion:e=>o.loadExclusion(n,t.exclude,e)});return{files:r.files.map(({file:e,metrics:t})=>({file:s.formatPath(e,r.displayRoot),metrics:t})),duplication:T(r,t),errors:r.errors,warnings:r.warnings,root:r.displayRoot}}async function y({repoRoot:e,canonicalTarget:t,targetFile:n},r,i){if(n===void 0&&t!==e&&await a.isIgnored(e,t))throw Error(`${s.formatPath(t,e)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`);let c=await a.listRepositoryFiles(e),l=new Set(n===void 0?[]:[n]),u=n!==void 0&&!c.has(s.formatPath(n,e))?s.formatPath(n,e):void 0;u!==void 0&&c.add(u);let d=o.keepPaths(await o.loadRepositoryExclusion(e,c,i.exclude),l),p=s.formatPath(t,e),m=await s.scanListedFiles(e,c,{...i,loadExclusion:()=>Promise.resolve(d)},l,n===void 0&&t!==e?p:``),h=T(m,i),g=n=>s.isWithinDirectory(f.default.join(e,n),t),_=m.files.map(({file:t,metrics:n})=>({file:s.formatPath(t,e),metrics:n})),v=_.filter(({file:e})=>g(e)),y=n=>t===e||n.startsWith(`${p}/`)||n.startsWith(`${p}:`),S;if(r!==void 0){S=await a.resolveMergeBase(e,r);let t=await a.listChangedFiles(e,S);u!==void 0&&t.push({status:`added`,headPath:u});let n=t.filter(({status:e,headPath:t})=>e!==`deleted`&&g(t));v=await x(_,n,e,S),y=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:v,duplication:h,...b(m,y),root:e,mergeBase:S}}function b(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function x(e,t,n,r){let i=new Map(e.map(e=>[e.file,e]));return(await(0,l.mapConcurrently)(t,d.default.availableParallelism()*2,async e=>{let t=i.get(e.headPath);if(t===void 0)return[];let{metrics:o}=t,s={headStart:1,headCount:o.lines.total},c=e.basePath===void 0?[s]:await a.listLineHunks(n,r,e.basePath,e.headPath);return e.basePath!==void 0&&await S(c,o.language,()=>a.readFileAt(n,r,e.basePath??``)),o.language===`python`&&c.some(({headCount:e})=>e===0)&&C(c,await(0,u.readFile)(f.default.join(n,e.headPath),`utf8`)),[{...t,hunks:c}]})).flat()}async function S(e,n,i){let a=e.filter(({deletedLines:e})=>e?.some(e=>r.isDirectiveLine(e)));if(a.length===0)return;let o=new Set(t.measureCode(await i(),{language:n}).codeLineNumbers);for(let e of a)e.deletesDirective=(e.deletedLines??[]).some((t,n)=>r.isDirectiveLine(t)&&!o.has((e.baseStart??0)+n))}function C(e,t){let n=t.split(/\r\n|\n|\r/u);for(let t of e)if(!(t.headCount>0))for(;t.headStart>0&&w(n[t.headStart-1]??``);)--t.headStart}function w(e){return/^\s*(?:#.*)?$/u.test(e)&&!r.isDirectiveLine(e)}function T(e,t){if(e.fatalError)throw Error(e.fatalError);return s.addCrossFileDuplication(e,t),{crossFile:e.crossFileDuplication,minSimilarityPercent:t.duplication.minSimilarityPercent}}function E(e,t,r){let{violations:i}=r,a=r.allowedLimitCount>0?`, ${r.allowedLimitCount} exceeded limits allowed by directives`:``,o=`${t.files.length} ${e.base===void 0?``:`changed `}files, ${r.checkedFunctionCount} functions checked${a}${O(e,t)}`;t.errors.length>0?s.writeStdout(`Check could not complete: ${t.errors.length} measurement failures (details on stderr); ${k(i)} in the measured files (${o}).\n`):i.length===0?s.writeStdout(`No threshold violations: ${o}.\n`):s.writeStdout(`Threshold violations: ${k(i)} (${o}).\n`);for(let e of i)s.writeStdout(`${j(e)}\n`);for(let e of r.directiveProblems)s.writeStdout(`${D(e)}\n`);let c=new Set(i.flatMap(({exceeded:e})=>e.map(({metric:e})=>e))),l=n.thresholds.filter(e=>c.has(n.metricNameOf(e)));if(l.length>0){s.writeStdout(`
|
|
2
2
|
How to fix:
|
|
3
|
-
`);for(let{label:e,hint:t}of
|
|
4
|
-
`)}exports.runCheckCommand=
|
|
3
|
+
`);for(let{label:e,hint:t}of l)s.writeStdout(`- ${e}: ${t}\n`)}for(let e of t.warnings)s.writeStderr(`Warning: ${e}\n`);for(let e of t.errors)s.writeStderr(`Error: ${e}\n`)}function D({file:e,line:t,message:n}){return`warning: ${e}:${t}: ${n}`}function O(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function k(e){let t=A(e);return`${t.errorViolationCount} errors, ${t.warningViolationCount} warnings (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function A(e){let t=t=>e.filter(e=>e.kind===t).length,n=e.filter(({level:e})=>e===`error`).length;return{violationCount:e.length,errorViolationCount:n,warningViolationCount:e.length-n,functionViolationCount:t(`function`),fileViolationCount:t(`file`),duplicationViolationCount:t(`duplication`)}}function j(e){let t=e.exceeded.map(({metric:t,label:n,value:r,level:i,limit:a})=>`${n} ${Math.ceil(r*10)/10} (${i===e.level?``:`${i} `}max ${e.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a}${t===`functionCognitiveComplexity`?M(e):``})`).join(`, `);return`${e.level}: ${N(e,t)}`}function M({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${P(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function N(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${F(e)} ${P(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>F(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${F(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function P(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function F({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function I(e,t,n){s.writeStdout(JSON.stringify({passed:!h(n.violations)&&t.errors.length===0,base:e.base,mergeBase:t.mergeBase,summary:{...A(n.violations),checkedFileCount:t.files.length,checkedFunctionCount:n.checkedFunctionCount,allowedLimitCount:n.allowedLimitCount},violations:n.violations,directiveProblems:n.directiveProblems,errors:t.errors,warnings:t.warnings},void 0,2)+`
|
|
4
|
+
`)}exports.runCheckCommand=p,exports.scanCheckScope=_;
|
|
5
5
|
//# sourceMappingURL=checkCommand.cjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"checkCommand.cjs","names":["resolveTarget","loadConfig","configSearchDirectory","resolveOptions","checkThresholds","resolveLimits","getErrorMessage","readFile","path","realpath","stat","getLanguage","resolveRepoRoot","scanTarget","loadExclusion","formatPath","isIgnored","listRepositoryFiles","keepPaths","loadRepositoryExclusion","scanListedFiles","isWithinDirectory","resolveMergeBase","listChangedFiles","mapConcurrently","os","listLineHunks","thresholds","metricNameOf","require_runtime","require_thresholds","require_cliConfig","require_git","require_exclusion","require_scan","require_thresholdCheck","_willbooster_shared_lib","node_fs_promises","node_path","node_os","runCheckCommand","resolvedTarget","target","config","cliOptions","options","scope","scanCheckScope","result","cliLimits","endFileViolationsAtLastLine","printJsonReport","printTextReport","hasErrors","error","violations","level","violation","kind","content","root","canonicalTarget","targetFile","repoRoot","base","scanDirectoryWalk","scanRepository","searchDirectory","scan","absolutePaths","file","measureDuplication","repositoryFiles","explicitFiles","unlistedTarget","exclusion","targetPath","duplication","isInTarget","relativePath","measuredFiles","files","isCovered","mergeBase","changedFiles","changesInTarget","status","headPath","attachHunks","partitionErrors","measuredByPath","measured","changed","wholeFile","metrics","hunks","headCount","anchorDeletionsToCode","lines","hunk","checked","describeBase","describeCounts","formatViolation","violatedMetrics","exceeded","metric","violatedThresholds","threshold","label","hint","warning","counts","countViolations","count","errorViolationCount","value","limit","describeLargestBlocks","describeViolation","blocks","largestBlocks","startLine","endLine","name","toOneLine","cognitiveComplexity","formatLocation","partners","partner","listed"],"sources":["../src/checkCommand.ts"],"sourcesContent":["import { readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\nimport { loadConfig, resolveOptions, type CliOptions, type ResolvedOptions } from './cliConfig.js';\nimport { keepPaths, loadExclusion, loadRepositoryExclusion } from './exclusion.js';\nimport {\n isIgnored,\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n resolveMergeBase,\n resolveRepoRoot,\n type ChangedFile,\n type LineHunk,\n} from './git.js';\nimport {\n addCrossFileDuplication,\n configSearchDirectory,\n formatPath,\n getLanguage,\n isWithinDirectory,\n resolveTarget,\n scanListedFiles,\n scanTarget,\n writeStderr,\n writeStdout,\n type ScanResult,\n} from './scan.js';\nimport {\n checkThresholds,\n type BlockLocation,\n type CheckedFile,\n type CheckedDuplication,\n type CheckResult,\n type Violation,\n} from './thresholdCheck.js';\nimport { metricNameOf, resolveLimits, thresholds, type LimitsByLevel } from './thresholds.js';\n\n/** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */\nexport interface CheckCliOptions extends CliOptions {\n base?: string;\n}\n\n/** The measured files a check covers and the duplication they take part in. */\nexport interface CheckScope {\n files: CheckedFile[];\n duplication: CheckedDuplication;\n /** Measurement failures of files the check covers. */\n errors: string[];\n warnings: string[];\n /** The directory the files' paths are relative to. */\n root: string;\n mergeBase?: string;\n}\n\n/** Caps how many partner locations a duplicated block lists so the report stays scannable. */\nconst maxListedPartners = 3;\n\n/**\n * Reports every exceeded threshold of the target or, with `base`, of what the working tree changed\n * under it since the merge-base with that ref. Exit codes: 0 no error-level violations, 1 error-level\n * violations, 2 files the check covers could not be measured.\n */\nexport async function runCheckCommand(\n target: string,\n cliOptions: CheckCliOptions,\n cliLimits: LimitsByLevel\n): Promise<void> {\n try {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const scope = await scanCheckScope(resolvedTarget, cliOptions.base, options);\n const result = checkThresholds(scope.files, scope.duplication, resolveLimits(cliLimits, config.config.thresholds));\n await endFileViolationsAtLastLine(result.violations, scope.root);\n\n if (options.json) {\n printJsonReport(cliOptions, scope, result);\n } else {\n printTextReport(cliOptions, scope, result);\n }\n if (scope.errors.length > 0) {\n process.exitCode = 2;\n } else if (hasErrors(result.violations)) {\n process.exitCode = 1;\n }\n } catch (error) {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nfunction hasErrors(violations: readonly Violation[]): boolean {\n return violations.some(({ level }) => level === 'error');\n}\n\n/**\n * A file's measured line count includes the empty line after a final line terminator, which is\n * not a line of the file, so file-level violations end at the last line that exists.\n */\nasync function endFileViolationsAtLastLine(violations: readonly Violation[], root: string): Promise<void> {\n for (const violation of violations.filter(({ kind }) => kind === 'file')) {\n const content = await readFile(path.join(root, violation.file), 'utf8');\n if (/[\\n\\r]$/u.test(content)) {\n violation.endLine = Math.max(violation.endLine - 1, 1);\n }\n }\n}\n\n/**\n * Inside a git repository, the scope is the repository's git-visible files under the target, and\n * duplication is detected against all of them; `base` only narrows the scope to what changed.\n * Outside a repository, where there is no change to narrow to, the target directory is walked.\n */\nexport async function scanCheckScope(\n resolvedTarget: string,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n const canonicalTarget = await realpath(resolvedTarget);\n const targetStat = await stat(canonicalTarget);\n const targetFile = targetStat.isFile() ? canonicalTarget : undefined;\n if (targetFile && !getLanguage(targetFile, options, true)) {\n throw new Error(`${path.basename(targetFile)}: unsupported file type`);\n }\n let repoRoot;\n try {\n repoRoot = await realpath(await resolveRepoRoot(targetFile ? path.dirname(canonicalTarget) : canonicalTarget));\n } catch (error) {\n if (base !== undefined) {\n throw error;\n }\n return await scanDirectoryWalk(canonicalTarget, options);\n }\n return await scanRepository({ repoRoot, canonicalTarget, targetFile }, base, options);\n}\n\nasync function scanDirectoryWalk(canonicalTarget: string, options: ResolvedOptions): Promise<CheckScope> {\n const searchDirectory = await configSearchDirectory(canonicalTarget);\n const scan = await scanTarget(canonicalTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n duplication: measureDuplication(scan, options),\n errors: scan.errors,\n warnings: scan.warnings,\n root: scan.displayRoot,\n };\n}\n\ninterface RepositoryTarget {\n repoRoot: string;\n canonicalTarget: string;\n /** Set when the target is a file, which is measured even when git ignores it or it is excluded. */\n targetFile?: string;\n}\n\n/**\n * Measures every git-visible file (tracked or untracked non-ignored) of the repository, so that\n * code copied from outside the target is detected as duplication, and keeps the files under the\n * target: all of them or, with `base`, the changed ones with their line hunks.\n */\nasync function scanRepository(\n { repoRoot, canonicalTarget, targetFile }: RepositoryTarget,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n // Only git-visible files are measured, so a directory git ignores would pass as a check of no\n // files. A directory git lists but whose sources it ignores is not told apart from a repository\n // or package whose only sources are ignored build output, which must pass.\n if (targetFile === undefined && canonicalTarget !== repoRoot && (await isIgnored(repoRoot, canonicalTarget))) {\n throw new Error(\n `${formatPath(canonicalTarget, repoRoot)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`\n );\n }\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const explicitFiles = new Set(targetFile === undefined ? [] : [targetFile]);\n const unlistedTarget =\n targetFile !== undefined && !repositoryFiles.has(formatPath(targetFile, repoRoot))\n ? formatPath(targetFile, repoRoot)\n : undefined;\n if (unlistedTarget !== undefined) {\n repositoryFiles.add(unlistedTarget);\n }\n const exclusion = keepPaths(await loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude), explicitFiles);\n // Scan errors start with the file or directory they concern.\n const targetPath = formatPath(canonicalTarget, repoRoot);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(exclusion) },\n explicitFiles,\n targetFile === undefined && canonicalTarget !== repoRoot ? targetPath : ''\n );\n const duplication = measureDuplication(scan, options);\n const isInTarget = (relativePath: string): boolean =>\n isWithinDirectory(path.join(repoRoot, relativePath), canonicalTarget);\n const measuredFiles = scan.files.map(({ file, metrics }) => ({ file: formatPath(file, repoRoot), metrics }));\n let files = measuredFiles.filter(({ file }) => isInTarget(file));\n let isCovered = (error: string): boolean =>\n canonicalTarget === repoRoot || error.startsWith(`${targetPath}/`) || error.startsWith(`${targetPath}:`);\n let mergeBase;\n if (base !== undefined) {\n mergeBase = await resolveMergeBase(repoRoot, base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n // A targeted file git does not list has no diff; all of it counts as added.\n if (unlistedTarget !== undefined) {\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const changesInTarget = changedFiles.filter(({ status, headPath }) => status !== 'deleted' && isInTarget(headPath));\n files = await attachHunks(measuredFiles, changesInTarget, repoRoot, mergeBase);\n isCovered = (error) => changesInTarget.some(({ headPath }) => error.startsWith(`${headPath}:`));\n }\n return {\n files,\n duplication,\n ...partitionErrors(scan, isCovered),\n root: repoRoot,\n mergeBase,\n };\n}\n\n/** Only a failure on a file the check covers leaves it incomplete; the others are warnings. */\nfunction partitionErrors(\n scan: ScanResult,\n isCovered: (error: string) => boolean\n): Pick<CheckScope, 'errors' | 'warnings'> {\n return {\n errors: scan.errors.filter((error) => isCovered(error)),\n warnings: [...scan.warnings, ...scan.errors.filter((error) => !isCovered(error))],\n };\n}\n\n/** The measured changed files with their line hunks against the merge-base. */\nasync function attachHunks(\n measuredFiles: readonly CheckedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n mergeBase: string\n): Promise<CheckedFile[]> {\n const measuredByPath = new Map(measuredFiles.map((measured) => [measured.file, measured]));\n const files = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const measured = measuredByPath.get(changed.headPath);\n // A changed file the scan left out (unsupported, excluded, or failed) has nothing to check.\n if (measured === undefined) {\n return [];\n }\n const { metrics } = measured;\n const wholeFile: LineHunk = { headStart: 1, headCount: metrics.lines.total };\n const hunks =\n changed.basePath === undefined\n ? [wholeFile]\n : await listLineHunks(repoRoot, mergeBase, changed.basePath, changed.headPath);\n if (metrics.language === 'python' && hunks.some(({ headCount }) => headCount === 0)) {\n anchorDeletionsToCode(hunks, await readFile(path.join(repoRoot, changed.headPath), 'utf8'));\n }\n return [{ ...measured, hunks }];\n });\n return files.flat();\n}\n\n/**\n * Moves each pure deletion up to the last code line before it. A Python function ends at its last\n * statement, so the blank and comment-only lines left above a deleted tail lie outside the span the\n * deletion shortened; anchored to that statement, the deletion counts as touching the function.\n */\nfunction anchorDeletionsToCode(hunks: LineHunk[], content: string): void {\n const lines = content.split(/\\r\\n|\\n|\\r/u);\n for (const hunk of hunks) {\n if (hunk.headCount > 0) continue;\n while (hunk.headStart > 0 && /^\\s*(?:#.*)?$/u.test(lines[hunk.headStart - 1] ?? '')) {\n hunk.headStart -= 1;\n }\n }\n}\n\n/** A run-wide failure (a missing target or native addon) leaves nothing to check. */\nfunction measureDuplication(scan: ScanResult, options: ResolvedOptions): CheckedDuplication {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return {\n crossFile: scan.crossFileDuplication,\n minSimilarityPercent: options.duplication.minSimilarityPercent,\n };\n}\n\nfunction printTextReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n const { violations } = result;\n const checked = `${scope.files.length} ${cliOptions.base === undefined ? '' : 'changed '}files, ${result.checkedFunctionCount} functions checked${describeBase(cliOptions, scope)}`;\n if (scope.errors.length > 0) {\n // Unmeasured files were not checked, so \"no violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Check could not complete: ${scope.errors.length} measurement failures (details on stderr); ${describeCounts(violations)} in the measured files (${checked}).\\n`\n );\n } else if (violations.length === 0) {\n writeStdout(`No threshold violations: ${checked}.\\n`);\n } else {\n writeStdout(`Threshold violations: ${describeCounts(violations)} (${checked}).\\n`);\n }\n for (const violation of violations) {\n writeStdout(`${formatViolation(violation)}\\n`);\n }\n\n const violatedMetrics = new Set(violations.flatMap(({ exceeded }) => exceeded.map(({ metric }) => metric)));\n const violatedThresholds = thresholds.filter((threshold) => violatedMetrics.has(metricNameOf(threshold)));\n if (violatedThresholds.length > 0) {\n writeStdout('\\nHow to fix:\\n');\n for (const { label, hint } of violatedThresholds) {\n writeStdout(`- ${label}: ${hint}\\n`);\n }\n }\n\n for (const warning of scope.warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of scope.errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction describeBase(cliOptions: CheckCliOptions, scope: CheckScope): string {\n return scope.mergeBase === undefined ? '' : `; base ${cliOptions.base}, merge-base ${scope.mergeBase.slice(0, 12)}`;\n}\n\nfunction describeCounts(violations: readonly Violation[]): string {\n const counts = countViolations(violations);\n return `${counts.errorViolationCount} errors, ${counts.warningViolationCount} warnings (${counts.functionViolationCount} functions, ${counts.fileViolationCount} files, ${counts.duplicationViolationCount} duplicated blocks)`;\n}\n\nfunction countViolations(violations: readonly Violation[]): {\n violationCount: number;\n errorViolationCount: number;\n warningViolationCount: number;\n functionViolationCount: number;\n fileViolationCount: number;\n duplicationViolationCount: number;\n} {\n const count = (kind: Violation['kind']): number => violations.filter((violation) => violation.kind === kind).length;\n const errorViolationCount = violations.filter(({ level }) => level === 'error').length;\n return {\n violationCount: violations.length,\n errorViolationCount,\n warningViolationCount: violations.length - errorViolationCount,\n functionViolationCount: count('function'),\n fileViolationCount: count('file'),\n duplicationViolationCount: count('duplication'),\n };\n}\n\n/** One violation as a single line: its level, the location, then every limit it exceeds. */\nfunction formatViolation(violation: Violation): string {\n const exceeded = violation.exceeded\n .map(({ metric, label, value, level, limit }) => {\n // Halstead values are fractional; rounding up keeps a violating value above the printed maximum.\n const roundedValue = Math.ceil(value * 10) / 10;\n const levelPrefix = level === violation.level ? '' : `${level} `;\n // A duplicated block violates from its limit on, so the largest allowed code-line count is\n // the last whole number below it; a limit of 0 allows none.\n const maxAllowed = violation.kind === 'duplication' ? Math.max(Math.ceil(limit) - 1, 0) : limit;\n const blocks = metric === 'functionCognitiveComplexity' ? describeLargestBlocks(violation) : '';\n return `${label} ${roundedValue} (${levelPrefix}max ${maxAllowed}${blocks})`;\n })\n .join(', ');\n return `${violation.level}: ${describeViolation(violation, exceeded)}`;\n}\n\nfunction describeLargestBlocks({ largestBlocks = [] }: Violation): string {\n const blocks = largestBlocks.map(\n ({ startLine, endLine, name, cognitiveComplexity }) =>\n `L${startLine}${endLine === startLine ? '' : `-${endLine}`}${name ? ` ${toOneLine(name)}` : ''} ${cognitiveComplexity}`\n );\n return blocks.length > 0 ? `; largest parts ${blocks.join(', ')}` : '';\n}\n\nfunction describeViolation(violation: Violation, exceeded: string): string {\n switch (violation.kind) {\n case 'file': {\n return `${violation.file}: ${exceeded}`;\n }\n case 'function': {\n return `${formatLocation(violation)} ${toOneLine(violation.name ?? '')}: ${exceeded}`;\n }\n case 'duplication': {\n const partners = (violation.partners ?? []).map((partner) => formatLocation(partner));\n const listed = [...partners.slice(0, maxListedPartners), ...(partners.length > maxListedPartners ? ['...'] : [])];\n return `${formatLocation(violation)}: ${exceeded}${listed.length > 0 ? `, also at ${listed.join(', ')}` : ''}`;\n }\n }\n}\n\n/** A computed name can span lines in the source; the report keeps one line per function. */\nfunction toOneLine(name: string): string {\n return name.replaceAll(/\\s*[\\n\\r]\\s*/gu, ' ');\n}\n\nfunction formatLocation({ file, startLine, endLine }: BlockLocation): string {\n return `${file}:${startLine}-${endLine}`;\n}\n\nfunction printJsonReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n writeStdout(\n JSON.stringify(\n {\n passed: !hasErrors(result.violations) && scope.errors.length === 0,\n base: cliOptions.base,\n mergeBase: scope.mergeBase,\n summary: {\n ...countViolations(result.violations),\n checkedFileCount: scope.files.length,\n checkedFunctionCount: result.checkedFunctionCount,\n },\n violations: result.violations,\n errors: scope.errors,\n warnings: scope.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"checkCommand.cjs","names":["resolveTarget","loadConfig","configSearchDirectory","resolveOptions","checkThresholds","resolveLimits","getErrorMessage","mapConcurrently","os","parseAllowDirectives","readFile","path","realpath","stat","getLanguage","resolveRepoRoot","scanTarget","loadExclusion","formatPath","isIgnored","listRepositoryFiles","keepPaths","loadRepositoryExclusion","scanListedFiles","isWithinDirectory","resolveMergeBase","listChangedFiles","listLineHunks","readFileAt","isDirectiveLine","measureCode","thresholds","metricNameOf","require_runtime","require_metrics","require_thresholds","require_allowDirectives","require_cliConfig","require_git","require_exclusion","require_scan","require_thresholdCheck","_willbooster_shared_lib","node_fs_promises","node_os","node_path","runCheckCommand","resolvedTarget","target","config","cliOptions","options","scope","scanCheckScope","files","attachAllowDirectives","result","cliLimits","endFileViolationsAtLastLine","printJsonReport","printTextReport","hasErrors","error","checked","root","violations","level","violation","kind","content","canonicalTarget","targetFile","repoRoot","base","scanDirectoryWalk","scanRepository","searchDirectory","scan","absolutePaths","file","measureDuplication","repositoryFiles","explicitFiles","unlistedTarget","exclusion","targetPath","duplication","isInTarget","relativePath","measuredFiles","isCovered","mergeBase","changedFiles","changesInTarget","status","headPath","attachHunks","partitionErrors","measuredByPath","measured","changed","wholeFile","metrics","hunks","markDeletedDirectives","headCount","anchorDeletionsToCode","candidates","deletedLines","line","baseCodeLines","readBase","hunk","index","lines","isPassedByAnchoring","allowed","describeBase","describeCounts","formatViolation","problem","formatDirectiveProblem","violatedMetrics","exceeded","metric","violatedThresholds","threshold","label","hint","warning","message","counts","countViolations","count","errorViolationCount","value","limit","describeLargestBlocks","describeViolation","blocks","largestBlocks","startLine","endLine","name","toOneLine","cognitiveComplexity","formatLocation","partners","partner","listed"],"sources":["../src/checkCommand.ts"],"sourcesContent":["import { readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\n\nimport { getErrorMessage, mapConcurrently } from '@willbooster/shared-lib';\n\nimport { isDirectiveLine, parseAllowDirectives } from './allowDirectives.js';\nimport { type CliOptions, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport { keepPaths, loadExclusion, loadRepositoryExclusion } from './exclusion.js';\nimport {\n type ChangedFile,\n isIgnored,\n type LineHunk,\n listChangedFiles,\n listLineHunks,\n listRepositoryFiles,\n readFileAt,\n resolveMergeBase,\n resolveRepoRoot,\n} from './git.js';\nimport { measureCode } from './metrics.js';\nimport {\n addCrossFileDuplication,\n configSearchDirectory,\n formatPath,\n getLanguage,\n isWithinDirectory,\n resolveTarget,\n scanListedFiles,\n type ScanResult,\n scanTarget,\n writeStderr,\n writeStdout,\n} from './scan.js';\nimport {\n type BlockLocation,\n type CheckedDuplication,\n type CheckedFile,\n type CheckResult,\n checkThresholds,\n type LocatedDirectiveProblem,\n type Violation,\n} from './thresholdCheck.js';\nimport { type LimitsByLevel, metricNameOf, resolveLimits, thresholds } from './thresholds.js';\n\n/** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */\nexport interface CheckCliOptions extends CliOptions {\n base?: string;\n}\n\n/** The measured files a check covers and the duplication they take part in. */\nexport interface CheckScope {\n files: CheckedFile[];\n duplication: CheckedDuplication;\n /** Measurement failures of files the check covers. */\n errors: string[];\n warnings: string[];\n /** The directory the files' paths are relative to. */\n root: string;\n mergeBase?: string;\n}\n\n/** Caps how many partner locations a duplicated block lists so the report stays scannable. */\nconst maxListedPartners = 3;\n\n/**\n * Reports every exceeded threshold of the target or, with `base`, of what the working tree changed\n * under it since the merge-base with that ref. Exit codes: 0 no error-level violations, 1 error-level\n * violations, 2 files the check covers could not be measured.\n */\nexport async function runCheckCommand(\n target: string,\n cliOptions: CheckCliOptions,\n cliLimits: LimitsByLevel\n): Promise<void> {\n try {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const scope = await scanCheckScope(resolvedTarget, cliOptions.base, options);\n const files = await attachAllowDirectives(scope.files, scope.root);\n const result = checkThresholds(files, scope.duplication, resolveLimits(cliLimits, config.config.thresholds));\n await endFileViolationsAtLastLine(result.violations, scope.root);\n\n if (options.json) {\n printJsonReport(cliOptions, scope, result);\n } else {\n printTextReport(cliOptions, scope, result);\n }\n if (scope.errors.length > 0) {\n process.exitCode = 2;\n } else if (hasErrors(result.violations)) {\n process.exitCode = 1;\n }\n } catch (error) {\n writeStderr(`Error: ${getErrorMessage(error)}\\n`);\n process.exitCode = 2;\n }\n}\n\nasync function attachAllowDirectives(files: readonly CheckedFile[], root: string): Promise<CheckedFile[]> {\n return await mapConcurrently(files, os.availableParallelism() * 2, async (checked) => ({\n ...checked,\n allowDirectives: parseAllowDirectives(await readFile(path.join(root, checked.file), 'utf8'), checked.metrics),\n }));\n}\n\nfunction hasErrors(violations: readonly Violation[]): boolean {\n return violations.some(({ level }) => level === 'error');\n}\n\n/**\n * A file's measured line count includes the empty line after a final line terminator, which is\n * not a line of the file, so file-level violations end at the last line that exists.\n */\nasync function endFileViolationsAtLastLine(violations: readonly Violation[], root: string): Promise<void> {\n for (const violation of violations.filter(({ kind }) => kind === 'file')) {\n const content = await readFile(path.join(root, violation.file), 'utf8');\n if (/[\\n\\r]$/u.test(content)) {\n violation.endLine = Math.max(violation.endLine - 1, 1);\n }\n }\n}\n\n/**\n * Inside a git repository, the scope is the repository's git-visible files under the target, and\n * duplication is detected against all of them; `base` only narrows the scope to what changed.\n * Outside a repository, where there is no change to narrow to, the target directory is walked.\n */\nexport async function scanCheckScope(\n resolvedTarget: string,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n const canonicalTarget = await realpath(resolvedTarget);\n const targetStat = await stat(canonicalTarget);\n const targetFile = targetStat.isFile() ? canonicalTarget : undefined;\n if (targetFile && !getLanguage(targetFile, options, true)) {\n throw new Error(`${path.basename(targetFile)}: unsupported file type`);\n }\n let repoRoot;\n try {\n repoRoot = await realpath(await resolveRepoRoot(targetFile ? path.dirname(canonicalTarget) : canonicalTarget));\n } catch (error) {\n if (base !== undefined) {\n throw error;\n }\n return await scanDirectoryWalk(canonicalTarget, options);\n }\n return await scanRepository({ repoRoot, canonicalTarget, targetFile }, base, options);\n}\n\nasync function scanDirectoryWalk(canonicalTarget: string, options: ResolvedOptions): Promise<CheckScope> {\n const searchDirectory = await configSearchDirectory(canonicalTarget);\n const scan = await scanTarget(canonicalTarget, {\n ...options,\n loadExclusion: (absolutePaths) => loadExclusion(searchDirectory, options.exclude, absolutePaths),\n });\n return {\n files: scan.files.map(({ file, metrics }) => ({ file: formatPath(file, scan.displayRoot), metrics })),\n duplication: measureDuplication(scan, options),\n errors: scan.errors,\n warnings: scan.warnings,\n root: scan.displayRoot,\n };\n}\n\ninterface RepositoryTarget {\n repoRoot: string;\n canonicalTarget: string;\n /** Set when the target is a file, which is measured even when git ignores it or it is excluded. */\n targetFile?: string;\n}\n\n/**\n * Measures every git-visible file (tracked or untracked non-ignored) of the repository, so that\n * code copied from outside the target is detected as duplication, and keeps the files under the\n * target: all of them or, with `base`, the changed ones with their line hunks.\n */\nasync function scanRepository(\n { repoRoot, canonicalTarget, targetFile }: RepositoryTarget,\n base: string | undefined,\n options: ResolvedOptions\n): Promise<CheckScope> {\n // Only git-visible files are measured, so a directory git ignores would pass as a check of no\n // files. A directory git lists but whose sources it ignores is not told apart from a repository\n // or package whose only sources are ignored build output, which must pass.\n if (targetFile === undefined && canonicalTarget !== repoRoot && (await isIgnored(repoRoot, canonicalTarget))) {\n throw new Error(\n `${formatPath(canonicalTarget, repoRoot)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`\n );\n }\n const repositoryFiles = await listRepositoryFiles(repoRoot);\n const explicitFiles = new Set(targetFile === undefined ? [] : [targetFile]);\n const unlistedTarget =\n targetFile !== undefined && !repositoryFiles.has(formatPath(targetFile, repoRoot))\n ? formatPath(targetFile, repoRoot)\n : undefined;\n if (unlistedTarget !== undefined) {\n repositoryFiles.add(unlistedTarget);\n }\n const exclusion = keepPaths(await loadRepositoryExclusion(repoRoot, repositoryFiles, options.exclude), explicitFiles);\n // Scan errors start with the file or directory they concern.\n const targetPath = formatPath(canonicalTarget, repoRoot);\n const scan = await scanListedFiles(\n repoRoot,\n repositoryFiles,\n { ...options, loadExclusion: () => Promise.resolve(exclusion) },\n explicitFiles,\n targetFile === undefined && canonicalTarget !== repoRoot ? targetPath : ''\n );\n const duplication = measureDuplication(scan, options);\n const isInTarget = (relativePath: string): boolean =>\n isWithinDirectory(path.join(repoRoot, relativePath), canonicalTarget);\n const measuredFiles = scan.files.map(({ file, metrics }) => ({ file: formatPath(file, repoRoot), metrics }));\n let files = measuredFiles.filter(({ file }) => isInTarget(file));\n let isCovered = (error: string): boolean =>\n canonicalTarget === repoRoot || error.startsWith(`${targetPath}/`) || error.startsWith(`${targetPath}:`);\n let mergeBase;\n if (base !== undefined) {\n mergeBase = await resolveMergeBase(repoRoot, base);\n const changedFiles = await listChangedFiles(repoRoot, mergeBase);\n // A targeted file git does not list has no diff; all of it counts as added.\n if (unlistedTarget !== undefined) {\n changedFiles.push({ status: 'added', headPath: unlistedTarget });\n }\n const changesInTarget = changedFiles.filter(({ status, headPath }) => status !== 'deleted' && isInTarget(headPath));\n files = await attachHunks(measuredFiles, changesInTarget, repoRoot, mergeBase);\n isCovered = (error) => changesInTarget.some(({ headPath }) => error.startsWith(`${headPath}:`));\n }\n return {\n files,\n duplication,\n ...partitionErrors(scan, isCovered),\n root: repoRoot,\n mergeBase,\n };\n}\n\n/** Only a failure on a file the check covers leaves it incomplete; the others are warnings. */\nfunction partitionErrors(\n scan: ScanResult,\n isCovered: (error: string) => boolean\n): Pick<CheckScope, 'errors' | 'warnings'> {\n return {\n errors: scan.errors.filter((error) => isCovered(error)),\n warnings: [...scan.warnings, ...scan.errors.filter((error) => !isCovered(error))],\n };\n}\n\n/** The measured changed files with their line hunks against the merge-base. */\nasync function attachHunks(\n measuredFiles: readonly CheckedFile[],\n changedFiles: readonly ChangedFile[],\n repoRoot: string,\n mergeBase: string\n): Promise<CheckedFile[]> {\n const measuredByPath = new Map(measuredFiles.map((measured) => [measured.file, measured]));\n const files = await mapConcurrently(changedFiles, os.availableParallelism() * 2, async (changed) => {\n const measured = measuredByPath.get(changed.headPath);\n // A changed file the scan left out (unsupported, excluded, or failed) has nothing to check.\n if (measured === undefined) {\n return [];\n }\n const { metrics } = measured;\n const wholeFile: LineHunk = { headStart: 1, headCount: metrics.lines.total };\n const hunks =\n changed.basePath === undefined\n ? [wholeFile]\n : await listLineHunks(repoRoot, mergeBase, changed.basePath, changed.headPath);\n if (changed.basePath !== undefined) {\n await markDeletedDirectives(hunks, metrics.language, () =>\n readFileAt(repoRoot, mergeBase, changed.basePath ?? '')\n );\n }\n if (metrics.language === 'python' && hunks.some(({ headCount }) => headCount === 0)) {\n anchorDeletionsToCode(hunks, await readFile(path.join(repoRoot, changed.headPath), 'utf8'));\n }\n return [{ ...measured, hunks }];\n });\n return files.flat();\n}\n\n/**\n * Marks the hunks deleting a directive. A deleted line reads like one when it holds the directive\n * syntax, but only a comment-only line of the base file is one, not a line of a multi-line string,\n * so the base file is measured when a deleted line reads like one.\n */\nasync function markDeletedDirectives(\n hunks: LineHunk[],\n language: string,\n readBase: () => Promise<string>\n): Promise<void> {\n const candidates = hunks.filter(({ deletedLines }) => deletedLines?.some((line) => isDirectiveLine(line)));\n if (candidates.length === 0) return;\n const baseCodeLines = new Set(measureCode(await readBase(), { language }).codeLineNumbers);\n for (const hunk of candidates) {\n hunk.deletesDirective = (hunk.deletedLines ?? []).some(\n (line, index) => isDirectiveLine(line) && !baseCodeLines.has((hunk.baseStart ?? 0) + index)\n );\n }\n}\n\n/**\n * Moves each pure deletion up to the last code line or directive before it. A Python function ends at its last\n * statement, so the blank and comment-only lines left above a deleted tail lie outside the span the\n * deletion shortened; anchored to that statement, the deletion counts as touching the function.\n */\nfunction anchorDeletionsToCode(hunks: LineHunk[], content: string): void {\n const lines = content.split(/\\r\\n|\\n|\\r/u);\n for (const hunk of hunks) {\n if (hunk.headCount > 0) continue;\n while (hunk.headStart > 0 && isPassedByAnchoring(lines[hunk.headStart - 1] ?? '')) {\n hunk.headStart -= 1;\n }\n }\n}\n\n// A deletion right below a directive stays there, where it may have attached or detached it.\nfunction isPassedByAnchoring(line: string): boolean {\n return /^\\s*(?:#.*)?$/u.test(line) && !isDirectiveLine(line);\n}\n\n/** A run-wide failure (a missing target or native addon) leaves nothing to check. */\nfunction measureDuplication(scan: ScanResult, options: ResolvedOptions): CheckedDuplication {\n if (scan.fatalError) {\n throw new Error(scan.fatalError);\n }\n addCrossFileDuplication(scan, options);\n return {\n crossFile: scan.crossFileDuplication,\n minSimilarityPercent: options.duplication.minSimilarityPercent,\n };\n}\n\nfunction printTextReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n const { violations } = result;\n const allowed =\n result.allowedLimitCount > 0 ? `, ${result.allowedLimitCount} exceeded limits allowed by directives` : '';\n const checked = `${scope.files.length} ${cliOptions.base === undefined ? '' : 'changed '}files, ${result.checkedFunctionCount} functions checked${allowed}${describeBase(cliOptions, scope)}`;\n if (scope.errors.length > 0) {\n // Unmeasured files were not checked, so \"no violations\" would be vacuous; never claim a pass.\n writeStdout(\n `Check could not complete: ${scope.errors.length} measurement failures (details on stderr); ${describeCounts(violations)} in the measured files (${checked}).\\n`\n );\n } else if (violations.length === 0) {\n writeStdout(`No threshold violations: ${checked}.\\n`);\n } else {\n writeStdout(`Threshold violations: ${describeCounts(violations)} (${checked}).\\n`);\n }\n for (const violation of violations) {\n writeStdout(`${formatViolation(violation)}\\n`);\n }\n for (const problem of result.directiveProblems) {\n writeStdout(`${formatDirectiveProblem(problem)}\\n`);\n }\n\n const violatedMetrics = new Set(violations.flatMap(({ exceeded }) => exceeded.map(({ metric }) => metric)));\n const violatedThresholds = thresholds.filter((threshold) => violatedMetrics.has(metricNameOf(threshold)));\n if (violatedThresholds.length > 0) {\n writeStdout('\\nHow to fix:\\n');\n for (const { label, hint } of violatedThresholds) {\n writeStdout(`- ${label}: ${hint}\\n`);\n }\n }\n\n for (const warning of scope.warnings) {\n writeStderr(`Warning: ${warning}\\n`);\n }\n for (const error of scope.errors) {\n writeStderr(`Error: ${error}\\n`);\n }\n}\n\nfunction formatDirectiveProblem({ file, line, message }: LocatedDirectiveProblem): string {\n return `warning: ${file}:${line}: ${message}`;\n}\n\nfunction describeBase(cliOptions: CheckCliOptions, scope: CheckScope): string {\n return scope.mergeBase === undefined ? '' : `; base ${cliOptions.base}, merge-base ${scope.mergeBase.slice(0, 12)}`;\n}\n\nfunction describeCounts(violations: readonly Violation[]): string {\n const counts = countViolations(violations);\n return `${counts.errorViolationCount} errors, ${counts.warningViolationCount} warnings (${counts.functionViolationCount} functions, ${counts.fileViolationCount} files, ${counts.duplicationViolationCount} duplicated blocks)`;\n}\n\nfunction countViolations(violations: readonly Violation[]): {\n violationCount: number;\n errorViolationCount: number;\n warningViolationCount: number;\n functionViolationCount: number;\n fileViolationCount: number;\n duplicationViolationCount: number;\n} {\n const count = (kind: Violation['kind']): number => violations.filter((violation) => violation.kind === kind).length;\n const errorViolationCount = violations.filter(({ level }) => level === 'error').length;\n return {\n violationCount: violations.length,\n errorViolationCount,\n warningViolationCount: violations.length - errorViolationCount,\n functionViolationCount: count('function'),\n fileViolationCount: count('file'),\n duplicationViolationCount: count('duplication'),\n };\n}\n\n/** One violation as a single line: its level, the location, then every limit it exceeds. */\nfunction formatViolation(violation: Violation): string {\n const exceeded = violation.exceeded\n .map(({ metric, label, value, level, limit }) => {\n // Halstead values are fractional; rounding up keeps a violating value above the printed maximum.\n const roundedValue = Math.ceil(value * 10) / 10;\n const levelPrefix = level === violation.level ? '' : `${level} `;\n // A duplicated block violates from its limit on, so the largest allowed code-line count is\n // the last whole number below it; a limit of 0 allows none.\n const maxAllowed = violation.kind === 'duplication' ? Math.max(Math.ceil(limit) - 1, 0) : limit;\n const blocks = metric === 'functionCognitiveComplexity' ? describeLargestBlocks(violation) : '';\n return `${label} ${roundedValue} (${levelPrefix}max ${maxAllowed}${blocks})`;\n })\n .join(', ');\n return `${violation.level}: ${describeViolation(violation, exceeded)}`;\n}\n\nfunction describeLargestBlocks({ largestBlocks = [] }: Violation): string {\n const blocks = largestBlocks.map(\n ({ startLine, endLine, name, cognitiveComplexity }) =>\n `L${startLine}${endLine === startLine ? '' : `-${endLine}`}${name ? ` ${toOneLine(name)}` : ''} ${cognitiveComplexity}`\n );\n return blocks.length > 0 ? `; largest parts ${blocks.join(', ')}` : '';\n}\n\nfunction describeViolation(violation: Violation, exceeded: string): string {\n switch (violation.kind) {\n case 'file': {\n return `${violation.file}: ${exceeded}`;\n }\n case 'function': {\n return `${formatLocation(violation)} ${toOneLine(violation.name ?? '')}: ${exceeded}`;\n }\n case 'duplication': {\n const partners = (violation.partners ?? []).map((partner) => formatLocation(partner));\n const listed = [...partners.slice(0, maxListedPartners), ...(partners.length > maxListedPartners ? ['...'] : [])];\n return `${formatLocation(violation)}: ${exceeded}${listed.length > 0 ? `, also at ${listed.join(', ')}` : ''}`;\n }\n }\n}\n\n/** A computed name can span lines in the source; the report keeps one line per function. */\nfunction toOneLine(name: string): string {\n return name.replaceAll(/\\s*[\\n\\r]\\s*/gu, ' ');\n}\n\nfunction formatLocation({ file, startLine, endLine }: BlockLocation): string {\n return `${file}:${startLine}-${endLine}`;\n}\n\nfunction printJsonReport(cliOptions: CheckCliOptions, scope: CheckScope, result: CheckResult): void {\n writeStdout(\n JSON.stringify(\n {\n passed: !hasErrors(result.violations) && scope.errors.length === 0,\n base: cliOptions.base,\n mergeBase: scope.mergeBase,\n summary: {\n ...countViolations(result.violations),\n checkedFileCount: scope.files.length,\n checkedFunctionCount: result.checkedFunctionCount,\n allowedLimitCount: result.allowedLimitCount,\n },\n violations: result.violations,\n directiveProblems: result.directiveProblems,\n errors: scope.errors,\n warnings: scope.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n"],"mappings":"A,icAsEA,eAAsB8C,EACpB,EACA,EACA,EACe,CACf,GAAI,CACF,IAAMC,EAAiB/C,EAAAA,cAAcgD,CAAM,EACrCC,EAAS,MAAMhD,EAAAA,WAAWiD,EAAW,OAAQ,MAAMhD,EAAAA,sBAAsB6C,CAAc,CAAC,EACxFI,EAAUhD,EAAAA,eAAe+C,EAAYD,CAAM,EAC3CG,EAAQ,MAAMC,EAAeN,EAAgBG,EAAW,KAAMC,CAAO,EACrEG,EAAQ,MAAMC,EAAsBH,EAAM,MAAOA,EAAM,IAAI,EAC3DI,EAASpD,EAAAA,gBAAgBkD,EAAOF,EAAM,YAAa/C,EAAAA,cAAcoD,EAAWR,EAAO,OAAO,UAAU,CAAC,EAC3G,MAAMS,EAA4BF,EAAO,WAAYJ,EAAM,IAAI,EAE3DD,EAAQ,KACVQ,EAAgBT,EAAYE,EAAOI,CAAM,EAEzCI,EAAgBV,EAAYE,EAAOI,CAAM,EAEvCJ,EAAM,OAAO,OAAS,EACxB,QAAQ,SAAW,EACVS,EAAUL,EAAO,UAAU,IACpC,QAAQ,SAAW,EAEvB,OAASM,EAAO,CACdtB,EAAA,YAAY,WAAA,EAAUlC,EAAAA,gBAAAA,CAAgBwD,CAAK,EAAE,GAAG,EAChD,QAAQ,SAAW,CACrB,CACF,CAEA,eAAeP,EAAsB,EAA+B,EAAsC,CACxG,OAAO,MAAA,EAAMhD,EAAAA,gBAAAA,CAAgB+C,EAAO9C,EAAAA,QAAG,qBAAqB,EAAI,EAAG,KAAO,KAAa,CACrF,GAAGuD,EACH,gBAAiBtD,EAAAA,qBAAqB,MAAA,EAAMC,EAAAA,SAAAA,CAASC,EAAAA,QAAK,KAAKqD,EAAMD,EAAQ,IAAI,EAAG,MAAM,EAAGA,EAAQ,OAAO,CAC9G,EAAE,CACJ,CAEA,SAASF,EAAU,EAA2C,CAC5D,OAAOI,EAAW,MAAM,CAAE,WAAYC,IAAU,OAAO,CACzD,CAMA,eAAeR,EAA4B,EAAkC,EAA6B,CACxG,IAAK,IAAMS,KAAaF,EAAW,QAAQ,CAAE,UAAWG,IAAS,MAAM,EAAG,CACxE,IAAMC,EAAU,MAAA,EAAM3D,EAAAA,SAAAA,CAASC,EAAAA,QAAK,KAAKqD,EAAMG,EAAU,IAAI,EAAG,MAAM,EAClE,WAAW,KAAKE,CAAO,IACzB,EAAU,QAAU,KAAK,IAAIF,EAAU,QAAU,EAAG,CAAC,EAEzD,CACF,CAOA,eAAsBd,EACpB,EACA,EACA,EACqB,CACrB,IAAMiB,EAAkB,MAAA,EAAM1D,EAAAA,SAAAA,CAASmC,CAAc,EAE/CwB,GAAa,MAAA,EADM1D,EAAAA,KAAAA,CAAKyD,CAAe,EAAA,CACf,OAAO,EAAIA,EAAkB,IAAA,GAC3D,GAAIC,GAAc,CAACzD,EAAAA,YAAYyD,EAAYpB,EAAS,EAAI,EACtD,MAAU,MAAM,GAAGxC,EAAAA,QAAK,SAAS4D,CAAU,EAAE,wBAAwB,EAEvE,IAAIC,EACJ,GAAI,CACF,EAAW,MAAA,EAAM5D,EAAAA,SAAAA,CAAS,MAAMG,EAAAA,gBAAgBwD,EAAa5D,EAAAA,QAAK,QAAQ2D,CAAe,EAAIA,CAAe,CAAC,CAC/G,OAASR,EAAO,CACd,GAAIW,IAAS,IAAA,GACX,MAAMX,EAER,OAAO,MAAMY,EAAkBJ,EAAiBnB,CAAO,CACzD,CACA,OAAO,MAAMwB,EAAe,CAAE,WAAU,kBAAiB,YAAW,EAAGF,EAAMtB,CAAO,CACtF,CAEA,eAAeuB,EAAkB,EAAyB,EAA+C,CACvG,IAAME,EAAkB,MAAM1E,EAAAA,sBAAsBoE,CAAe,EAC7DO,EAAO,MAAM7D,EAAAA,WAAWsD,EAAiB,CAC7C,GAAGnB,EACH,cAAgB,GAAkBlC,EAAAA,cAAc2D,EAAiBzB,EAAQ,QAAS2B,CAAa,CACjG,CAAC,EACD,MAAO,CACL,MAAOD,EAAK,MAAM,KAAK,CAAE,OAAM,cAAe,CAAE,KAAM3D,EAAAA,WAAW6D,EAAMF,EAAK,WAAW,EAAG,SAAQ,EAAE,EACpG,YAAaG,EAAmBH,EAAM1B,CAAO,EAC7C,OAAQ0B,EAAK,OACb,SAAUA,EAAK,SACf,KAAMA,EAAK,WACb,CACF,CAcA,eAAeF,EACb,CAAE,WAAU,kBAAiB,cAC7B,EACA,EACqB,CAIrB,GAAIJ,IAAe,IAAA,IAAaD,IAAoBE,GAAa,MAAMrD,EAAAA,UAAUqD,EAAUF,CAAe,EACxG,MAAU,MACR,GAAGpD,EAAAA,WAAWoD,EAAiBE,CAAQ,EAAE,gGAC3C,EAEF,IAAMS,EAAkB,MAAM7D,EAAAA,oBAAoBoD,CAAQ,EACpDU,EAAgB,IAAI,IAAIX,IAAe,IAAA,GAAY,CAAC,EAAI,CAACA,CAAU,CAAC,EACpEY,EACJZ,IAAe,IAAA,IAAa,CAACU,EAAgB,IAAI/D,EAAAA,WAAWqD,EAAYC,CAAQ,CAAC,EAC7EtD,EAAAA,WAAWqD,EAAYC,CAAQ,EAC/B,IAAA,GACFW,IAAmB,IAAA,IACrBF,EAAgB,IAAIE,CAAc,EAEpC,IAAMC,EAAY/D,EAAAA,UAAU,MAAMC,EAAAA,wBAAwBkD,EAAUS,EAAiB9B,EAAQ,OAAO,EAAG+B,CAAa,EAE9GG,EAAanE,EAAAA,WAAWoD,EAAiBE,CAAQ,EACjDK,EAAO,MAAMtD,EAAAA,gBACjBiD,EACAS,EACA,CAAE,GAAG9B,EAAS,kBAAqB,QAAQ,QAAQiC,CAAS,CAAE,EAC9DF,EACAX,IAAe,IAAA,IAAaD,IAAoBE,EAAWa,EAAa,EAC1E,EACMC,EAAcN,EAAmBH,EAAM1B,CAAO,EAC9CoC,EAAc,GAClB/D,EAAAA,kBAAkBb,EAAAA,QAAK,KAAK6D,EAAUgB,CAAY,EAAGlB,CAAe,EAChEmB,EAAgBZ,EAAK,MAAM,KAAK,CAAE,OAAM,cAAe,CAAE,KAAM3D,EAAAA,WAAW6D,EAAMP,CAAQ,EAAG,SAAQ,EAAE,EACvGlB,EAAQmC,EAAc,QAAQ,CAAE,UAAWF,EAAWR,CAAI,CAAC,EAC3DW,EAAa,GACfpB,IAAoBE,GAAYV,EAAM,WAAW,GAAGuB,EAAW,EAAE,GAAKvB,EAAM,WAAW,GAAGuB,EAAW,EAAE,EACrGM,EACJ,GAAIlB,IAAS,IAAA,GAAW,CACtB,EAAY,MAAMhD,EAAAA,iBAAiB+C,EAAUC,CAAI,EACjD,IAAMmB,EAAe,MAAMlE,EAAAA,iBAAiB8C,EAAUmB,CAAS,EAE3DR,IAAmB,IAAA,IACrBS,EAAa,KAAK,CAAE,OAAQ,QAAS,SAAUT,CAAe,CAAC,EAEjE,IAAMU,EAAkBD,EAAa,QAAQ,CAAE,SAAQ,cAAeE,IAAW,WAAaP,EAAWQ,CAAQ,CAAC,EAClH,EAAQ,MAAMC,EAAYP,EAAeI,EAAiBrB,EAAUmB,CAAS,EAC7E,EAAa,GAAUE,EAAgB,MAAM,CAAE,cAAe/B,EAAM,WAAW,GAAGiC,EAAS,EAAE,CAAC,CAChG,CACA,MAAO,CACL,QACA,cACA,GAAGE,EAAgBpB,EAAMa,CAAS,EAClC,KAAMlB,EACN,WACF,CACF,CAGA,SAASyB,EACP,EACA,EACyC,CACzC,MAAO,CACL,OAAQpB,EAAK,OAAO,OAAQ,GAAUa,EAAU5B,CAAK,CAAC,EACtD,SAAU,CAAC,GAAGe,EAAK,SAAU,GAAGA,EAAK,OAAO,OAAQ,GAAU,CAACa,EAAU5B,CAAK,CAAC,CAAC,CAClF,CACF,CAGA,eAAekC,EACb,EACA,EACA,EACA,EACwB,CACxB,IAAME,EAAiB,IAAI,IAAIT,EAAc,IAAK,GAAa,CAACU,EAAS,KAAMA,CAAQ,CAAC,CAAC,EAuBzF,OAAO,MAAA,EAtBa5F,EAAAA,gBAAAA,CAAgBqF,EAAcpF,EAAAA,QAAG,qBAAqB,EAAI,EAAG,KAAO,IAAY,CAClG,IAAM2F,EAAWD,EAAe,IAAIE,EAAQ,QAAQ,EAEpD,GAAID,IAAa,IAAA,GACf,MAAO,CAAC,EAEV,GAAM,CAAE,WAAYA,EACdE,EAAsB,CAAE,UAAW,EAAG,UAAWC,EAAQ,MAAM,KAAM,EACrEC,EACJH,EAAQ,WAAa,IAAA,GACjB,CAACC,CAAS,EACV,MAAM1E,EAAAA,cAAc6C,EAAUmB,EAAWS,EAAQ,SAAUA,EAAQ,QAAQ,EASjF,OARIA,EAAQ,WAAa,IAAA,IACvB,MAAMI,EAAsBD,EAAOD,EAAQ,aACzC1E,EAAAA,WAAW4C,EAAUmB,EAAWS,EAAQ,UAAY,EAAE,CACxD,EAEEE,EAAQ,WAAa,UAAYC,EAAM,MAAM,CAAE,eAAgBE,IAAc,CAAC,GAChFC,EAAsBH,EAAO,MAAA,EAAM7F,EAAAA,SAAAA,CAASC,EAAAA,QAAK,KAAK6D,EAAU4B,EAAQ,QAAQ,EAAG,MAAM,CAAC,EAErF,CAAC,CAAE,GAAGD,EAAU,OAAM,CAAC,CAChC,CAAC,EAAA,CACY,KAAK,CACpB,CAOA,eAAeK,EACb,EACA,EACA,EACe,CACf,IAAMG,EAAaJ,EAAM,QAAQ,CAAE,kBAAmBK,GAAc,KAAM,GAAS/E,EAAAA,gBAAgBgF,CAAI,CAAC,CAAC,EACzG,GAAIF,EAAW,SAAW,EAAG,OAC7B,IAAMG,EAAgB,IAAI,IAAIhF,EAAAA,YAAY,MAAMiF,EAAS,EAAG,CAAE,UAAS,CAAC,CAAC,CAAC,eAAe,EACzF,IAAK,IAAMC,KAAQL,EACjB,EAAK,kBAAoBK,EAAK,cAAgB,CAAC,EAAA,CAAG,MAC/C,EAAM,IAAUnF,EAAAA,gBAAgBgF,CAAI,GAAK,CAACC,EAAc,KAAKE,EAAK,WAAa,GAAKC,CAAK,CAC5F,CAEJ,CAOA,SAASP,EAAsB,EAAmB,EAAuB,CACvE,IAAMQ,EAAQ7C,EAAQ,MAAM,aAAa,EACzC,IAAK,IAAM2C,KAAQT,EACjB,GAAI,IAAK,UAAY,GACrB,KAAOS,EAAK,UAAY,GAAKG,EAAoBD,EAAMF,EAAK,UAAY,IAAM,EAAE,GAC9E,IAAK,SAGX,CAGA,SAASG,EAAoB,EAAuB,CAClD,MAAO,iBAAiB,KAAKN,CAAI,GAAK,CAAChF,EAAAA,gBAAgBgF,CAAI,CAC7D,CAGA,SAAS7B,EAAmB,EAAkB,EAA8C,CAC1F,GAAIH,EAAK,WACP,MAAU,MAAMA,EAAK,UAAU,EAGjC,OADArC,EAAA,wBAAwBqC,EAAM1B,CAAO,EAC9B,CACL,UAAW0B,EAAK,qBAChB,qBAAsB1B,EAAQ,YAAY,oBAC5C,CACF,CAEA,SAASS,EAAgB,EAA6B,EAAmB,EAA2B,CAClG,GAAM,CAAE,cAAeJ,EACjB4D,EACJ5D,EAAO,kBAAoB,EAAI,KAAKA,EAAO,kBAAkB,wCAA0C,GACnGO,EAAU,GAAGX,EAAM,MAAM,OAAO,GAAGF,EAAW,OAAS,IAAA,GAAY,GAAK,WAAW,SAASM,EAAO,qBAAqB,oBAAoB4D,IAAUC,EAAanE,EAAYE,CAAK,IACtLA,EAAM,OAAO,OAAS,EAExBZ,EAAA,YACE,6BAA6BY,EAAM,OAAO,OAAO,6CAA6CkE,EAAerD,CAAU,EAAE,0BAA0BF,EAAQ,KAC7J,EACSE,EAAW,SAAW,EAC/BzB,EAAA,YAAY,4BAA4BuB,EAAQ,IAAI,EAEpDvB,EAAA,YAAY,yBAAyB8E,EAAerD,CAAU,EAAE,IAAIF,EAAQ,KAAK,EAEnF,IAAK,IAAMI,KAAaF,EACtBzB,EAAA,YAAY,GAAG+E,EAAgBpD,CAAS,EAAE,GAAG,EAE/C,IAAK,IAAMqD,KAAWhE,EAAO,kBAC3BhB,EAAA,YAAY,GAAGiF,EAAuBD,CAAO,EAAE,GAAG,EAGpD,IAAME,EAAkB,IAAI,IAAIzD,EAAW,SAAS,CAAE,cAAe0D,EAAS,KAAK,CAAE,YAAaC,CAAM,CAAC,CAAC,EACpGC,EAAqB9F,EAAAA,WAAW,OAAQ,GAAc2F,EAAgB,IAAI1F,EAAAA,aAAa8F,CAAS,CAAC,CAAC,EACxG,GAAID,EAAmB,OAAS,EAAG,CACjCrF,EAAA,YAAY;;CAAiB,EAC7B,IAAK,GAAM,CAAE,QAAO,UAAUqF,EAC5BrF,EAAA,YAAY,KAAKuF,EAAM,IAAIC,EAAK,GAAG,CAEvC,CAEA,IAAK,IAAMC,KAAW7E,EAAM,SAC1BZ,EAAA,YAAY,YAAYyF,EAAQ,GAAG,EAErC,IAAK,IAAMnE,KAASV,EAAM,OACxBZ,EAAA,YAAY,UAAUsB,EAAM,GAAG,CAEnC,CAEA,SAAS2D,EAAuB,CAAE,OAAM,OAAM,WAA4C,CACxF,MAAO,YAAY1C,EAAK,GAAG8B,EAAK,IAAIqB,GACtC,CAEA,SAASb,EAAa,EAA6B,EAA2B,CAC5E,OAAOjE,EAAM,YAAc,IAAA,GAAY,GAAK,UAAUF,EAAW,KAAK,eAAeE,EAAM,UAAU,MAAM,EAAG,EAAE,GAClH,CAEA,SAASkE,EAAe,EAA0C,CAChE,IAAMa,EAASC,EAAgBnE,CAAU,EACzC,MAAO,GAAGkE,EAAO,oBAAoB,WAAWA,EAAO,sBAAsB,aAAaA,EAAO,uBAAuB,cAAcA,EAAO,mBAAmB,UAAUA,EAAO,0BAA0B,oBAC7M,CAEA,SAASC,EAAgB,EAOvB,CACA,IAAMC,EAAS,GAAoCpE,EAAW,OAAQ,GAAcE,EAAU,OAASC,CAAI,CAAC,CAAC,OACvGkE,EAAsBrE,EAAW,QAAQ,CAAE,WAAYC,IAAU,OAAO,CAAC,CAAC,OAChF,MAAO,CACL,eAAgBD,EAAW,OAC3B,sBACA,sBAAuBA,EAAW,OAASqE,EAC3C,uBAAwBD,EAAM,UAAU,EACxC,mBAAoBA,EAAM,MAAM,EAChC,0BAA2BA,EAAM,aAAa,CAChD,CACF,CAGA,SAASd,EAAgB,EAA8B,CACrD,IAAMI,EAAWxD,EAAU,SACxB,KAAK,CAAE,SAAQ,QAAO,QAAO,QAAO,WAQ5B,GAAG4D,EAAM,GANK,KAAK,KAAKQ,EAAQ,EAAE,EAAI,GAMb,IALZrE,IAAUC,EAAU,MAAQ,GAAK,GAAGD,EAAM,GAKd,MAF7BC,EAAU,OAAS,cAAgB,KAAK,IAAI,KAAK,KAAKqE,CAAK,EAAI,EAAG,CAAC,EAAIA,IAC3EZ,IAAW,8BAAgCa,EAAsBtE,CAAS,EAAI,GACnB,EAC3E,CAAC,CACD,KAAK,IAAI,EACZ,MAAO,GAAGA,EAAU,MAAM,IAAIuE,EAAkBvE,EAAWwD,CAAQ,GACrE,CAEA,SAASc,EAAsB,CAAE,gBAAgB,CAAC,GAAwB,CACxE,IAAME,EAASC,EAAc,KAC1B,CAAE,YAAW,UAAS,OAAM,yBAC3B,IAAIC,IAAYC,IAAYD,EAAY,GAAK,IAAIC,MAAYC,EAAO,IAAIC,EAAUD,CAAI,IAAM,GAAG,GAAGE,GACtG,EACA,OAAON,EAAO,OAAS,EAAI,mBAAmBA,EAAO,KAAK,IAAI,IAAM,EACtE,CAEA,SAASD,EAAkB,EAAsB,EAA0B,CACzE,OAAQvE,EAAU,KAAlB,CACE,IAAK,OACH,MAAO,GAAGA,EAAU,KAAK,IAAIwD,IAE/B,IAAK,WACH,MAAO,GAAGuB,EAAe/E,CAAS,EAAE,GAAG6E,EAAU7E,EAAU,MAAQ,EAAE,EAAE,IAAIwD,IAE7E,IAAK,cAAe,CAClB,IAAMwB,GAAYhF,EAAU,UAAY,CAAC,EAAA,CAAG,IAAK,GAAY+E,EAAeE,CAAO,CAAC,EAC9EC,EAAS,CAAC,GAAGF,EAAS,MAAM,EAAG,CAAiB,EAAG,GAAIA,EAAS,OAAS,EAAoB,CAAC,KAAK,EAAI,CAAC,CAAE,EAChH,MAAO,GAAGD,EAAe/E,CAAS,EAAE,IAAIwD,IAAW0B,EAAO,OAAS,EAAI,aAAaA,EAAO,KAAK,IAAI,IAAM,IAC5G,CACF,CACF,CAGA,SAASL,EAAU,EAAsB,CACvC,OAAOD,EAAK,WAAW,iBAAkB,GAAG,CAC9C,CAEA,SAASG,EAAe,CAAE,OAAM,YAAW,WAAkC,CAC3E,MAAO,GAAGnE,EAAK,GAAG8D,EAAU,GAAGC,GACjC,CAEA,SAASnF,EAAgB,EAA6B,EAAmB,EAA2B,CAClGnB,EAAA,YACE,KAAK,UACH,CACE,OAAQ,CAACqB,EAAUL,EAAO,UAAU,GAAKJ,EAAM,OAAO,SAAW,EACjE,KAAMF,EAAW,KACjB,UAAWE,EAAM,UACjB,QAAS,CACP,GAAGgF,EAAgB5E,EAAO,UAAU,EACpC,iBAAkBJ,EAAM,MAAM,OAC9B,qBAAsBI,EAAO,qBAC7B,kBAAmBA,EAAO,iBAC5B,EACA,WAAYA,EAAO,WACnB,kBAAmBA,EAAO,kBAC1B,OAAQJ,EAAM,OACd,SAAUA,EAAM,QAClB,EACA,IAAA,GACA,CACF,EAAI;CACN,CACF,C"}
|
package/dist/checkCommand.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type CliOptions, type ResolvedOptions } from './cliConfig.js';
|
|
2
|
-
import { type
|
|
2
|
+
import { type CheckedDuplication, type CheckedFile } from './thresholdCheck.js';
|
|
3
3
|
import { type LimitsByLevel } from './thresholds.js';
|
|
4
4
|
/** Raw options of the `check` subcommand; every field is undefined unless the flag was passed. */
|
|
5
5
|
export interface CheckCliOptions extends CliOptions {
|
package/dist/checkCommand.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import{metricNameOf as
|
|
1
|
+
import{measureCode as e}from"./metrics.js";import{metricNameOf as t,resolveLimits as n,thresholds as r}from"./thresholds.js";import{isDirectiveLine as i,parseAllowDirectives as a}from"./allowDirectives.js";import{loadConfig as o,resolveOptions as s}from"./cliConfig.js";import{isIgnored as c,listChangedFiles as l,listLineHunks as u,listRepositoryFiles as d,readFileAt as f,resolveMergeBase as p,resolveRepoRoot as m}from"./git.js";import{keepPaths as ee,loadExclusion as h,loadRepositoryExclusion as g}from"./exclusion.js";import{addCrossFileDuplication as _,configSearchDirectory as v,formatPath as y,getLanguage as b,isWithinDirectory as te,resolveTarget as x,scanListedFiles as S,scanTarget as C,writeStderr as w,writeStdout as T}from"./scan.js";import{checkThresholds as E}from"./thresholdCheck.js";import{getErrorMessage as D,mapConcurrently as O}from"@willbooster/shared-lib";import{readFile as k,realpath as A,stat as j}from"node:fs/promises";import M from"node:os";import N from"node:path";async function P(e,t,r){try{let i=x(e),a=await o(t.config,await v(i)),c=s(t,a),l=await R(i,t.base,c),u=await F(l.files,l.root),d=E(u,l.duplication,n(r,a.config.thresholds));await L(d.violations,l.root),c.json?ae(t,l,d):q(t,l,d),l.errors.length>0?process.exitCode=2:I(d.violations)&&(process.exitCode=1)}catch(e){w(`Error: ${D(e)}\n`),process.exitCode=2}}async function F(e,t){return await O(e,M.availableParallelism()*2,async e=>({...e,allowDirectives:a(await k(N.join(t,e.file),`utf8`),e.metrics)}))}function I(e){return e.some(({level:e})=>e===`error`)}async function L(e,t){for(let n of e.filter(({kind:e})=>e===`file`)){let e=await k(N.join(t,n.file),`utf8`);/[\n\r]$/u.test(e)&&(n.endLine=Math.max(n.endLine-1,1))}}async function R(e,t,n){let r=await A(e),i=(await j(r)).isFile()?r:void 0;if(i&&!b(i,n,!0))throw Error(`${N.basename(i)}: unsupported file type`);let a;try{a=await A(await m(i?N.dirname(r):r))}catch(e){if(t!==void 0)throw e;return await z(r,n)}return await B({repoRoot:a,canonicalTarget:r,targetFile:i},t,n)}async function z(e,t){let n=await v(e),r=await C(e,{...t,loadExclusion:e=>h(n,t.exclude,e)});return{files:r.files.map(({file:e,metrics:t})=>({file:y(e,r.displayRoot),metrics:t})),duplication:K(r,t),errors:r.errors,warnings:r.warnings,root:r.displayRoot}}async function B({repoRoot:e,canonicalTarget:t,targetFile:n},r,i){if(n===void 0&&t!==e&&await c(e,t))throw Error(`${y(t,e)}: git ignores this directory, so no file in it is checked; target a file to check it regardless`);let a=await d(e),o=new Set(n===void 0?[]:[n]),s=n!==void 0&&!a.has(y(n,e))?y(n,e):void 0;s!==void 0&&a.add(s);let u=ee(await g(e,a,i.exclude),o),f=y(t,e),m=await S(e,a,{...i,loadExclusion:()=>Promise.resolve(u)},o,n===void 0&&t!==e?f:``),h=K(m,i),_=n=>te(N.join(e,n),t),v=m.files.map(({file:t,metrics:n})=>({file:y(t,e),metrics:n})),b=v.filter(({file:e})=>_(e)),x=n=>t===e||n.startsWith(`${f}/`)||n.startsWith(`${f}:`),C;if(r!==void 0){C=await p(e,r);let t=await l(e,C);s!==void 0&&t.push({status:`added`,headPath:s});let n=t.filter(({status:e,headPath:t})=>e!==`deleted`&&_(t));b=await H(v,n,e,C),x=e=>n.some(({headPath:t})=>e.startsWith(`${t}:`))}return{files:b,duplication:h,...V(m,x),root:e,mergeBase:C}}function V(e,t){return{errors:e.errors.filter(e=>t(e)),warnings:[...e.warnings,...e.errors.filter(e=>!t(e))]}}async function H(e,t,n,r){let i=new Map(e.map(e=>[e.file,e]));return(await O(t,M.availableParallelism()*2,async e=>{let t=i.get(e.headPath);if(t===void 0)return[];let{metrics:a}=t,o={headStart:1,headCount:a.lines.total},s=e.basePath===void 0?[o]:await u(n,r,e.basePath,e.headPath);return e.basePath!==void 0&&await U(s,a.language,()=>f(n,r,e.basePath??``)),a.language===`python`&&s.some(({headCount:e})=>e===0)&&W(s,await k(N.join(n,e.headPath),`utf8`)),[{...t,hunks:s}]})).flat()}async function U(t,n,r){let a=t.filter(({deletedLines:e})=>e?.some(e=>i(e)));if(a.length===0)return;let o=new Set(e(await r(),{language:n}).codeLineNumbers);for(let e of a)e.deletesDirective=(e.deletedLines??[]).some((t,n)=>i(t)&&!o.has((e.baseStart??0)+n))}function W(e,t){let n=t.split(/\r\n|\n|\r/u);for(let t of e)if(!(t.headCount>0))for(;t.headStart>0&&G(n[t.headStart-1]??``);)--t.headStart}function G(e){return/^\s*(?:#.*)?$/u.test(e)&&!i(e)}function K(e,t){if(e.fatalError)throw Error(e.fatalError);return _(e,t),{crossFile:e.crossFileDuplication,minSimilarityPercent:t.duplication.minSimilarityPercent}}function q(e,n,i){let{violations:a}=i,o=i.allowedLimitCount>0?`, ${i.allowedLimitCount} exceeded limits allowed by directives`:``,s=`${n.files.length} ${e.base===void 0?``:`changed `}files, ${i.checkedFunctionCount} functions checked${o}${Y(e,n)}`;n.errors.length>0?T(`Check could not complete: ${n.errors.length} measurement failures (details on stderr); ${X(a)} in the measured files (${s}).\n`):a.length===0?T(`No threshold violations: ${s}.\n`):T(`Threshold violations: ${X(a)} (${s}).\n`);for(let e of a)T(`${ne(e)}\n`);for(let e of i.directiveProblems)T(`${J(e)}\n`);let c=new Set(a.flatMap(({exceeded:e})=>e.map(({metric:e})=>e))),l=r.filter(e=>c.has(t(e)));if(l.length>0){T(`
|
|
2
2
|
How to fix:
|
|
3
|
-
`);for(let{label:e,hint:t}of
|
|
4
|
-
`)}export{
|
|
3
|
+
`);for(let{label:e,hint:t}of l)T(`- ${e}: ${t}\n`)}for(let e of n.warnings)w(`Warning: ${e}\n`);for(let e of n.errors)w(`Error: ${e}\n`)}function J({file:e,line:t,message:n}){return`warning: ${e}:${t}: ${n}`}function Y(e,t){return t.mergeBase===void 0?``:`; base ${e.base}, merge-base ${t.mergeBase.slice(0,12)}`}function X(e){let t=Z(e);return`${t.errorViolationCount} errors, ${t.warningViolationCount} warnings (${t.functionViolationCount} functions, ${t.fileViolationCount} files, ${t.duplicationViolationCount} duplicated blocks)`}function Z(e){let t=t=>e.filter(e=>e.kind===t).length,n=e.filter(({level:e})=>e===`error`).length;return{violationCount:e.length,errorViolationCount:n,warningViolationCount:e.length-n,functionViolationCount:t(`function`),fileViolationCount:t(`file`),duplicationViolationCount:t(`duplication`)}}function ne(e){let t=e.exceeded.map(({metric:t,label:n,value:r,level:i,limit:a})=>`${n} ${Math.ceil(r*10)/10} (${i===e.level?``:`${i} `}max ${e.kind===`duplication`?Math.max(Math.ceil(a)-1,0):a}${t===`functionCognitiveComplexity`?re(e):``})`).join(`, `);return`${e.level}: ${ie(e,t)}`}function re({largestBlocks:e=[]}){let t=e.map(({startLine:e,endLine:t,name:n,cognitiveComplexity:r})=>`L${e}${t===e?``:`-${t}`}${n?` ${Q(n)}`:``} ${r}`);return t.length>0?`; largest parts ${t.join(`, `)}`:``}function ie(e,t){switch(e.kind){case`file`:return`${e.file}: ${t}`;case`function`:return`${$(e)} ${Q(e.name??``)}: ${t}`;case`duplication`:{let n=(e.partners??[]).map(e=>$(e)),r=[...n.slice(0,3),...n.length>3?[`...`]:[]];return`${$(e)}: ${t}${r.length>0?`, also at ${r.join(`, `)}`:``}`}}}function Q(e){return e.replaceAll(/\s*[\n\r]\s*/gu,` `)}function $({file:e,startLine:t,endLine:n}){return`${e}:${t}-${n}`}function ae(e,t,n){T(JSON.stringify({passed:!I(n.violations)&&t.errors.length===0,base:e.base,mergeBase:t.mergeBase,summary:{...Z(n.violations),checkedFileCount:t.files.length,checkedFunctionCount:n.checkedFunctionCount,allowedLimitCount:n.allowedLimitCount},violations:n.violations,directiveProblems:n.directiveProblems,errors:t.errors,warnings:t.warnings},void 0,2)+`
|
|
4
|
+
`)}export{P as runCheckCommand,R as scanCheckScope};
|
|
5
5
|
//# sourceMappingURL=checkCommand.js.map
|