@emeryld/manager 1.4.4 → 1.4.6
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.
|
@@ -23,18 +23,12 @@ export function reportViolations(violations, reportingMode) {
|
|
|
23
23
|
return false;
|
|
24
24
|
}
|
|
25
25
|
console.log(colors.bold('Format checker violations:'));
|
|
26
|
-
console.log('');
|
|
27
26
|
if (reportingMode === 'group') {
|
|
28
|
-
let hasPrintedGroupBlock = false;
|
|
29
27
|
for (const type of TYPE_ORDER) {
|
|
30
28
|
const entries = violations.filter((violation) => violation.type === type);
|
|
31
29
|
if (!entries.length)
|
|
32
30
|
continue;
|
|
33
31
|
entries.sort((a, b) => b.severity - a.severity);
|
|
34
|
-
if (hasPrintedGroupBlock) {
|
|
35
|
-
console.log('');
|
|
36
|
-
}
|
|
37
|
-
hasPrintedGroupBlock = true;
|
|
38
32
|
console.log(colors.bold(`- ${TYPE_LABELS[type]} (${entries.length})`));
|
|
39
33
|
for (const entry of entries) {
|
|
40
34
|
const relativePath = path.relative(rootDir, entry.file) || entry.file;
|
|
@@ -59,10 +53,10 @@ export function reportViolations(violations, reportingMode) {
|
|
|
59
53
|
});
|
|
60
54
|
}
|
|
61
55
|
}
|
|
56
|
+
console.log('');
|
|
62
57
|
}
|
|
63
58
|
}
|
|
64
59
|
else {
|
|
65
|
-
let hasPrintedFileBlock = false;
|
|
66
60
|
const violationsByFile = new Map();
|
|
67
61
|
for (const entry of violations) {
|
|
68
62
|
const bucket = violationsByFile.get(entry.file);
|
|
@@ -76,10 +70,6 @@ export function reportViolations(violations, reportingMode) {
|
|
|
76
70
|
const sortedFiles = [...violationsByFile.entries()].sort(([a], [b]) => a.localeCompare(b));
|
|
77
71
|
for (const [file, entries] of sortedFiles) {
|
|
78
72
|
const relativePath = path.relative(rootDir, file) || file;
|
|
79
|
-
if (hasPrintedFileBlock) {
|
|
80
|
-
console.log('');
|
|
81
|
-
}
|
|
82
|
-
hasPrintedFileBlock = true;
|
|
83
73
|
console.log(colors.bold(relativePath));
|
|
84
74
|
const sortedEntries = [...entries].sort((a, b) => (a.line ?? 0) - (b.line ?? 0));
|
|
85
75
|
for (const entry of sortedEntries) {
|
|
@@ -107,6 +97,8 @@ export function reportViolations(violations, reportingMode) {
|
|
|
107
97
|
}
|
|
108
98
|
logViolationEntry(entry, ' ', locationDescriptor);
|
|
109
99
|
}
|
|
100
|
+
console.log('');
|
|
101
|
+
console.log('');
|
|
110
102
|
}
|
|
111
103
|
}
|
|
112
104
|
return true;
|
|
@@ -1,8 +1,9 @@
|
|
|
1
|
+
// analysis/analysis.ts
|
|
1
2
|
import { readFile } from 'node:fs/promises';
|
|
2
3
|
import { collectDuplicateViolations, recordDuplicateLines, } from './duplicates.js';
|
|
3
4
|
import { countIndentation, groupIndentationViolations, } from './indentation.js';
|
|
4
5
|
import { collectFunctionRecords } from './functions.js';
|
|
5
|
-
import { markImportLines, normalizeLine, resolveScriptKind } from './utils.js';
|
|
6
|
+
import { isPunctuationOnlyLine, markImportLines, normalizeLine, resolveScriptKind, } from './utils.js';
|
|
6
7
|
import { collectRequiredVariables } from './variables.js';
|
|
7
8
|
import * as ts from 'typescript';
|
|
8
9
|
export async function analyzeFiles(files, limits) {
|
|
@@ -29,6 +30,7 @@ async function analyzeSingleFile(filePath, limits, duplicates, fileSnapshots) {
|
|
|
29
30
|
normalized: normalizeLine(line),
|
|
30
31
|
trimmed: line.trim(),
|
|
31
32
|
isImport: Boolean(importFlags[index]),
|
|
33
|
+
isPunctuationOnly: isPunctuationOnlyLine(line),
|
|
32
34
|
raw: line,
|
|
33
35
|
}));
|
|
34
36
|
const violations = [];
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
// analysis/duplicates.ts
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import { getLineColumnRange } from './utils.js';
|
|
3
4
|
import { collectRequiredVariables } from './variables.js';
|
|
5
|
+
function trimRightKeepIndent(line) {
|
|
6
|
+
// Preserve leading indentation exactly; remove trailing whitespace only.
|
|
7
|
+
return line.replace(/[ \t]+$/g, '');
|
|
8
|
+
}
|
|
4
9
|
export function recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines, fileSnapshots) {
|
|
5
10
|
const minLines = Math.max(1, limits.minDuplicateLines);
|
|
6
11
|
const registerSnapshot = () => {
|
|
@@ -10,8 +15,11 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
|
|
|
10
15
|
normalizedLines.forEach((entry, index) => {
|
|
11
16
|
if (!entry.normalized)
|
|
12
17
|
return;
|
|
13
|
-
|
|
14
|
-
if (
|
|
18
|
+
// Do not treat punctuation-only lines as standalone duplicate lines.
|
|
19
|
+
if (entry.isPunctuationOnly)
|
|
20
|
+
return;
|
|
21
|
+
const snippet = trimRightKeepIndent(entry.raw);
|
|
22
|
+
if (!snippet.trim())
|
|
15
23
|
return;
|
|
16
24
|
if (entry.isImport)
|
|
17
25
|
return;
|
|
@@ -26,18 +34,14 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
|
|
|
26
34
|
if (slice.some((entry) => !entry.normalized))
|
|
27
35
|
continue;
|
|
28
36
|
const key = slice.map((entry) => entry.normalized).join('\n');
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
if (!snippetLines.length)
|
|
37
|
+
// Require at least one non-empty, non-punctuation-only line to consider this a meaningful duplicate.
|
|
38
|
+
const meaningfulLineCount = slice.filter((entry) => entry.trimmed.length > 0 && !entry.isPunctuationOnly).length;
|
|
39
|
+
if (meaningfulLineCount === 0)
|
|
33
40
|
continue;
|
|
34
41
|
if (slice.some((entry) => entry.isImport))
|
|
35
42
|
continue;
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
.join('\n');
|
|
39
|
-
if (!snippet)
|
|
40
|
-
continue;
|
|
43
|
+
// Determine the first/last non-empty lines (we still include punctuation-only lines
|
|
44
|
+
// inside that non-empty span when building the snippet).
|
|
41
45
|
const firstNonEmptyIndex = slice.findIndex((entry) => entry.trimmed.length > 0);
|
|
42
46
|
let lastNonEmptyIndex = -1;
|
|
43
47
|
for (let offset = slice.length - 1; offset >= 0; offset -= 1) {
|
|
@@ -52,6 +56,11 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
|
|
|
52
56
|
const endLine = i + 1 + lastNonEmptyIndex;
|
|
53
57
|
const { startColumn } = getLineColumnRange(slice[firstNonEmptyIndex].raw);
|
|
54
58
|
const { endColumn } = getLineColumnRange(slice[lastNonEmptyIndex].raw);
|
|
59
|
+
// Keep real indentation in the reported snippet; remove trailing whitespace only.
|
|
60
|
+
const snippet = slice
|
|
61
|
+
.slice(firstNonEmptyIndex, lastNonEmptyIndex + 1)
|
|
62
|
+
.map((entry) => trimRightKeepIndent(entry.raw))
|
|
63
|
+
.join('\n');
|
|
55
64
|
registerSnapshot();
|
|
56
65
|
addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet, sourceFile, lines);
|
|
57
66
|
}
|
|
@@ -103,13 +112,13 @@ function ensureFileSnapshot(fileSnapshots, filePath, normalizedLines, lines, sou
|
|
|
103
112
|
});
|
|
104
113
|
}
|
|
105
114
|
function fingerprintLine(entry) {
|
|
115
|
+
if (entry.isImport)
|
|
116
|
+
return undefined;
|
|
106
117
|
if (entry.normalized)
|
|
107
118
|
return entry.normalized;
|
|
108
119
|
const collapsed = entry.trimmed.replace(/\s+/g, ' ');
|
|
109
120
|
if (!collapsed)
|
|
110
121
|
return undefined;
|
|
111
|
-
if (!/[A-Za-z0-9]/.test(collapsed))
|
|
112
|
-
return undefined;
|
|
113
122
|
return collapsed.toLowerCase();
|
|
114
123
|
}
|
|
115
124
|
function buildOccurrenceInfo(occurrence, fileSnapshots) {
|
|
@@ -136,14 +145,22 @@ function expandDuplicateTracker(tracker, fileSnapshots) {
|
|
|
136
145
|
if (infos.length < 2)
|
|
137
146
|
return undefined;
|
|
138
147
|
expandRangeAcrossOccurrences(infos);
|
|
139
|
-
const primary = infos
|
|
140
|
-
|
|
148
|
+
const primary = infos.reduce((current, info) => {
|
|
149
|
+
if (!current)
|
|
150
|
+
return info;
|
|
151
|
+
const pathComparison = current.file.localeCompare(info.file);
|
|
152
|
+
if (pathComparison > 0)
|
|
153
|
+
return info;
|
|
154
|
+
if (pathComparison === 0 && info.startIndex < current.startIndex) {
|
|
155
|
+
return info;
|
|
156
|
+
}
|
|
157
|
+
return current;
|
|
158
|
+
}, infos[0]);
|
|
159
|
+
const snippetLines = primary.snapshot.normalizedLines
|
|
141
160
|
.slice(primary.startIndex, primary.endIndex + 1)
|
|
142
|
-
.map((entry) => entry.
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
return undefined;
|
|
146
|
-
const snippet = lines.map((line) => line.replace(/\s+/g, ' ')).join('\n');
|
|
161
|
+
.map((entry) => trimRightKeepIndent(entry.raw));
|
|
162
|
+
// Preserve indentation; do not collapse whitespace.
|
|
163
|
+
const snippet = snippetLines.join('\n');
|
|
147
164
|
const startLine = primary.startIndex + 1;
|
|
148
165
|
const endLine = primary.endIndex + 1;
|
|
149
166
|
const startColumn = getLineColumnRange(primary.snapshot.normalizedLines[primary.startIndex].raw).startColumn;
|
|
@@ -154,9 +171,16 @@ function expandDuplicateTracker(tracker, fileSnapshots) {
|
|
|
154
171
|
endLine,
|
|
155
172
|
endColumn,
|
|
156
173
|
});
|
|
174
|
+
const detailInfos = [...infos].sort((a, b) => {
|
|
175
|
+
const pathComparison = a.file.localeCompare(b.file);
|
|
176
|
+
if (pathComparison !== 0)
|
|
177
|
+
return pathComparison;
|
|
178
|
+
return a.startIndex - b.startIndex;
|
|
179
|
+
});
|
|
180
|
+
const detail = detailInfos.map(formatOccurrenceDetail).join('\n');
|
|
157
181
|
return {
|
|
158
182
|
snippet,
|
|
159
|
-
detail
|
|
183
|
+
detail,
|
|
160
184
|
requiredVariables,
|
|
161
185
|
};
|
|
162
186
|
}
|
|
@@ -194,12 +218,8 @@ function formatOccurrenceDetail(info) {
|
|
|
194
218
|
const endLine = info.endIndex + 1;
|
|
195
219
|
const startEntry = info.snapshot.normalizedLines[info.startIndex];
|
|
196
220
|
const endEntry = info.snapshot.normalizedLines[info.endIndex];
|
|
197
|
-
const startColumn = startEntry
|
|
198
|
-
|
|
199
|
-
: 1;
|
|
200
|
-
const endColumn = endEntry
|
|
201
|
-
? getLineColumnRange(endEntry.raw).endColumn
|
|
202
|
-
: 1;
|
|
221
|
+
const startColumn = startEntry ? getLineColumnRange(startEntry.raw).startColumn : 1;
|
|
222
|
+
const endColumn = endEntry ? getLineColumnRange(endEntry.raw).endColumn : 1;
|
|
203
223
|
const relativePath = path.relative(process.cwd(), info.file);
|
|
204
224
|
return `${relativePath}:${startLine}:${startColumn}/${endLine}:${endColumn}`;
|
|
205
225
|
}
|
|
@@ -1,11 +1,24 @@
|
|
|
1
|
+
// analysis/utils.ts
|
|
1
2
|
import path from 'node:path';
|
|
2
3
|
import * as ts from 'typescript';
|
|
4
|
+
// Lines that are only punctuation should still participate in duplicate matching/expansion,
|
|
5
|
+
// but should NOT be treated as "fully duplicate lines" on their own.
|
|
6
|
+
export function isPunctuationOnlyLine(line) {
|
|
7
|
+
const trimmed = line.trim();
|
|
8
|
+
if (!trimmed)
|
|
9
|
+
return false;
|
|
10
|
+
// Only these punctuation characters: {}();,<>[]
|
|
11
|
+
return /^[\{\}\(\);,<>[\]]+$/.test(trimmed);
|
|
12
|
+
}
|
|
3
13
|
export function normalizeLine(line) {
|
|
4
14
|
const trimmed = line.trim();
|
|
5
15
|
if (!trimmed)
|
|
6
16
|
return undefined;
|
|
7
|
-
|
|
8
|
-
|
|
17
|
+
// Keep punctuation-only lines in the duplicate fingerprint (so blocks that include them match),
|
|
18
|
+
// but they'll be excluded from single-line duplicate violations in duplicates.ts.
|
|
19
|
+
if (isPunctuationOnlyLine(trimmed)) {
|
|
20
|
+
return `__punct__:${trimmed}`;
|
|
21
|
+
}
|
|
9
22
|
const collapse = trimmed.replace(/\s+/g, ' ');
|
|
10
23
|
const noNumbers = collapse.replace(/\d+/g, '#');
|
|
11
24
|
return noNumbers.toLowerCase();
|