@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
- 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
  }
@@ -147,13 +156,11 @@ function expandDuplicateTracker(tracker, fileSnapshots) {
147
156
  }
148
157
  return current;
149
158
  }, infos[0]);
150
- const lines = primary.snapshot.normalizedLines
159
+ const snippetLines = primary.snapshot.normalizedLines
151
160
  .slice(primary.startIndex, primary.endIndex + 1)
152
- .map((entry) => entry.trimmed)
153
- .filter(Boolean);
154
- if (!lines.length)
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
- ? getLineColumnRange(startEntry.raw).startColumn
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
- 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.5",
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",