@archlinter/eslint-plugin 0.11.0-canary.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.
package/dist/index.cjs ADDED
@@ -0,0 +1,971 @@
1
+ "use strict";
2
+ var __create = Object.create;
3
+ var __defProp = Object.defineProperty;
4
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
5
+ var __getOwnPropNames = Object.getOwnPropertyNames;
6
+ var __getProtoOf = Object.getPrototypeOf;
7
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
8
+ var __export = (target, all) => {
9
+ for (var name in all)
10
+ __defProp(target, name, { get: all[name], enumerable: true });
11
+ };
12
+ var __copyProps = (to, from, except, desc) => {
13
+ if (from && typeof from === "object" || typeof from === "function") {
14
+ for (let key of __getOwnPropNames(from))
15
+ if (!__hasOwnProp.call(to, key) && key !== except)
16
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
17
+ }
18
+ return to;
19
+ };
20
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
21
+ // If the importer is in node compatibility mode or this is not an ESM
22
+ // file that has been converted to a CommonJS file using a Babel-
23
+ // compatible transform (i.e. "__esModule" has not been set), then set
24
+ // "default" to the CommonJS "module.exports" for node compatibility.
25
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
26
+ mod
27
+ ));
28
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // src/index.ts
31
+ var index_exports = {};
32
+ __export(index_exports, {
33
+ clearAllCaches: () => clearAllCaches,
34
+ default: () => index_default,
35
+ flatRecommended: () => flatRecommended,
36
+ flatStrict: () => flatStrict,
37
+ notifyFileChanged: () => notifyFileChanged,
38
+ recommended: () => recommended,
39
+ rules: () => rules,
40
+ strict: () => strict
41
+ });
42
+ module.exports = __toCommonJS(index_exports);
43
+
44
+ // src/utils/cache.ts
45
+ var import_core = require("@archlinter/core");
46
+
47
+ // src/utils/project-root.ts
48
+ var import_node_fs = require("fs");
49
+ var import_node_path = require("path");
50
+ var PROJECT_ROOT_MARKERS = [
51
+ ".archlint.yaml",
52
+ // Explicit archlint config
53
+ ".archlint.yml",
54
+ "pnpm-workspace.yaml",
55
+ // pnpm monorepo
56
+ "lerna.json",
57
+ // Lerna monorepo
58
+ "nx.json",
59
+ // Nx monorepo
60
+ "rush.json",
61
+ // Rush monorepo
62
+ ".git"
63
+ // Git root (fallback)
64
+ ];
65
+ var rootCache = /* @__PURE__ */ new Map();
66
+ function findProjectRoot(filePath) {
67
+ const cached = rootCache.get(filePath);
68
+ if (cached) return cached;
69
+ let dir = (0, import_node_path.dirname)(filePath);
70
+ let foundRoot = null;
71
+ while (true) {
72
+ for (const marker of PROJECT_ROOT_MARKERS) {
73
+ if ((0, import_node_fs.existsSync)((0, import_node_path.join)(dir, marker))) {
74
+ foundRoot = dir;
75
+ break;
76
+ }
77
+ }
78
+ const parent = (0, import_node_path.dirname)(dir);
79
+ if (parent === dir) break;
80
+ dir = parent;
81
+ }
82
+ const result = foundRoot ?? process.cwd();
83
+ rootCache.set(filePath, result);
84
+ return result;
85
+ }
86
+
87
+ // src/utils/cache.ts
88
+ var import_node_fs2 = require("fs");
89
+ var crypto = __toESM(require("crypto"), 1);
90
+ var import_xxhash_wasm = __toESM(require("xxhash-wasm"), 1);
91
+ var { createHash } = crypto;
92
+ var xxhash = null;
93
+ (0, import_xxhash_wasm.default)().then((h) => {
94
+ xxhash = h;
95
+ diskFileCache.clear();
96
+ }).catch(() => {
97
+ });
98
+ var DEBUG_PATTERN = process.env.DEBUG || "";
99
+ var DEBUG = process.env.ARCHLINT_DEBUG === "1" || DEBUG_PATTERN.includes("archlint:") || DEBUG_PATTERN === "*";
100
+ var FORCE_RESCAN = process.env.ARCHLINT_FORCE_RESCAN === "1";
101
+ var NO_BUFFER_CHECK = process.env.ARCHLINT_NO_BUFFER_CHECK === "1";
102
+ function debug(namespace, ...args) {
103
+ if (DEBUG) {
104
+ const ts = (/* @__PURE__ */ new Date()).toISOString().split("T")[1].slice(0, 12);
105
+ console.error(` ${ts} archlint:${namespace}`, ...args);
106
+ }
107
+ }
108
+ function computeHash(content) {
109
+ if (xxhash) {
110
+ return xxhash.h64(content).toString(16);
111
+ }
112
+ if (typeof crypto.hash === "function") {
113
+ return crypto.hash("md5", content, "hex");
114
+ }
115
+ return createHash("md5").update(content).digest("hex");
116
+ }
117
+ var diskFileCache = /* @__PURE__ */ new Map();
118
+ function getDiskFileHash(filePath) {
119
+ try {
120
+ const stats = (0, import_node_fs2.statSync)(filePath);
121
+ const mtime = stats.mtimeMs;
122
+ const size = stats.size;
123
+ const cached = diskFileCache.get(filePath);
124
+ if (cached && cached.mtime === mtime && cached.size === size) {
125
+ return cached.hash;
126
+ }
127
+ const content = (0, import_node_fs2.readFileSync)(filePath, "utf8");
128
+ const hash2 = computeHash(content);
129
+ diskFileCache.set(filePath, { hash: hash2, mtime, size });
130
+ return hash2;
131
+ } catch {
132
+ return null;
133
+ }
134
+ }
135
+ function isUnsavedFile(filename, bufferText) {
136
+ if (NO_BUFFER_CHECK) {
137
+ return false;
138
+ }
139
+ if (isVirtualFile(filename)) {
140
+ debug("cache", "Virtual file detected:", filename);
141
+ return true;
142
+ }
143
+ if (!bufferText) {
144
+ return false;
145
+ }
146
+ const diskHash = getDiskFileHash(filename);
147
+ if (diskHash === null) {
148
+ debug("cache", "New file (not on disk):", filename);
149
+ return true;
150
+ }
151
+ const cached = diskFileCache.get(filename);
152
+ if (cached && Buffer.byteLength(bufferText, "utf8") !== cached.size) {
153
+ debug("cache", "Size mismatch detected:", filename);
154
+ return true;
155
+ }
156
+ const bufferHash = computeHash(bufferText);
157
+ const isUnsaved = bufferHash !== diskHash;
158
+ if (isUnsaved) {
159
+ debug("cache", "Unsaved file detected (hash mismatch):", filename);
160
+ }
161
+ return isUnsaved;
162
+ }
163
+ function isVirtualFile(filename) {
164
+ return filename === "<input>" || filename === "<text>" || filename.startsWith("untitled:") || filename.includes("stdin");
165
+ }
166
+ var projectStates = /* @__PURE__ */ new Map();
167
+ function getFileMtime(filePath) {
168
+ try {
169
+ return (0, import_node_fs2.statSync)(filePath).mtimeMs;
170
+ } catch {
171
+ return 0;
172
+ }
173
+ }
174
+ function initializeProjectState(projectRoot, filePath, currentMtime) {
175
+ debug("cache", "First scan for project:", projectRoot);
176
+ const analyzer = new import_core.ArchlintAnalyzer(projectRoot, { cache: true, git: false });
177
+ const state = {
178
+ analyzer,
179
+ result: null,
180
+ state: "in_progress" /* InProgress */,
181
+ lastScanTime: 0,
182
+ fileMtimes: /* @__PURE__ */ new Map()
183
+ };
184
+ projectStates.set(projectRoot, state);
185
+ try {
186
+ const start = Date.now();
187
+ state.result = analyzer.scanSync();
188
+ state.state = "ready" /* Ready */;
189
+ state.lastScanTime = Date.now();
190
+ state.fileMtimes.set(filePath, currentMtime);
191
+ debug("cache", "Initial scan complete", {
192
+ duration: Date.now() - start,
193
+ smellCount: state.result?.smells?.length
194
+ });
195
+ } catch (error) {
196
+ state.state = "error" /* Error */;
197
+ console.error("[archlint] Analysis failed:", error instanceof Error ? error.message : error);
198
+ }
199
+ return state;
200
+ }
201
+ function performRescan(state, filePath) {
202
+ debug("cache", "Triggering rescan", { filePath });
203
+ try {
204
+ state.analyzer.invalidate([filePath]);
205
+ const start = Date.now();
206
+ state.result = state.analyzer.rescanSync();
207
+ state.lastScanTime = Date.now();
208
+ debug("cache", "Rescan complete", {
209
+ duration: Date.now() - start,
210
+ smellCount: state.result?.smells?.length
211
+ });
212
+ } catch (error) {
213
+ debug("cache", "Rescan error", error instanceof Error ? error.message : error);
214
+ }
215
+ }
216
+ function shouldRescanFile(state, filePath, currentMtime) {
217
+ const lastMtime = state.fileMtimes.get(filePath);
218
+ const fileChanged = lastMtime !== void 0 && currentMtime > lastMtime;
219
+ const isNewFile = lastMtime === void 0;
220
+ return fileChanged || FORCE_RESCAN && isNewFile;
221
+ }
222
+ function handleFileChange(state, filePath, currentMtime) {
223
+ const lastMtime = state.fileMtimes.get(filePath);
224
+ const fileChanged = lastMtime !== void 0 && currentMtime > lastMtime;
225
+ const needsRescan = shouldRescanFile(state, filePath, currentMtime);
226
+ state.fileMtimes.set(filePath, currentMtime);
227
+ if (needsRescan && state.state === "ready" /* Ready */) {
228
+ performRescan(state, filePath);
229
+ } else {
230
+ debug("cache", "Using cached", {
231
+ file: filePath.split("/").pop(),
232
+ lastMtime: lastMtime ?? "new",
233
+ fileChanged
234
+ });
235
+ }
236
+ }
237
+ function isAnalysisReady(filePath, options) {
238
+ const { projectRoot: projectRootOverride, bufferText } = options ?? {};
239
+ const projectRoot = projectRootOverride ?? findProjectRoot(filePath);
240
+ let state = projectStates.get(projectRoot);
241
+ const currentMtime = getFileMtime(filePath);
242
+ const unsaved = isUnsavedFile(filePath, bufferText);
243
+ debug("cache", "isAnalysisReady", {
244
+ filePath,
245
+ projectRoot,
246
+ currentMtime,
247
+ hasState: !!state,
248
+ unsaved
249
+ });
250
+ if (!state) {
251
+ state = initializeProjectState(projectRoot, filePath, currentMtime);
252
+ return state.state;
253
+ }
254
+ if (unsaved) {
255
+ debug("cache", "Unsaved file - using cached results", { file: filePath.split("/").pop() });
256
+ return state.state;
257
+ }
258
+ handleFileChange(state, filePath, currentMtime);
259
+ return state.state;
260
+ }
261
+ function notifyFileChanged(filePath, projectRootOverride) {
262
+ const projectRoot = projectRootOverride ?? findProjectRoot(filePath);
263
+ const state = projectStates.get(projectRoot);
264
+ if (state && state.state === "ready" /* Ready */) {
265
+ try {
266
+ state.result = state.analyzer.rescanSync();
267
+ state.lastScanTime = Date.now();
268
+ } catch {
269
+ }
270
+ }
271
+ }
272
+ function getAnalysis(filePath, projectRootOverride) {
273
+ const projectRoot = projectRootOverride ?? findProjectRoot(filePath);
274
+ const state = projectStates.get(projectRoot);
275
+ if (!state || state.state !== "ready" /* Ready */) {
276
+ return null;
277
+ }
278
+ return state.result;
279
+ }
280
+ function analyzeWithOverlay(filePath, content, projectRootOverride) {
281
+ const projectRoot = projectRootOverride ?? findProjectRoot(filePath);
282
+ const state = projectStates.get(projectRoot);
283
+ if (!state || state.state !== "ready" /* Ready */) {
284
+ debug("overlay", "Analysis not ready, skipping overlay analysis");
285
+ return [];
286
+ }
287
+ try {
288
+ const start = Date.now();
289
+ const result = state.analyzer.scanIncrementalWithOverlaySync([filePath], {
290
+ [filePath]: content
291
+ });
292
+ debug("overlay", "Overlay analysis complete", {
293
+ file: filePath.split("/").pop(),
294
+ duration: Date.now() - start,
295
+ smellCount: result.smells.length
296
+ });
297
+ return result.smells;
298
+ } catch (error) {
299
+ debug("overlay", "Overlay analysis failed", error instanceof Error ? error.message : error);
300
+ return [];
301
+ }
302
+ }
303
+ function matchesSmellType(smellType, detectorId) {
304
+ const words = detectorId.toLowerCase().split("_");
305
+ const baseType = smellType.split(/[\s{]/)[0].toLowerCase();
306
+ const mappings = {
307
+ dead_code: ["deadcode"],
308
+ dead_symbols: ["deadsymbol"],
309
+ cycles: ["cyclicdependency", "cyclicdependencycluster"],
310
+ high_coupling: ["highcoupling"],
311
+ high_complexity: ["highcomplexity"],
312
+ long_params: ["longparameterlist"],
313
+ deep_nesting: ["deepnesting"],
314
+ god_module: ["godmodule"],
315
+ barrel_file_abuse: ["barrelfileabuse"],
316
+ layer_violation: ["layerviolation"],
317
+ sdp_violation: ["sdpviolation"],
318
+ hub_module: ["hubmodule"]
319
+ };
320
+ const patterns = mappings[detectorId] || [words.join("")];
321
+ return patterns.some((p) => baseType.includes(p));
322
+ }
323
+ function getSmellsForFile(filePath, detectorId, projectRootOverride, bufferText) {
324
+ const projectRoot = projectRootOverride ?? findProjectRoot(filePath);
325
+ if (bufferText && isUnsavedFile(filePath, bufferText)) {
326
+ debug("smell", "Using overlay analysis for unsaved file", filePath.split("/").pop());
327
+ const allSmells = analyzeWithOverlay(filePath, bufferText, projectRoot);
328
+ return filterSmellsByDetector(allSmells, filePath, detectorId);
329
+ }
330
+ const result = getAnalysis(filePath, projectRoot);
331
+ if (!result) {
332
+ debug("smell", "no result for", filePath);
333
+ return [];
334
+ }
335
+ return filterSmellsByDetector(result.smells, filePath, detectorId);
336
+ }
337
+ function filterSmellsByDetector(smells, filePath, detectorId) {
338
+ const normalizedPath = filePath.replaceAll("\\", "/");
339
+ const filtered = smells.filter((s) => {
340
+ if (!matchesSmellType(s.smell.smellType, detectorId)) {
341
+ return false;
342
+ }
343
+ return s.smell.files.some((f) => f.replaceAll("\\", "/") === normalizedPath);
344
+ });
345
+ if (filtered.length > 0) {
346
+ debug("smell", `${detectorId}: ${filtered.length} smells for`, filePath.split("/").pop());
347
+ }
348
+ return filtered;
349
+ }
350
+ function clearAllCaches() {
351
+ projectStates.clear();
352
+ diskFileCache.clear();
353
+ }
354
+
355
+ // src/utils/smell-filter.ts
356
+ var import_node_path2 = require("path");
357
+ function getSmellLocationsForFile(smell, filePath, strategy, projectRoot) {
358
+ const normalized = filePath.replaceAll("\\", "/");
359
+ switch (strategy) {
360
+ case "critical-edges":
361
+ return getCriticalEdgeLocations(smell, normalized, projectRoot);
362
+ case "primary-file":
363
+ return getPrimaryFileLocation(smell, normalized);
364
+ case "source-file":
365
+ return getSourceFileLocation(smell, normalized);
366
+ case "all-files":
367
+ default:
368
+ return getAllFileLocations(smell, normalized);
369
+ }
370
+ }
371
+ function getCriticalEdgeLocations(smell, filePath, projectRoot) {
372
+ const cluster = smell.smell.cluster;
373
+ if (!cluster?.criticalEdges) return [];
374
+ return cluster.criticalEdges.filter((edge) => edge.from.replaceAll("\\", "/") === filePath).map((edge) => ({
375
+ line: edge.line,
376
+ column: edge.range?.startColumn ?? 0,
377
+ endLine: edge.range?.endLine,
378
+ endColumn: edge.range?.endColumn,
379
+ messageId: "cycle",
380
+ data: {
381
+ target: (0, import_node_path2.relative)(projectRoot, edge.to),
382
+ impact: edge.impact
383
+ }
384
+ }));
385
+ }
386
+ function createLocationFromSmell(loc, reason) {
387
+ return {
388
+ line: loc?.line ?? 1,
389
+ column: loc?.column ?? 0,
390
+ endLine: loc?.range?.endLine,
391
+ endColumn: loc?.range?.endColumn,
392
+ messageId: "smell",
393
+ data: { reason }
394
+ };
395
+ }
396
+ function getPrimaryFileLocation(smell, filePath) {
397
+ const firstFile = smell.smell.files[0];
398
+ if (!firstFile) return [];
399
+ const normalizedFirstFile = firstFile.replaceAll("\\", "/");
400
+ const normalizedPath = filePath.replaceAll("\\", "/");
401
+ if (normalizedFirstFile !== normalizedPath) return [];
402
+ const loc = smell.smell.locations[0];
403
+ return [createLocationFromSmell(loc, smell.explanation.reason)];
404
+ }
405
+ function getSourceFileLocation(smell, filePath) {
406
+ const sourceLoc = smell.smell.locations.find((l) => l.file.replaceAll("\\", "/") === filePath);
407
+ if (!sourceLoc) return [];
408
+ return [
409
+ {
410
+ line: sourceLoc.line,
411
+ column: sourceLoc.column ?? 0,
412
+ endLine: sourceLoc.range?.endLine,
413
+ endColumn: sourceLoc.range?.endColumn,
414
+ messageId: "violation",
415
+ data: { reason: smell.explanation.reason }
416
+ }
417
+ ];
418
+ }
419
+ function getAllFileLocations(smell, filePath) {
420
+ return smell.smell.locations.filter((l) => l.file.replaceAll("\\", "/") === filePath).map((l) => ({
421
+ line: l.line,
422
+ column: l.column ?? 0,
423
+ endLine: l.range?.endLine,
424
+ endColumn: l.range?.endColumn,
425
+ messageId: "smell",
426
+ data: { reason: l.description || smell.explanation.reason }
427
+ }));
428
+ }
429
+
430
+ // src/utils/rule-factory.ts
431
+ function createRuleMeta(options) {
432
+ return {
433
+ type: "problem",
434
+ docs: {
435
+ description: options.description,
436
+ category: options.category,
437
+ recommended: options.recommended,
438
+ url: `https://archlinter.dev/rules/${options.detectorId.replaceAll("_", "-")}`
439
+ },
440
+ schema: [
441
+ {
442
+ type: "object",
443
+ properties: {
444
+ projectRoot: {
445
+ type: "string",
446
+ description: "Override project root detection"
447
+ }
448
+ },
449
+ additionalProperties: false
450
+ }
451
+ ],
452
+ messages: {
453
+ analyzing: "archlint: analyzing project architecture...",
454
+ ...options.messages
455
+ }
456
+ };
457
+ }
458
+ function reportSmellLocations(context, smell, filename, strategy, projectRoot) {
459
+ const locations = getSmellLocationsForFile(smell, filename, strategy, projectRoot);
460
+ for (const loc of locations) {
461
+ const reportLoc = {
462
+ start: { line: loc.line, column: Math.max(0, (loc.column ?? 1) - 1) }
463
+ };
464
+ if (loc.endLine !== void 0 && loc.endColumn !== void 0) {
465
+ reportLoc.end = { line: loc.endLine, column: Math.max(0, loc.endColumn - 1) };
466
+ }
467
+ context.report({
468
+ loc: reportLoc,
469
+ messageId: loc.messageId,
470
+ data: loc.data
471
+ });
472
+ }
473
+ }
474
+ function createArchlintRule(options) {
475
+ return {
476
+ meta: createRuleMeta(options),
477
+ create(context) {
478
+ const filename = context.filename;
479
+ const ruleOptions = context.options[0] ?? {};
480
+ const projectRoot = ruleOptions.projectRoot ?? findProjectRoot(filename);
481
+ const sourceCode = context.sourceCode;
482
+ const bufferText = sourceCode.text;
483
+ return {
484
+ Program(node) {
485
+ if (isVirtualFile(filename)) {
486
+ return;
487
+ }
488
+ const state = isAnalysisReady(filename, {
489
+ projectRoot: ruleOptions.projectRoot,
490
+ bufferText
491
+ });
492
+ if (state === "not_started" /* NotStarted */) {
493
+ context.report({ node, messageId: "analyzing" });
494
+ return;
495
+ }
496
+ if (state === "in_progress" /* InProgress */) {
497
+ return;
498
+ }
499
+ const smells = getSmellsForFile(
500
+ filename,
501
+ options.detectorId,
502
+ ruleOptions.projectRoot,
503
+ bufferText
504
+ );
505
+ for (const smell of smells) {
506
+ reportSmellLocations(context, smell, filename, options.strategy, projectRoot);
507
+ }
508
+ }
509
+ };
510
+ }
511
+ };
512
+ }
513
+
514
+ // src/rules/no-cycles.ts
515
+ var noCycles = createArchlintRule({
516
+ detectorId: "cycles",
517
+ messageId: "cycle",
518
+ description: "Disallow cyclic dependencies between files",
519
+ category: "Architecture",
520
+ recommended: true,
521
+ strategy: "critical-edges",
522
+ messages: {
523
+ cycle: "Cyclic import to '{{target}}' ({{impact}} impact)",
524
+ cycleCluster: "File is part of cyclic dependency cluster ({{size}} files)"
525
+ }
526
+ });
527
+
528
+ // src/rules/no-god-modules.ts
529
+ var noGodModules = createArchlintRule({
530
+ detectorId: "god_module",
531
+ messageId: "smell",
532
+ description: "Disallow overly large and complex modules (God Modules)",
533
+ category: "Architecture",
534
+ recommended: true,
535
+ strategy: "primary-file",
536
+ messages: {
537
+ smell: "God Module detected: {{reason}}"
538
+ }
539
+ });
540
+
541
+ // src/rules/no-dead-code.ts
542
+ var noDeadCode = createArchlintRule({
543
+ detectorId: "dead_code",
544
+ messageId: "smell",
545
+ description: "Disallow dead code and unused exports",
546
+ category: "Architecture",
547
+ recommended: true,
548
+ strategy: "primary-file",
549
+ messages: {
550
+ smell: "Dead code detected: {{reason}}"
551
+ }
552
+ });
553
+
554
+ // src/rules/no-dead-symbols.ts
555
+ var noDeadSymbols = createArchlintRule({
556
+ detectorId: "dead_symbols",
557
+ messageId: "smell",
558
+ description: "Disallow unused functions, classes, and variables",
559
+ category: "Architecture",
560
+ recommended: true,
561
+ strategy: "all-files",
562
+ messages: {
563
+ smell: "Unused symbol detected: {{reason}}"
564
+ }
565
+ });
566
+
567
+ // src/rules/no-high-coupling.ts
568
+ var noHighCoupling = createArchlintRule({
569
+ detectorId: "high_coupling",
570
+ messageId: "smell",
571
+ description: "Disallow modules with excessively high coupling",
572
+ category: "Architecture",
573
+ recommended: true,
574
+ strategy: "primary-file",
575
+ messages: {
576
+ smell: "High coupling detected: {{reason}}"
577
+ }
578
+ });
579
+
580
+ // src/rules/no-barrel-abuse.ts
581
+ var noBarrelAbuse = createArchlintRule({
582
+ detectorId: "barrel_file_abuse",
583
+ messageId: "smell",
584
+ description: "Disallow abuse of barrel files",
585
+ category: "Architecture",
586
+ recommended: false,
587
+ strategy: "primary-file",
588
+ messages: {
589
+ smell: "Barrel file abuse: {{reason}}"
590
+ }
591
+ });
592
+
593
+ // src/rules/no-layer-violations.ts
594
+ var noLayerViolations = createArchlintRule({
595
+ detectorId: "layer_violation",
596
+ messageId: "violation",
597
+ description: "Disallow violations of defined architecture layers",
598
+ category: "Architecture",
599
+ recommended: true,
600
+ strategy: "source-file",
601
+ messages: {
602
+ violation: "Layer violation: {{reason}}"
603
+ }
604
+ });
605
+
606
+ // src/rules/no-sdp-violations.ts
607
+ var noSdpViolations = createArchlintRule({
608
+ detectorId: "sdp_violation",
609
+ messageId: "smell",
610
+ description: "Disallow violations of the Stable Dependencies Principle",
611
+ category: "Architecture",
612
+ recommended: false,
613
+ strategy: "primary-file",
614
+ messages: {
615
+ smell: "SDP violation: {{reason}}"
616
+ }
617
+ });
618
+
619
+ // src/rules/no-hub-modules.ts
620
+ var noHubModules = createArchlintRule({
621
+ detectorId: "hub_module",
622
+ messageId: "smell",
623
+ description: "Disallow modules that act as too many dependencies",
624
+ category: "Architecture",
625
+ recommended: false,
626
+ strategy: "primary-file",
627
+ messages: {
628
+ smell: "Hub module detected: {{reason}}"
629
+ }
630
+ });
631
+
632
+ // src/rules/no-deep-nesting.ts
633
+ var noDeepNesting = createArchlintRule({
634
+ detectorId: "deep_nesting",
635
+ messageId: "smell",
636
+ description: "Disallow excessively deep directory nesting",
637
+ category: "Architecture",
638
+ recommended: false,
639
+ strategy: "all-files",
640
+ messages: {
641
+ smell: "Deep nesting: {{reason}}"
642
+ }
643
+ });
644
+
645
+ // src/rules/no-long-params.ts
646
+ var noLongParams = createArchlintRule({
647
+ detectorId: "long_params",
648
+ messageId: "smell",
649
+ description: "Disallow functions with too many parameters",
650
+ category: "Architecture",
651
+ recommended: false,
652
+ strategy: "all-files",
653
+ messages: {
654
+ smell: "Too many parameters: {{reason}}"
655
+ }
656
+ });
657
+
658
+ // src/rules/no-high-complexity.ts
659
+ var noHighComplexity = createArchlintRule({
660
+ detectorId: "high_complexity",
661
+ messageId: "smell",
662
+ description: "Disallow functions with high cyclomatic complexity",
663
+ category: "Code Quality",
664
+ recommended: true,
665
+ strategy: "all-files",
666
+ messages: {
667
+ smell: "High complexity detected: {{reason}}"
668
+ }
669
+ });
670
+
671
+ // src/rules/no-code-clone.ts
672
+ var noCodeClone = createArchlintRule({
673
+ detectorId: "code_clone",
674
+ messageId: "smell",
675
+ description: "Disallow duplicated code blocks (code clones)",
676
+ category: "Code Quality",
677
+ recommended: true,
678
+ strategy: "all-files",
679
+ messages: {
680
+ smell: "Code clone detected: {{reason}}"
681
+ }
682
+ });
683
+
684
+ // src/rules/no-regression.ts
685
+ var import_core2 = require("@archlinter/core");
686
+ var fs = __toESM(require("fs"), 1);
687
+ var path = __toESM(require("path"), 1);
688
+ var diffCache = /* @__PURE__ */ new Map();
689
+ var firstFiles = /* @__PURE__ */ new Set();
690
+ var lastRunStamp = 0;
691
+ var noRegression = {
692
+ meta: {
693
+ type: "problem",
694
+ docs: {
695
+ description: "Detect architectural regressions compared to baseline",
696
+ recommended: false
697
+ },
698
+ messages: {
699
+ regression: "{{message}}",
700
+ noBaseline: "Baseline snapshot not found at {{path}}. Run: archlint snapshot -o {{path}}"
701
+ },
702
+ schema: [
703
+ {
704
+ type: "object",
705
+ properties: {
706
+ baseline: {
707
+ type: "string",
708
+ description: "Path to baseline snapshot"
709
+ },
710
+ failOn: {
711
+ type: "string",
712
+ enum: ["low", "medium", "high", "critical"],
713
+ description: "Minimum severity to report"
714
+ }
715
+ },
716
+ additionalProperties: false
717
+ }
718
+ ]
719
+ },
720
+ create(context) {
721
+ const currentRunStamp = Date.now();
722
+ if (currentRunStamp - lastRunStamp > 5e3) {
723
+ diffCache.clear();
724
+ firstFiles.clear();
725
+ }
726
+ lastRunStamp = currentRunStamp;
727
+ const { baseline, failOn } = getOptions(context);
728
+ const projectRoot = findProjectRoot(context.filename);
729
+ if (!ensureDiffResult(context, projectRoot, baseline)) {
730
+ return {};
731
+ }
732
+ return reportFromCache(context, projectRoot, failOn);
733
+ }
734
+ };
735
+ function getOptions(context) {
736
+ const options = context.options[0] ?? {};
737
+ return {
738
+ baseline: options.baseline ?? ".archlint-snapshot.json",
739
+ failOn: options.failOn ?? "low"
740
+ };
741
+ }
742
+ function ensureDiffResult(context, projectRoot, baselinePath) {
743
+ if (diffCache.has(projectRoot)) {
744
+ return true;
745
+ }
746
+ const absoluteBaseline = path.resolve(projectRoot, baselinePath);
747
+ if (!fs.existsSync(absoluteBaseline)) {
748
+ if (isFirstFile(context, projectRoot)) {
749
+ context.report({
750
+ loc: { line: 1, column: 0 },
751
+ messageId: "noBaseline",
752
+ data: { path: baselinePath }
753
+ });
754
+ }
755
+ return false;
756
+ }
757
+ try {
758
+ const result = (0, import_core2.runDiff)({
759
+ baselinePath: absoluteBaseline,
760
+ projectPath: projectRoot
761
+ });
762
+ diffCache.set(projectRoot, result);
763
+ return true;
764
+ } catch (error) {
765
+ if (isFirstFile(context, projectRoot)) {
766
+ context.report({
767
+ loc: { line: 1, column: 0 },
768
+ messageId: "regression",
769
+ data: { message: `Diff failed: ${error instanceof Error ? error.message : String(error)}` }
770
+ });
771
+ }
772
+ return false;
773
+ }
774
+ }
775
+ var SEVERITY_ORDER = { low: 0, medium: 1, high: 2, critical: 3 };
776
+ function getRegressionsToReport(result, failOn) {
777
+ const minSev = SEVERITY_ORDER[failOn] ?? 0;
778
+ return result.regressions.filter((r) => {
779
+ const sev = r.smell.severity.toLowerCase();
780
+ return (SEVERITY_ORDER[sev] ?? 0) >= minSev;
781
+ });
782
+ }
783
+ function reportFromCache(context, projectRoot, failOn) {
784
+ const result = diffCache.get(projectRoot);
785
+ if (!result?.hasRegressions) {
786
+ return {};
787
+ }
788
+ const regressions = getRegressionsToReport(result, failOn);
789
+ if (regressions.length === 0) {
790
+ return {};
791
+ }
792
+ return createVisitor(context, projectRoot, regressions);
793
+ }
794
+ function createVisitor(context, projectRoot, regressions) {
795
+ const filename = path.relative(projectRoot, context.filename);
796
+ const isFirst = isFirstFile(context, projectRoot);
797
+ return {
798
+ Program(node) {
799
+ for (const reg of regressions) {
800
+ if (shouldReportRegression(reg, filename, isFirst)) {
801
+ context.report({
802
+ node,
803
+ messageId: "regression",
804
+ data: { message: formatRegressionMessage(reg) }
805
+ });
806
+ }
807
+ }
808
+ }
809
+ };
810
+ }
811
+ function shouldReportRegression(reg, filename, isFirst) {
812
+ if (isFirst) {
813
+ return true;
814
+ }
815
+ const files = reg.smell?.files;
816
+ if (!files || !Array.isArray(files)) {
817
+ return false;
818
+ }
819
+ return files.some((f) => f === filename || filename.endsWith(f));
820
+ }
821
+ function formatRegressionMessage(reg) {
822
+ const smell = reg.smell;
823
+ const smellType = smell.smellType;
824
+ const files = smell.files.slice(0, 3).join(", ");
825
+ switch (reg.regressionType.type) {
826
+ case "NewSmell":
827
+ return `New architectural smell: ${smellType} in ${files}`;
828
+ case "SeverityIncrease":
829
+ return formatSeverityIncreaseMessage(reg.regressionType, smellType);
830
+ case "MetricWorsening":
831
+ return formatMetricWorseningMessage(reg.regressionType, smellType);
832
+ default:
833
+ return String(reg.message);
834
+ }
835
+ }
836
+ function formatSeverityIncreaseMessage(regType, smellType) {
837
+ return `Architectural smell ${smellType} worsened: severity increased from ${regType.from} to ${regType.to}`;
838
+ }
839
+ function formatMetricWorseningMessage(regType, smellType) {
840
+ const { metric, from, to, changePercent } = regType;
841
+ return `Architectural metric worsened: ${smellType} ${metric} ${from} \u2192 ${to} (+${(changePercent ?? 0).toFixed(0)}%)`;
842
+ }
843
+ function isFirstFile(context, projectRoot) {
844
+ if (!firstFiles.has(projectRoot)) {
845
+ firstFiles.add(projectRoot);
846
+ return true;
847
+ }
848
+ return false;
849
+ }
850
+
851
+ // src/rules/index.ts
852
+ var rules = {
853
+ "no-cycles": noCycles,
854
+ "no-god-modules": noGodModules,
855
+ "no-dead-code": noDeadCode,
856
+ "no-dead-symbols": noDeadSymbols,
857
+ "no-high-coupling": noHighCoupling,
858
+ "no-barrel-abuse": noBarrelAbuse,
859
+ "no-layer-violations": noLayerViolations,
860
+ "no-sdp-violations": noSdpViolations,
861
+ "no-hub-modules": noHubModules,
862
+ "no-deep-nesting": noDeepNesting,
863
+ "no-long-params": noLongParams,
864
+ "no-high-complexity": noHighComplexity,
865
+ "no-code-clone": noCodeClone,
866
+ "no-regression": noRegression
867
+ };
868
+
869
+ // src/configs/recommended.ts
870
+ var recommended = {
871
+ plugins: ["@archlinter"],
872
+ rules: {
873
+ "@archlinter/no-cycles": "error",
874
+ "@archlinter/no-god-modules": "warn",
875
+ "@archlinter/no-dead-code": "warn",
876
+ "@archlinter/no-dead-symbols": "warn",
877
+ "@archlinter/no-high-coupling": "warn",
878
+ "@archlinter/no-high-complexity": "error",
879
+ "@archlinter/no-layer-violations": "error",
880
+ "@archlinter/no-code-clone": "warn"
881
+ }
882
+ };
883
+ var strict = {
884
+ plugins: ["@archlinter"],
885
+ rules: {
886
+ "@archlinter/no-cycles": "error",
887
+ "@archlinter/no-god-modules": "error",
888
+ "@archlinter/no-dead-code": "error",
889
+ "@archlinter/no-dead-symbols": "error",
890
+ "@archlinter/no-high-coupling": "error",
891
+ "@archlinter/no-high-complexity": "error",
892
+ "@archlinter/no-barrel-abuse": "error",
893
+ "@archlinter/no-layer-violations": "error",
894
+ "@archlinter/no-sdp-violations": "error",
895
+ "@archlinter/no-hub-modules": "warn",
896
+ "@archlinter/no-deep-nesting": "error",
897
+ "@archlinter/no-long-params": "warn",
898
+ "@archlinter/no-code-clone": "error",
899
+ "@archlinter/no-regression": ["error", { failOn: "medium" }]
900
+ }
901
+ };
902
+
903
+ // src/configs/flat.ts
904
+ var flatRecommended = {
905
+ plugins: {
906
+ "@archlinter": { rules }
907
+ },
908
+ rules: {
909
+ "@archlinter/no-cycles": "error",
910
+ "@archlinter/no-god-modules": "warn",
911
+ "@archlinter/no-dead-code": "warn",
912
+ "@archlinter/no-dead-symbols": "warn",
913
+ "@archlinter/no-high-coupling": "warn",
914
+ "@archlinter/no-high-complexity": "error",
915
+ "@archlinter/no-layer-violations": "error",
916
+ "@archlinter/no-code-clone": "warn"
917
+ }
918
+ };
919
+ var flatStrict = {
920
+ plugins: {
921
+ "@archlinter": { rules }
922
+ },
923
+ rules: {
924
+ "@archlinter/no-cycles": "error",
925
+ "@archlinter/no-god-modules": "error",
926
+ "@archlinter/no-dead-code": "error",
927
+ "@archlinter/no-dead-symbols": "error",
928
+ "@archlinter/no-high-coupling": "error",
929
+ "@archlinter/no-high-complexity": "error",
930
+ "@archlinter/no-barrel-abuse": "error",
931
+ "@archlinter/no-layer-violations": "error",
932
+ "@archlinter/no-sdp-violations": "error",
933
+ "@archlinter/no-hub-modules": "warn",
934
+ "@archlinter/no-deep-nesting": "error",
935
+ "@archlinter/no-long-params": "warn",
936
+ "@archlinter/no-code-clone": "error",
937
+ "@archlinter/no-regression": ["error", { failOn: "medium" }]
938
+ }
939
+ };
940
+
941
+ // src/index.ts
942
+ var plugin = {
943
+ meta: {
944
+ name: "@archlinter/eslint-plugin",
945
+ version: "0.6.0-alpha.1"
946
+ },
947
+ rules,
948
+ configs: {
949
+ // Legacy configs (ESLint 8)
950
+ // @ts-expect-error - Legacy config format compatibility
951
+ recommended,
952
+ // @ts-expect-error - Legacy config format compatibility
953
+ strict,
954
+ // Flat configs (ESLint 9+)
955
+ "flat/recommended": flatRecommended,
956
+ "flat/strict": flatStrict
957
+ }
958
+ };
959
+ var index_default = plugin;
960
+ // Annotate the CommonJS export names for ESM import in node:
961
+ 0 && (module.exports = {
962
+ clearAllCaches,
963
+ flatRecommended,
964
+ flatStrict,
965
+ notifyFileChanged,
966
+ recommended,
967
+ rules,
968
+ strict
969
+ });
970
+ if (module.exports.default) { Object.assign(module.exports.default, module.exports); module.exports = module.exports.default; }
971
+ //# sourceMappingURL=index.cjs.map