@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
- const snippet = entry.trimmed.replace(/\s+/g, ' ');
14
- if (!snippet)
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
- const snippetLines = slice
30
- .map((entry) => entry.trimmed)
31
- .filter(Boolean);
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
- const snippet = snippetLines
37
- .map((line) => line.replace(/\s+/g, ' '))
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[0];
140
- const lines = primary.snapshot.normalizedLines
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.trimmed)
143
- .filter(Boolean);
144
- if (!lines.length)
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: infos.map(formatOccurrenceDetail).join('\n'),
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
- ? getLineColumnRange(startEntry.raw).startColumn
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
- if (/^[};()]+$/.test(trimmed))
8
- return undefined;
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();
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "1.4.4",
3
+ "version": "1.4.6",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",