vibeclean 1.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.
@@ -0,0 +1,204 @@
1
+ import * as acorn from "acorn";
2
+ import jsx from "acorn-jsx";
3
+ import * as walk from "acorn-walk";
4
+
5
+ const AcornWithJsx = acorn.Parser.extend(jsx());
6
+
7
+ const IMPORT_RE = /import\s+[^"'`]*?from\s+["'`]([^"'`]+)["'`]|import\s+["'`]([^"'`]+)["'`]/g;
8
+ const REQUIRE_RE = /require\(\s*["'`]([^"'`]+)["'`]\s*\)/g;
9
+
10
+ function stripTypeOnlySyntax(content) {
11
+ return content
12
+ .replace(/\bimport\s+type\s+/g, "import ")
13
+ .replace(/\bexport\s+type\s+\{[^}]*\}\s+from\s+["'`][^"'`]+["'`];?/g, "")
14
+ .replace(/\b(?:export\s+)?interface\s+[A-Za-z_$][A-Za-z0-9_$]*[\s\S]*?\}\s*/g, "")
15
+ .replace(/\b(?:export\s+)?type\s+[A-Za-z_$][A-Za-z0-9_$]*\s*=\s*[^;]+;/g, "")
16
+ .replace(/\s+as\s+const\b/g, "");
17
+ }
18
+
19
+ function tryParse(content, sourceType) {
20
+ try {
21
+ return {
22
+ ast: AcornWithJsx.parse(content, {
23
+ ecmaVersion: "latest",
24
+ sourceType,
25
+ allowHashBang: true
26
+ }),
27
+ error: null
28
+ };
29
+ } catch (error) {
30
+ return { ast: null, error };
31
+ }
32
+ }
33
+
34
+ export function parseAstWithMeta(content) {
35
+ const moduleResult = tryParse(content, "module");
36
+ if (moduleResult.ast) {
37
+ return { ast: moduleResult.ast, parseError: null, usedTypeSyntaxFallback: false };
38
+ }
39
+
40
+ const scriptResult = tryParse(content, "script");
41
+ if (scriptResult.ast) {
42
+ return { ast: scriptResult.ast, parseError: null, usedTypeSyntaxFallback: false };
43
+ }
44
+
45
+ const stripped = stripTypeOnlySyntax(content);
46
+ if (stripped !== content) {
47
+ const moduleStripped = tryParse(stripped, "module");
48
+ if (moduleStripped.ast) {
49
+ return { ast: moduleStripped.ast, parseError: moduleResult.error, usedTypeSyntaxFallback: true };
50
+ }
51
+
52
+ const scriptStripped = tryParse(stripped, "script");
53
+ if (scriptStripped.ast) {
54
+ return { ast: scriptStripped.ast, parseError: moduleResult.error, usedTypeSyntaxFallback: true };
55
+ }
56
+ }
57
+
58
+ return { ast: null, parseError: moduleResult.error || scriptResult.error, usedTypeSyntaxFallback: false };
59
+ }
60
+
61
+ export function parseAst(content) {
62
+ const result = parseAstWithMeta(content);
63
+ return result.ast;
64
+ }
65
+
66
+ function collectIdentifiersFallback(content) {
67
+ const names = new Set();
68
+ const reserved = new Set([
69
+ "if",
70
+ "for",
71
+ "while",
72
+ "switch",
73
+ "catch",
74
+ "return",
75
+ "function",
76
+ "class",
77
+ "const",
78
+ "let",
79
+ "var",
80
+ "import",
81
+ "export",
82
+ "default",
83
+ "new",
84
+ "typeof"
85
+ ]);
86
+ const patterns = [
87
+ /\b(?:const|let|var|function|class|interface|type|enum)\s+([A-Za-z_$][A-Za-z0-9_$]*)/g,
88
+ /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*=\s*(?:async\s*)?\([^)]*\)\s*=>/g,
89
+ /\b([A-Za-z_$][A-Za-z0-9_$]*)\s*\([^)]*\)\s*\{/g
90
+ ];
91
+
92
+ for (const pattern of patterns) {
93
+ for (const match of content.matchAll(pattern)) {
94
+ const value = match[1];
95
+ if (!reserved.has(value)) {
96
+ names.add(value);
97
+ }
98
+ }
99
+ }
100
+
101
+ return [...names];
102
+ }
103
+
104
+ export function traverseAst(node, onNode) {
105
+ if (!node || typeof node !== "object") {
106
+ return;
107
+ }
108
+
109
+ onNode(node);
110
+ for (const key of Object.keys(node)) {
111
+ const value = node[key];
112
+ if (!value) {
113
+ continue;
114
+ }
115
+
116
+ if (Array.isArray(value)) {
117
+ for (const child of value) {
118
+ if (child && typeof child.type === "string") {
119
+ traverseAst(child, onNode);
120
+ }
121
+ }
122
+ } else if (value && typeof value.type === "string") {
123
+ traverseAst(value, onNode);
124
+ }
125
+ }
126
+ }
127
+
128
+ export function collectIdentifiers(content) {
129
+ const ast = parseAst(content);
130
+ if (!ast) {
131
+ return collectIdentifiersFallback(content);
132
+ }
133
+
134
+ const names = [];
135
+ // Use acorn-walk when possible, then fall back to generic traversal for JSX-heavy files.
136
+ try {
137
+ walk.full(ast, (node) => {
138
+ if (node.type === "Identifier" && typeof node.name === "string") {
139
+ names.push(node.name);
140
+ }
141
+ });
142
+ } catch {
143
+ traverseAst(ast, (node) => {
144
+ if (node.type === "Identifier" && typeof node.name === "string") {
145
+ names.push(node.name);
146
+ }
147
+ });
148
+ }
149
+
150
+ return names;
151
+ }
152
+
153
+ export function collectImportSpecifiers(content) {
154
+ const imports = new Set();
155
+
156
+ for (const match of content.matchAll(IMPORT_RE)) {
157
+ const spec = match[1] || match[2];
158
+ if (spec) {
159
+ imports.add(spec);
160
+ }
161
+ }
162
+
163
+ for (const match of content.matchAll(REQUIRE_RE)) {
164
+ if (match[1]) {
165
+ imports.add(match[1]);
166
+ }
167
+ }
168
+
169
+ return [...imports];
170
+ }
171
+
172
+ export function packageRoot(specifier) {
173
+ if (!specifier || specifier.startsWith(".") || specifier.startsWith("/")) {
174
+ return null;
175
+ }
176
+
177
+ if (specifier.startsWith("@")) {
178
+ const parts = specifier.split("/");
179
+ return parts.length >= 2 ? `${parts[0]}/${parts[1]}` : specifier;
180
+ }
181
+
182
+ return specifier.split("/")[0];
183
+ }
184
+
185
+ export function countMatches(content, regex) {
186
+ const matches = content.match(regex);
187
+ return matches ? matches.length : 0;
188
+ }
189
+
190
+ export function severityFromScore(score) {
191
+ if (score >= 7) {
192
+ return "high";
193
+ }
194
+ if (score >= 4) {
195
+ return "medium";
196
+ }
197
+ return "low";
198
+ }
199
+
200
+ export function scoreFromRatio(ratio, weight = 10) {
201
+ const bounded = Math.min(1, Math.max(0, ratio));
202
+ return Math.round(bounded * weight);
203
+ }
204
+
@@ -0,0 +1,134 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ function countFindingsBySeverity(categories = []) {
5
+ const counts = {
6
+ low: 0,
7
+ medium: 0,
8
+ high: 0
9
+ };
10
+
11
+ for (const category of categories) {
12
+ for (const finding of category.findings || []) {
13
+ const severity = finding.severity || "low";
14
+ if (severity in counts) {
15
+ counts[severity] += 1;
16
+ } else {
17
+ counts.low += 1;
18
+ }
19
+ }
20
+ }
21
+
22
+ return counts;
23
+ }
24
+
25
+ function categorySnapshot(categories = []) {
26
+ const map = {};
27
+ for (const category of categories) {
28
+ map[category.id] = {
29
+ score: category.score,
30
+ totalIssues: category.totalIssues,
31
+ severity: category.severity
32
+ };
33
+ }
34
+ return map;
35
+ }
36
+
37
+ export function resolveBaselinePath(rootDir, baselineFile = ".vibeclean-baseline.json") {
38
+ if (path.isAbsolute(baselineFile)) {
39
+ return baselineFile;
40
+ }
41
+ return path.join(rootDir, baselineFile);
42
+ }
43
+
44
+ export function buildBaselineSnapshot(report) {
45
+ return {
46
+ version: 1,
47
+ generatedAt: new Date().toISOString(),
48
+ overallScore: report.overallScore,
49
+ totalIssues: report.totalIssues,
50
+ findingCounts: countFindingsBySeverity(report.categories),
51
+ categories: categorySnapshot(report.categories)
52
+ };
53
+ }
54
+
55
+ export async function readBaselineSnapshot(rootDir, baselineFile) {
56
+ const fullPath = resolveBaselinePath(rootDir, baselineFile);
57
+ const raw = await fs.readFile(fullPath, "utf8");
58
+ return {
59
+ path: fullPath,
60
+ snapshot: JSON.parse(raw)
61
+ };
62
+ }
63
+
64
+ export async function writeBaselineSnapshot(rootDir, baselineFile, report) {
65
+ const fullPath = resolveBaselinePath(rootDir, baselineFile);
66
+ const snapshot = buildBaselineSnapshot(report);
67
+ await fs.writeFile(fullPath, `${JSON.stringify(snapshot, null, 2)}\n`, "utf8");
68
+ return {
69
+ path: fullPath,
70
+ snapshot
71
+ };
72
+ }
73
+
74
+ export function compareAgainstBaseline(report, baselineSnapshot) {
75
+ const currentFindingCounts = countFindingsBySeverity(report.categories);
76
+ const baselineFindingCounts = baselineSnapshot.findingCounts || {
77
+ low: 0,
78
+ medium: 0,
79
+ high: 0
80
+ };
81
+
82
+ const deltas = {
83
+ score: report.overallScore - (baselineSnapshot.overallScore || 0),
84
+ totalIssues: report.totalIssues - (baselineSnapshot.totalIssues || 0),
85
+ highFindings: currentFindingCounts.high - (baselineFindingCounts.high || 0),
86
+ mediumFindings: currentFindingCounts.medium - (baselineFindingCounts.medium || 0)
87
+ };
88
+
89
+ const regressions = [];
90
+ if (deltas.score < 0) {
91
+ regressions.push(
92
+ `Baseline regression: overall score dropped by ${Math.abs(deltas.score)} (from ${baselineSnapshot.overallScore} to ${report.overallScore}).`
93
+ );
94
+ }
95
+ if (deltas.totalIssues > 0) {
96
+ regressions.push(
97
+ `Baseline regression: total issues increased by ${deltas.totalIssues} (from ${baselineSnapshot.totalIssues} to ${report.totalIssues}).`
98
+ );
99
+ }
100
+ if (deltas.highFindings > 0) {
101
+ regressions.push(
102
+ `Baseline regression: high-severity findings increased by ${deltas.highFindings}.`
103
+ );
104
+ }
105
+
106
+ const baselineCategoryMap = baselineSnapshot.categories || {};
107
+ const worsenedCategories = [];
108
+ for (const category of report.categories) {
109
+ const previous = baselineCategoryMap[category.id];
110
+ if (!previous) {
111
+ continue;
112
+ }
113
+ const scoreDelta = category.score - (previous.score || 0);
114
+ if (scoreDelta > 0) {
115
+ worsenedCategories.push(`${category.id} (+${scoreDelta})`);
116
+ }
117
+ }
118
+
119
+ if (worsenedCategories.length > 0) {
120
+ regressions.push(
121
+ `Baseline regression: category scores worsened in ${worsenedCategories.join(", ")}.`
122
+ );
123
+ }
124
+
125
+ return {
126
+ baselineGeneratedAt: baselineSnapshot.generatedAt || null,
127
+ baselineScore: baselineSnapshot.overallScore ?? null,
128
+ baselineTotalIssues: baselineSnapshot.totalIssues ?? null,
129
+ currentScore: report.overallScore,
130
+ currentTotalIssues: report.totalIssues,
131
+ deltas,
132
+ regressions
133
+ };
134
+ }
package/src/config.js ADDED
@@ -0,0 +1,207 @@
1
+ import fs from "node:fs/promises";
2
+ import path from "node:path";
3
+
4
+ const DEFAULT_CONFIG = {
5
+ maxFiles: 500,
6
+ maxFileSizeKb: 100,
7
+ ignore: [],
8
+ severity: "low",
9
+ profile: "app",
10
+ changedOnly: false,
11
+ changedBase: "HEAD",
12
+ failOn: null,
13
+ maxIssues: null,
14
+ minScore: null,
15
+ baseline: false,
16
+ baselineFile: ".vibeclean-baseline.json",
17
+ writeBaseline: false,
18
+ failOnRegression: true,
19
+ reportFormat: "text",
20
+ reportFile: null,
21
+ leftovers: {
22
+ allowConsolePaths: [],
23
+ ignoreTodoPaths: []
24
+ },
25
+ rules: {
26
+ naming: true,
27
+ patterns: true,
28
+ leftovers: true,
29
+ dependencies: true,
30
+ deadcode: true,
31
+ errorhandling: true
32
+ },
33
+ allowedPatterns: {
34
+ httpClient: null,
35
+ asyncStyle: null,
36
+ stateManagement: null
37
+ }
38
+ };
39
+ const VALID_SEVERITIES = new Set(["low", "medium", "high"]);
40
+ const VALID_REPORT_FORMATS = new Set(["text", "json", "markdown"]);
41
+
42
+ const PROFILE_PRESETS = {
43
+ app: {
44
+ ignore: []
45
+ },
46
+ library: {
47
+ ignore: ["examples/**", "example/**", "benchmarks/**", "benchmark/**", "playground/**"]
48
+ },
49
+ cli: {
50
+ ignore: ["test/**", "**/*.test.js", "**/*.spec.js"],
51
+ leftovers: {
52
+ allowConsolePaths: ["bin/", "scripts/", "test/"],
53
+ ignoreTodoPaths: ["test/"]
54
+ }
55
+ }
56
+ };
57
+ const VALID_PROFILES = new Set(Object.keys(PROFILE_PRESETS));
58
+
59
+ function deepMerge(base, override) {
60
+ const merged = { ...base };
61
+
62
+ for (const [key, value] of Object.entries(override || {})) {
63
+ if (
64
+ value &&
65
+ typeof value === "object" &&
66
+ !Array.isArray(value) &&
67
+ typeof merged[key] === "object" &&
68
+ !Array.isArray(merged[key])
69
+ ) {
70
+ merged[key] = deepMerge(merged[key], value);
71
+ } else {
72
+ merged[key] = value;
73
+ }
74
+ }
75
+
76
+ return merged;
77
+ }
78
+
79
+ async function readJson(filePath) {
80
+ const raw = await fs.readFile(filePath, "utf8");
81
+ return JSON.parse(raw);
82
+ }
83
+
84
+ function uniqueArray(input) {
85
+ return [...new Set((Array.isArray(input) ? input : []).filter(Boolean))];
86
+ }
87
+
88
+ function applyProfile(baseConfig, profileName) {
89
+ const normalized = typeof profileName === "string" ? profileName.toLowerCase() : "";
90
+ const selectedProfile = VALID_PROFILES.has(normalized) ? normalized : DEFAULT_CONFIG.profile;
91
+ const profilePreset = PROFILE_PRESETS[selectedProfile] || {};
92
+ const profiled = deepMerge(baseConfig, profilePreset);
93
+ profiled.profile = selectedProfile;
94
+ profiled.ignore = uniqueArray([...(baseConfig.ignore || []), ...(profilePreset.ignore || [])]);
95
+ return profiled;
96
+ }
97
+
98
+ export async function loadConfig(rootDir) {
99
+ const candidates = [".vibecleanrc", ".vibecleanrc.json"];
100
+ let fileConfig = {};
101
+
102
+ for (const candidate of candidates) {
103
+ const fullPath = path.join(rootDir, candidate);
104
+ try {
105
+ const stats = await fs.stat(fullPath);
106
+ if (!stats.isFile()) {
107
+ continue;
108
+ }
109
+ fileConfig = await readJson(fullPath);
110
+ break;
111
+ } catch {
112
+ // Missing file or invalid stats: try next config candidate.
113
+ }
114
+ }
115
+
116
+ return deepMerge(DEFAULT_CONFIG, fileConfig);
117
+ }
118
+
119
+ export function mergeConfig(base, overrides) {
120
+ const safeOverrides = { ...overrides };
121
+ const extraIgnore = [];
122
+ if (safeOverrides.ignore && typeof safeOverrides.ignore === "string") {
123
+ extraIgnore.push(
124
+ ...safeOverrides.ignore
125
+ .split(",")
126
+ .map((item) => item.trim())
127
+ .filter(Boolean)
128
+ );
129
+ delete safeOverrides.ignore;
130
+ } else if (Array.isArray(safeOverrides.ignore)) {
131
+ extraIgnore.push(...safeOverrides.ignore.filter(Boolean));
132
+ delete safeOverrides.ignore;
133
+ }
134
+
135
+ if (typeof safeOverrides.maxFiles === "string") {
136
+ safeOverrides.maxFiles = Number.parseInt(safeOverrides.maxFiles, 10);
137
+ }
138
+
139
+ if (typeof safeOverrides.maxIssues === "string") {
140
+ safeOverrides.maxIssues = Number.parseInt(safeOverrides.maxIssues, 10);
141
+ }
142
+
143
+ if (typeof safeOverrides.minScore === "string") {
144
+ safeOverrides.minScore = Number.parseInt(safeOverrides.minScore, 10);
145
+ }
146
+
147
+ if (typeof safeOverrides.minSeverity === "string") {
148
+ safeOverrides.severity = safeOverrides.minSeverity.toLowerCase();
149
+ }
150
+
151
+ if (typeof safeOverrides.profile === "string") {
152
+ safeOverrides.profile = safeOverrides.profile.toLowerCase();
153
+ if (!VALID_PROFILES.has(safeOverrides.profile)) {
154
+ delete safeOverrides.profile;
155
+ }
156
+ }
157
+
158
+ if (typeof safeOverrides.severity === "string") {
159
+ safeOverrides.severity = safeOverrides.severity.toLowerCase();
160
+ if (!VALID_SEVERITIES.has(safeOverrides.severity)) {
161
+ safeOverrides.severity = base.severity || DEFAULT_CONFIG.severity;
162
+ }
163
+ }
164
+
165
+ if (!Number.isFinite(safeOverrides.maxFiles) || safeOverrides.maxFiles <= 0) {
166
+ delete safeOverrides.maxFiles;
167
+ }
168
+
169
+ if (!Number.isFinite(safeOverrides.maxIssues) || safeOverrides.maxIssues < 0) {
170
+ delete safeOverrides.maxIssues;
171
+ }
172
+
173
+ if (!Number.isFinite(safeOverrides.minScore)) {
174
+ delete safeOverrides.minScore;
175
+ } else {
176
+ safeOverrides.minScore = Math.max(0, Math.min(100, Math.round(safeOverrides.minScore)));
177
+ }
178
+
179
+ if (typeof safeOverrides.failOn === "string") {
180
+ safeOverrides.failOn = safeOverrides.failOn.toLowerCase();
181
+ if (!VALID_SEVERITIES.has(safeOverrides.failOn)) {
182
+ safeOverrides.failOn = null;
183
+ }
184
+ } else if (safeOverrides.failOn != null) {
185
+ safeOverrides.failOn = null;
186
+ }
187
+
188
+ if (typeof safeOverrides.reportFormat === "string") {
189
+ safeOverrides.reportFormat = safeOverrides.reportFormat.toLowerCase();
190
+ if (!VALID_REPORT_FORMATS.has(safeOverrides.reportFormat)) {
191
+ safeOverrides.reportFormat = base.reportFormat || DEFAULT_CONFIG.reportFormat;
192
+ }
193
+ }
194
+
195
+ if (typeof safeOverrides.baselineFile === "string") {
196
+ safeOverrides.baselineFile = safeOverrides.baselineFile.trim() || DEFAULT_CONFIG.baselineFile;
197
+ }
198
+
199
+ const selectedProfile = safeOverrides.profile || base.profile || DEFAULT_CONFIG.profile;
200
+ const profiledBase = applyProfile(base, selectedProfile);
201
+ const merged = deepMerge(profiledBase, safeOverrides);
202
+ merged.ignore = uniqueArray([...(profiledBase.ignore || []), ...extraIgnore]);
203
+
204
+ return merged;
205
+ }
206
+
207
+ export { DEFAULT_CONFIG, PROFILE_PRESETS };
@@ -0,0 +1,125 @@
1
+ import fs from "node:fs/promises";
2
+
3
+ const TODO_LINE_RE = /^\s*\/\/\s*(TODO|FIXME|HACK|XXX)\b/i;
4
+ const AI_COMMENT_LINE_RE = /^\s*\/\/\s*(AI generated|Generated by|Created by Copilot)\b/i;
5
+ const NOISY_CONSOLE_LINE_RE = /^\s*console\.(log|debug|trace)\s*\(.*\)\s*;?\s*$/;
6
+ const CODE_LIKE_COMMENT_RE =
7
+ /^\s*\/\/.*(?:[;{}()[\]=]|\b(const|let|var|if|for|while|return|import|export|function|class)\b)/;
8
+
9
+ function identifyCommentedCodeLines(lines) {
10
+ const removable = new Set();
11
+ let streak = [];
12
+
13
+ const flush = () => {
14
+ if (streak.length >= 3) {
15
+ for (const lineIndex of streak) {
16
+ removable.add(lineIndex);
17
+ }
18
+ }
19
+ streak = [];
20
+ };
21
+
22
+ for (let i = 0; i < lines.length; i += 1) {
23
+ if (CODE_LIKE_COMMENT_RE.test(lines[i])) {
24
+ streak.push(i);
25
+ continue;
26
+ }
27
+ flush();
28
+ }
29
+ flush();
30
+
31
+ return removable;
32
+ }
33
+
34
+ function transformContent(content) {
35
+ const lines = content.split("\n");
36
+ const commentedCodeLines = identifyCommentedCodeLines(lines);
37
+
38
+ const stats = {
39
+ removedTodoLines: 0,
40
+ removedCommentedCodeLines: 0,
41
+ removedConsoleLines: 0,
42
+ filesChanged: 0
43
+ };
44
+
45
+ const kept = [];
46
+ for (let i = 0; i < lines.length; i += 1) {
47
+ const line = lines[i];
48
+
49
+ if (commentedCodeLines.has(i)) {
50
+ stats.removedCommentedCodeLines += 1;
51
+ continue;
52
+ }
53
+
54
+ if (TODO_LINE_RE.test(line) || AI_COMMENT_LINE_RE.test(line)) {
55
+ stats.removedTodoLines += 1;
56
+ continue;
57
+ }
58
+
59
+ if (NOISY_CONSOLE_LINE_RE.test(line)) {
60
+ stats.removedConsoleLines += 1;
61
+ continue;
62
+ }
63
+
64
+ kept.push(line);
65
+ }
66
+
67
+ const trimmed = [];
68
+ let blankStreak = 0;
69
+ for (const line of kept) {
70
+ if (line.trim() === "") {
71
+ blankStreak += 1;
72
+ if (blankStreak > 2) {
73
+ continue;
74
+ }
75
+ } else {
76
+ blankStreak = 0;
77
+ }
78
+ trimmed.push(line);
79
+ }
80
+
81
+ const nextContent =
82
+ content.endsWith("\n") || content.length === 0 ? `${trimmed.join("\n")}\n` : trimmed.join("\n");
83
+ const changed = nextContent !== content;
84
+ if (changed) {
85
+ stats.filesChanged = 1;
86
+ }
87
+
88
+ return {
89
+ changed,
90
+ content: nextContent,
91
+ stats
92
+ };
93
+ }
94
+
95
+ export async function applySafeFixes(files) {
96
+ const updates = [];
97
+ const totals = {
98
+ filesChanged: 0,
99
+ removedTodoLines: 0,
100
+ removedCommentedCodeLines: 0,
101
+ removedConsoleLines: 0
102
+ };
103
+
104
+ for (const file of files) {
105
+ const result = transformContent(file.content);
106
+ if (!result.changed) {
107
+ continue;
108
+ }
109
+
110
+ updates.push(
111
+ fs.writeFile(file.path, result.content, "utf8").then(() => {
112
+ file.content = result.content;
113
+ })
114
+ );
115
+
116
+ totals.filesChanged += result.stats.filesChanged;
117
+ totals.removedTodoLines += result.stats.removedTodoLines;
118
+ totals.removedCommentedCodeLines += result.stats.removedCommentedCodeLines;
119
+ totals.removedConsoleLines += result.stats.removedConsoleLines;
120
+ }
121
+
122
+ await Promise.all(updates);
123
+ return totals;
124
+ }
125
+