code-gauge 1.14.0 → 3.0.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 +97 -108
- package/dist/cli.cjs +3 -3
- package/dist/cli.cjs.map +1 -1
- package/dist/cli.js +3 -3
- package/dist/cli.js.map +1 -1
- package/dist/cliConfig.cjs +1 -1
- package/dist/cliConfig.cjs.map +1 -1
- package/dist/cliConfig.d.ts +16 -84
- package/dist/cliConfig.js +1 -1
- package/dist/cliConfig.js.map +1 -1
- package/dist/crossFileDuplication.cjs +1 -1
- package/dist/crossFileDuplication.cjs.map +1 -1
- package/dist/crossFileDuplication.d.ts +24 -10
- package/dist/crossFileDuplication.js +1 -1
- package/dist/crossFileDuplication.js.map +1 -1
- package/dist/duplication.cjs +1 -1
- package/dist/duplication.cjs.map +1 -1
- package/dist/duplication.d.ts +153 -4
- package/dist/duplication.js +1 -1
- package/dist/duplication.js.map +1 -1
- package/dist/index.cjs +1 -1
- package/dist/index.d.ts +3 -3
- package/dist/index.js +1 -1
- package/dist/languages.cjs +1 -1
- package/dist/languages.cjs.map +1 -1
- package/dist/languages.js +1 -1
- package/dist/languages.js.map +1 -1
- package/dist/metrics.cjs +1 -2
- package/dist/metrics.cjs.map +1 -1
- package/dist/metrics.d.ts +9 -4
- package/dist/metrics.js +1 -2
- package/dist/metrics.js.map +1 -1
- package/dist/nativeMetrics.cjs +1 -1
- package/dist/nativeMetrics.cjs.map +1 -1
- package/dist/nativeMetrics.d.ts +2 -2
- package/dist/nativeMetrics.js +1 -1
- package/dist/nativeMetrics.js.map +1 -1
- package/dist/ncss.cjs +1 -1
- package/dist/ncss.cjs.map +1 -1
- package/dist/ncss.d.ts +11 -5
- package/dist/ncss.js +1 -1
- package/dist/ncss.js.map +1 -1
- package/dist/types.d.ts +14 -79
- package/package.json +7 -5
- package/dist/architectureMetrics.cjs +0 -2
- package/dist/architectureMetrics.cjs.map +0 -1
- package/dist/architectureMetrics.d.ts +0 -41
- package/dist/architectureMetrics.js +0 -2
- package/dist/architectureMetrics.js.map +0 -1
- package/dist/typescriptProject.cjs +0 -3
- package/dist/typescriptProject.cjs.map +0 -1
- package/dist/typescriptProject.d.ts +0 -24
- package/dist/typescriptProject.js +0 -3
- package/dist/typescriptProject.js.map +0 -1
package/README.md
CHANGED
|
@@ -5,7 +5,11 @@
|
|
|
5
5
|
[](https://github.com/semantic-release/semantic-release)
|
|
6
6
|
[](https://github.com/WillBooster/shared/tree/main/packages/wbfy)
|
|
7
7
|
|
|
8
|
-
A command-line tool
|
|
8
|
+
A command-line tool that ranks the files of a project by refactoring priority, built for AI-agent
|
|
9
|
+
workflows: an agent asked to "refactor this repository" runs `code-gauge` and starts from the top of
|
|
10
|
+
the list. Measurement uses tree-sitter, and the output is deliberately small — only the metrics that
|
|
11
|
+
tell an agent _what to change_ are measured and reported, so nothing in the output anchors an agent
|
|
12
|
+
toward out-of-scope "improvements". A [programmatic API](#programmatic-api) is also available.
|
|
9
13
|
|
|
10
14
|
## Getting started
|
|
11
15
|
|
|
@@ -18,150 +22,135 @@ npm install -g code-gauge
|
|
|
18
22
|
code-gauge path/to/project
|
|
19
23
|
```
|
|
20
24
|
|
|
21
|
-
The CLI scans JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++ files. By
|
|
25
|
+
The CLI scans JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++ files. By
|
|
26
|
+
default it skips generated, vendor, test, and tool directories and prints the top 10 refactoring
|
|
27
|
+
candidates:
|
|
22
28
|
|
|
23
|
-
|
|
29
|
+
```
|
|
30
|
+
Measured 123 files under /path/to/project (code LOC 45678, NCSS 23456, functions 1789)
|
|
31
|
+
|
|
32
|
+
Refactoring candidates (top 10 of 123):
|
|
33
|
+
1. src/metrics.ts (score 2.87): worst function measure (L120-310) cognitive 42, NCSS 220, nesting 6; duplicated lines 180 (20%, shared with src/other.ts); file NCSS 1240
|
|
34
|
+
...
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
## Ranking model
|
|
24
38
|
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
| `--config <path>` | Use this config file instead of the auto-detected `code-gauge.config.json`. |
|
|
28
|
-
| `--include-tests` | Include test files and test directories. |
|
|
29
|
-
| `--tsconfig <path>` | Use this `tsconfig.json` instead of the auto-detected one. |
|
|
30
|
-
| `--max-findings <n>` | Maximum number of findings to print (default: 20). |
|
|
31
|
-
| `--largest-files <n>` | List the `n` largest files by code LOC (config key: `largestFiles`). |
|
|
32
|
-
| `--json` | Print machine-readable JSON. |
|
|
33
|
-
| `--fail-on-risk` | Exit with code 1 when any high-risk finding is reported. |
|
|
34
|
-
| `--fail-on-error` | Exit with code 1 when any file or directory cannot be scanned. |
|
|
35
|
-
| `--<metric>-threshold <n>` | Override a risk threshold (see below). |
|
|
39
|
+
Every file gets a score that is the sum of its repository-relative percentile ranks (each in `[0, 1)`)
|
|
40
|
+
over three dimensions:
|
|
36
41
|
|
|
37
|
-
|
|
42
|
+
- **Worst-function cognitive complexity** — the SonarSource cognitive-complexity model, the
|
|
43
|
+
measure of understanding effort with the strongest empirical support among structural metrics.
|
|
44
|
+
- **Duplicated lines** — distinct lines covered by within-file duplicate blocks or cross-file
|
|
45
|
+
duplicate occurrences (Type-1/2 clones, gapped and near-miss Type-3 clones).
|
|
46
|
+
- **File NCSS** — non-commenting source statements, a comment- and formatting-independent size
|
|
47
|
+
measure calibrated against PMD's `NcssCount`.
|
|
38
48
|
|
|
39
|
-
|
|
49
|
+
Ranking is relative to the scanned project, so no absolute thresholds are involved: the top of the
|
|
50
|
+
list is worth refactoring first regardless of where any cutoff would sit. Each reported file carries
|
|
51
|
+
the concrete evidence (worst function with location, duplication with partner files, file size) so
|
|
52
|
+
an agent can act on it directly.
|
|
40
53
|
|
|
41
|
-
|
|
54
|
+
## Options
|
|
55
|
+
|
|
56
|
+
| Option | Description |
|
|
57
|
+
| ------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
58
|
+
| `--config <path>` | Use this config file instead of the auto-detected `code-gauge.config.json`. |
|
|
59
|
+
| `--top <n>` | Number of top-ranked files to report (default: 10). |
|
|
60
|
+
| `--include-tests` | Include test files and test directories. |
|
|
61
|
+
| `--json` | Print machine-readable JSON. |
|
|
62
|
+
| `--fail-on-error` | Exit with code 1 when any file or directory cannot be scanned. |
|
|
63
|
+
| `--duplication-min-tokens <n>` | Minimum normalized token count for a duplicate region (default 40). |
|
|
64
|
+
| `--duplication-max-gap-tokens <n>` | Maximum token gap merged into one gapped clone group; 0 disables (default 30). |
|
|
65
|
+
| `--duplication-min-similarity-percent <n>` | Minimum similarity percent for near-miss clones; 100 = exact only (default 70). |
|
|
66
|
+
|
|
67
|
+
## Configuration
|
|
68
|
+
|
|
69
|
+
`code-gauge` looks for `code-gauge.config.json` by walking up from the target directory (override
|
|
70
|
+
with `--config`). The following config reproduces every built-in default:
|
|
42
71
|
|
|
43
72
|
```json
|
|
44
73
|
{
|
|
45
|
-
"thresholds": {
|
|
46
|
-
"fileLoc": 500,
|
|
47
|
-
"functionLoc": 120,
|
|
48
|
-
"componentLoc": 350,
|
|
49
|
-
"cognitive": 25,
|
|
50
|
-
"cyclomatic": 20,
|
|
51
|
-
"call": 50,
|
|
52
|
-
"import": 25,
|
|
53
|
-
"fanOut": 10,
|
|
54
|
-
"parameter": 8,
|
|
55
|
-
"duplicateBlock": 2,
|
|
56
|
-
"duplicationRatioPercent": 30,
|
|
57
|
-
"crossFileDuplicateBlock": 2,
|
|
58
|
-
"transitiveDependency": 25,
|
|
59
|
-
"structuralBreadth": 8,
|
|
60
|
-
"structuralCoordination": 300,
|
|
61
|
-
"stateMutation": 50,
|
|
62
|
-
"duplicateSymbolGroup": 5
|
|
63
|
-
},
|
|
64
74
|
"duplication": {
|
|
65
75
|
"minTokens": 40,
|
|
66
76
|
"maxGapTokens": 30,
|
|
67
77
|
"minSimilarityPercent": 70
|
|
68
78
|
},
|
|
69
|
-
"
|
|
70
|
-
"python": { "stateMutation": 90, "structuralCoordination": 350 },
|
|
71
|
-
"ruby": { "stateMutation": 90, "structuralCoordination": 350 },
|
|
72
|
-
"react": { "import": 30 }
|
|
73
|
-
},
|
|
74
|
-
"maxFindings": 20,
|
|
79
|
+
"rank": { "top": 10 },
|
|
75
80
|
"includeTests": false,
|
|
76
|
-
"failOnRisk": false,
|
|
77
81
|
"failOnError": false
|
|
78
82
|
}
|
|
79
83
|
```
|
|
80
84
|
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
| `fileLoc` | `--file-loc-threshold` | file's code LOC is large. |
|
|
84
|
-
| `functionLoc` | `--function-loc-threshold` | function's physical LOC span is large. |
|
|
85
|
-
| `componentLoc` | `--component-loc-threshold` | React component's physical LOC span is large. |
|
|
86
|
-
| `cognitive` | `--cognitive-threshold` | function's cognitive complexity is high. |
|
|
87
|
-
| `cyclomatic` | `--cyclomatic-threshold` | function's cyclomatic complexity is high. |
|
|
88
|
-
| `call` | `--call-threshold` | function makes many calls. |
|
|
89
|
-
| `import` | `--import-threshold` | file has many unique import sources. |
|
|
90
|
-
| `fanOut` | `--fan-out-threshold` | function calls many other in-file functions. |
|
|
91
|
-
| `parameter` | `--parameter-threshold` | function declares many parameters. |
|
|
92
|
-
| `duplicateBlock` | `--duplicate-block-threshold` | file contains copy-pasted code blocks. |
|
|
93
|
-
| `duplicationRatioPercent` | `--duplication-ratio-percent-threshold` | large percentage of a file's code lines is duplicated. |
|
|
94
|
-
| `crossFileDuplicateBlock` | `--cross-file-duplicate-block-threshold` | file shares copy-pasted code blocks with other files. |
|
|
95
|
-
| `transitiveDependency` | `--transitive-dependency-threshold` | file transitively reaches many local files. |
|
|
96
|
-
| `structuralBreadth` | `--structural-breadth-threshold` | file coordinates many structural concerns. |
|
|
97
|
-
| `structuralCoordination` | `--structural-coordination-threshold` | file's structural coordination score is high. |
|
|
98
|
-
| `stateMutation` | `--state-mutation-threshold` | file mutates state heavily. |
|
|
99
|
-
| `duplicateSymbolGroup` | `--duplicate-symbol-group-threshold` | file shares many duplicated symbols with others. |
|
|
100
|
-
|
|
101
|
-
### Per-language thresholds
|
|
102
|
-
|
|
103
|
-
Some metrics distribute very differently by language or file type, so a single global threshold either
|
|
104
|
-
over-flags one language or under-flags another. `languageThresholds` overrides individual thresholds for a
|
|
105
|
-
profile without repeating the whole set. Each file resolves its thresholds as **base → its language profile →
|
|
106
|
-
the `react` profile** (the last applies when the file contains a React component), so later profiles win.
|
|
107
|
-
|
|
108
|
-
Valid profile keys are `javascript`, `jsx`, `typescript`, `tsx`, `python`, `go`, `rust`, `java`, `ruby`, `c`,
|
|
109
|
-
`cpp`, and `react`. Built-in
|
|
110
|
-
overrides raise `stateMutation` and `structuralCoordination` for Python and Ruby (every binding is an
|
|
111
|
-
assignment, so these run far higher than in TypeScript) and raise `import` for React files (which pull in
|
|
112
|
-
many components).
|
|
113
|
-
Anything you specify is merged on top of the built-in overrides, so `{ "python": { "stateMutation": 8 } }`
|
|
114
|
-
restores the global value for Python while keeping the other built-in adjustments.
|
|
115
|
-
|
|
116
|
-
```json
|
|
117
|
-
{
|
|
118
|
-
"languageThresholds": {
|
|
119
|
-
"python": { "stateMutation": 120 },
|
|
120
|
-
"react": { "componentLoc": 400, "import": 35 }
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
```
|
|
124
|
-
|
|
125
|
-
Command-line `--<metric>-threshold` flags set the global base only; use the config file for per-language tuning.
|
|
85
|
+
The command line wins over the config file, which wins over the defaults. Unknown settings are
|
|
86
|
+
rejected so stale configuration fails loudly.
|
|
126
87
|
|
|
127
88
|
### Duplication detection settings
|
|
128
89
|
|
|
129
|
-
The `duplication`
|
|
90
|
+
The `duplication` section tunes how clones are detected:
|
|
130
91
|
|
|
131
|
-
- `minTokens` (default 40): minimum normalized token count for a region to count as a duplicate.
|
|
132
|
-
|
|
133
|
-
- `
|
|
92
|
+
- `minTokens` (default 40): minimum normalized token count for a region to count as a duplicate.
|
|
93
|
+
Raise it to report only substantial copies; lower it to catch small ones.
|
|
94
|
+
- `maxGapTokens` (default 30): copies edited in one spot split into two exact matches around the
|
|
95
|
+
edit; adjacent matches separated by at most this many tokens are merged back into a single gapped
|
|
96
|
+
(Type-3) clone group. `0` disables merging. Applies to within-file detection and to cross-file
|
|
97
|
+
matching alike.
|
|
98
|
+
- `minSimilarityPercent` (default 70): blocks the exact pipeline misses are additionally compared by
|
|
99
|
+
similarity (n-gram filtration, then token-level longest-common-subsequence verification, following
|
|
100
|
+
NIL and NiCad), so a near-miss (Type-3) clone with scattered small edits is still reported when
|
|
101
|
+
both blocks are at least this similar and share more than half of their content-bearing tokens.
|
|
102
|
+
`100` disables near-miss detection. Applies to within-file detection only.
|
|
134
103
|
|
|
135
|
-
Custom detection settings are measured by the TypeScript backend; the native backend implements the
|
|
104
|
+
Custom detection settings are measured by the TypeScript backend; the native backend implements the
|
|
105
|
+
defaults only.
|
|
136
106
|
|
|
137
107
|
## Metrics
|
|
138
108
|
|
|
109
|
+
`measureCode` reports, per file:
|
|
110
|
+
|
|
139
111
|
- Physical LOC, code lines, comment-only lines, and blank lines
|
|
140
|
-
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
-
|
|
148
|
-
-
|
|
149
|
-
-
|
|
150
|
-
|
|
112
|
+
- Per-function cognitive complexity (following the SonarSource specification, except its recursion
|
|
113
|
+
increment, which is not counted; cross-validated against PMD's Java rules), plus the file-level
|
|
114
|
+
total and maximum
|
|
115
|
+
- Per-function and per-file NCSS (non-commenting source statements), calibrated against PMD's
|
|
116
|
+
`NcssCount` rule for Java and generalized to every supported language; unlike PMD, package and
|
|
117
|
+
import declarations count, and statement-shaped content is counted uniformly in expression
|
|
118
|
+
positions too
|
|
119
|
+
- Per-function and file-level nesting depth
|
|
120
|
+
- Per-function parameter counts and locations (name, node type, line span)
|
|
121
|
+
- Within-file duplication: copy-pasted blocks matched on normalized tokens (identifiers anonymized
|
|
122
|
+
consistently, literals by kind, and literal-dense data tables excluded unless their values also
|
|
123
|
+
match), with adjacent matches around a small edit merged into gapped (Type-3) clone groups and
|
|
124
|
+
near-miss (Type-3) clones matched by token-LCS similarity, plus duplicated line count and ratio
|
|
125
|
+
- Cross-file duplication (via `measureCrossFileDuplication`): copy-pasted blocks shared between
|
|
126
|
+
files, matched with the same normalization and reported as groups with their file locations
|
|
127
|
+
- Halstead base counts, vocabulary, length, volume, and effort
|
|
128
|
+
|
|
129
|
+
Metrics that the validation literature shows to be weakly grounded or that invite misdirected
|
|
130
|
+
"improvements" (cyclomatic complexity, call-graph fan-in/fan-out, coupling and cohesion counts,
|
|
131
|
+
maintainability index, and similar) are intentionally not measured; see
|
|
132
|
+
[issue #44](https://github.com/WillBooster/code-gauge/issues/44) for the rationale and references.
|
|
151
133
|
|
|
152
134
|
## Supported languages
|
|
153
135
|
|
|
154
|
-
Built-in parsers cover JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++.
|
|
136
|
+
Built-in parsers cover JavaScript, JSX, TypeScript, TSX, Python, Go, Rust, Java, Ruby, C, and C++.
|
|
137
|
+
Additional tree-sitter grammars can be registered with `TreeMeasurer.registerLanguage`.
|
|
155
138
|
|
|
156
139
|
## Native (Rust) backend
|
|
157
140
|
|
|
158
|
-
Measurement is also implemented as a Rust addon that produces bit-identical metrics roughly 13x
|
|
141
|
+
Measurement is also implemented as a Rust addon that produces bit-identical metrics roughly 13x
|
|
142
|
+
faster than the TypeScript backend (the tree-sitter grammar crates are pinned to the same versions
|
|
143
|
+
as the npm grammar packages). With a [Rust toolchain](https://rustup.rs) installed, build it once:
|
|
159
144
|
|
|
160
145
|
```sh
|
|
161
146
|
yarn build-native
|
|
162
147
|
```
|
|
163
148
|
|
|
164
|
-
`measureCode` and the CLI pick up `native/code-gauge.node` automatically and fall back to the
|
|
149
|
+
`measureCode` and the CLI pick up `native/code-gauge.node` automatically and fall back to the
|
|
150
|
+
TypeScript implementation when the addon is missing (for example, on npm installs) or when a custom
|
|
151
|
+
language has been registered. Set `CODE_GAUGE_NATIVE=0` to force the TypeScript backend, and compare
|
|
152
|
+
both with `yarn benchmark` (requires `yarn build` first). `isNativeBackendAvailable()` reports which
|
|
153
|
+
backend is in use.
|
|
165
154
|
|
|
166
155
|
## Programmatic API
|
|
167
156
|
|
|
@@ -180,5 +169,5 @@ function score(value) {
|
|
|
180
169
|
{ language: 'javascript' }
|
|
181
170
|
);
|
|
182
171
|
|
|
183
|
-
console.log(metrics.
|
|
172
|
+
console.log(metrics.maxCognitiveComplexity);
|
|
184
173
|
```
|
package/dist/cli.cjs
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
|
-
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./architectureMetrics.cjs"),i=require("./cliConfig.cjs"),a=require("./typescriptProject.cjs");let o=require("node:fs/promises"),s=require("node:os");s=e.__toESM(s,1);let c=require("node:path");c=e.__toESM(c,1);let l=require("commander");const u=new Map([[`.c`,`c`],[`.c++`,`cpp`],[`.cc`,`cpp`],[`.cjs`,`javascript`],[`.cp`,`cpp`],[`.cpp`,`cpp`],[`.tcc`,`cpp`],[`.cts`,`typescript`],[`.cxx`,`cpp`],[`.go`,`go`],[`.h`,`cpp`],[`.hh`,`cpp`],[`.hpp`,`cpp`],[`.hxx`,`cpp`],[`.java`,`java`],[`.js`,`javascript`],[`.jsx`,`jsx`],[`.mjs`,`javascript`],[`.mts`,`typescript`],[`.py`,`python`],[`.rb`,`ruby`],[`.rs`,`rust`],[`.ts`,`typescript`],[`.tsx`,`tsx`]]),d=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`target`,`test-fixtures`,`vendor`,`venv`]),f=new Set([`__tests__`,`test`,`tests`,`spec`]),p=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,m=/Test\.java$/u;h().catch(e=>{Q(`Error: ${$(e)}\n`),process.exitCode=1});async function h(){let e=new l.Command().name(`code-gauge`).description(`Measure code metrics and list high-risk findings.`).argument(`[target]`,`file or directory to measure`,`.`).option(`--config <path>`,`config file to use instead of the auto-detected ${i.configFileName}`).option(`--file-loc-threshold <number>`,`minimum file code LOC to report`,Y).option(`--function-loc-threshold <number>`,`minimum function physical LOC span to report`,Y).option(`--component-loc-threshold <number>`,`minimum React component physical LOC span to report`,Y).option(`--cognitive-threshold <number>`,`minimum cognitive complexity to report`,Y).option(`--cyclomatic-threshold <number>`,`minimum cyclomatic complexity to report`,Y).option(`--call-threshold <number>`,`minimum function call count to report`,Y).option(`--import-threshold <number>`,`minimum unique import sources per file to report`,Y).option(`--fan-out-threshold <number>`,`minimum intra-file fan-out per function to report`,Y).option(`--parameter-threshold <number>`,`minimum function parameter count to report`,Y).option(`--duplicate-block-threshold <number>`,`minimum count of duplicated code blocks per file to report`,Y).option(`--duplication-ratio-percent-threshold <number>`,`minimum percentage (1-100) of duplicated lines per file to report`,J).option(`--cross-file-duplicate-block-threshold <number>`,`minimum count of cross-file duplicate block groups per file to report`,Y).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,Y).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,ce).option(`--duplication-min-similarity-percent <number>`,`minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)`,J).option(`--transitive-dependency-threshold <number>`,`minimum transitively reachable local files to report`,Y).option(`--structural-breadth-threshold <number>`,`minimum structural breadth score to report`,Y).option(`--structural-coordination-threshold <number>`,`minimum structural coordination score to report`,Y).option(`--state-mutation-threshold <number>`,`minimum state mutation score to report`,Y).option(`--duplicate-symbol-group-threshold <number>`,`minimum duplicate symbol group count to report`,Y).option(`--max-findings <number>`,`maximum number of risk findings to print`,Y).option(`--largest-files <number>`,`number of largest files by code LOC to list`,Y).option(`--include-tests`,`include test files and test directories`).option(`--tsconfig <path>`,`TypeScript project file to use instead of auto-detected tsconfig.json`).option(`--json`,`print JSON output`).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`).option(`--fail-on-risk`,`exit with code 1 when high-risk findings are found`);e.action(async(e,t)=>{let n=g(e),r=await i.loadConfig(t.config,await _(n)),a=i.resolveOptions(t,r),o=await v(n,a);T(o),await ne(o),await ee(o,a,n);let s=ie(o.files,o.architecture,o.crossFileDuplication,o.componentFunctionKeys,o.namedComponentFunctionKeys,a,o.displayRoot);a.json?P(o,s,a):F(n,o,s,a),(o.fatalError||a.failOnError&&o.errors.length>0||a.failOnRisk&&s.length>0)&&(process.exitCode=1)}),await e.parseAsync()}function g(e){return e===`~`?s.default.homedir():e.startsWith(`~/`)?c.default.join(s.default.homedir(),e.slice(2)):c.default.resolve(e)}async function _(e){try{return(await(0,o.stat)(e)).isDirectory()?e:c.default.dirname(e)}catch{return c.default.dirname(e)}}async function v(e,t){let n=[],r=[],i=[],a=e;try{a=await(0,o.realpath)(e)}catch{}let s=c.default.dirname(a),l;try{l=await(0,o.stat)(a)}catch(e){let t=`${X(a,s)}: ${$(e)}`;return{displayRoot:s,files:n,errors:[t],warnings:i,fatalError:t}}if(l.isFile()){let e=c.default.dirname(a),o=q(a,t,!0);if(!o){let t=`${X(a,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:i,fatalError:t}}let s=y(t,n,r,i,e);return await w(a,o,`single-file`,s,a),{displayRoot:e,files:n,errors:r,warnings:i}}return await S(a,y(t,n,r,i,a)),{displayRoot:a,files:n,errors:r,warnings:i}}function y(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function ee(e,t,n){if(e.fatalError||e.files.length===0)return;let r=t.tsconfig,i=r!==void 0;if(!i&&!e.files.some(({file:e})=>b(e)))return;let o=r?g(r):await x(n);if(o)try{e.typeScriptProject=await a.measureTypeScriptProject(o,e.files.map(({file:e})=>e)),e.componentFunctionKeys=new Set(e.typeScriptProject.reactComponentFunctions.map(e=>A(e.file,e.startLine,e.startColumn))),e.namedComponentFunctionKeys=new Set(e.typeScriptProject.reactComponentFunctions.flatMap(e=>e.name?[j(e.file,e.name,e.startLine)]:[]))}catch(t){i&&e.errors.push(`${X(o,e.displayRoot)}: ${$(t)}`)}}function b(e){return[`.cjs`,`.cts`,`.js`,`.jsx`,`.mjs`,`.mts`,`.ts`,`.tsx`].includes(c.default.extname(e))}async function x(e){let t=(await(0,o.stat)(e)).isDirectory()?e:c.default.dirname(e);for(;;){let e=c.default.join(t,`tsconfig.json`);if(await te(e))return e;let n=c.default.dirname(t);if(n===t)return;t=n}}async function te(e){try{return(await(0,o.stat)(e)).isFile()}catch{return!1}}async function ne(e){if(!e.fatalError)try{e.architecture=r.measureArchitecture(e.files.map(({file:e,metrics:t})=>({file:e,metrics:t})),e.displayRoot)}catch(t){e.errors.push(`architecture metrics: ${$(t)}`)}}async function S(e,t){let n;try{n=await(0,o.realpath)(e)}catch(n){t.errors.push(`${X(e,t.rootDirectory)}: ${$(n)}`);return}if(!K(n,t.rootDirectory)||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r;try{r=await(0,o.readdir)(e,{withFileTypes:!0})}catch(n){t.errors.push(`${X(e,t.rootDirectory)}: ${$(n)}`);return}for(let n of r){let r=c.default.join(e,n.name);if(n.isSymbolicLink()){await re(n.name,r,t);continue}if(n.isDirectory()){if(G(n.name,t.options))continue;await S(r,t);continue}n.isFile()&&await C(r,t)}}async function re(e,t,n){let r;try{r=await(0,o.realpath)(t)}catch(e){n.errors.push(`${X(t,n.rootDirectory)}: ${$(e)}`);return}if(!K(r,n.rootDirectory))return;let i;try{i=await(0,o.stat)(t)}catch(e){n.errors.push(`${X(t,n.rootDirectory)}: ${$(e)}`);return}if(i.isDirectory()){if(G(e,n.options)||G(c.default.basename(r),n.options))return;await S(t,n);return}i.isFile()&&await C(t,n,r,r)}async function C(e,t,n=e,r){let i=q(n,t.options);i&&await w(e,i,`directory`,t,r)}async function w(e,t,r,i,a){try{let s=a??await(0,o.realpath)(e);if(i.visitedFiles.has(s))return;i.visitedFiles.add(s);let c=await(0,o.readFile)(e,`utf8`),l={language:t,duplication:i.options.duplication},u={file:e,metrics:n.measureCode(c,l)};if(r===`directory`)try{u.duplicationCandidates=n.collectDuplicationCandidates(c,l)}catch(t){i.warnings.push(`${X(e,i.rootDirectory)}: cross-file duplication candidates unavailable: ${$(t)}`)}i.files.push(u)}catch(t){i.errors.push(`${X(e,i.rootDirectory)}: ${$(t)}`)}}function T(e){if(e.fatalError||e.files.length<2)return;let n=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:X(t,e.displayRoot),candidates:n}]:[]);n.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(n))}function ie(e,t,n,r,a,o,s){let c=new Map(t?.files.map(e=>[e.file,e])),l=e.flatMap(({file:e,metrics:t})=>{let l=t.functions.some(t=>t.returnsJsx||k(e,t,r,a)),u=i.resolveThresholds(o,t.language,l);return[...ae(e,t,c.get(X(e,s)),n,u,s),...t.functions.flatMap(n=>oe(e,t.language,n,u,s,r,a))]});return l.sort(N),l}function ae(e,t,n,r,i,a){let o=[],s=X(e,a);E(o,`file LOC`,t.lines.code,i.fileLoc),E(o,`import sources`,t.coupling.importSourceCount,i.import);let c=O(t.duplication.duplicateBlockGroups);return E(o,`duplicated blocks`,t.duplication.duplicateBlockCount,i.duplicateBlock,c),E(o,`duplicated lines (%)`,Math.floor(t.duplication.duplicationRatio*100),i.duplicationRatioPercent,c),r&&E(o,`cross-file duplicated blocks`,Object.hasOwn(r.duplicateBlockGroupCountByFile,s)?r.duplicateBlockGroupCountByFile[s]??0:0,i.crossFileDuplicateBlock,D(r,s)),n&&((t.lines.code>=100||n.directLocalDependencyCount>=8)&&E(o,`transitive local dependencies`,n.transitiveLocalDependencyCount,i.transitiveDependency),(o.length>0||n.directLocalDependencyCount>=8||n.structuralCoordination.score>=i.structuralCoordination)&&E(o,`structural breadth`,n.structuralBreadthScore,i.structuralBreadth),E(o,`structural coordination`,n.structuralCoordination.score,i.structuralCoordination),E(o,`state mutation`,n.structuralCoordination.stateMutationScore,i.stateMutation),E(o,`duplicate symbol groups`,n.duplicateSymbolGroupCount,i.duplicateSymbolGroup)),o.length===0?[]:[{file:s,language:t.language,kind:`file`,cyclomaticComplexity:t.cyclomaticComplexity,cognitiveComplexity:t.cognitiveComplexity,triggers:o,score:M(o)}]}function oe(e,t,n,r,i,a,o){let s=n.endLine-n.startLine+1,c=k(e,n,a,o),l=c?`component`:`function`,u=[];return E(u,`cognitive complexity`,n.cognitiveComplexity,r.cognitive),E(u,`cyclomatic complexity`,n.cyclomaticComplexity,r.cyclomatic),E(u,c?`component LOC`:`function LOC`,s,se(c,r)),E(u,`function calls`,n.callCount,r.call),E(u,`fan-out`,n.fanOut,r.fanOut),E(u,`parameters`,n.parameterCount,r.parameter),u.length===0?[]:[{file:X(e,i),language:t,kind:l,name:n.name??`<anonymous>`,startLine:n.startLine,endLine:n.endLine,cyclomaticComplexity:n.cyclomaticComplexity,cognitiveComplexity:n.cognitiveComplexity,triggers:u,score:M(u)}]}function E(e,t,n,r,i){n<r||e.push({metric:t,value:n,threshold:r,score:n/r,detail:i})}function D(e,t){let n=e.groups.filter(e=>e.files.includes(t));if(n.length!==0)return`${n.slice(0,3).map(e=>e.occurrences.map(({file:e,startLine:n,endLine:r})=>e===t?`${n}-${r}`:`${e}:${n}-${r}`).join(` ~ `)).join(`; `)}${n.length>3?`; ...`:``}`}function O(e){if(e.length!==0)return e.map(e=>e.map(({startLine:e,endLine:t})=>`${e}-${t}`).join(` ~ `)).join(`; `)}function k(e,t,n,r){return n?.has(A(e,t.startLine,t.startColumn))||(t.name?r?.has(j(e,t.name,t.startLine)):!1)||!1}function se(e,t){return e?t.componentLoc:t.functionLoc}function A(e,t,n){return`${c.default.resolve(e)}:${t}:${n}`}function j(e,t,n){return`${c.default.resolve(e)}:${t}:${n}`}function M(e){return Math.max(...e.map(e=>e.score))}function N(e,t){return t.score-e.score||e.file.localeCompare(t.file)||(e.startLine??0)-(t.startLine??0)||(e.endLine??0)-(t.endLine??0)||e.kind.localeCompare(t.kind)}function P(e,t,n){let r=W(e.files),i=t.slice(0,n.maxFindings);Z(JSON.stringify({summary:r,thresholds:n.thresholds,profileThresholds:n.profileThresholds,totalRisks:t.length,truncated:i.length<t.length,largestFiles:n.largestFiles>0?I(e.files,n.largestFiles,e.displayRoot):void 0,architecture:e.architecture,crossFileDuplication:e.crossFileDuplication,typeScriptProject:e.typeScriptProject,risks:i,errors:e.errors,warnings:e.warnings},void 0,2)+`
|
|
3
|
-
`)}function
|
|
4
|
-
`);else{let e=n.slice(0,r.
|
|
2
|
+
"use strict";const e=require("./_virtual/_rolldown/runtime.cjs"),t=require("./crossFileDuplication.cjs"),n=require("./metrics.cjs"),r=require("./cliConfig.cjs");let i=require("node:fs/promises"),a=require("node:os");a=e.__toESM(a,1);let o=require("node:path");o=e.__toESM(o,1);let s=require("commander");const c=new Map([[`.c`,`c`],[`.c++`,`cpp`],[`.cc`,`cpp`],[`.cjs`,`javascript`],[`.cp`,`cpp`],[`.cpp`,`cpp`],[`.tcc`,`cpp`],[`.cts`,`typescript`],[`.cxx`,`cpp`],[`.go`,`go`],[`.h`,`cpp`],[`.hh`,`cpp`],[`.hpp`,`cpp`],[`.hxx`,`cpp`],[`.java`,`java`],[`.js`,`javascript`],[`.jsx`,`jsx`],[`.mjs`,`javascript`],[`.mts`,`typescript`],[`.py`,`python`],[`.rb`,`ruby`],[`.rs`,`rust`],[`.ts`,`typescript`],[`.tsx`,`tsx`]]),l=new Set([`.agents`,`.claude`,`.cursor`,`.git`,`.next`,`.playwright-cli`,`.tox`,`.tmp`,`.turbo`,`.venv`,`.yarn`,`__fixtures__`,`__generated__`,`__pycache__`,`coverage`,`dist`,`fixtures`,`generated`,`node_modules`,`target`,`test-fixtures`,`vendor`,`venv`]),u=new Set([`__tests__`,`test`,`tests`,`spec`]),d=/(?:^test(?:[_-].*)?|\.(?:spec|test)|[_-](?:test|spec))\.[^.]+$/iu,f=/Test\.java$/u;p().catch(e=>{B(`Error: ${V(e)}\n`),process.exitCode=1});async function p(){let e=new s.Command().name(`code-gauge`).description(`Rank the files of a project by refactoring priority.`).argument(`[target]`,`file or directory to measure`,`.`).option(`--config <path>`,`config file to use instead of the auto-detected ${r.configFileName}`).option(`--top <number>`,`number of top-ranked files to report (default: 10)`,L).option(`--duplication-min-tokens <number>`,`minimum normalized token count for a duplicate region (default 40)`,L).option(`--duplication-max-gap-tokens <number>`,`maximum token gap merged into one gapped clone group; 0 disables merging (default 30)`,I).option(`--duplication-min-similarity-percent <number>`,`minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)`,F).option(`--include-tests`,`include test files and test directories`).option(`--json`,`print JSON output`).option(`--fail-on-error`,`exit with code 1 when files or directories cannot be scanned`);e.action(async(e,t)=>{let n=m(e),i=await r.loadConfig(t.config,await h(n)),a=r.resolveOptions(t,i),o=await g(n,a);S(o,a);let s=C(o,a.top);a.json?O(o,s,a):k(n,o,s,a),(o.fatalError||a.failOnError&&o.errors.length>0)&&(process.exitCode=1)}),await e.parseAsync()}function m(e){return e===`~`?a.default.homedir():e.startsWith(`~/`)?o.default.join(a.default.homedir(),e.slice(2)):o.default.resolve(e)}async function h(e){try{return(await(0,i.stat)(e)).isDirectory()?e:o.default.dirname(e)}catch{return o.default.dirname(e)}}async function g(e,t){let n=[],r=[],a=[],s=e;try{s=await(0,i.realpath)(e)}catch{}let c=o.default.dirname(s),l;try{l=await(0,i.stat)(s)}catch(e){let t=`${R(s,c)}: ${V(e)}`;return{displayRoot:c,files:n,errors:[t],warnings:a,fatalError:t}}if(l.isFile()){let e=o.default.dirname(s),i=P(s,t,!0);if(!i){let t=`${R(s,e)}: unsupported file type`;return{displayRoot:e,files:n,errors:[t],warnings:a,fatalError:t}}let c=_(t,n,r,a,e);return await x(s,i,`single-file`,c,s),{displayRoot:e,files:n,errors:r,warnings:a}}return await v(s,_(t,n,r,a,s)),{displayRoot:s,files:n,errors:r,warnings:a}}function _(e,t,n,r,i){return{options:e,files:t,errors:n,warnings:r,visitedDirectories:new Set,visitedFiles:new Set,rootDirectory:i}}async function v(e,t){let n;try{n=await(0,i.realpath)(e)}catch(n){t.errors.push(`${R(e,t.rootDirectory)}: ${V(n)}`);return}if(!N(n,t.rootDirectory)||t.visitedDirectories.has(n))return;t.visitedDirectories.add(n);let r;try{r=await(0,i.readdir)(e,{withFileTypes:!0})}catch(n){t.errors.push(`${R(e,t.rootDirectory)}: ${V(n)}`);return}for(let n of r){let r=o.default.join(e,n.name);if(n.isSymbolicLink()){await y(n.name,r,t);continue}if(n.isDirectory()){if(M(n.name,t.options))continue;await v(r,t);continue}n.isFile()&&await b(r,t)}}async function y(e,t,n){let r;try{r=await(0,i.realpath)(t)}catch(e){n.errors.push(`${R(t,n.rootDirectory)}: ${V(e)}`);return}if(!N(r,n.rootDirectory))return;let a;try{a=await(0,i.stat)(t)}catch(e){n.errors.push(`${R(t,n.rootDirectory)}: ${V(e)}`);return}if(a.isDirectory()){if(M(e,n.options)||M(o.default.basename(r),n.options))return;await v(t,n);return}a.isFile()&&await b(t,n,r,r)}async function b(e,t,n=e,r){let i=P(n,t.options);i&&await x(e,i,`directory`,t,r)}async function x(e,t,r,a,o){try{let s=o??await(0,i.realpath)(e);if(a.visitedFiles.has(s))return;a.visitedFiles.add(s);let c=await(0,i.readFile)(e,`utf8`),l={language:t,duplication:a.options.duplication},u={file:e,metrics:n.measureCode(c,l)};if(r===`directory`)try{u.duplicationCandidates=n.collectCrossFileDuplicationFileData(c,l)}catch(t){a.warnings.push(`${R(e,a.rootDirectory)}: cross-file duplication candidates unavailable: ${V(t)}`)}a.files.push(u)}catch(t){a.errors.push(`${R(e,a.rootDirectory)}: ${V(t)}`)}}function S(e,n){if(e.fatalError||e.files.length<2)return;let r=e.files.flatMap(({file:t,duplicationCandidates:n})=>n?[{file:R(t,e.displayRoot),...n}]:[]);r.length<2||(e.crossFileDuplication=t.measureCrossFileDuplication(r,n.duplication))}function C(e,t){let n=e.files.map(({file:t,metrics:n})=>{let r=R(t,e.displayRoot),i=D(n,e.crossFileDuplication,r);return{file:r,worstFunction:E(n.functions),duplicatedLineCount:i.size,duplicatedLineRatio:n.lines.code===0?0:i.size/n.lines.code,crossFilePartners:[],ncss:n.ncssCount,codeLines:n.lines.code}}),r=T(n.map(e=>e.worstFunction?.cognitiveComplexity??0)),i=T(n.map(e=>e.duplicatedLineCount)),a=T(n.map(e=>e.ncss)),o=n.map(e=>({...e,score:r(e.worstFunction?.cognitiveComplexity??0)+i(e.duplicatedLineCount)+a(e.ncss)})).toSorted((e,t)=>t.score-e.score||t.ncss-e.ncss||e.file.localeCompare(t.file));return w(o.slice(0,t),e.crossFileDuplication),o}function w(e,t){if(!t)return;let n=new Map(e.map(e=>[e.file,new Set]));for(let e of t.groups)for(let t of e.files){let r=n.get(t);if(r)for(let n of e.files)n!==t&&r.add(n)}for(let t of e)t.crossFilePartners=[...n.get(t.file)??[]].toSorted()}function T(e){let t=e.toSorted((e,t)=>e-t);return e=>{let n=0,r=t.length;for(;n<r;){let i=n+r>>1;t[i]<e?n=i+1:r=i}return t.length===0?0:n/t.length}}function E(e){let t;for(let n of e)(!t||n.cognitiveComplexity>t.cognitiveComplexity||n.cognitiveComplexity===t.cognitiveComplexity&&n.ncss>t.ncss)&&(t=n);if(t)return{name:t.name??`<anonymous>`,startLine:t.startLine,endLine:t.endLine,cognitiveComplexity:t.cognitiveComplexity,ncss:t.ncss,nestingDepth:t.nestingDepth}}function D(e,t,n){let r=new Set(e.duplication.duplicateLineNumbers),i=t&&Object.hasOwn(t.duplicateLineNumbersByFile,n)?t.duplicateLineNumbersByFile[n]??[]:[];for(let e of i)r.add(e);return r}function O(e,t,n){let r=t.slice(0,n.top);z(JSON.stringify({summary:j(e.files),totalRankedFiles:t.length,truncated:r.length<t.length,files:r,errors:e.errors,warnings:e.warnings},void 0,2)+`
|
|
3
|
+
`)}function k(e,t,n,r){if(t.fatalError){B(`Error: ${t.fatalError}\n`);return}let i=j(t.files);if(z(`Measured ${i.fileCount} files under ${e} (code LOC ${i.linesOfCode}, NCSS ${i.ncssCount}, functions ${i.functionCount})\n`),n.length===0)z(`No measurable files found.
|
|
4
|
+
`);else{let e=n.slice(0,r.top),t=n.length>e.length?` of ${n.length}`:``;z(`\nRefactoring candidates (top ${e.length}${t}):\n`);for(let[t,n]of e.entries())z(`${t+1}. ${A(n)}\n`)}if(t.warnings.length>0){B(`\nDegraded ${t.warnings.length} files (measured, but excluded from cross-file matching):\n`);for(let e of t.warnings.slice(0,10))B(`- ${e}\n`);t.warnings.length>10&&B(`- ... ${t.warnings.length-10} more\n`)}if(t.errors.length>0){B(`\nSkipped ${t.errors.length} files or directories:\n`);for(let e of t.errors.slice(0,10))B(`- ${e}\n`);t.errors.length>10&&B(`- ... ${t.errors.length-10} more\n`)}}function A(e){let t=[];if(e.worstFunction){let n=e.worstFunction;t.push(`worst function ${n.name} (L${n.startLine}-${n.endLine}) cognitive ${n.cognitiveComplexity}, NCSS ${n.ncss}, nesting ${n.nestingDepth}`)}if(e.duplicatedLineCount>0){let n=e.crossFilePartners.length>0?`, shared with ${e.crossFilePartners.slice(0,3).join(`, `)}${e.crossFilePartners.length>3?`, ...`:``}`:``;t.push(`duplicated lines ${e.duplicatedLineCount} (${Math.round(e.duplicatedLineRatio*100)}%${n})`)}return t.push(`file NCSS ${e.ncss}`),`${e.file} (score ${e.score.toFixed(2)}): ${t.join(`; `)}`}function j(e){let t=0,n=0,r=0,i=0;for(let a of e)t+=a.metrics.functions.length,n+=a.metrics.lines.code,r=Math.max(r,a.metrics.maxCognitiveComplexity),i+=a.metrics.ncssCount;return{fileCount:e.length,functionCount:t,linesOfCode:n,maxCognitiveComplexity:r,ncssCount:i}}function M(e,t){return l.has(e)?!0:!t.includeTests&&u.has(e)}function N(e,t){let n=o.default.relative(t,e);return n===``||n!==`..`&&!n.startsWith(`..${o.default.sep}`)&&!o.default.isAbsolute(n)}function P(e,t,n=!1){let r=e.toLowerCase();if(!(!n&&(r.endsWith(`.d.ts`)||r.endsWith(`.d.mts`)||r.endsWith(`.d.cts`)||r.endsWith(`.min.js`)||r.endsWith(`.pnp.cjs`)))&&!(!n&&!t.includeTests&&(d.test(o.default.basename(e))||f.test(o.default.basename(e)))))return o.default.extname(e)===`.C`?`cpp`:c.get(o.default.extname(r))}function F(e){let t=L(e);if(t>100)throw new s.InvalidArgumentError(`Expected an integer between 1 and 100.`);return t}function I(e){if(!/^\d+$/u.test(e))throw new s.InvalidArgumentError(`Expected a non-negative integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<0)throw new s.InvalidArgumentError(`Expected a non-negative integer.`);return t}function L(e){if(!/^[1-9]\d*$/u.test(e))throw new s.InvalidArgumentError(`Expected a positive integer.`);let t=Number(e);if(!Number.isSafeInteger(t)||t<1)throw new s.InvalidArgumentError(`Expected a positive integer.`);return t}function R(e,t){return o.default.relative(t,e)||o.default.basename(e)}function z(e){process.stdout.write(e)}function B(e){process.stderr.write(e)}function V(e){return e instanceof Error?e.message:String(e)}
|
|
5
5
|
//# sourceMappingURL=cli.cjs.map
|
package/dist/cli.cjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"cli.cjs","names":["Command","configFileName","loadConfig","resolveOptions","os","path","stat","realpath","measureTypeScriptProject","measureArchitecture","readdir","readFile","measureCode","collectDuplicationCandidates","measureCrossFileDuplication","resolveThresholds","InvalidArgumentError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { measureArchitecture, type ArchitectureFileMetrics, type ArchitectureMetrics } from './architectureMetrics.js';\nimport {\n type CliOptions,\n configFileName,\n loadConfig,\n type ResolvedOptions,\n resolveOptions,\n resolveThresholds,\n type Thresholds,\n} from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicateCandidate } from './duplication.js';\nimport { collectDuplicationCandidates, measureCode } from './metrics.js';\nimport { measureTypeScriptProject, type TypeScriptProjectMetrics } from './typescriptProject.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicateCandidate[];\n}\n\ninterface RiskTrigger {\n /** Optional location hint (e.g. duplicated block line ranges) appended to the printed trigger. */\n detail?: string;\n metric: string;\n score: number;\n threshold: number;\n value: number;\n}\n\ninterface RiskFinding {\n cognitiveComplexity: number;\n cyclomaticComplexity: number;\n endLine?: number;\n file: string;\n kind: 'component' | 'file' | 'function';\n language: LanguageName;\n name?: string;\n score: number;\n startLine?: number;\n triggers: RiskTrigger[];\n}\n\ninterface ScanResult {\n architecture?: ArchitectureMetrics;\n componentFunctionKeys?: Set<string>;\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n namedComponentFunctionKeys?: Set<string>;\n typeScriptProject?: TypeScriptProjectMetrics;\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\n/** Caps the `Duplicate symbols` section so large repositories do not flood the report. */\nconst maxDuplicateSymbolGroupLines = 10;\n/** Caps the `Cross-file duplicate blocks` section so large repositories do not flood the report. */\nconst maxCrossFileDuplicateGroupLines = 10;\n/** Caps how many cross-file group locations a single risk finding repeats as detail. */\nconst maxCrossFileDuplicateDetailGroups = 3;\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit tests use a case-sensitive `Test.java` suffix; case-insensitive matching would catch\n// production files like `contest.java`.\nconst javaTestFilePattern = /Test\\.java$/u;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Measure code metrics and list high-risk findings.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option('--file-loc-threshold <number>', 'minimum file code LOC to report', parsePositiveInteger)\n .option('--function-loc-threshold <number>', 'minimum function physical LOC span to report', parsePositiveInteger)\n .option(\n '--component-loc-threshold <number>',\n 'minimum React component physical LOC span to report',\n parsePositiveInteger\n )\n .option('--cognitive-threshold <number>', 'minimum cognitive complexity to report', parsePositiveInteger)\n .option('--cyclomatic-threshold <number>', 'minimum cyclomatic complexity to report', parsePositiveInteger)\n .option('--call-threshold <number>', 'minimum function call count to report', parsePositiveInteger)\n .option('--import-threshold <number>', 'minimum unique import sources per file to report', parsePositiveInteger)\n .option('--fan-out-threshold <number>', 'minimum intra-file fan-out per function to report', parsePositiveInteger)\n .option('--parameter-threshold <number>', 'minimum function parameter count to report', parsePositiveInteger)\n .option(\n '--duplicate-block-threshold <number>',\n 'minimum count of duplicated code blocks per file to report',\n parsePositiveInteger\n )\n .option(\n '--duplication-ratio-percent-threshold <number>',\n 'minimum percentage (1-100) of duplicated lines per file to report',\n parsePercentInteger\n )\n .option(\n '--cross-file-duplicate-block-threshold <number>',\n 'minimum count of cross-file duplicate block groups per file to report',\n parsePositiveInteger\n )\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveInteger\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option(\n '--transitive-dependency-threshold <number>',\n 'minimum transitively reachable local files to report',\n parsePositiveInteger\n )\n .option(\n '--structural-breadth-threshold <number>',\n 'minimum structural breadth score to report',\n parsePositiveInteger\n )\n .option(\n '--structural-coordination-threshold <number>',\n 'minimum structural coordination score to report',\n parsePositiveInteger\n )\n .option('--state-mutation-threshold <number>', 'minimum state mutation score to report', parsePositiveInteger)\n .option(\n '--duplicate-symbol-group-threshold <number>',\n 'minimum duplicate symbol group count to report',\n parsePositiveInteger\n )\n .option('--max-findings <number>', 'maximum number of risk findings to print', parsePositiveInteger)\n .option('--largest-files <number>', 'number of largest files by code LOC to list', parsePositiveInteger)\n .option('--include-tests', 'include test files and test directories')\n .option('--tsconfig <path>', 'TypeScript project file to use instead of auto-detected tsconfig.json')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned')\n .option('--fail-on-risk', 'exit with code 1 when high-risk findings are found');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, options);\n addCrossFileDuplication(result);\n await addArchitectureMetrics(result);\n await addTypeScriptProjectMetrics(result, options, resolvedTarget);\n const risks = findRiskyFunctions(\n result.files,\n result.architecture,\n result.crossFileDuplication,\n result.componentFunctionKeys,\n result.namedComponentFunctionKeys,\n options,\n result.displayRoot\n );\n\n if (options.json) {\n printJson(result, risks, options);\n } else {\n printTextReport(resolvedTarget, result, risks, options);\n }\n\n if (\n result.fatalError ||\n (options.failOnError && result.errors.length > 0) ||\n (options.failOnRisk && risks.length > 0)\n ) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nasync function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ResolvedOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nasync function scanTarget(target: string, options: ResolvedOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return { displayRoot, files, errors, warnings };\n }\n\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\nfunction makeScanContext(\n options: ResolvedOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\nasync function addTypeScriptProjectMetrics(\n result: ScanResult,\n options: ResolvedOptions,\n resolvedTarget: string\n): Promise<void> {\n if (result.fatalError) {\n return;\n }\n if (result.files.length === 0) {\n return;\n }\n\n const explicitConfigFile = options.tsconfig;\n const isExplicitConfig = explicitConfigFile !== undefined;\n if (!isExplicitConfig && !result.files.some(({ file }) => isTypeScriptProjectCandidateFile(file))) {\n return;\n }\n\n const configFile = explicitConfigFile ? resolveTarget(explicitConfigFile) : await findNearestTsconfig(resolvedTarget);\n if (!configFile) {\n return;\n }\n\n try {\n result.typeScriptProject = await measureTypeScriptProject(\n configFile,\n result.files.map(({ file }) => file)\n );\n result.componentFunctionKeys = new Set(\n result.typeScriptProject.reactComponentFunctions.map((component) =>\n functionLocationKey(component.file, component.startLine, component.startColumn)\n )\n );\n result.namedComponentFunctionKeys = new Set(\n result.typeScriptProject.reactComponentFunctions.flatMap((component) =>\n component.name ? [functionNameLocationKey(component.file, component.name, component.startLine)] : []\n )\n );\n } catch (error) {\n if (isExplicitConfig) {\n result.errors.push(`${formatPath(configFile, result.displayRoot)}: ${formatError(error)}`);\n }\n }\n}\n\nfunction isTypeScriptProjectCandidateFile(file: string): boolean {\n return ['.cjs', '.cts', '.js', '.jsx', '.mjs', '.mts', '.ts', '.tsx'].includes(path.extname(file));\n}\n\nasync function findNearestTsconfig(target: string): Promise<string | undefined> {\n const targetStat = await stat(target);\n let currentDirectory = targetStat.isDirectory() ? target : path.dirname(target);\n while (true) {\n const configFile = path.join(currentDirectory, 'tsconfig.json');\n if (await fileExists(configFile)) {\n return configFile;\n }\n\n const parentDirectory = path.dirname(currentDirectory);\n if (parentDirectory === currentDirectory) {\n return undefined;\n }\n currentDirectory = parentDirectory;\n }\n}\n\nasync function fileExists(file: string): Promise<boolean> {\n try {\n const fileStat = await stat(file);\n return fileStat.isFile();\n } catch {\n return false;\n }\n}\n\nasync function addArchitectureMetrics(result: ScanResult): Promise<void> {\n if (result.fatalError) {\n return;\n }\n\n try {\n result.architecture = measureArchitecture(\n result.files.map(({ file, metrics }) => ({ file, metrics })),\n result.displayRoot\n );\n } catch (error) {\n result.errors.push(`architecture metrics: ${formatError(error)}`);\n }\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, context.rootDirectory)) {\n return;\n }\n\n if (context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, context.rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection failing (it always parses with the JavaScript binding, which can give\n // up where the native backend measured fine) must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectDuplicationCandidates(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nfunction addCrossFileDuplication(result: ScanResult): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), candidates: duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles);\n}\n\nfunction findRiskyFunctions(\n files: FileMetrics[],\n architecture: ArchitectureMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n componentFunctionKeys: Set<string> | undefined,\n namedComponentFunctionKeys: Set<string> | undefined,\n options: ResolvedOptions,\n displayRoot: string\n): RiskFinding[] {\n const architectureByFile = new Map(architecture?.files.map((file) => [file.file, file]));\n const findings = files.flatMap(({ file, metrics }) => {\n const isReactFile = metrics.functions.some(\n (fn) => fn.returnsJsx || isReactComponent(file, fn, componentFunctionKeys, namedComponentFunctionKeys)\n );\n const thresholds = resolveThresholds(options, metrics.language, isReactFile);\n return [\n ...findRiskyFileMetrics(\n file,\n metrics,\n architectureByFile.get(formatPath(file, displayRoot)),\n crossFileDuplication,\n thresholds,\n displayRoot\n ),\n ...metrics.functions.flatMap((fn) =>\n findRiskyFunctionMetrics(\n file,\n metrics.language,\n fn,\n thresholds,\n displayRoot,\n componentFunctionKeys,\n namedComponentFunctionKeys\n )\n ),\n ];\n });\n\n findings.sort(compareRiskFindings);\n return findings;\n}\n\nfunction findRiskyFileMetrics(\n file: string,\n metrics: CodeMetrics,\n architecture: ArchitectureFileMetrics | undefined,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n thresholds: Thresholds,\n displayRoot: string\n): RiskFinding[] {\n const triggers: RiskTrigger[] = [];\n const formattedFile = formatPath(file, displayRoot);\n addTrigger(triggers, 'file LOC', metrics.lines.code, thresholds.fileLoc);\n addTrigger(triggers, 'import sources', metrics.coupling.importSourceCount, thresholds.import);\n const duplicateBlockDetail = formatDuplicateBlockGroups(metrics.duplication.duplicateBlockGroups);\n addTrigger(\n triggers,\n 'duplicated blocks',\n metrics.duplication.duplicateBlockCount,\n thresholds.duplicateBlock,\n duplicateBlockDetail\n );\n // Maximal-region selection deliberately compresses adjacent clones into few blocks, so severity\n // must track line coverage, not the block count. Flooring compares like the unrounded ratio\n // against the integer threshold (29.5% must not trigger a >= 30 threshold). The block ranges are\n // repeated as detail because this trigger can fire alone, and a percentage without locations is\n // not actionable.\n addTrigger(\n triggers,\n 'duplicated lines (%)',\n Math.floor(metrics.duplication.duplicationRatio * 100),\n thresholds.duplicationRatioPercent,\n duplicateBlockDetail\n );\n if (crossFileDuplication) {\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n addTrigger(\n triggers,\n 'cross-file duplicated blocks',\n Object.hasOwn(crossFileDuplication.duplicateBlockGroupCountByFile, formattedFile)\n ? (crossFileDuplication.duplicateBlockGroupCountByFile[formattedFile] ?? 0)\n : 0,\n thresholds.crossFileDuplicateBlock,\n formatCrossFileDuplicateDetail(crossFileDuplication, formattedFile)\n );\n }\n if (architecture) {\n const hasFileScaleRisk = metrics.lines.code >= 100 || architecture.directLocalDependencyCount >= 8;\n if (hasFileScaleRisk) {\n addTrigger(\n triggers,\n 'transitive local dependencies',\n architecture.transitiveLocalDependencyCount,\n thresholds.transitiveDependency\n );\n }\n if (\n triggers.length > 0 ||\n architecture.directLocalDependencyCount >= 8 ||\n architecture.structuralCoordination.score >= thresholds.structuralCoordination\n ) {\n addTrigger(triggers, 'structural breadth', architecture.structuralBreadthScore, thresholds.structuralBreadth);\n }\n addTrigger(\n triggers,\n 'structural coordination',\n architecture.structuralCoordination.score,\n thresholds.structuralCoordination\n );\n addTrigger(\n triggers,\n 'state mutation',\n architecture.structuralCoordination.stateMutationScore,\n thresholds.stateMutation\n );\n addTrigger(\n triggers,\n 'duplicate symbol groups',\n architecture.duplicateSymbolGroupCount,\n thresholds.duplicateSymbolGroup\n );\n }\n if (triggers.length === 0) {\n return [];\n }\n\n return [\n {\n file: formattedFile,\n language: metrics.language,\n kind: 'file',\n cyclomaticComplexity: metrics.cyclomaticComplexity,\n cognitiveComplexity: metrics.cognitiveComplexity,\n triggers,\n score: maxTriggerScore(triggers),\n },\n ];\n}\n\nfunction findRiskyFunctionMetrics(\n file: string,\n language: LanguageName,\n fn: FunctionMetrics,\n thresholds: Thresholds,\n displayRoot: string,\n componentFunctionKeys?: Set<string>,\n namedComponentFunctionKeys?: Set<string>\n): RiskFinding[] {\n const loc = fn.endLine - fn.startLine + 1;\n const isComponent = isReactComponent(file, fn, componentFunctionKeys, namedComponentFunctionKeys);\n const kind = isComponent ? 'component' : 'function';\n const triggers: RiskTrigger[] = [];\n addTrigger(triggers, 'cognitive complexity', fn.cognitiveComplexity, thresholds.cognitive);\n addTrigger(triggers, 'cyclomatic complexity', fn.cyclomaticComplexity, thresholds.cyclomatic);\n addTrigger(triggers, isComponent ? 'component LOC' : 'function LOC', loc, getLocThreshold(isComponent, thresholds));\n addTrigger(triggers, 'function calls', fn.callCount, thresholds.call);\n addTrigger(triggers, 'fan-out', fn.fanOut, thresholds.fanOut);\n addTrigger(triggers, 'parameters', fn.parameterCount, thresholds.parameter);\n if (triggers.length === 0) {\n return [];\n }\n\n return [\n {\n file: formatPath(file, displayRoot),\n language,\n kind,\n name: fn.name ?? '<anonymous>',\n startLine: fn.startLine,\n endLine: fn.endLine,\n cyclomaticComplexity: fn.cyclomaticComplexity,\n cognitiveComplexity: fn.cognitiveComplexity,\n triggers,\n score: maxTriggerScore(triggers),\n },\n ];\n}\n\nfunction addTrigger(triggers: RiskTrigger[], metric: string, value: number, threshold: number, detail?: string): void {\n if (value < threshold) {\n return;\n }\n\n triggers.push({ metric, value, threshold, score: value / threshold, detail });\n}\n\n/** Formats the cross-file groups a file participates in as `12-34 ~ b.ts:56-78; ...` (capped). */\nfunction formatCrossFileDuplicateDetail(\n crossFileDuplication: CrossFileDuplicationMetrics,\n formattedFile: string\n): string | undefined {\n const involved = crossFileDuplication.groups.filter((group) => group.files.includes(formattedFile));\n if (involved.length === 0) {\n return undefined;\n }\n const formatted = involved\n .slice(0, maxCrossFileDuplicateDetailGroups)\n .map((group) =>\n group.occurrences\n .map(({ file, startLine, endLine }) =>\n file === formattedFile ? `${startLine}-${endLine}` : `${file}:${startLine}-${endLine}`\n )\n .join(' ~ ')\n )\n .join('; ');\n const truncatedSuffix = involved.length > maxCrossFileDuplicateDetailGroups ? '; ...' : '';\n return `${formatted}${truncatedSuffix}`;\n}\n\n/** Formats duplicated block groups as `12-34 ~ 56-78; 90-99 ~ 100-109` (copies joined by ` ~ `, groups by `; `). */\nfunction formatDuplicateBlockGroups(groups: { endLine: number; startLine: number }[][]): string | undefined {\n if (groups.length === 0) {\n return undefined;\n }\n\n return groups.map((group) => group.map(({ startLine, endLine }) => `${startLine}-${endLine}`).join(' ~ ')).join('; ');\n}\n\nfunction isReactComponent(\n file: string,\n fn: FunctionMetrics,\n componentFunctionKeys: Set<string> | undefined,\n namedComponentFunctionKeys: Set<string> | undefined\n): boolean {\n return (\n componentFunctionKeys?.has(functionLocationKey(file, fn.startLine, fn.startColumn)) ||\n (fn.name ? namedComponentFunctionKeys?.has(functionNameLocationKey(file, fn.name, fn.startLine)) : false) ||\n false\n );\n}\n\nfunction getLocThreshold(isComponent: boolean, thresholds: Thresholds): number {\n return isComponent ? thresholds.componentLoc : thresholds.functionLoc;\n}\n\nfunction functionLocationKey(file: string, startLine: number, startColumn: number): string {\n return `${path.resolve(file)}:${startLine}:${startColumn}`;\n}\n\nfunction functionNameLocationKey(file: string, name: string, startLine: number): string {\n return `${path.resolve(file)}:${name}:${startLine}`;\n}\n\nfunction maxTriggerScore(triggers: RiskTrigger[]): number {\n return Math.max(...triggers.map((trigger) => trigger.score));\n}\n\nfunction compareRiskFindings(left: RiskFinding, right: RiskFinding): number {\n return (\n right.score - left.score ||\n left.file.localeCompare(right.file) ||\n (left.startLine ?? 0) - (right.startLine ?? 0) ||\n (left.endLine ?? 0) - (right.endLine ?? 0) ||\n left.kind.localeCompare(right.kind)\n );\n}\n\nfunction printJson(result: ScanResult, risks: RiskFinding[], options: ResolvedOptions): void {\n const summary = summarize(result.files);\n const reportedRisks = risks.slice(0, options.maxFindings);\n writeStdout(\n JSON.stringify(\n {\n summary,\n thresholds: options.thresholds,\n profileThresholds: options.profileThresholds,\n totalRisks: risks.length,\n truncated: reportedRisks.length < risks.length,\n largestFiles:\n options.largestFiles > 0\n ? findLargestFiles(result.files, options.largestFiles, result.displayRoot)\n : undefined,\n architecture: result.architecture,\n crossFileDuplication: result.crossFileDuplication,\n typeScriptProject: result.typeScriptProject,\n risks: reportedRisks,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(target: string, result: ScanResult, risks: RiskFinding[], options: ResolvedOptions): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const { thresholds } = options;\n const summary = summarize(result.files);\n writeStdout(`Measured ${summary.fileCount} files under ${target}\\n`);\n writeStdout(\n `LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount}, max cyclomatic ${summary.maxCyclomaticComplexity}, max cognitive ${summary.maxCognitiveComplexity}\\n`\n );\n writeStdout(\n `Calls ${summary.callCount}, internal edges ${summary.internalCallCount}, max call depth ${summary.maxCallDepth}, imports ${summary.importSourceCount}, exports ${summary.exportCount}\\n`\n );\n writeStdout(\n `Type annotations ${summary.typeAnnotationCount}, type aliases ${summary.typeAliasCount}, interfaces ${summary.interfaceCount}, avg cohesion ${summary.averageFunctionIdentifierOverlap.toFixed(2)}\\n`\n );\n if (result.architecture) {\n writeStdout(`${formatArchitectureMetrics(result.architecture)}\\n`);\n }\n if (result.typeScriptProject) {\n writeStdout(`${formatTypeScriptProjectMetrics(result.typeScriptProject)}\\n`);\n }\n writeStdout(\n `Risk thresholds: file LOC >= ${thresholds.fileLoc}, function LOC >= ${thresholds.functionLoc}, component LOC >= ${thresholds.componentLoc}, cognitive >= ${thresholds.cognitive}, cyclomatic >= ${thresholds.cyclomatic}, calls >= ${thresholds.call}, imports >= ${thresholds.import}, fan-out >= ${thresholds.fanOut}, parameters >= ${thresholds.parameter}, duplicated blocks >= ${thresholds.duplicateBlock}, duplicated lines (%) >= ${thresholds.duplicationRatioPercent}, cross-file duplicated blocks >= ${thresholds.crossFileDuplicateBlock}\\n`\n );\n const profileOverrides = formatProfileOverrides(options.profileThresholds);\n if (profileOverrides) {\n writeStdout(`Per-language overrides: ${profileOverrides}\\n`);\n }\n\n if (risks.length === 0) {\n writeStdout('No high-risk findings found.\\n');\n } else {\n const reportedRisks = risks.slice(0, options.maxFindings);\n const totalSuffix = risks.length > reportedRisks.length ? ` of ${risks.length}` : '';\n writeStdout(`\\nHigh-risk findings (top ${reportedRisks.length}${totalSuffix}):\\n`);\n for (const risk of reportedRisks) {\n writeStdout(`${formatRiskLocation(risk)} ${formatRiskName(risk)} ${formatRiskMetrics(risk)}\\n`);\n }\n }\n\n const crossFileGroups = result.crossFileDuplication?.groups ?? [];\n if (crossFileGroups.length > 0) {\n const reportedGroups = crossFileGroups.slice(0, maxCrossFileDuplicateGroupLines);\n const totalSuffix = crossFileGroups.length > reportedGroups.length ? ` of ${crossFileGroups.length}` : '';\n writeStdout(`\\nCross-file duplicate blocks (top ${reportedGroups.length}${totalSuffix}):\\n`);\n for (const group of reportedGroups) {\n writeStdout(\n `${group.tokenCount} tokens: ${group.occurrences\n .map(({ file, startLine, endLine }) => `${file}:${startLine}-${endLine}`)\n .join(', ')}\\n`\n );\n }\n }\n\n const duplicateSymbolGroups = result.architecture?.duplicateSymbolGroups ?? [];\n if (duplicateSymbolGroups.length > 0) {\n const reportedGroups = duplicateSymbolGroups\n .toSorted((left, right) => right.files.length - left.files.length || left.name.localeCompare(right.name))\n .slice(0, maxDuplicateSymbolGroupLines);\n const totalSuffix =\n duplicateSymbolGroups.length > reportedGroups.length ? ` of ${duplicateSymbolGroups.length}` : '';\n writeStdout(`\\nDuplicate symbols (top ${reportedGroups.length}${totalSuffix}):\\n`);\n for (const group of reportedGroups) {\n writeStdout(\n `${group.name}: ${group.declarations.map((declaration) => `${declaration.file}:${declaration.line}`).join(', ')}\\n`\n );\n }\n }\n\n if (options.largestFiles > 0) {\n const largestFiles = findLargestFiles(result.files, options.largestFiles, result.displayRoot);\n writeStdout(`\\nLargest files by code LOC (top ${largestFiles.length}):\\n`);\n for (const { file, codeLoc } of largestFiles) {\n writeStdout(`${file} (code LOC ${codeLoc})\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\nfunction findLargestFiles(\n files: FileMetrics[],\n count: number,\n displayRoot: string\n): { file: string; codeLoc: number }[] {\n return files\n .map(({ file, metrics }) => ({ file: formatPath(file, displayRoot), codeLoc: metrics.lines.code }))\n .toSorted((left, right) => right.codeLoc - left.codeLoc || left.file.localeCompare(right.file))\n .slice(0, count);\n}\n\nfunction formatProfileOverrides(profileThresholds: ResolvedOptions['profileThresholds']): string {\n return Object.entries(profileThresholds)\n .map(\n ([profile, overrides]) =>\n `${profile} { ${Object.entries(overrides)\n .map(([metric, value]) => `${metric} ${value}`)\n .join(', ')} }`\n )\n .join('; ');\n}\n\nfunction formatRiskLocation(risk: RiskFinding): string {\n return risk.startLine === undefined || risk.endLine === undefined\n ? risk.file\n : `${risk.file}:${risk.startLine}-${risk.endLine}`;\n}\n\nfunction formatRiskName(risk: RiskFinding): string {\n return risk.name ? `${risk.kind} ${risk.name}` : risk.kind;\n}\n\nfunction formatRiskMetrics(risk: RiskFinding): string {\n const triggerText = risk.triggers\n .map(\n (trigger) =>\n `${trigger.metric} ${formatMetricValue(trigger.value)} >= ${formatMetricValue(trigger.threshold)}${trigger.detail ? ` [${trigger.detail}]` : ''}`\n )\n .join(', ');\n return `(${triggerText}; cyclomatic ${risk.cyclomaticComplexity}, cognitive ${risk.cognitiveComplexity})`;\n}\n\nfunction formatMetricValue(value: number): string {\n return Number.isInteger(value) ? String(value) : value.toFixed(2);\n}\n\nfunction formatArchitectureMetrics(metrics: ArchitectureMetrics): string {\n const maxStateMutationScore = Math.max(\n 0,\n ...metrics.files.map((file) => file.structuralCoordination.stateMutationScore)\n );\n return `Architecture max reachable files ${metrics.maxTransitiveLocalDependencyCount}, max structural breadth ${metrics.maxStructuralBreadthScore}, max structural coordination ${metrics.maxStructuralCoordinationScore}, max state mutation ${maxStateMutationScore}, duplicate symbol groups ${metrics.duplicateSymbolGroups.length}`;\n}\n\nfunction formatTypeScriptProjectMetrics(metrics: TypeScriptProjectMetrics): string {\n return `TypeScript project root files ${metrics.rootFileCount}, measured roots ${metrics.measuredRootFileCount}, semantic diagnostics ${metrics.semanticDiagnosticCount}, resolved calls ${metrics.resolvedCallExpressionCount}/${metrics.callExpressionCount} (${(metrics.resolvedCallExpressionRatio * 100).toFixed(1)}%)`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n maxCyclomaticComplexity: number;\n ncssCount: number;\n callCount: number;\n internalCallCount: number;\n maxCallDepth: number;\n importSourceCount: number;\n relativeImportCount: number;\n externalImportCount: number;\n exportCount: number;\n averageFunctionIdentifierOverlap: number;\n typeAnnotationCount: number;\n typeAliasCount: number;\n interfaceCount: number;\n genericParameterCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCyclomaticComplexity = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n let callCount = 0;\n let internalCallCount = 0;\n let maxCallDepth = 0;\n let importSourceCount = 0;\n let relativeImportCount = 0;\n let externalImportCount = 0;\n let exportCount = 0;\n let cohesionTotal = 0;\n let typeAnnotationCount = 0;\n let typeAliasCount = 0;\n let interfaceCount = 0;\n let genericParameterCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functionCount;\n linesOfCode += file.metrics.lines.code;\n maxCyclomaticComplexity = Math.max(maxCyclomaticComplexity, file.metrics.maxCyclomaticComplexity);\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n callCount += file.metrics.callGraph.callCount;\n internalCallCount += file.metrics.callGraph.internalCallCount;\n maxCallDepth = Math.max(maxCallDepth, file.metrics.callGraph.maxCallDepth);\n importSourceCount += file.metrics.coupling.importSourceCount;\n relativeImportCount += file.metrics.coupling.relativeImportCount;\n externalImportCount += file.metrics.coupling.externalImportCount;\n exportCount += file.metrics.coupling.exportCount;\n cohesionTotal += file.metrics.cohesion.averageFunctionIdentifierOverlap;\n typeAnnotationCount += file.metrics.typeComplexity.typeAnnotationCount;\n typeAliasCount += file.metrics.typeComplexity.typeAliasCount;\n interfaceCount += file.metrics.typeComplexity.interfaceCount;\n genericParameterCount += file.metrics.typeComplexity.genericParameterCount;\n }\n\n return {\n fileCount: files.length,\n functionCount,\n linesOfCode,\n maxCyclomaticComplexity,\n maxCognitiveComplexity,\n ncssCount,\n callCount,\n internalCallCount,\n maxCallDepth,\n importSourceCount,\n relativeImportCount,\n externalImportCount,\n exportCount,\n averageFunctionIdentifierOverlap: files.length === 0 ? 0 : cohesionTotal / files.length,\n typeAnnotationCount,\n typeAliasCount,\n interfaceCount,\n genericParameterCount,\n };\n}\n\nfunction shouldSkipDirectory(name: string, options: ResolvedOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\nfunction getLanguage(file: string, options: ResolvedOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || javaTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (path.extname(file) === '.C') {\n return 'cpp';\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"cli.cjs","names":["Command","configFileName","loadConfig","resolveOptions","os","path","stat","realpath","readdir","readFile","measureCode","collectCrossFileDuplicationFileData","measureCrossFileDuplication","InvalidArgumentError"],"sources":["../src/cli.ts"],"sourcesContent":["#!/usr/bin/env node\n\nimport { readdir, readFile, realpath, stat } from 'node:fs/promises';\nimport os from 'node:os';\nimport path from 'node:path';\nimport { Command, InvalidArgumentError } from 'commander';\nimport { type CliOptions, configFileName, loadConfig, type ResolvedOptions, resolveOptions } from './cliConfig.js';\nimport { measureCrossFileDuplication, type CrossFileDuplicationMetrics } from './crossFileDuplication.js';\nimport type { CrossFileDuplicationFileData } from './duplication.js';\nimport { collectCrossFileDuplicationFileData, measureCode } from './metrics.js';\nimport type { CodeMetrics, FunctionMetrics, LanguageName } from './types.js';\n\ninterface FileMetrics {\n file: string;\n metrics: CodeMetrics;\n /** Cross-file duplicate candidates and token/statement data, collected only for directory scans. */\n duplicationCandidates?: CrossFileDuplicationFileData;\n}\n\ninterface ScanResult {\n crossFileDuplication?: CrossFileDuplicationMetrics;\n displayRoot: string;\n errors: string[];\n /** Non-fatal degradations (e.g. cross-file candidates unavailable); the file is still measured. */\n warnings: string[];\n fatalError?: string;\n files: FileMetrics[];\n}\n\n/** The worst (highest-cognitive-complexity) function of a file, reported as the ranking evidence. */\ninterface WorstFunction {\n name: string;\n startLine: number;\n endLine: number;\n cognitiveComplexity: number;\n ncss: number;\n nestingDepth: number;\n}\n\n/**\n * One ranked refactoring candidate. The score is the sum of the file's repo-relative percentile\n * ranks (each in [0, 1)) over three dimensions: worst-function cognitive complexity, duplicated\n * lines (within-file and cross-file combined), and file NCSS. Ranking is relative to the scanned\n * project, so no absolute threshold is involved.\n */\ninterface RankedFile {\n file: string;\n score: number;\n worstFunction?: WorstFunction;\n /** Distinct lines covered by within-file duplicate blocks or cross-file duplicate occurrences. */\n duplicatedLineCount: number;\n /** duplicatedLineCount / code lines (0 when the file has no code). */\n duplicatedLineRatio: number;\n /** Other files sharing cross-file duplicate blocks; filled only for the reported top files. */\n crossFilePartners: string[];\n ncss: number;\n codeLines: number;\n}\n\nconst languageByExtension = new Map<string, LanguageName>([\n ['.c', 'c'],\n ['.c++', 'cpp'],\n ['.cc', 'cpp'],\n ['.cjs', 'javascript'],\n ['.cp', 'cpp'],\n ['.cpp', 'cpp'],\n ['.tcc', 'cpp'],\n ['.cts', 'typescript'],\n ['.cxx', 'cpp'],\n ['.go', 'go'],\n // Headers may be C or C++; the C++ grammar parses both.\n ['.h', 'cpp'],\n ['.hh', 'cpp'],\n ['.hpp', 'cpp'],\n ['.hxx', 'cpp'],\n ['.java', 'java'],\n ['.js', 'javascript'],\n ['.jsx', 'jsx'],\n ['.mjs', 'javascript'],\n ['.mts', 'typescript'],\n ['.py', 'python'],\n ['.rb', 'ruby'],\n ['.rs', 'rust'],\n ['.ts', 'typescript'],\n ['.tsx', 'tsx'],\n]);\n\nconst ignoredDirectoryNames = new Set([\n '.agents',\n '.claude',\n '.cursor',\n '.git',\n '.next',\n '.playwright-cli',\n '.tox',\n '.tmp',\n '.turbo',\n '.venv',\n '.yarn',\n '__fixtures__',\n '__generated__',\n '__pycache__',\n 'coverage',\n 'dist',\n 'fixtures',\n 'generated',\n 'node_modules',\n 'target',\n 'test-fixtures',\n 'vendor',\n 'venv',\n]);\n\n/** Caps how many cross-file partners a ranked file lists so the report stays scannable. */\nconst maxCrossFilePartners = 3;\n\nconst testDirectoryNames = new Set(['__tests__', 'test', 'tests', 'spec']);\nconst testFilePattern = /(?:^test(?:[_-].*)?|\\.(?:spec|test)|[_-](?:test|spec))\\.[^.]+$/iu;\n// JUnit tests use a case-sensitive `Test.java` suffix; case-insensitive matching would catch\n// production files like `contest.java`.\nconst javaTestFilePattern = /Test\\.java$/u;\n\n// oxlint-disable-next-line unicorn/prefer-top-level-await -- CommonJS build output cannot preserve top-level await.\nvoid main().catch((error: unknown) => {\n writeStderr(`Error: ${formatError(error)}\\n`);\n process.exitCode = 1;\n});\n\nasync function main(): Promise<void> {\n const program = new Command()\n .name('code-gauge')\n .description('Rank the files of a project by refactoring priority.')\n .argument('[target]', 'file or directory to measure', '.')\n .option('--config <path>', `config file to use instead of the auto-detected ${configFileName}`)\n .option('--top <number>', 'number of top-ranked files to report (default: 10)', parsePositiveInteger)\n .option(\n '--duplication-min-tokens <number>',\n 'minimum normalized token count for a duplicate region (default 40)',\n parsePositiveInteger\n )\n .option(\n '--duplication-max-gap-tokens <number>',\n 'maximum token gap merged into one gapped clone group; 0 disables merging (default 30)',\n parseNonNegativeInteger\n )\n .option(\n '--duplication-min-similarity-percent <number>',\n 'minimum similarity percent (1-100) for near-miss (Type-3) clone blocks; 100 reports exact matches only (default 70)',\n parsePercentInteger\n )\n .option('--include-tests', 'include test files and test directories')\n .option('--json', 'print JSON output')\n .option('--fail-on-error', 'exit with code 1 when files or directories cannot be scanned');\n\n program.action(async (target: string, cliOptions: CliOptions) => {\n const resolvedTarget = resolveTarget(target);\n const config = await loadConfig(cliOptions.config, await configSearchDirectory(resolvedTarget));\n const options = resolveOptions(cliOptions, config);\n const result = await scanTarget(resolvedTarget, options);\n addCrossFileDuplication(result, options);\n const rankedFiles = rankFiles(result, options.top);\n\n if (options.json) {\n printJson(result, rankedFiles, options);\n } else {\n printTextReport(resolvedTarget, result, rankedFiles, options);\n }\n\n if (result.fatalError || (options.failOnError && result.errors.length > 0)) {\n process.exitCode = 1;\n }\n });\n\n await program.parseAsync();\n}\n\nfunction resolveTarget(target: string): string {\n if (target === '~') {\n return os.homedir();\n }\n\n if (target.startsWith('~/')) {\n return path.join(os.homedir(), target.slice(2));\n }\n\n return path.resolve(target);\n}\n\n/** Returns the directory from which the config file search should start (the target itself if it is a directory). */\nasync function configSearchDirectory(target: string): Promise<string> {\n try {\n const targetStat = await stat(target);\n return targetStat.isDirectory() ? target : path.dirname(target);\n } catch {\n return path.dirname(target);\n }\n}\n\n/** Shared state of one scan, threaded through the directory walk instead of positional plumbing. */\ninterface ScanContext {\n options: ResolvedOptions;\n files: FileMetrics[];\n errors: string[];\n warnings: string[];\n visitedDirectories: Set<string>;\n visitedFiles: Set<string>;\n /** Scan root: paths are displayed relative to it, and symbolic links may not escape it. */\n rootDirectory: string;\n}\n\nasync function scanTarget(target: string, options: ResolvedOptions): Promise<ScanResult> {\n const files: FileMetrics[] = [];\n const errors: string[] = [];\n const warnings: string[] = [];\n let canonicalTarget = target;\n try {\n canonicalTarget = await realpath(target);\n } catch {\n // stat below reports missing targets with the original path.\n }\n\n const fallbackDisplayRoot = path.dirname(canonicalTarget);\n let targetStat;\n\n try {\n targetStat = await stat(canonicalTarget);\n } catch (error) {\n const fatalError = `${formatPath(canonicalTarget, fallbackDisplayRoot)}: ${formatError(error)}`;\n return { displayRoot: fallbackDisplayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n if (targetStat.isFile()) {\n const displayRoot = path.dirname(canonicalTarget);\n const language = getLanguage(canonicalTarget, options, true);\n if (!language) {\n const fatalError = `${formatPath(canonicalTarget, displayRoot)}: unsupported file type`;\n return { displayRoot, files, errors: [fatalError], warnings, fatalError };\n }\n\n const context = makeScanContext(options, files, errors, warnings, displayRoot);\n await measureFile(canonicalTarget, language, 'single-file', context, canonicalTarget);\n return { displayRoot, files, errors, warnings };\n }\n\n await scanDirectory(canonicalTarget, makeScanContext(options, files, errors, warnings, canonicalTarget));\n return { displayRoot: canonicalTarget, files, errors, warnings };\n}\n\nfunction makeScanContext(\n options: ResolvedOptions,\n files: FileMetrics[],\n errors: string[],\n warnings: string[],\n rootDirectory: string\n): ScanContext {\n return { options, files, errors, warnings, visitedDirectories: new Set(), visitedFiles: new Set(), rootDirectory };\n}\n\nasync function scanDirectory(directory: string, context: ScanContext): Promise<void> {\n let resolvedDirectory;\n try {\n resolvedDirectory = await realpath(directory);\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedDirectory, context.rootDirectory)) {\n return;\n }\n\n if (context.visitedDirectories.has(resolvedDirectory)) {\n return;\n }\n context.visitedDirectories.add(resolvedDirectory);\n\n let entries;\n try {\n entries = await readdir(directory, { withFileTypes: true });\n } catch (error) {\n context.errors.push(`${formatPath(directory, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n for (const entry of entries) {\n const entryPath = path.join(directory, entry.name);\n if (entry.isSymbolicLink()) {\n await scanSymbolicLink(entry.name, entryPath, context);\n continue;\n }\n\n if (entry.isDirectory()) {\n if (shouldSkipDirectory(entry.name, context.options)) {\n continue;\n }\n await scanDirectory(entryPath, context);\n continue;\n }\n\n if (entry.isFile()) {\n await measureScannableFile(entryPath, context);\n }\n }\n}\n\nasync function scanSymbolicLink(name: string, entryPath: string, context: ScanContext): Promise<void> {\n let resolvedPath;\n try {\n resolvedPath = await realpath(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (!isWithinDirectory(resolvedPath, context.rootDirectory)) {\n return;\n }\n\n let entryStat;\n try {\n entryStat = await stat(entryPath);\n } catch (error) {\n context.errors.push(`${formatPath(entryPath, context.rootDirectory)}: ${formatError(error)}`);\n return;\n }\n\n if (entryStat.isDirectory()) {\n if (\n shouldSkipDirectory(name, context.options) ||\n shouldSkipDirectory(path.basename(resolvedPath), context.options)\n ) {\n return;\n }\n await scanDirectory(entryPath, context);\n return;\n }\n\n if (entryStat.isFile()) {\n await measureScannableFile(entryPath, context, resolvedPath, resolvedPath);\n }\n}\n\nasync function measureScannableFile(\n file: string,\n context: ScanContext,\n languageFile = file,\n realFile?: string\n): Promise<void> {\n const language = getLanguage(languageFile, context.options);\n if (language) {\n await measureFile(file, language, 'directory', context, realFile);\n }\n}\n\nasync function measureFile(\n file: string,\n language: LanguageName,\n mode: 'single-file' | 'directory',\n context: ScanContext,\n realFile?: string\n): Promise<void> {\n try {\n const resolvedFile = realFile ?? (await realpath(file));\n if (context.visitedFiles.has(resolvedFile)) {\n return;\n }\n context.visitedFiles.add(resolvedFile);\n\n const code = await readFile(file, 'utf8');\n const measureOptions = { language, duplication: context.options.duplication };\n const fileMetrics: FileMetrics = { file, metrics: measureCode(code, measureOptions) };\n // Only directory scans compare files against each other; a single-file target has no peers.\n // Candidate collection failing (it always parses with the JavaScript binding, which can give\n // up where the native backend measured fine) must not discard the measured metrics.\n if (mode === 'directory') {\n try {\n fileMetrics.duplicationCandidates = collectCrossFileDuplicationFileData(code, measureOptions);\n } catch (error) {\n // A warning, not an error: the file's metrics are complete, only its participation in\n // cross-file matching is lost, so it is not \"skipped\" and must not fail --fail-on-error.\n context.warnings.push(\n `${formatPath(file, context.rootDirectory)}: cross-file duplication candidates unavailable: ${formatError(error)}`\n );\n }\n }\n context.files.push(fileMetrics);\n } catch (error) {\n context.errors.push(`${formatPath(file, context.rootDirectory)}: ${formatError(error)}`);\n }\n}\n\n/** Runs after the scan so every measured file's candidates participate. */\nfunction addCrossFileDuplication(result: ScanResult, options: ResolvedOptions): void {\n if (result.fatalError || result.files.length < 2) {\n return;\n }\n const sourceFiles = result.files.flatMap(({ file, duplicationCandidates }) =>\n duplicationCandidates ? [{ file: formatPath(file, result.displayRoot), ...duplicationCandidates }] : []\n );\n if (sourceFiles.length < 2) {\n return;\n }\n result.crossFileDuplication = measureCrossFileDuplication(sourceFiles, options.duplication);\n}\n\nfunction rankFiles(result: ScanResult, top: number): RankedFile[] {\n const candidates = result.files.map(({ file, metrics }) => {\n const formattedFile = formatPath(file, result.displayRoot);\n const duplicatedLines = collectDuplicatedLines(metrics, result.crossFileDuplication, formattedFile);\n return {\n file: formattedFile,\n worstFunction: findWorstFunction(metrics.functions),\n duplicatedLineCount: duplicatedLines.size,\n duplicatedLineRatio: metrics.lines.code === 0 ? 0 : duplicatedLines.size / metrics.lines.code,\n crossFilePartners: [] as string[],\n ncss: metrics.ncssCount,\n codeLines: metrics.lines.code,\n };\n });\n\n const cognitivePercentile = makePercentile(candidates.map((c) => c.worstFunction?.cognitiveComplexity ?? 0));\n const duplicationPercentile = makePercentile(candidates.map((c) => c.duplicatedLineCount));\n const ncssPercentile = makePercentile(candidates.map((c) => c.ncss));\n\n const ranked = candidates\n .map((candidate) => ({\n ...candidate,\n score:\n cognitivePercentile(candidate.worstFunction?.cognitiveComplexity ?? 0) +\n duplicationPercentile(candidate.duplicatedLineCount) +\n ncssPercentile(candidate.ncss),\n }))\n .toSorted(\n (left, right) => right.score - left.score || right.ncss - left.ncss || left.file.localeCompare(right.file)\n );\n // Partner evidence is attached only to the files that will be reported: expanding partners for\n // every scanned file first would retain TH(F^2) strings when one clone group spans F files, and\n // rescanning all groups per file would be O(files x groups) on the post-scan ranking step.\n attachCrossFilePartners(ranked.slice(0, top), result.crossFileDuplication);\n return ranked;\n}\n\n/** One pass over the groups fills the reported files' partner lists (other files sharing a group). */\nfunction attachCrossFilePartners(\n reportedFiles: RankedFile[],\n crossFileDuplication: CrossFileDuplicationMetrics | undefined\n): void {\n if (!crossFileDuplication) {\n return;\n }\n const partnersByFile = new Map(reportedFiles.map((ranked) => [ranked.file, new Set<string>()]));\n for (const group of crossFileDuplication.groups) {\n for (const file of group.files) {\n const partners = partnersByFile.get(file);\n if (!partners) {\n continue;\n }\n for (const partner of group.files) {\n if (partner !== file) {\n partners.add(partner);\n }\n }\n }\n }\n for (const ranked of reportedFiles) {\n ranked.crossFilePartners = [...(partnersByFile.get(ranked.file) ?? [])].toSorted();\n }\n}\n\n/**\n * Percentile rank within the scanned project: the fraction of files with a strictly smaller\n * value, in [0, 1). Relative ranking needs no absolute threshold, which sidesteps the metric\n * calibration problem entirely — the top of the list is worth refactoring first regardless of\n * where any cutoff would sit.\n */\nfunction makePercentile(values: number[]): (value: number) => number {\n const sorted = values.toSorted((left, right) => left - right);\n return (value: number) => {\n let low = 0;\n let high = sorted.length;\n while (low < high) {\n const middle = (low + high) >> 1;\n if ((sorted[middle] as number) < value) {\n low = middle + 1;\n } else {\n high = middle;\n }\n }\n return sorted.length === 0 ? 0 : low / sorted.length;\n };\n}\n\n/** The highest-cognitive-complexity function; NCSS breaks ties so the larger body is reported. */\nfunction findWorstFunction(functions: FunctionMetrics[]): WorstFunction | undefined {\n let worst: FunctionMetrics | undefined;\n for (const fn of functions) {\n if (\n !worst ||\n fn.cognitiveComplexity > worst.cognitiveComplexity ||\n (fn.cognitiveComplexity === worst.cognitiveComplexity && fn.ncss > worst.ncss)\n ) {\n worst = fn;\n }\n }\n if (!worst) {\n return undefined;\n }\n return {\n name: worst.name ?? '<anonymous>',\n startLine: worst.startLine,\n endLine: worst.endLine,\n cognitiveComplexity: worst.cognitiveComplexity,\n ncss: worst.ncss,\n nestingDepth: worst.nestingDepth,\n };\n}\n\n/**\n * Distinct 1-based code lines covered by within-file or cross-file duplicated content. Both\n * sources expose the exact lines carrying matched tokens (never block bounding ranges, which\n * would over-count comment/blank lines and the unmatched gap of a merged clone), so the union is\n * a subset of the file's code lines and the derived ratio can never exceed 1.\n */\nfunction collectDuplicatedLines(\n metrics: CodeMetrics,\n crossFileDuplication: CrossFileDuplicationMetrics | undefined,\n formattedFile: string\n): Set<number> {\n const lines = new Set(metrics.duplication.duplicateLineNumbers);\n // Object.hasOwn: a file named like an Object.prototype member must not read an inherited value.\n const crossFileLines =\n crossFileDuplication && Object.hasOwn(crossFileDuplication.duplicateLineNumbersByFile, formattedFile)\n ? (crossFileDuplication.duplicateLineNumbersByFile[formattedFile] ?? [])\n : [];\n for (const line of crossFileLines) {\n lines.add(line);\n }\n return lines;\n}\n\nfunction printJson(result: ScanResult, rankedFiles: RankedFile[], options: ResolvedOptions): void {\n const reportedFiles = rankedFiles.slice(0, options.top);\n writeStdout(\n JSON.stringify(\n {\n summary: summarize(result.files),\n totalRankedFiles: rankedFiles.length,\n truncated: reportedFiles.length < rankedFiles.length,\n files: reportedFiles,\n errors: result.errors,\n warnings: result.warnings,\n },\n undefined,\n 2\n ) + '\\n'\n );\n}\n\nfunction printTextReport(\n target: string,\n result: ScanResult,\n rankedFiles: RankedFile[],\n options: ResolvedOptions\n): void {\n if (result.fatalError) {\n writeStderr(`Error: ${result.fatalError}\\n`);\n return;\n }\n\n const summary = summarize(result.files);\n writeStdout(\n `Measured ${summary.fileCount} files under ${target} (code LOC ${summary.linesOfCode}, NCSS ${summary.ncssCount}, functions ${summary.functionCount})\\n`\n );\n\n if (rankedFiles.length === 0) {\n writeStdout('No measurable files found.\\n');\n } else {\n const reportedFiles = rankedFiles.slice(0, options.top);\n const totalSuffix = rankedFiles.length > reportedFiles.length ? ` of ${rankedFiles.length}` : '';\n writeStdout(`\\nRefactoring candidates (top ${reportedFiles.length}${totalSuffix}):\\n`);\n for (const [index, ranked] of reportedFiles.entries()) {\n writeStdout(`${index + 1}. ${formatRankedFile(ranked)}\\n`);\n }\n }\n\n if (result.warnings.length > 0) {\n writeStderr(`\\nDegraded ${result.warnings.length} files (measured, but excluded from cross-file matching):\\n`);\n for (const warning of result.warnings.slice(0, 10)) {\n writeStderr(`- ${warning}\\n`);\n }\n if (result.warnings.length > 10) {\n writeStderr(`- ... ${result.warnings.length - 10} more\\n`);\n }\n }\n\n if (result.errors.length > 0) {\n writeStderr(`\\nSkipped ${result.errors.length} files or directories:\\n`);\n for (const error of result.errors.slice(0, 10)) {\n writeStderr(`- ${error}\\n`);\n }\n if (result.errors.length > 10) {\n writeStderr(`- ... ${result.errors.length - 10} more\\n`);\n }\n }\n}\n\n/** One ranked file as a single line: the location, the evidence, and where the duplication points. */\nfunction formatRankedFile(ranked: RankedFile): string {\n const reasons: string[] = [];\n if (ranked.worstFunction) {\n const fn = ranked.worstFunction;\n reasons.push(\n `worst function ${fn.name} (L${fn.startLine}-${fn.endLine}) cognitive ${fn.cognitiveComplexity}, NCSS ${fn.ncss}, nesting ${fn.nestingDepth}`\n );\n }\n if (ranked.duplicatedLineCount > 0) {\n const partnersSuffix =\n ranked.crossFilePartners.length > 0\n ? `, shared with ${ranked.crossFilePartners.slice(0, maxCrossFilePartners).join(', ')}${ranked.crossFilePartners.length > maxCrossFilePartners ? ', ...' : ''}`\n : '';\n reasons.push(\n `duplicated lines ${ranked.duplicatedLineCount} (${Math.round(ranked.duplicatedLineRatio * 100)}%${partnersSuffix})`\n );\n }\n reasons.push(`file NCSS ${ranked.ncss}`);\n return `${ranked.file} (score ${ranked.score.toFixed(2)}): ${reasons.join('; ')}`;\n}\n\nfunction summarize(files: FileMetrics[]): {\n fileCount: number;\n functionCount: number;\n linesOfCode: number;\n maxCognitiveComplexity: number;\n ncssCount: number;\n} {\n let functionCount = 0;\n let linesOfCode = 0;\n let maxCognitiveComplexity = 0;\n let ncssCount = 0;\n\n for (const file of files) {\n functionCount += file.metrics.functions.length;\n linesOfCode += file.metrics.lines.code;\n maxCognitiveComplexity = Math.max(maxCognitiveComplexity, file.metrics.maxCognitiveComplexity);\n ncssCount += file.metrics.ncssCount;\n }\n\n return { fileCount: files.length, functionCount, linesOfCode, maxCognitiveComplexity, ncssCount };\n}\n\nfunction shouldSkipDirectory(name: string, options: ResolvedOptions): boolean {\n if (ignoredDirectoryNames.has(name)) {\n return true;\n }\n\n if (options.includeTests) {\n return false;\n }\n\n return testDirectoryNames.has(name);\n}\n\nfunction isWithinDirectory(candidate: string, directory: string): boolean {\n const relative = path.relative(directory, candidate);\n return relative === '' || (relative !== '..' && !relative.startsWith(`..${path.sep}`) && !path.isAbsolute(relative));\n}\n\nfunction getLanguage(file: string, options: ResolvedOptions, explicitTarget = false): LanguageName | undefined {\n const lowerFile = file.toLowerCase();\n if (\n !explicitTarget &&\n (lowerFile.endsWith('.d.ts') ||\n lowerFile.endsWith('.d.mts') ||\n lowerFile.endsWith('.d.cts') ||\n lowerFile.endsWith('.min.js') ||\n lowerFile.endsWith('.pnp.cjs'))\n ) {\n return undefined;\n }\n\n if (\n !explicitTarget &&\n !options.includeTests &&\n (testFilePattern.test(path.basename(file)) || javaTestFilePattern.test(path.basename(file)))\n ) {\n return undefined;\n }\n\n // GCC treats an uppercase `.C` as C++; lowercasing first would misparse it with the C grammar.\n if (path.extname(file) === '.C') {\n return 'cpp';\n }\n\n return languageByExtension.get(path.extname(lowerFile));\n}\n\nfunction parsePercentInteger(value: string): number {\n const parsed = parsePositiveInteger(value);\n if (parsed > 100) {\n throw new InvalidArgumentError('Expected an integer between 1 and 100.');\n }\n return parsed;\n}\n\nfunction parseNonNegativeInteger(value: string): number {\n if (!/^\\d+$/u.test(value)) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 0) {\n throw new InvalidArgumentError('Expected a non-negative integer.');\n }\n return parsed;\n}\n\nfunction parsePositiveInteger(value: string): number {\n if (!/^[1-9]\\d*$/u.test(value)) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n\n const parsed = Number(value);\n if (!Number.isSafeInteger(parsed) || parsed < 1) {\n throw new InvalidArgumentError('Expected a positive integer.');\n }\n return parsed;\n}\n\nfunction formatPath(file: string, base: string): string {\n return path.relative(base, file) || path.basename(file);\n}\n\nfunction writeStdout(message: string): void {\n process.stdout.write(message);\n}\n\nfunction writeStderr(message: string): void {\n process.stderr.write(message);\n}\n\nfunction formatError(error: unknown): string {\n return error instanceof Error ? error.message : String(error);\n}\n"],"mappings":"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"}
|