@emeryld/manager 1.4.1 → 1.4.2

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.
@@ -5,9 +5,9 @@ import { rootDir } from '../helper-cli/env.js';
5
5
  export const REPORTING_MODES = ['group', 'file'];
6
6
  export const DEFAULT_LIMITS = {
7
7
  maxFunctionLength: 150,
8
- maxMethodLength: 200,
8
+ maxMethodLength: 150,
9
9
  maxIndentationDepth: 6,
10
- maxFunctionsPerFile: 16,
10
+ maxFunctionsPerFile: 25,
11
11
  maxComponentsPerFile: 8,
12
12
  maxFileLength: 500,
13
13
  maxDuplicateLineOccurrences: 3,
@@ -8,13 +8,14 @@ import * as ts from 'typescript';
8
8
  export async function analyzeFiles(files, limits) {
9
9
  const violations = [];
10
10
  const duplicateMap = new Map();
11
+ const fileSnapshots = new Map();
11
12
  for (const file of files) {
12
- violations.push(...(await analyzeSingleFile(file, limits, duplicateMap)));
13
+ violations.push(...(await analyzeSingleFile(file, limits, duplicateMap, fileSnapshots)));
13
14
  }
14
- violations.push(...collectDuplicateViolations(duplicateMap, limits));
15
+ violations.push(...collectDuplicateViolations(duplicateMap, limits, fileSnapshots));
15
16
  return violations;
16
17
  }
17
- async function analyzeSingleFile(filePath, limits, duplicates) {
18
+ async function analyzeSingleFile(filePath, limits, duplicates, fileSnapshots) {
18
19
  let content;
19
20
  try {
20
21
  content = await readFile(filePath, 'utf-8');
@@ -48,7 +49,7 @@ async function analyzeSingleFile(filePath, limits, duplicates) {
48
49
  }
49
50
  });
50
51
  const sourceFile = ts.createSourceFile(filePath, content, ts.ScriptTarget.ESNext, true, resolveScriptKind(filePath));
51
- recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines);
52
+ recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines, fileSnapshots);
52
53
  const indentationGroups = groupIndentationViolations(indentationViolations, lines, limits.maxIndentationDepth);
53
54
  indentationGroups
54
55
  .sort((a, b) => b.severity - a.severity)
@@ -1,8 +1,11 @@
1
1
  import path from 'node:path';
2
2
  import { getLineColumnRange } from './utils.js';
3
3
  import { collectRequiredVariables } from './variables.js';
4
- export function recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines) {
4
+ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicates, sourceFile, lines, fileSnapshots) {
5
5
  const minLines = Math.max(1, limits.minDuplicateLines);
6
+ const registerSnapshot = () => {
7
+ ensureFileSnapshot(fileSnapshots, filePath, normalizedLines, lines, sourceFile);
8
+ };
6
9
  if (limits.minDuplicateLines <= 0 || minLines === 1) {
7
10
  normalizedLines.forEach((entry, index) => {
8
11
  if (!entry.normalized)
@@ -13,6 +16,7 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
13
16
  if (entry.isImport)
14
17
  return;
15
18
  const { startColumn, endColumn } = getLineColumnRange(entry.raw);
19
+ registerSnapshot();
16
20
  addDuplicateOccurrence(duplicates, entry.normalized, filePath, index + 1, startColumn, index + 1, endColumn, snippet, sourceFile, lines);
17
21
  });
18
22
  return;
@@ -48,6 +52,7 @@ export function recordDuplicateLines(normalizedLines, filePath, limits, duplicat
48
52
  const endLine = i + 1 + lastNonEmptyIndex;
49
53
  const { startColumn } = getLineColumnRange(slice[firstNonEmptyIndex].raw);
50
54
  const { endColumn } = getLineColumnRange(slice[lastNonEmptyIndex].raw);
55
+ registerSnapshot();
51
56
  addDuplicateOccurrence(duplicates, key, filePath, startLine, startColumn, endLine, endColumn, snippet, sourceFile, lines);
52
57
  }
53
58
  }
@@ -88,28 +93,140 @@ function addDuplicateOccurrence(duplicates, key, filePath, startLine, startColum
88
93
  ],
89
94
  });
90
95
  }
91
- export function collectDuplicateViolations(duplicates, limits) {
96
+ function ensureFileSnapshot(fileSnapshots, filePath, normalizedLines, lines, sourceFile) {
97
+ if (fileSnapshots.has(filePath))
98
+ return;
99
+ fileSnapshots.set(filePath, {
100
+ sourceFile,
101
+ normalizedLines,
102
+ lines,
103
+ });
104
+ }
105
+ function fingerprintLine(entry) {
106
+ if (entry.normalized)
107
+ return entry.normalized;
108
+ const collapsed = entry.trimmed.replace(/\s+/g, ' ');
109
+ if (!collapsed)
110
+ return undefined;
111
+ if (!/[A-Za-z0-9]/.test(collapsed))
112
+ return undefined;
113
+ return collapsed.toLowerCase();
114
+ }
115
+ function buildOccurrenceInfo(occurrence, fileSnapshots) {
116
+ const snapshot = fileSnapshots.get(occurrence.file);
117
+ if (!snapshot)
118
+ return undefined;
119
+ const totalLines = snapshot.normalizedLines.length;
120
+ if (totalLines === 0)
121
+ return undefined;
122
+ const clampIndex = (value) => Math.max(0, Math.min(totalLines - 1, value));
123
+ const rawStart = clampIndex(occurrence.line - 1);
124
+ const rawEnd = clampIndex(occurrence.endLine - 1);
125
+ return {
126
+ file: occurrence.file,
127
+ snapshot,
128
+ startIndex: Math.min(rawStart, rawEnd),
129
+ endIndex: Math.max(rawStart, rawEnd),
130
+ };
131
+ }
132
+ function expandDuplicateTracker(tracker, fileSnapshots) {
133
+ const infos = tracker.occurrences
134
+ .map((occurrence) => buildOccurrenceInfo(occurrence, fileSnapshots))
135
+ .filter((info) => Boolean(info));
136
+ if (infos.length < 2)
137
+ return undefined;
138
+ expandRangeAcrossOccurrences(infos);
139
+ const primary = infos[0];
140
+ const lines = primary.snapshot.normalizedLines
141
+ .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');
147
+ const startLine = primary.startIndex + 1;
148
+ const endLine = primary.endIndex + 1;
149
+ const startColumn = getLineColumnRange(primary.snapshot.normalizedLines[primary.startIndex].raw).startColumn;
150
+ const endColumn = getLineColumnRange(primary.snapshot.normalizedLines[primary.endIndex].raw).endColumn;
151
+ const requiredVariables = collectRequiredVariables(primary.snapshot.sourceFile, primary.snapshot.lines, {
152
+ startLine,
153
+ startColumn,
154
+ endLine,
155
+ endColumn,
156
+ });
157
+ return {
158
+ snippet,
159
+ detail: infos.map(formatOccurrenceDetail).join('\n'),
160
+ requiredVariables,
161
+ };
162
+ }
163
+ function expandRangeAcrossOccurrences(infos) {
164
+ while (tryExpandRange(infos, 'left')) { }
165
+ while (tryExpandRange(infos, 'right')) { }
166
+ }
167
+ function tryExpandRange(infos, direction) {
168
+ const fingerprints = [];
169
+ for (const info of infos) {
170
+ const nextIndex = direction === 'left' ? info.startIndex - 1 : info.endIndex + 1;
171
+ if (nextIndex < 0 || nextIndex >= info.snapshot.normalizedLines.length) {
172
+ return false;
173
+ }
174
+ const fingerprint = fingerprintLine(info.snapshot.normalizedLines[nextIndex]);
175
+ if (!fingerprint)
176
+ return false;
177
+ fingerprints.push(fingerprint);
178
+ }
179
+ if (fingerprints.some((value) => value !== fingerprints[0])) {
180
+ return false;
181
+ }
182
+ for (const info of infos) {
183
+ if (direction === 'left') {
184
+ info.startIndex -= 1;
185
+ }
186
+ else {
187
+ info.endIndex += 1;
188
+ }
189
+ }
190
+ return true;
191
+ }
192
+ function formatOccurrenceDetail(info) {
193
+ const startLine = info.startIndex + 1;
194
+ const endLine = info.endIndex + 1;
195
+ const startEntry = info.snapshot.normalizedLines[info.startIndex];
196
+ 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;
203
+ const relativePath = path.relative(process.cwd(), info.file);
204
+ return `${relativePath}:${startLine}:${startColumn}/${endLine}:${endColumn}`;
205
+ }
206
+ export function collectDuplicateViolations(duplicates, limits, fileSnapshots) {
92
207
  const violations = [];
93
208
  for (const tracker of duplicates.values()) {
94
209
  if (tracker.count <= limits.maxDuplicateLineOccurrences)
95
210
  continue;
96
211
  if (tracker.occurrences.length === 0)
97
212
  continue;
213
+ const expanded = expandDuplicateTracker(tracker, fileSnapshots);
98
214
  violations.push({
99
215
  type: 'duplicateLine',
100
216
  file: tracker.occurrences[0].file,
101
217
  line: tracker.occurrences[0].line,
102
218
  severity: tracker.count - limits.maxDuplicateLineOccurrences,
103
219
  message: `Repeated ${tracker.count} times (max ${limits.maxDuplicateLineOccurrences})`,
104
- detail: tracker.occurrences
105
- .map((occurrence) => {
106
- const relativePath = path.relative(process.cwd(), occurrence.file);
107
- return `${relativePath}:${occurrence.line}:${occurrence.startColumn}/${occurrence.endLine}:${occurrence.endColumn}`;
108
- })
109
- .join('\n'),
110
- snippet: tracker.snippet,
220
+ detail: expanded?.detail ??
221
+ tracker.occurrences
222
+ .map((occurrence) => {
223
+ const relativePath = path.relative(process.cwd(), occurrence.file);
224
+ return `${relativePath}:${occurrence.line}:${occurrence.startColumn}/${occurrence.endLine}:${occurrence.endColumn}`;
225
+ })
226
+ .join('\n'),
227
+ snippet: expanded?.snippet ?? tracker.snippet,
111
228
  repeatCount: tracker.count,
112
- requiredVariables: tracker.requiredVariables,
229
+ requiredVariables: expanded?.requiredVariables ?? tracker.requiredVariables,
113
230
  });
114
231
  }
115
232
  return violations;
@@ -2,9 +2,11 @@ import path from 'node:path';
2
2
  import * as ts from 'typescript';
3
3
  export function normalizeLine(line) {
4
4
  const trimmed = line.trim();
5
- if (trimmed.length < 12)
5
+ if (!trimmed)
6
6
  return undefined;
7
7
  const collapse = trimmed.replace(/\s+/g, ' ');
8
+ const letterCount = (collapse.match(/[A-Za-z]/g) ?? []).length;
9
+ // if (collapse.length < 12 && letterCount < 3) return undefined
8
10
  const noNumbers = collapse.replace(/\d+/g, '#');
9
11
  return noNumbers.toLowerCase();
10
12
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@emeryld/manager",
3
- "version": "1.4.1",
3
+ "version": "1.4.2",
4
4
  "description": "Interactive manager for pnpm monorepos (update/test/build/publish).",
5
5
  "license": "MIT",
6
6
  "type": "module",