@emeryld/manager 1.4.5 → 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.
|
@@ -98,6 +98,7 @@ export function reportViolations(violations, reportingMode) {
|
|
|
98
98
|
logViolationEntry(entry, ' ', locationDescriptor);
|
|
99
99
|
}
|
|
100
100
|
console.log('');
|
|
101
|
+
console.log('');
|
|
101
102
|
}
|
|
102
103
|
}
|
|
103
104
|
return true;
|
|
@@ -115,5 +116,4 @@ function logViolationEntry(entry, messagePrefix, locationDescriptor) {
|
|
|
115
116
|
if (locationDescriptor) {
|
|
116
117
|
console.log(` ${colors.dim(locationDescriptor)}`);
|
|
117
118
|
}
|
|
118
|
-
console.log('');
|
|
119
119
|
}
|
|
@@ -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
|
}
|
|
@@ -147,13 +156,11 @@ function expandDuplicateTracker(tracker, fileSnapshots) {
|
|
|
147
156
|
}
|
|
148
157
|
return current;
|
|
149
158
|
}, infos[0]);
|
|
150
|
-
const
|
|
159
|
+
const snippetLines = primary.snapshot.normalizedLines
|
|
151
160
|
.slice(primary.startIndex, primary.endIndex + 1)
|
|
152
|
-
.map((entry) => entry.
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
return undefined;
|
|
156
|
-
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');
|
|
157
164
|
const startLine = primary.startIndex + 1;
|
|
158
165
|
const endLine = primary.endIndex + 1;
|
|
159
166
|
const startColumn = getLineColumnRange(primary.snapshot.normalizedLines[primary.startIndex].raw).startColumn;
|
|
@@ -211,12 +218,8 @@ function formatOccurrenceDetail(info) {
|
|
|
211
218
|
const endLine = info.endIndex + 1;
|
|
212
219
|
const startEntry = info.snapshot.normalizedLines[info.startIndex];
|
|
213
220
|
const endEntry = info.snapshot.normalizedLines[info.endIndex];
|
|
214
|
-
const startColumn = startEntry
|
|
215
|
-
|
|
216
|
-
: 1;
|
|
217
|
-
const endColumn = endEntry
|
|
218
|
-
? getLineColumnRange(endEntry.raw).endColumn
|
|
219
|
-
: 1;
|
|
221
|
+
const startColumn = startEntry ? getLineColumnRange(startEntry.raw).startColumn : 1;
|
|
222
|
+
const endColumn = endEntry ? getLineColumnRange(endEntry.raw).endColumn : 1;
|
|
220
223
|
const relativePath = path.relative(process.cwd(), info.file);
|
|
221
224
|
return `${relativePath}:${startLine}:${startColumn}/${endLine}:${endColumn}`;
|
|
222
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();
|