@emeryld/manager 1.1.0 → 1.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +8 -0
- package/dist/cli/interactive-settings.js +191 -0
- package/dist/create-package/shared.js +3 -1
- package/dist/create-package/variants/client/client_vite_r.js +85 -5
- package/dist/directory-picker.js +158 -0
- package/dist/format-checker/cli/options.js +18 -2
- package/dist/format-checker/cli/prompts.js +27 -193
- package/dist/format-checker/index.js +68 -8
- package/dist/format-checker/report.js +3 -0
- package/dist/format-checker/scan/analysis.js +13 -3
- package/dist/format-checker/scan/duplicates.js +12 -4
- package/dist/format-checker/scan/functions.js +1 -3
- package/dist/format-checker/scan/variables.js +246 -0
- package/dist/menu.js +9 -0
- package/dist/robot/cli/prompts.js +102 -0
- package/dist/robot/cli/settings.js +120 -0
- package/dist/robot/config.js +83 -0
- package/dist/robot/coordinator.js +151 -0
- package/dist/robot/extractors/classes.js +41 -0
- package/dist/robot/extractors/components.js +57 -0
- package/dist/robot/extractors/constants.js +42 -0
- package/dist/robot/extractors/functions.js +40 -0
- package/dist/robot/extractors/shared.js +99 -0
- package/dist/robot/extractors/types.js +43 -0
- package/dist/robot/index.js +1 -0
- package/dist/robot/types.js +1 -0
- package/dist/utils/export.js +99 -0
- package/dist/utils/run.js +3 -0
- package/package.json +1 -1
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { colors } from '../../utils/log.js';
|
|
2
|
-
import { stdin as input
|
|
3
|
-
import { askLine } from '../../prompts.js';
|
|
2
|
+
import { stdin as input } from 'node:process';
|
|
3
|
+
import { askLine, promptSingleKey } from '../../prompts.js';
|
|
4
4
|
import { formatValue, parseInteractiveValue, SETTING_DESCRIPTORS, validateLimits, } from './settings.js';
|
|
5
|
+
import { promptInteractiveSettings, } from '../../cli/interactive-settings.js';
|
|
5
6
|
const READY_PROMPT = colors.dim('Limits retained. Use the navigation above to adjust and confirm again.');
|
|
6
7
|
export async function promptLimits(defaults) {
|
|
7
8
|
const supportsInteractive = typeof input.setRawMode === 'function' && input.isTTY;
|
|
@@ -19,14 +20,16 @@ export async function promptLimits(defaults) {
|
|
|
19
20
|
}
|
|
20
21
|
async function confirmExecution() {
|
|
21
22
|
const question = colors.cyan('Run the format checker with these limits? (Y/n): ');
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
23
|
+
return promptSingleKey(question, (key, raw) => {
|
|
24
|
+
if (!key)
|
|
25
|
+
return undefined;
|
|
26
|
+
if (key === 'y' || key === 'yes')
|
|
25
27
|
return true;
|
|
26
|
-
if (
|
|
28
|
+
if (key === 'n' || key === 'no')
|
|
27
29
|
return false;
|
|
28
|
-
console.log(colors.yellow('Answer "yes" or "no"
|
|
29
|
-
|
|
30
|
+
console.log(colors.yellow('Answer "yes" or "no".'));
|
|
31
|
+
return undefined;
|
|
32
|
+
});
|
|
30
33
|
}
|
|
31
34
|
async function promptLimitsSequential(defaults) {
|
|
32
35
|
console.log(colors.dim('Enter a number to override a limit or press Enter to keep the default.'));
|
|
@@ -44,193 +47,24 @@ async function promptLimitsSequential(defaults) {
|
|
|
44
47
|
};
|
|
45
48
|
}
|
|
46
49
|
async function promptLimitsInteractive(defaults) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
renderedLines: 0,
|
|
64
|
-
};
|
|
65
|
-
const limits = { ...defaults };
|
|
66
|
-
const writableLimits = limits;
|
|
67
|
-
const cleanup = () => {
|
|
68
|
-
if (state.renderedLines > 0) {
|
|
69
|
-
output.write(`\x1b[${state.renderedLines}A`);
|
|
70
|
-
output.write('\x1b[0J');
|
|
71
|
-
state.renderedLines = 0;
|
|
72
|
-
}
|
|
73
|
-
output.write('\x1b[?25h');
|
|
74
|
-
if (!wasRaw) {
|
|
75
|
-
input.setRawMode(false);
|
|
76
|
-
input.pause();
|
|
77
|
-
}
|
|
78
|
-
input.off('data', onData);
|
|
79
|
-
};
|
|
80
|
-
const finalize = () => {
|
|
81
|
-
cleanup();
|
|
82
|
-
console.log();
|
|
83
|
-
resolve(limits);
|
|
84
|
-
};
|
|
85
|
-
const render = () => {
|
|
86
|
-
const lines = buildInteractiveLines(limits, state.selectedIndex, state.editingIndex, state.editBuffer, state.errorMessage);
|
|
87
|
-
if (state.renderedLines > 0) {
|
|
88
|
-
output.write(`\x1b[${state.renderedLines}A`);
|
|
89
|
-
output.write('\x1b[0J');
|
|
90
|
-
}
|
|
91
|
-
lines.forEach((line) => console.log(line));
|
|
92
|
-
state.renderedLines = lines.length;
|
|
93
|
-
};
|
|
94
|
-
const startEditing = () => {
|
|
95
|
-
state.editingIndex = state.selectedIndex;
|
|
96
|
-
state.editBuffer = '';
|
|
97
|
-
state.justFocused = true;
|
|
98
|
-
state.errorMessage = '';
|
|
99
|
-
};
|
|
100
|
-
const moveSelection = (delta) => {
|
|
101
|
-
state.selectedIndex =
|
|
102
|
-
(state.selectedIndex + delta + SETTING_DESCRIPTORS.length) %
|
|
103
|
-
SETTING_DESCRIPTORS.length;
|
|
104
|
-
state.editingIndex = null;
|
|
105
|
-
state.editBuffer = '';
|
|
106
|
-
state.justFocused = false;
|
|
107
|
-
state.errorMessage = '';
|
|
108
|
-
render();
|
|
109
|
-
};
|
|
110
|
-
const commitEdit = () => {
|
|
111
|
-
if (state.editingIndex === null)
|
|
112
|
-
return;
|
|
113
|
-
const descriptor = SETTING_DESCRIPTORS[state.editingIndex];
|
|
114
|
-
const parsed = parseInteractiveValue(descriptor, state.editBuffer);
|
|
115
|
-
if (parsed.error) {
|
|
116
|
-
state.errorMessage = parsed.error;
|
|
117
|
-
process.stdout.write('\x07');
|
|
118
|
-
render();
|
|
119
|
-
return;
|
|
120
|
-
}
|
|
121
|
-
if (parsed.value !== undefined) {
|
|
122
|
-
writableLimits[descriptor.key] = parsed.value;
|
|
123
|
-
}
|
|
124
|
-
state.editingIndex = null;
|
|
125
|
-
state.editBuffer = '';
|
|
126
|
-
state.justFocused = false;
|
|
127
|
-
state.errorMessage = '';
|
|
128
|
-
render();
|
|
129
|
-
};
|
|
130
|
-
const handlePrintable = (typedChar) => {
|
|
131
|
-
if (state.editingIndex !== state.selectedIndex) {
|
|
132
|
-
startEditing();
|
|
133
|
-
}
|
|
134
|
-
if (state.justFocused) {
|
|
135
|
-
state.editBuffer = typedChar;
|
|
136
|
-
state.justFocused = false;
|
|
137
|
-
}
|
|
138
|
-
else {
|
|
139
|
-
state.editBuffer += typedChar;
|
|
140
|
-
}
|
|
141
|
-
render();
|
|
142
|
-
};
|
|
143
|
-
const handleBackspace = () => {
|
|
144
|
-
if (state.editingIndex !== state.selectedIndex) {
|
|
145
|
-
startEditing();
|
|
146
|
-
state.justFocused = false;
|
|
147
|
-
}
|
|
148
|
-
if (state.editBuffer.length > 0) {
|
|
149
|
-
state.editBuffer = state.editBuffer.slice(0, -1);
|
|
150
|
-
render();
|
|
151
|
-
return;
|
|
152
|
-
}
|
|
153
|
-
process.stdout.write('\x07');
|
|
154
|
-
};
|
|
155
|
-
const onData = (buffer) => {
|
|
156
|
-
const ascii = buffer.length === 1 ? buffer[0] : undefined;
|
|
157
|
-
const isPrintable = ascii !== undefined && ascii >= 0x20 && ascii <= 0x7e;
|
|
158
|
-
const typedChar = isPrintable && ascii !== undefined ? String.fromCharCode(ascii) : '';
|
|
159
|
-
const isArrowUp = buffer.equals(Buffer.from([0x1b, 0x5b, 0x41]));
|
|
160
|
-
const isArrowDown = buffer.equals(Buffer.from([0x1b, 0x5b, 0x42]));
|
|
161
|
-
const isEnter = buffer.length === 1 && (buffer[0] === 0x0d || buffer[0] === 0x0a);
|
|
162
|
-
const isEscape = buffer.length === 1 && buffer[0] === 0x1b;
|
|
163
|
-
const isCtrlC = buffer.length === 1 && buffer[0] === 0x03;
|
|
164
|
-
const isBackspace = buffer.length === 1 && (buffer[0] === 0x7f || buffer[0] === 0x08);
|
|
165
|
-
if (isCtrlC || isEscape) {
|
|
166
|
-
cleanup();
|
|
167
|
-
process.exit(1);
|
|
168
|
-
}
|
|
169
|
-
if (isArrowUp ||
|
|
170
|
-
(buffer.length === 1 && (buffer[0] === 0x6b || buffer[0] === 0x4b))) {
|
|
171
|
-
moveSelection(-1);
|
|
172
|
-
return;
|
|
173
|
-
}
|
|
174
|
-
if (isArrowDown ||
|
|
175
|
-
(buffer.length === 1 && (buffer[0] === 0x6a || buffer[0] === 0x4a))) {
|
|
176
|
-
moveSelection(1);
|
|
177
|
-
return;
|
|
178
|
-
}
|
|
179
|
-
if (isEnter) {
|
|
180
|
-
if (state.editingIndex === state.selectedIndex) {
|
|
181
|
-
commitEdit();
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
const validation = validateLimits(limits);
|
|
185
|
-
if (validation) {
|
|
186
|
-
state.errorMessage = validation;
|
|
187
|
-
process.stdout.write('\x07');
|
|
188
|
-
render();
|
|
189
|
-
return;
|
|
190
|
-
}
|
|
191
|
-
finalize();
|
|
192
|
-
return;
|
|
193
|
-
}
|
|
194
|
-
if (isBackspace) {
|
|
195
|
-
handleBackspace();
|
|
196
|
-
return;
|
|
197
|
-
}
|
|
198
|
-
if (isPrintable && typedChar) {
|
|
199
|
-
handlePrintable(typedChar);
|
|
200
|
-
return;
|
|
201
|
-
}
|
|
202
|
-
process.stdout.write('\x07');
|
|
203
|
-
};
|
|
204
|
-
input.on('data', onData);
|
|
205
|
-
render();
|
|
50
|
+
const descriptors = SETTING_DESCRIPTORS.map((descriptor) => ({
|
|
51
|
+
key: descriptor.key,
|
|
52
|
+
label: descriptor.label,
|
|
53
|
+
unit: descriptor.unit,
|
|
54
|
+
format: (value) => formatValue(value, descriptor),
|
|
55
|
+
parse: (buffer) => parseInteractiveValue(descriptor, buffer),
|
|
56
|
+
}));
|
|
57
|
+
return promptInteractiveSettings({
|
|
58
|
+
title: 'Format checker settings (type to edit values)',
|
|
59
|
+
descriptors,
|
|
60
|
+
initial: defaults,
|
|
61
|
+
instructions: [
|
|
62
|
+
'Use ↑/↓ to change rows, type to replace the highlighted value, Backspace to clear characters, and Enter to validate and confirm the selection.',
|
|
63
|
+
'Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.',
|
|
64
|
+
],
|
|
65
|
+
validate: validateLimits,
|
|
206
66
|
});
|
|
207
67
|
}
|
|
208
|
-
function buildInteractiveLines(limits, selectedIndex, editingIndex, editBuffer, errorMessage) {
|
|
209
|
-
const heading = colors.bold('Format checker settings (type to edit values)');
|
|
210
|
-
const lines = [heading, ''];
|
|
211
|
-
SETTING_DESCRIPTORS.forEach((descriptor, index) => {
|
|
212
|
-
const isSelected = index === selectedIndex;
|
|
213
|
-
const pointer = isSelected ? colors.green('➤') : ' ';
|
|
214
|
-
const label = descriptor.unit
|
|
215
|
-
? `${descriptor.label} (${descriptor.unit})`
|
|
216
|
-
: descriptor.label;
|
|
217
|
-
const isActive = editingIndex === index;
|
|
218
|
-
const baseValue = formatValue(limits[descriptor.key], descriptor);
|
|
219
|
-
const displayValue = isActive
|
|
220
|
-
? colors.yellow(editBuffer.length > 0 ? editBuffer : baseValue)
|
|
221
|
-
: colors.magenta(baseValue);
|
|
222
|
-
const labelColor = isSelected ? colors.green : colors.cyan;
|
|
223
|
-
const line = `${pointer} ${labelColor(label)}: ${displayValue}`;
|
|
224
|
-
lines.push(line);
|
|
225
|
-
});
|
|
226
|
-
lines.push('');
|
|
227
|
-
lines.push(colors.dim('Use ↑/↓ to change rows, type to replace the highlighted value, Backspace to clear characters, and Enter to validate and confirm the selection.'));
|
|
228
|
-
lines.push(colors.dim('Press Esc/Ctrl+C to abort, or hit Enter again after reviewing the summary to continue.'));
|
|
229
|
-
if (errorMessage) {
|
|
230
|
-
lines.push(colors.red(`⚠ ${errorMessage}`));
|
|
231
|
-
}
|
|
232
|
-
return lines;
|
|
233
|
-
}
|
|
234
68
|
async function promptNumber(label, fallback, unit, options) {
|
|
235
69
|
const question = colors.cyan(`${label} (${unit}) [default ${fallback}]: `);
|
|
236
70
|
while (true) {
|
|
@@ -1,4 +1,7 @@
|
|
|
1
1
|
import { colors } from '../utils/log.js';
|
|
2
|
+
import { captureConsoleOutput, exportReportLines, promptExportMode, } from '../utils/export.js';
|
|
3
|
+
import { promptDirectorySelection, describeDirectorySelection } from '../directory-picker.js';
|
|
4
|
+
import { runHelperCli } from '../helper-cli.js';
|
|
2
5
|
import { rootDir } from '../helper-cli/env.js';
|
|
3
6
|
import { loadFormatLimits, } from './config.js';
|
|
4
7
|
import { collectSourceFiles, analyzeFiles } from './scan/index.js';
|
|
@@ -8,11 +11,57 @@ import { parseScanCliArgs, printScanUsage } from './cli/options.js';
|
|
|
8
11
|
export async function runFormatChecker() {
|
|
9
12
|
console.log(colors.cyan('Gathering defaults from .vscode/settings.json (manager.formatChecker)'));
|
|
10
13
|
const defaults = await loadFormatLimits();
|
|
11
|
-
|
|
12
|
-
|
|
14
|
+
let limits = defaults;
|
|
15
|
+
let exportMode = 'console';
|
|
16
|
+
while (true) {
|
|
17
|
+
let lastAction;
|
|
18
|
+
const scripts = [
|
|
19
|
+
{
|
|
20
|
+
name: 'Run format checker',
|
|
21
|
+
emoji: '🧮',
|
|
22
|
+
description: `${formatLimitsSummary(limits)} · export=${exportMode}`,
|
|
23
|
+
handler: async () => {
|
|
24
|
+
const selection = await promptDirectorySelection({
|
|
25
|
+
title: 'Select directory for format scan',
|
|
26
|
+
});
|
|
27
|
+
if (!selection)
|
|
28
|
+
return;
|
|
29
|
+
lastAction = 'run';
|
|
30
|
+
await executeFormatCheck(limits, exportMode, selection.absolutePath, describeDirectorySelection(selection));
|
|
31
|
+
},
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: 'Adjust limits',
|
|
35
|
+
emoji: '⚙️',
|
|
36
|
+
description: formatLimitsSummary(limits),
|
|
37
|
+
handler: async () => {
|
|
38
|
+
limits = await promptLimits(limits);
|
|
39
|
+
lastAction = 'configure';
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
{
|
|
43
|
+
name: 'Change export mode',
|
|
44
|
+
emoji: '📤',
|
|
45
|
+
description: `Current: ${exportMode}`,
|
|
46
|
+
handler: async () => {
|
|
47
|
+
exportMode = await promptExportMode();
|
|
48
|
+
lastAction = 'export';
|
|
49
|
+
},
|
|
50
|
+
},
|
|
51
|
+
];
|
|
52
|
+
const ran = await runHelperCli({
|
|
53
|
+
title: 'Format checker helper',
|
|
54
|
+
scripts,
|
|
55
|
+
argv: [],
|
|
56
|
+
});
|
|
57
|
+
if (!ran)
|
|
58
|
+
return;
|
|
59
|
+
if (lastAction === 'run')
|
|
60
|
+
return;
|
|
61
|
+
}
|
|
13
62
|
}
|
|
14
63
|
export async function runFormatCheckerScanCli(argv) {
|
|
15
|
-
const { overrides, help } = parseScanCliArgs(argv);
|
|
64
|
+
const { overrides, help, exportMode } = parseScanCliArgs(argv);
|
|
16
65
|
if (help) {
|
|
17
66
|
printScanUsage();
|
|
18
67
|
return;
|
|
@@ -20,19 +69,30 @@ export async function runFormatCheckerScanCli(argv) {
|
|
|
20
69
|
const defaults = await loadFormatLimits();
|
|
21
70
|
const limits = { ...defaults, ...overrides };
|
|
22
71
|
console.log(colors.cyan('Running format checker scan (machine-friendly)'));
|
|
23
|
-
await executeFormatCheck(limits);
|
|
72
|
+
await executeFormatCheck(limits, exportMode);
|
|
24
73
|
}
|
|
25
|
-
async function executeFormatCheck(limits) {
|
|
26
|
-
console.log(colors.magenta(
|
|
27
|
-
const files = await collectSourceFiles(
|
|
74
|
+
async function executeFormatCheck(limits, exportMode, scanRoot = rootDir, scopeLabel = 'workspace') {
|
|
75
|
+
console.log(colors.magenta(`Scanning ${scopeLabel} for source files...`));
|
|
76
|
+
const files = await collectSourceFiles(scanRoot);
|
|
28
77
|
if (files.length === 0) {
|
|
29
78
|
console.log(colors.yellow('No source files were found to analyze.'));
|
|
30
79
|
return;
|
|
31
80
|
}
|
|
32
81
|
console.log(colors.magenta(`Analyzing ${files.length} files`));
|
|
33
82
|
const violations = await analyzeFiles(files, limits);
|
|
34
|
-
const hadViolations =
|
|
83
|
+
const hadViolations = await handleViolations(violations, limits.reportingMode, exportMode);
|
|
35
84
|
if (!hadViolations) {
|
|
36
85
|
console.log(colors.green('Workspace meets the configured format limits.'));
|
|
37
86
|
}
|
|
38
87
|
}
|
|
88
|
+
async function handleViolations(violations, reportingMode, exportMode) {
|
|
89
|
+
if (exportMode === 'console') {
|
|
90
|
+
return reportViolations(violations, reportingMode);
|
|
91
|
+
}
|
|
92
|
+
const { result, lines } = captureConsoleOutput(() => reportViolations(violations, reportingMode));
|
|
93
|
+
await exportReportLines('format-checker', 'txt', lines);
|
|
94
|
+
return result;
|
|
95
|
+
}
|
|
96
|
+
function formatLimitsSummary(limits) {
|
|
97
|
+
return `limits: fn≤${limits.maxFunctionLength} · indent≤${limits.maxIndentationDepth} · reporting=${limits.reportingMode} · exportOnly=${limits.exportOnly}`;
|
|
98
|
+
}
|
|
@@ -109,6 +109,9 @@ function logViolationEntry(entry, messagePrefix, locationDescriptor) {
|
|
|
109
109
|
console.log(` ${colors.yellow(line)}`);
|
|
110
110
|
});
|
|
111
111
|
}
|
|
112
|
+
if (entry.requiredVariables?.length) {
|
|
113
|
+
console.log(` ${colors.dim('Required variables:')} ${entry.requiredVariables.join(', ')}`);
|
|
114
|
+
}
|
|
112
115
|
if (locationDescriptor) {
|
|
113
116
|
console.log(` ${colors.dim(locationDescriptor)}`);
|
|
114
117
|
}
|
|
@@ -2,7 +2,9 @@ import { readFile } from 'node:fs/promises';
|
|
|
2
2
|
import { collectDuplicateViolations, recordDuplicateLines, } from './duplicates.js';
|
|
3
3
|
import { countIndentation, groupIndentationViolations, } from './indentation.js';
|
|
4
4
|
import { collectFunctionRecords } from './functions.js';
|
|
5
|
-
import { markImportLines, normalizeLine } from './utils.js';
|
|
5
|
+
import { markImportLines, normalizeLine, resolveScriptKind } from './utils.js';
|
|
6
|
+
import { collectRequiredVariables } from './variables.js';
|
|
7
|
+
import * as ts from 'typescript';
|
|
6
8
|
export async function analyzeFiles(files, limits) {
|
|
7
9
|
const violations = [];
|
|
8
10
|
const duplicateMap = new Map();
|
|
@@ -45,12 +47,19 @@ async function analyzeSingleFile(filePath, limits, duplicates) {
|
|
|
45
47
|
indentationViolations.push({ line: index + 1, indent });
|
|
46
48
|
}
|
|
47
49
|
});
|
|
48
|
-
|
|
50
|
+
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ESNext, true, resolveScriptKind(filePath));
|
|
51
|
+
recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines);
|
|
49
52
|
const indentationGroups = groupIndentationViolations(indentationViolations, lines, limits.maxIndentationDepth);
|
|
50
53
|
indentationGroups
|
|
51
54
|
.sort((a, b) => b.severity - a.severity)
|
|
52
55
|
.slice(0, 4)
|
|
53
56
|
.forEach((group) => {
|
|
57
|
+
const requiredVariables = collectRequiredVariables(sourceFile, lines, {
|
|
58
|
+
startLine: group.startLine,
|
|
59
|
+
startColumn: group.startColumn,
|
|
60
|
+
endLine: group.endLine,
|
|
61
|
+
endColumn: group.endColumn,
|
|
62
|
+
});
|
|
54
63
|
violations.push({
|
|
55
64
|
type: 'indentation',
|
|
56
65
|
file: filePath,
|
|
@@ -59,9 +68,10 @@ async function analyzeSingleFile(filePath, limits, duplicates) {
|
|
|
59
68
|
message: `Indentation level ${group.maxIndent} exceeds max ${limits.maxIndentationDepth}`,
|
|
60
69
|
snippet: group.snippet,
|
|
61
70
|
detail: `${group.startLine}:${group.startColumn}/${group.endLine}:${group.endColumn}`,
|
|
71
|
+
requiredVariables,
|
|
62
72
|
});
|
|
63
73
|
});
|
|
64
|
-
const allFunctions = collectFunctionRecords(
|
|
74
|
+
const allFunctions = collectFunctionRecords(sourceFile);
|
|
65
75
|
const functions = limits.exportOnly
|
|
66
76
|
? allFunctions.filter((record) => record.isExported)
|
|
67
77
|
: allFunctions;
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
2
|
import { getLineColumnRange } from './utils.js';
|
|
3
|
-
|
|
3
|
+
import { collectRequiredVariables } from './variables.js';
|
|
4
|
+
export function recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines) {
|
|
4
5
|
const minLines = Math.max(1, limits.minDuplicateLines);
|
|
5
6
|
if (limits.minDuplicateLines <= 0 || minLines === 1) {
|
|
6
7
|
normalizedLines.forEach((entry, index) => {
|
|
@@ -12,7 +13,7 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
|
|
|
12
13
|
if (entry.isImport)
|
|
13
14
|
return;
|
|
14
15
|
const { startColumn, endColumn } = getLineColumnRange(entry.raw);
|
|
15
|
-
addDuplicateOccurrence(duplicates, entry.normalized, filePath, index + 1, startColumn, index + 1, endColumn, snippet);
|
|
16
|
+
addDuplicateOccurrence(duplicates, entry.normalized, filePath, index + 1, startColumn, index + 1, endColumn, snippet, sourceFile, lines);
|
|
16
17
|
});
|
|
17
18
|
return;
|
|
18
19
|
}
|
|
@@ -47,10 +48,10 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
|
|
|
47
48
|
const endLine = i + 1 + lastNonEmptyIndex;
|
|
48
49
|
const { startColumn } = getLineColumnRange(slice[firstNonEmptyIndex].raw);
|
|
49
50
|
const { endColumn } = getLineColumnRange(slice[lastNonEmptyIndex].raw);
|
|
50
|
-
addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet);
|
|
51
|
+
addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet, sourceFile, lines);
|
|
51
52
|
}
|
|
52
53
|
}
|
|
53
|
-
function addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet) {
|
|
54
|
+
function addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet, sourceFile, lines) {
|
|
54
55
|
const tracker = duplicates.get(key);
|
|
55
56
|
if (tracker) {
|
|
56
57
|
tracker.count += 1;
|
|
@@ -69,6 +70,12 @@ function addDuplicateOccurrence(duplicates, key, filePath, startLine, startColum
|
|
|
69
70
|
duplicates.set(key, {
|
|
70
71
|
count: 1,
|
|
71
72
|
snippet,
|
|
73
|
+
requiredVariables: collectRequiredVariables(sourceFile, lines, {
|
|
74
|
+
startLine,
|
|
75
|
+
startColumn,
|
|
76
|
+
endLine,
|
|
77
|
+
endColumn,
|
|
78
|
+
}),
|
|
72
79
|
occurrences: [
|
|
73
80
|
{
|
|
74
81
|
file: filePath,
|
|
@@ -102,6 +109,7 @@ export function collectDuplicateViolations(duplicates, limits) {
|
|
|
102
109
|
.join('\n'),
|
|
103
110
|
snippet: tracker.snippet,
|
|
104
111
|
repeatCount: tracker.count,
|
|
112
|
+
requiredVariables: tracker.requiredVariables,
|
|
105
113
|
});
|
|
106
114
|
}
|
|
107
115
|
return violations;
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import * as ts from 'typescript';
|
|
2
|
-
|
|
3
|
-
export function collectFunctionRecords(content, filePath) {
|
|
4
|
-
const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ESNext, true, resolveScriptKind(filePath));
|
|
2
|
+
export function collectFunctionRecords(sourceFile) {
|
|
5
3
|
const records = [];
|
|
6
4
|
function visit(node) {
|
|
7
5
|
if ((ts.isFunctionDeclaration(node) ||
|