@emeryld/manager 1.4.5 → 1.4.7

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;
@@ -25,19 +33,14 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
25
33
  const slice = normalizedLines.slice(i, i + minLines);
26
34
  if (slice.some((entry) => !entry.normalized))
27
35
  continue;
28
- 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)
33
- continue;
34
36
  if (slice.some((entry) => entry.isImport))
35
37
  continue;
36
- const snippet = snippetLines
37
- .map((line) => line.replace(/\s+/g, ' '))
38
- .join('\n');
39
- if (!snippet)
38
+ // Punctuation-only lines should not count as a line for the minDuplicateLines threshold.
39
+ // i.e., we require >= minLines "real" lines (non-empty AND not punctuation-only).
40
+ const realLineCount = slice.filter((entry) => entry.trimmed.length > 0 && !entry.isPunctuationOnly).length;
41
+ if (realLineCount < minLines)
40
42
  continue;
43
+ const key = slice.map((entry) => entry.normalized).join('\n');
41
44
  const firstNonEmptyIndex = slice.findIndex((entry) => entry.trimmed.length > 0);
42
45
  let lastNonEmptyIndex = -1;
43
46
  for (let offset = slice.length - 1; offset >= 0; offset -= 1) {
@@ -52,6 +55,11 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
52
55
  const endLine = i + 1 + lastNonEmptyIndex;
53
56
  const { startColumn } = getLineColumnRange(slice[firstNonEmptyIndex].raw);
54
57
  const { endColumn } = getLineColumnRange(slice[lastNonEmptyIndex].raw);
58
+ // Keep punctuation-only lines inside the snippet, but keep indentation.
59
+ const snippet = slice
60
+ .slice(firstNonEmptyIndex, lastNonEmptyIndex + 1)
61
+ .map((entry) => trimRightKeepIndent(entry.raw))
62
+ .join('\n');
55
63
  registerSnapshot();
56
64
  addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet, sourceFile, lines);
57
65
  }
@@ -147,13 +155,11 @@ function expandDuplicateTracker(tracker, fileSnapshots) {
147
155
  }
148
156
  return current;
149
157
  }, infos[0]);
150
- const lines = primary.snapshot.normalizedLines
158
+ const snippetLines = primary.snapshot.normalizedLines
151
159
  .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');
160
+ .map((entry) => trimRightKeepIndent(entry.raw));
161
+ // Preserve indentation; do not collapse whitespace.
162
+ const snippet = snippetLines.join('\n');
157
163
  const startLine = primary.startIndex + 1;
158
164
  const endLine = primary.endIndex + 1;
159
165
  const startColumn = getLineColumnRange(primary.snapshot.normalizedLines[primary.startIndex].raw).startColumn;
@@ -211,12 +217,8 @@ function formatOccurrenceDetail(info) {
211
217
  const endLine = info.endIndex + 1;
212
218
  const startEntry = info.snapshot.normalizedLines[info.startIndex];
213
219
  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;
220
+ const startColumn = startEntry ? getLineColumnRange(startEntry.raw).startColumn : 1;
221
+ const endColumn = endEntry ? getLineColumnRange(endEntry.raw).endColumn : 1;
220
222
  const relativePath = path.relative(process.cwd(), info.file);
221
223
  return `${relativePath}:${startLine}:${startColumn}/${endLine}:${endColumn}`;
222
224
  }
@@ -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.7",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",