eslint-plugin-unslop 0.1.0

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,1119 @@
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
+ default: () => index_default
34
+ });
35
+ module.exports = __toCommonJS(index_exports);
36
+
37
+ // package.json
38
+ var package_default = {
39
+ name: "eslint-plugin-unslop",
40
+ version: "0.1.0",
41
+ description: "ESLint plugin with rules for reducing AI-generated code smells",
42
+ type: "module",
43
+ main: "./dist/index.cjs",
44
+ module: "./dist/index.js",
45
+ types: "./dist/index.d.ts",
46
+ exports: {
47
+ ".": {
48
+ types: "./dist/index.d.ts",
49
+ import: "./dist/index.js",
50
+ require: "./dist/index.cjs"
51
+ }
52
+ },
53
+ files: [
54
+ "dist"
55
+ ],
56
+ sideEffects: false,
57
+ engines: {
58
+ node: ">=18"
59
+ },
60
+ scripts: {
61
+ build: "tsup",
62
+ format: "prettier . --write",
63
+ "format:check": "prettier . --check",
64
+ knip: "knip",
65
+ lint: "npm run build && eslint .",
66
+ test: "vitest run",
67
+ "test:watch": "vitest",
68
+ typecheck: "tsc --noEmit",
69
+ prepublishOnly: "npm run build"
70
+ },
71
+ keywords: [
72
+ "eslint",
73
+ "eslintplugin",
74
+ "eslint-plugin",
75
+ "ai",
76
+ "code-quality",
77
+ "unslop"
78
+ ],
79
+ license: "MIT",
80
+ peerDependencies: {
81
+ eslint: ">=9.0.0",
82
+ typescript: ">=5.0.0"
83
+ },
84
+ peerDependenciesMeta: {
85
+ typescript: {
86
+ optional: true
87
+ }
88
+ },
89
+ devDependencies: {
90
+ "@eslint/js": "^9.39.4",
91
+ "@types/estree": "^1.0.8",
92
+ "@types/node": "^22.18.0",
93
+ "@typescript-eslint/parser": "^8.57.2",
94
+ eslint: "^9.39.1",
95
+ globals: "^17.4.0",
96
+ knip: "^6.1.0",
97
+ prettier: "^3.8.1",
98
+ tsup: "^8.5.0",
99
+ typescript: "^5.9.3",
100
+ "typescript-eslint": "^8.57.2",
101
+ vitest: "^3.2.4"
102
+ }
103
+ };
104
+
105
+ // src/utils/string-literal-listener.ts
106
+ function createStringLiteralListener(includeEscapedUnicode, visitLiteral) {
107
+ function inspect(node) {
108
+ const text = getStringValue(node, includeEscapedUnicode);
109
+ if (text == void 0) {
110
+ return;
111
+ }
112
+ visitLiteral(node, text);
113
+ }
114
+ return {
115
+ Literal: inspect,
116
+ TemplateLiteral: inspect
117
+ };
118
+ }
119
+ function getStringValue(node, includeEscapedUnicode) {
120
+ if (node.type === "TemplateLiteral") {
121
+ return node.quasis.map(
122
+ (q) => includeEscapedUnicode ? q.value.raw ?? q.value.cooked ?? "" : q.value.cooked ?? ""
123
+ ).join("");
124
+ }
125
+ if (typeof node.value !== "string") {
126
+ return void 0;
127
+ }
128
+ return includeEscapedUnicode ? node.raw ?? node.value : node.value;
129
+ }
130
+
131
+ // src/rules/no-special-unicode.ts
132
+ var no_special_unicode_default = {
133
+ meta: {
134
+ type: "problem",
135
+ docs: {
136
+ description: "Disallow special unicode punctuation and whitespace in strings",
137
+ recommended: true
138
+ },
139
+ schema: [],
140
+ messages: {
141
+ bannedCharacter: "String contains {{name}} (U+{{code}}). Use the ASCII equivalent."
142
+ }
143
+ },
144
+ create(context) {
145
+ return createStringLiteralListener(false, (node, text) => {
146
+ if (!BANNED_CHARS_RE.test(text)) {
147
+ return;
148
+ }
149
+ for (const [char, name] of BANNED_CHARS) {
150
+ if (!text.includes(char)) {
151
+ continue;
152
+ }
153
+ const code = char.codePointAt(0) ?? 0;
154
+ context.report({
155
+ node,
156
+ messageId: "bannedCharacter",
157
+ data: {
158
+ name,
159
+ code: code.toString(16).toUpperCase().padStart(4, "0")
160
+ }
161
+ });
162
+ }
163
+ });
164
+ }
165
+ };
166
+ var BANNED_CHARS = /* @__PURE__ */ new Map([
167
+ ["\u201C", "left double quotation mark"],
168
+ ["\u201D", "right double quotation mark"],
169
+ ["\u2018", "left single quotation mark"],
170
+ ["\u2019", "right single quotation mark"],
171
+ ["\xA0", "non-breaking space"],
172
+ ["\u202F", "narrow no-break space"],
173
+ ["\u2007", "figure space"],
174
+ ["\u2008", "punctuation space"],
175
+ ["\u2009", "thin space"],
176
+ ["\u200A", "hair space"],
177
+ ["\u200B", "zero-width space"],
178
+ ["\u2002", "en space"],
179
+ ["\u2003", "em space"],
180
+ ["\u205F", "medium mathematical space"],
181
+ ["\u3000", "ideographic space"],
182
+ ["\uFEFF", "zero-width no-break space"],
183
+ ["\u2013", "en dash"],
184
+ ["\u2014", "em dash"],
185
+ ["\u2026", "horizontal ellipsis"]
186
+ ]);
187
+ var BANNED_CHARS_RE = new RegExp([...BANNED_CHARS.keys()].join("|"));
188
+
189
+ // src/rules/no-unicode-escape.ts
190
+ var no_unicode_escape_default = {
191
+ meta: {
192
+ type: "suggestion",
193
+ docs: {
194
+ description: "Prefer literal unicode characters over \\uXXXX escape sequences",
195
+ recommended: true
196
+ },
197
+ schema: [],
198
+ messages: {
199
+ preferLiteral: "Use the actual character instead of a \\uXXXX escape sequence."
200
+ }
201
+ },
202
+ create(context) {
203
+ return createStringLiteralListener(true, (node, text) => {
204
+ if (UNICODE_ESCAPE_RE.test(text)) {
205
+ context.report({ node, messageId: "preferLiteral" });
206
+ }
207
+ });
208
+ }
209
+ };
210
+ var UNICODE_ESCAPE_RE = /\\u[0-9a-fA-F]{4}/;
211
+
212
+ // src/rules/no-deep-imports.ts
213
+ var import_node_fs2 = require("fs");
214
+ var import_node_path3 = __toESM(require("path"), 1);
215
+
216
+ // src/utils/path-helpers.ts
217
+ var import_node_path = __toESM(require("path"), 1);
218
+ function toPosix(filePath) {
219
+ return filePath.split(import_node_path.default.sep).join("/");
220
+ }
221
+ function isInsidePath(filePath, parentPath) {
222
+ const relativePath = import_node_path.default.relative(parentPath, filePath);
223
+ return relativePath === "" || !relativePath.startsWith("..") && !import_node_path.default.isAbsolute(relativePath);
224
+ }
225
+
226
+ // src/utils/rule-options.ts
227
+ function readSourceRootOption(options) {
228
+ const option = options[0];
229
+ if (!isRecord(option) || !("sourceRoot" in option)) {
230
+ return void 0;
231
+ }
232
+ return typeof option.sourceRoot === "string" ? option.sourceRoot : void 0;
233
+ }
234
+ function readDirsOption(options, rootMode = "file") {
235
+ const option = options[0];
236
+ if (!isRecord(option) || !("dirs" in option)) {
237
+ return [];
238
+ }
239
+ const rootModeResolved = readRootMode(option, rootMode);
240
+ const { dirs } = option;
241
+ if (!Array.isArray(dirs)) {
242
+ return [];
243
+ }
244
+ return dirs.flatMap((entry) => {
245
+ const parsed = parseDirEntry(entry, rootModeResolved);
246
+ return parsed ? [parsed] : [];
247
+ });
248
+ }
249
+ function readRootMode(option, fallback) {
250
+ if (!("mode" in option)) {
251
+ return fallback;
252
+ }
253
+ return option.mode === "dir" ? "dir" : "file";
254
+ }
255
+ function parseDirEntry(entry, defaultMode) {
256
+ if (!isRecord(entry) || !("path" in entry)) {
257
+ return void 0;
258
+ }
259
+ if (typeof entry.path !== "string") {
260
+ return void 0;
261
+ }
262
+ const modeValue = "mode" in entry ? entry.mode : void 0;
263
+ const mode = modeValue === "dir" ? "dir" : defaultMode;
264
+ return { path: entry.path, mode };
265
+ }
266
+ function isRecord(value) {
267
+ return value != void 0 && typeof value === "object";
268
+ }
269
+
270
+ // src/utils/source-root.ts
271
+ var import_node_fs = require("fs");
272
+ var import_node_path2 = __toESM(require("path"), 1);
273
+ function resolveSourceContext(filename, sourceRootOverride) {
274
+ if (filename === "<input>") {
275
+ return void 0;
276
+ }
277
+ const absoluteFilename = import_node_path2.default.isAbsolute(filename) ? filename : import_node_path2.default.resolve(filename);
278
+ if (sourceRootOverride) {
279
+ const projectRoot = findNearestPackageRoot(import_node_path2.default.dirname(absoluteFilename)) ?? process.cwd();
280
+ return buildContext(
281
+ absoluteFilename,
282
+ projectRoot,
283
+ import_node_path2.default.isAbsolute(sourceRootOverride) ? sourceRootOverride : import_node_path2.default.join(projectRoot, sourceRootOverride)
284
+ );
285
+ }
286
+ const packageRoot = findNearestPackageRoot(import_node_path2.default.dirname(absoluteFilename));
287
+ if (packageRoot) {
288
+ return buildContext(
289
+ absoluteFilename,
290
+ packageRoot,
291
+ (0, import_node_fs.existsSync)(import_node_path2.default.join(packageRoot, "src")) ? import_node_path2.default.join(packageRoot, "src") : packageRoot
292
+ );
293
+ }
294
+ return resolveLegacySourceRoot(absoluteFilename);
295
+ }
296
+ function findNearestPackageRoot(startDirectory) {
297
+ let directory = startDirectory;
298
+ while (true) {
299
+ if ((0, import_node_fs.existsSync)(import_node_path2.default.join(directory, "package.json"))) {
300
+ return directory;
301
+ }
302
+ const parent = import_node_path2.default.dirname(directory);
303
+ if (parent === directory) {
304
+ return void 0;
305
+ }
306
+ directory = parent;
307
+ }
308
+ }
309
+ function resolveLegacySourceRoot(filename) {
310
+ const normalized = toPosix(filename);
311
+ const sourceIndex = normalized.lastIndexOf("/src/");
312
+ if (sourceIndex === -1) {
313
+ return void 0;
314
+ }
315
+ const projectRoot = normalized.slice(0, sourceIndex);
316
+ const sourceRoot = import_node_path2.default.join(projectRoot, "src");
317
+ return buildContext(filename, projectRoot, sourceRoot);
318
+ }
319
+ function buildContext(filename, projectRoot, sourceRoot) {
320
+ if (!isInsidePath(filename, sourceRoot)) {
321
+ return void 0;
322
+ }
323
+ return {
324
+ projectRoot,
325
+ sourceRoot,
326
+ sourceRelativePath: toPosix(import_node_path2.default.relative(sourceRoot, filename))
327
+ };
328
+ }
329
+
330
+ // src/rules/no-deep-imports.ts
331
+ var NO_DEEP_IMPORTS_SCHEMA = [
332
+ {
333
+ type: "object",
334
+ properties: {
335
+ sourceRoot: { type: "string" }
336
+ },
337
+ additionalProperties: false
338
+ }
339
+ ];
340
+ var no_deep_imports_default = {
341
+ meta: {
342
+ type: "problem",
343
+ docs: {
344
+ description: "Forbid deep imports inside the same top-level folder",
345
+ recommended: true
346
+ },
347
+ messages: {
348
+ tooDeep: "{{sourceRelativePath}}: {{targetRelativePath}} is too deep (max 1 level below importer)."
349
+ },
350
+ schema: NO_DEEP_IMPORTS_SCHEMA
351
+ },
352
+ create(context) {
353
+ const filename = context.filename;
354
+ const sourceRootOption = readSourceRootOption(context.options);
355
+ const sourceContext = resolveSourceContext(filename, sourceRootOption);
356
+ if (!sourceContext) {
357
+ return {};
358
+ }
359
+ const { sourceRelativePath, sourceRoot } = sourceContext;
360
+ return {
361
+ ImportDeclaration(node) {
362
+ const specifier = typeof node.source.value === "string" ? node.source.value : "";
363
+ const violation = findViolation(specifier, filename, sourceRoot, sourceRelativePath);
364
+ if (violation) {
365
+ context.report({
366
+ node,
367
+ messageId: "tooDeep",
368
+ data: violation
369
+ });
370
+ }
371
+ }
372
+ };
373
+ }
374
+ };
375
+ function findViolation(specifier, filename, sourceRoot, sourceRelativePath) {
376
+ const targetSourceRelative = resolveImportSourceRelative(specifier, filename, sourceRoot);
377
+ if (!targetSourceRelative) {
378
+ return void 0;
379
+ }
380
+ const targetRelativePath = resolveTarget(targetSourceRelative, sourceRoot);
381
+ if (!targetRelativePath) {
382
+ return void 0;
383
+ }
384
+ const folderScope = folderScopeFromPath(sourceRelativePath);
385
+ if (!isInScope(targetRelativePath, folderScope)) {
386
+ return void 0;
387
+ }
388
+ const importerDepth = depthWithinScope(sourceRelativePath, folderScope);
389
+ const targetDepth = depthWithinScope(targetRelativePath, folderScope);
390
+ if (targetDepth <= importerDepth + 1) {
391
+ return void 0;
392
+ }
393
+ return {
394
+ sourceRelativePath,
395
+ targetRelativePath: stripTsExtension(targetRelativePath)
396
+ };
397
+ }
398
+ function resolveImportSourceRelative(specifier, filename, sourceRoot) {
399
+ const normalizedSpecifier = specifier.replace(/\.js$/, "");
400
+ if (normalizedSpecifier.startsWith("@/")) {
401
+ return normalizedSpecifier.slice(2);
402
+ }
403
+ if (!normalizedSpecifier.startsWith(".")) {
404
+ return void 0;
405
+ }
406
+ const absoluteTarget = import_node_path3.default.resolve(import_node_path3.default.dirname(filename), normalizedSpecifier);
407
+ const targetSourceRelative = import_node_path3.default.relative(sourceRoot, absoluteTarget);
408
+ if (targetSourceRelative.startsWith("..")) {
409
+ return void 0;
410
+ }
411
+ return toPosix(targetSourceRelative);
412
+ }
413
+ function resolveTarget(targetSourceRelative, sourceRoot) {
414
+ const cacheKey = `${sourceRoot}\0${targetSourceRelative}`;
415
+ if (resolveCache.has(cacheKey)) {
416
+ return resolveCache.get(cacheKey);
417
+ }
418
+ const candidates = [
419
+ targetSourceRelative + ".ts",
420
+ targetSourceRelative + ".tsx",
421
+ targetSourceRelative + "/index.ts",
422
+ targetSourceRelative + "/index.tsx"
423
+ ];
424
+ const candidate = candidates.find((entry) => (0, import_node_fs2.existsSync)(import_node_path3.default.join(sourceRoot, entry)));
425
+ const targetRelativePath = candidate ? toPosix(candidate) : void 0;
426
+ resolveCache.set(cacheKey, targetRelativePath);
427
+ return targetRelativePath;
428
+ }
429
+ function folderScopeFromPath(sourceRelativePath) {
430
+ const [firstPart = ""] = sourceRelativePath.split("/");
431
+ return stripTsExtension(firstPart);
432
+ }
433
+ function isInScope(targetRelativePath, scope) {
434
+ const targetNoExtension = stripTsExtension(targetRelativePath);
435
+ return targetNoExtension === scope || targetNoExtension.startsWith(`${scope}/`);
436
+ }
437
+ function depthWithinScope(relativePath, scope) {
438
+ const withoutExtension = stripTsExtension(relativePath);
439
+ if (withoutExtension === scope) {
440
+ return 0;
441
+ }
442
+ const suffix = withoutExtension.slice(scope.length + 1);
443
+ return suffix.split("/").length - 1;
444
+ }
445
+ var resolveCache = /* @__PURE__ */ new Map();
446
+ function stripTsExtension(filePath) {
447
+ return filePath.replace(/\.tsx?$/, "");
448
+ }
449
+
450
+ // src/rules/no-false-sharing/analysis.ts
451
+ var import_node_fs3 = require("fs");
452
+ var import_node_path4 = __toESM(require("path"), 1);
453
+ var import_typescript = __toESM(require("typescript"), 1);
454
+ function runConsumerCheck(program, projectRoot, sourceRoot, directories) {
455
+ const checker = program.getTypeChecker();
456
+ const sourceFiles = program.getSourceFiles().filter((sf) => isInsidePath(sf.fileName, sourceRoot));
457
+ const modules = indexModules(directories, projectRoot, sourceRoot);
458
+ const barrels = indexBarrels(directories, projectRoot, sourceFiles, checker);
459
+ const ctx = { modules, barrels, checker };
460
+ countConsumers(sourceFiles, sourceRoot, ctx);
461
+ return collectErrors(modules);
462
+ }
463
+ function indexModules(directories, projectRoot, sourceRoot) {
464
+ const modules = /* @__PURE__ */ new Map();
465
+ for (const dir of directories) {
466
+ const absDir = import_node_path4.default.join(projectRoot, dir.path);
467
+ for (const file of listModuleFiles(absDir)) {
468
+ modules.set(file, {
469
+ relativePath: toPosix(import_node_path4.default.relative(sourceRoot, file)),
470
+ mode: dir.mode,
471
+ consumers: /* @__PURE__ */ new Set()
472
+ });
473
+ }
474
+ }
475
+ return modules;
476
+ }
477
+ function listModuleFiles(directory) {
478
+ if (!(0, import_node_fs3.existsSync)(directory)) {
479
+ return [];
480
+ }
481
+ return (0, import_node_fs3.readdirSync)(directory, { withFileTypes: true }).filter(
482
+ (e) => e.isFile() && (e.name.endsWith(".ts") || e.name.endsWith(".tsx")) && e.name !== "index.ts" && !isTestFile(e.name)
483
+ ).map((e) => import_node_path4.default.join(directory, e.name));
484
+ }
485
+ function indexBarrels(directories, projectRoot, sourceFiles, checker) {
486
+ const barrels = /* @__PURE__ */ new Map();
487
+ const sfByPath = new Map(sourceFiles.map((sf) => [sf.fileName, sf]));
488
+ for (const dir of directories) {
489
+ const barrelPath = import_node_path4.default.join(projectRoot, dir.path, "index.ts");
490
+ const barrelSf = sfByPath.get(barrelPath);
491
+ if (barrelSf) {
492
+ barrels.set(barrelPath, buildBarrelMap(barrelSf, checker));
493
+ }
494
+ }
495
+ return barrels;
496
+ }
497
+ function buildBarrelMap(barrelFile, checker) {
498
+ const map = /* @__PURE__ */ new Map();
499
+ for (const stmt of barrelFile.statements) {
500
+ if (!isNamedReExport(stmt)) continue;
501
+ const targetPath = resolveModulePath(stmt.moduleSpecifier, checker);
502
+ if (!targetPath) continue;
503
+ for (const el of stmt.exportClause.elements) {
504
+ if (!el.isTypeOnly) {
505
+ map.set(el.name.text, targetPath);
506
+ }
507
+ }
508
+ }
509
+ return map;
510
+ }
511
+ function isNamedReExport(stmt) {
512
+ return import_typescript.default.isExportDeclaration(stmt) && !stmt.isTypeOnly && !!stmt.moduleSpecifier && !!stmt.exportClause && import_typescript.default.isNamedExports(stmt.exportClause);
513
+ }
514
+ function countConsumers(sourceFiles, sourceRoot, ctx) {
515
+ for (const sf of sourceFiles) {
516
+ if (ctx.modules.has(sf.fileName) || isTestFile(sf.fileName)) continue;
517
+ const consumerPath = toPosix(import_node_path4.default.relative(sourceRoot, sf.fileName));
518
+ for (const stmt of sf.statements) {
519
+ if (import_typescript.default.isImportDeclaration(stmt)) {
520
+ recordImport(stmt, consumerPath, ctx);
521
+ }
522
+ }
523
+ }
524
+ }
525
+ function isTestFile(filePath) {
526
+ return TEST_FILE_RE.test(filePath);
527
+ }
528
+ var TEST_FILE_RE = /\.(test|integration-test|test-suite)\.ts$/;
529
+ function recordImport(stmt, consumerPath, ctx) {
530
+ const targetPath = resolveModulePath(stmt.moduleSpecifier, ctx.checker);
531
+ if (!targetPath) return;
532
+ const direct = ctx.modules.get(targetPath);
533
+ if (direct) {
534
+ direct.consumers.add(deriveEntity(consumerPath, direct.mode));
535
+ return;
536
+ }
537
+ const barrelMap = ctx.barrels.get(targetPath);
538
+ if (barrelMap) {
539
+ resolveBarrelImports(stmt, barrelMap, consumerPath, ctx.modules);
540
+ }
541
+ }
542
+ function resolveBarrelImports(stmt, barrelMap, consumerPath, modules) {
543
+ const bindings = stmt.importClause?.namedBindings;
544
+ if (!bindings) return;
545
+ const paths = import_typescript.default.isNamespaceImport(bindings) ? [...barrelMap.values()] : resolveNamedImports(bindings, barrelMap);
546
+ for (const resolved of paths) {
547
+ const entry = modules.get(resolved);
548
+ entry?.consumers.add(deriveEntity(consumerPath, entry.mode));
549
+ }
550
+ }
551
+ function resolveNamedImports(bindings, barrelMap) {
552
+ const result = [];
553
+ for (const el of bindings.elements) {
554
+ const name = el.propertyName?.text ?? el.name.text;
555
+ const resolved = barrelMap.get(name);
556
+ if (resolved) {
557
+ result.push(resolved);
558
+ }
559
+ }
560
+ return result;
561
+ }
562
+ function resolveModulePath(specifier, checker) {
563
+ const symbol = checker.getSymbolAtLocation(specifier);
564
+ return symbol?.declarations?.[0]?.getSourceFile().fileName;
565
+ }
566
+ function collectErrors(modules) {
567
+ const errors = /* @__PURE__ */ new Map();
568
+ for (const [, entry] of modules) {
569
+ if (entry.consumers.size >= 2) continue;
570
+ const consumers = [...entry.consumers];
571
+ const description = consumers.length === 0 ? "not imported by any entity" : `only used by: ${consumers.join(", ")}`;
572
+ errors.set(entry.relativePath, [`${description} -> Must be used by 2+ entities`]);
573
+ }
574
+ return errors;
575
+ }
576
+ function deriveEntity(consumerPath, mode) {
577
+ if (mode === "file") {
578
+ return consumerPath;
579
+ }
580
+ return consumerPath.split("/").slice(0, -1).slice(0, MAX_DIR_DEPTH).join("/");
581
+ }
582
+ var MAX_DIR_DEPTH = 3;
583
+
584
+ // src/rules/no-false-sharing.ts
585
+ var SCHEMA = [
586
+ {
587
+ type: "object",
588
+ properties: {
589
+ dirs: {
590
+ type: "array",
591
+ items: {
592
+ type: "object",
593
+ properties: {
594
+ path: { type: "string" },
595
+ mode: { type: "string", enum: ["file", "dir"] }
596
+ },
597
+ required: ["path"],
598
+ additionalProperties: false
599
+ }
600
+ },
601
+ mode: { type: "string", enum: ["file", "dir"] },
602
+ sourceRoot: { type: "string" }
603
+ },
604
+ required: ["dirs"],
605
+ additionalProperties: false
606
+ }
607
+ ];
608
+ var no_false_sharing_default = {
609
+ meta: {
610
+ type: "problem",
611
+ docs: {
612
+ description: "Require selected modules to be shared across at least two consumer entities",
613
+ recommended: false
614
+ },
615
+ schema: SCHEMA
616
+ },
617
+ create(context) {
618
+ const dirs = readDirsOption(context.options);
619
+ if (dirs.length === 0) {
620
+ return {};
621
+ }
622
+ const filename = context.filename;
623
+ const sourceRootOption = readSourceRootOption(context.options);
624
+ const sourceContext = resolveSourceContext(filename, sourceRootOption);
625
+ if (!sourceContext) {
626
+ return {};
627
+ }
628
+ const { sourceRelativePath, sourceRoot, projectRoot } = sourceContext;
629
+ return {
630
+ Program(node) {
631
+ const program = extractTsProgram(context);
632
+ if (!program) {
633
+ return;
634
+ }
635
+ const errors = runConsumerCheck(program, projectRoot, sourceRoot, dirs);
636
+ const fileErrors = errors.get(sourceRelativePath);
637
+ if (!fileErrors) {
638
+ return;
639
+ }
640
+ for (const message of fileErrors) {
641
+ context.report({ node, message });
642
+ }
643
+ }
644
+ };
645
+ }
646
+ };
647
+ function extractTsProgram(context) {
648
+ const services = context.sourceCode.parserServices;
649
+ if (!isRecord2(services) || !("program" in services)) {
650
+ return void 0;
651
+ }
652
+ const program = services.program;
653
+ if (!isTsProgram(program)) {
654
+ return void 0;
655
+ }
656
+ return program;
657
+ }
658
+ function isTsProgram(value) {
659
+ return isRecord2(value) && "getTypeChecker" in value;
660
+ }
661
+ function isRecord2(value) {
662
+ return value != void 0 && typeof value === "object";
663
+ }
664
+
665
+ // src/rules/read-friendly-order/class-order.ts
666
+ function reportClassOrdering(program, context) {
667
+ for (const classNode of collectClassDeclarations(program)) {
668
+ const members = collectClassMembers(classNode);
669
+ reportConstructorOrder(members, classNode, context);
670
+ reportPublicFieldOrder(members, context);
671
+ reportClassDependencyOrder(members, context);
672
+ }
673
+ }
674
+ function collectClassDeclarations(program) {
675
+ const classes = [];
676
+ for (const statement of getTopLevelStatements(program)) {
677
+ const classNode = extractClassNode(statement);
678
+ if (classNode) classes.push(classNode);
679
+ }
680
+ return classes;
681
+ }
682
+ function extractClassNode(statement) {
683
+ if (statement.type === "ClassDeclaration") return statement;
684
+ if (statement.type === "ExportNamedDeclaration" && statement.declaration?.type === "ClassDeclaration") {
685
+ return statement.declaration;
686
+ }
687
+ if (statement.type === "ExportDefaultDeclaration" && statement.declaration.type === "ClassDeclaration") {
688
+ return statement.declaration;
689
+ }
690
+ return void 0;
691
+ }
692
+ function reportConstructorOrder(members, classNode, context) {
693
+ const ctor = members.find((m) => m.kind === "constructor");
694
+ if (!ctor || ctor.index === 0) return;
695
+ context.report({
696
+ node: ctor.node,
697
+ messageId: "constructorFirst",
698
+ data: { className: classNode.id?.name ?? "anonymous class" }
699
+ });
700
+ }
701
+ function reportPublicFieldOrder(members, context) {
702
+ const ctorIndex = members.find((m) => m.kind === "constructor")?.index ?? -1;
703
+ const startIndex = ctorIndex >= 0 ? ctorIndex + 1 : 0;
704
+ let seenOther = false;
705
+ for (const member of members) {
706
+ if (member.index < startIndex) continue;
707
+ if (member.kind === "public-field") {
708
+ if (seenOther) {
709
+ context.report({
710
+ node: member.node,
711
+ messageId: "publicFieldOrder",
712
+ data: { memberName: member.name }
713
+ });
714
+ }
715
+ continue;
716
+ }
717
+ seenOther = true;
718
+ }
719
+ }
720
+ function reportClassDependencyOrder(members, context) {
721
+ const others = members.filter((m) => m.kind === "other");
722
+ for (const member of others) {
723
+ const consumer = findFirstClassConsumer(others, member, context);
724
+ if (!consumer) continue;
725
+ context.report({
726
+ node: member.node,
727
+ messageId: "moveMemberBelow",
728
+ data: { memberName: member.name, consumerName: consumer.name }
729
+ });
730
+ }
731
+ }
732
+ function collectClassMembers(classNode) {
733
+ const members = [];
734
+ for (const [index, raw] of classNode.body.body.entries()) {
735
+ const named = toNamedMember(raw);
736
+ if (!named) continue;
737
+ members.push({ name: named.name, node: named.node, index, kind: classifyMember(named.node) });
738
+ }
739
+ return members;
740
+ }
741
+ function toNamedMember(raw) {
742
+ if (!hasMemberKey(raw)) return void 0;
743
+ const name = readMemberName(raw.key);
744
+ return name ? { name, node: raw } : void 0;
745
+ }
746
+ function hasMemberKey(value) {
747
+ return "key" in value && isNode(value.key);
748
+ }
749
+ function readMemberName(key) {
750
+ if (key.type === "Identifier") return key.name;
751
+ if (key.type === "Literal" && typeof key.value === "string") return key.value;
752
+ if (key.type === "PrivateIdentifier") return `#${key.name}`;
753
+ return void 0;
754
+ }
755
+ function classifyMember(member) {
756
+ if (member.type === "MethodDefinition" && member.kind === "constructor") return "constructor";
757
+ if (isPublicField(member)) return "public-field";
758
+ return "other";
759
+ }
760
+ function isPublicField(member) {
761
+ if (member.type !== "PropertyDefinition" || member.static) return false;
762
+ if (member.key.type === "PrivateIdentifier") return false;
763
+ if (!("accessibility" in member)) return true;
764
+ return member.accessibility === void 0 || member.accessibility === "public";
765
+ }
766
+ function findFirstClassConsumer(members, member, context) {
767
+ const pattern = new RegExp(`\\bthis(?:\\?\\.|\\.)${escapeRegex(member.name)}\\b`);
768
+ for (const candidate of members) {
769
+ if (candidate.index <= member.index) continue;
770
+ if (pattern.test(context.sourceCode.getText(candidate.node))) return candidate;
771
+ }
772
+ return void 0;
773
+ }
774
+ function escapeRegex(value) {
775
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
776
+ }
777
+ function isNode(value) {
778
+ return !!value && typeof value === "object" && "type" in value && typeof value.type === "string";
779
+ }
780
+
781
+ // src/rules/read-friendly-order/test-order.ts
782
+ function reportTestOrdering(program, context) {
783
+ const entries = collectTestPhaseEntries(program);
784
+ if (!entries.some((entry) => entry.kind === "test")) {
785
+ return;
786
+ }
787
+ reportSetupOrder(entries, context);
788
+ reportTeardownOrder(entries, context);
789
+ }
790
+ function collectTestPhaseEntries(program) {
791
+ const entries = [];
792
+ for (const [index, statement] of getTopLevelStatements(program).entries()) {
793
+ const entry = toTestPhaseEntry(statement, index);
794
+ if (entry) {
795
+ entries.push(entry);
796
+ }
797
+ }
798
+ return entries;
799
+ }
800
+ function toTestPhaseEntry(statement, index) {
801
+ if (statement.type !== "ExpressionStatement" || statement.expression.type !== "CallExpression") {
802
+ return void 0;
803
+ }
804
+ const rootName = getCallRootName(statement.expression);
805
+ if (!rootName) {
806
+ return void 0;
807
+ }
808
+ const kind = classifyHookName(rootName);
809
+ if (!kind) {
810
+ return void 0;
811
+ }
812
+ return { index, kind, hookName: rootName, node: statement };
813
+ }
814
+ function classifyHookName(name) {
815
+ if (SETUP_HOOKS.has(name)) return "setup";
816
+ if (TEARDOWN_HOOKS.has(name)) return "teardown";
817
+ if (TEST_CALLS.has(name)) return "test";
818
+ return void 0;
819
+ }
820
+ var SETUP_HOOKS = /* @__PURE__ */ new Set(["beforeAll", "beforeEach", "before"]);
821
+ var TEARDOWN_HOOKS = /* @__PURE__ */ new Set(["afterAll", "afterEach", "after"]);
822
+ var TEST_CALLS = /* @__PURE__ */ new Set(["test", "it"]);
823
+ function getCallRootName(call) {
824
+ const callee = call.callee;
825
+ if (callee.type === "Identifier") return callee.name;
826
+ if (callee.type === "MemberExpression") return readObjectRoot(callee.object);
827
+ if (callee.type === "CallExpression") return getCallRootName(callee);
828
+ return void 0;
829
+ }
830
+ function readObjectRoot(node) {
831
+ if (node.type === "Identifier") return node.name;
832
+ if (node.type === "MemberExpression") return readObjectRoot(node.object);
833
+ if (node.type === "CallExpression") return getCallRootName(node);
834
+ return void 0;
835
+ }
836
+ function reportSetupOrder(entries, context) {
837
+ const firstTeardown = findFirstIndex(entries, "teardown");
838
+ const firstTest = findFirstIndex(entries, "test");
839
+ for (const entry of entries) {
840
+ if (entry.kind !== "setup") continue;
841
+ if (firstTeardown >= 0 && entry.index > firstTeardown) {
842
+ context.report({
843
+ node: entry.node,
844
+ messageId: "setupBeforeTeardown",
845
+ data: { hookName: entry.hookName }
846
+ });
847
+ continue;
848
+ }
849
+ if (firstTest >= 0 && entry.index > firstTest) {
850
+ context.report({
851
+ node: entry.node,
852
+ messageId: "setupBeforeTests",
853
+ data: { hookName: entry.hookName }
854
+ });
855
+ }
856
+ }
857
+ }
858
+ function reportTeardownOrder(entries, context) {
859
+ const firstTest = findFirstIndex(entries, "test");
860
+ if (firstTest < 0) return;
861
+ for (const entry of entries) {
862
+ if (entry.kind !== "teardown" || entry.index <= firstTest) continue;
863
+ context.report({
864
+ node: entry.node,
865
+ messageId: "teardownBeforeTests",
866
+ data: { hookName: entry.hookName }
867
+ });
868
+ }
869
+ }
870
+ function findFirstIndex(entries, kind) {
871
+ const match = entries.find((entry) => entry.kind === kind);
872
+ return match ? match.index : -1;
873
+ }
874
+
875
+ // src/rules/read-friendly-order.ts
876
+ var READ_FRIENDLY_ORDER_MESSAGES = {
877
+ moveHelperBelow: 'Place helper "{{helperName}}" below the top-level symbol "{{symbolName}}" that depends on it.',
878
+ moveConstantBelow: 'Place constant "{{constantName}}" below the top-level symbol "{{symbolName}}" that uses it.',
879
+ constructorFirst: 'Place constructor first in class "{{className}}".',
880
+ publicFieldOrder: 'Place public field "{{memberName}}" right after constructor and before other class members.',
881
+ moveMemberBelow: 'Place class member "{{memberName}}" below member "{{consumerName}}" that depends on it.',
882
+ setupBeforeTeardown: 'Place setup hook "{{hookName}}" before teardown hooks in this test file.',
883
+ setupBeforeTests: 'Place setup hook "{{hookName}}" before test cases in this test file.',
884
+ teardownBeforeTests: 'Place teardown hook "{{hookName}}" before test cases in this test file.'
885
+ };
886
+ var read_friendly_order_default = {
887
+ meta: {
888
+ type: "suggestion",
889
+ docs: {
890
+ description: "Enforce top-level symbols before helper declarations they depend on",
891
+ recommended: false
892
+ },
893
+ schema: [],
894
+ messages: READ_FRIENDLY_ORDER_MESSAGES
895
+ },
896
+ create(context) {
897
+ return {
898
+ Program(program) {
899
+ reportTopLevelOrdering(program, context);
900
+ reportClassOrdering(program, context);
901
+ reportTestOrdering(program, context);
902
+ }
903
+ };
904
+ }
905
+ };
906
+ function reportTopLevelOrdering(program, context) {
907
+ const body = getTopLevelStatements(program);
908
+ const helpers = collectHelpers(body);
909
+ const refs = collectReferences(context.sourceCode.scopeManager.globalScope);
910
+ const cyclicNames = findCyclicHelperNames(body, helpers, refs);
911
+ for (const helper of helpers) {
912
+ if (cyclicNames.has(helper.name)) continue;
913
+ if (hasEagerReference(body, helper, refs)) continue;
914
+ const consumer = findFirstConsumer(body, helper, refs);
915
+ if (!consumer) continue;
916
+ context.report({
917
+ node: helper.node,
918
+ messageId: helper.kind === "constant" ? "moveConstantBelow" : "moveHelperBelow",
919
+ data: {
920
+ helperName: helper.name,
921
+ constantName: helper.name,
922
+ symbolName: getSymbolName(consumer)
923
+ }
924
+ });
925
+ }
926
+ }
927
+ function getTopLevelStatements(program) {
928
+ return program.body.filter((s) => s.type !== "ImportDeclaration");
929
+ }
930
+ function collectHelpers(body) {
931
+ const helpers = [];
932
+ for (const [index, statement] of body.entries()) {
933
+ helpers.push(...collectHelperEntries(statement, index));
934
+ }
935
+ return helpers;
936
+ }
937
+ function collectHelperEntries(statement, index) {
938
+ const typeName = getTypeDeclarationName(statement);
939
+ if (typeName) return [{ name: typeName, node: statement, index, kind: "helper" }];
940
+ if (statement.type === "FunctionDeclaration" && statement.id) {
941
+ return [{ name: statement.id.name, node: statement, index, kind: "helper" }];
942
+ }
943
+ if (statement.type === "ExportNamedDeclaration") {
944
+ return collectExportedHelpers(statement, index);
945
+ }
946
+ if (statement.type === "VariableDeclaration") {
947
+ return collectVariableHelpers(statement, index);
948
+ }
949
+ return [];
950
+ }
951
+ function getTypeDeclarationName(statement) {
952
+ const t = statement.type;
953
+ if (t !== "TSTypeAliasDeclaration" && t !== "TSInterfaceDeclaration") return void 0;
954
+ return hasIdentifierId(statement) ? statement.id.name : void 0;
955
+ }
956
+ function hasIdentifierId(value) {
957
+ if (!("id" in value)) return false;
958
+ const { id } = value;
959
+ return !!id && typeof id === "object" && "type" in id && "name" in id && id.type === "Identifier";
960
+ }
961
+ function collectExportedHelpers(statement, index) {
962
+ const decl = statement.declaration;
963
+ if (!decl) return [];
964
+ if (decl.type === "FunctionDeclaration" && decl.id) {
965
+ return [{ name: decl.id.name, node: statement, index, kind: "helper" }];
966
+ }
967
+ if (decl.type === "VariableDeclaration") return collectVariableHelpers(decl, index);
968
+ return [];
969
+ }
970
+ function collectVariableHelpers(decl, index) {
971
+ const helpers = [];
972
+ for (const item of decl.declarations) {
973
+ if (item.id.type !== "Identifier") continue;
974
+ const isFn = isFunctionInit(item.init ?? null);
975
+ if (!isFn && decl.kind !== "const") continue;
976
+ helpers.push({
977
+ name: item.id.name,
978
+ node: item,
979
+ index,
980
+ kind: isFn ? "helper" : "constant"
981
+ });
982
+ }
983
+ return helpers;
984
+ }
985
+ function isFunctionInit(node) {
986
+ return node?.type === "ArrowFunctionExpression" || node?.type === "FunctionExpression";
987
+ }
988
+ function findFirstConsumer(body, helper, refs) {
989
+ for (let i = helper.index + 1; i < body.length; i += 1) {
990
+ const stmt = body[i];
991
+ if (stmt.type === "ExportNamedDeclaration" && !stmt.declaration) continue;
992
+ if (statementUsesName(stmt, helper.name, refs)) return stmt;
993
+ }
994
+ return void 0;
995
+ }
996
+ function findCyclicHelperNames(body, helpers, refs) {
997
+ const deps = /* @__PURE__ */ new Map();
998
+ for (const helper of helpers) {
999
+ const helperDeps = /* @__PURE__ */ new Set();
1000
+ for (const other of helpers) {
1001
+ if (other.name !== helper.name && statementUsesName(body[helper.index], other.name, refs)) {
1002
+ helperDeps.add(other.name);
1003
+ }
1004
+ }
1005
+ deps.set(helper.name, helperDeps);
1006
+ }
1007
+ const cyclic = /* @__PURE__ */ new Set();
1008
+ for (const helper of helpers) {
1009
+ if (canReachSelf(helper.name, deps)) cyclic.add(helper.name);
1010
+ }
1011
+ return cyclic;
1012
+ }
1013
+ function statementUsesName(statement, name, refs) {
1014
+ const range = statement.range;
1015
+ if (!range) return false;
1016
+ for (const ref of refs) {
1017
+ if (ref.identifier.name !== name) continue;
1018
+ const idRange = ref.identifier.range;
1019
+ if (!idRange) continue;
1020
+ if (idRange[0] >= range[0] && idRange[1] <= range[1]) return true;
1021
+ }
1022
+ return false;
1023
+ }
1024
+ function canReachSelf(start, deps) {
1025
+ const visited = /* @__PURE__ */ new Set();
1026
+ const queue = [...deps.get(start) ?? []];
1027
+ while (queue.length > 0) {
1028
+ const current = queue.shift();
1029
+ if (current === start) return true;
1030
+ if (visited.has(current)) continue;
1031
+ visited.add(current);
1032
+ for (const dep of deps.get(current) ?? []) queue.push(dep);
1033
+ }
1034
+ return false;
1035
+ }
1036
+ function collectReferences(scope) {
1037
+ if (!scope) return [];
1038
+ const refs = [...scope.references];
1039
+ for (const child of scope.childScopes) {
1040
+ refs.push(...collectReferences(child));
1041
+ }
1042
+ return refs;
1043
+ }
1044
+ function hasEagerReference(body, helper, refs) {
1045
+ for (let i = helper.index + 1; i < body.length; i += 1) {
1046
+ const stmt = body[i];
1047
+ if (stmt.type === "ExportNamedDeclaration" && !stmt.declaration) continue;
1048
+ if (statementHasEagerUse(stmt, helper.name, refs)) return true;
1049
+ }
1050
+ return false;
1051
+ }
1052
+ function statementHasEagerUse(statement, name, refs) {
1053
+ const range = statement.range;
1054
+ if (!range) return false;
1055
+ for (const ref of refs) {
1056
+ if (ref.identifier.name !== name || isTypeReference(ref)) continue;
1057
+ if (isEagerRefInRange(ref, range)) return true;
1058
+ }
1059
+ return false;
1060
+ }
1061
+ function isEagerRefInRange(ref, range) {
1062
+ const idRange = ref.identifier.range;
1063
+ if (!idRange) return false;
1064
+ const inside = idRange[0] >= range[0] && idRange[1] <= range[1];
1065
+ return inside && (ref.from.type === "module" || ref.from.type === "global");
1066
+ }
1067
+ function isTypeReference(ref) {
1068
+ return "isTypeReference" in ref && ref.isTypeReference === true;
1069
+ }
1070
+ function getSymbolName(statement) {
1071
+ if (statement.type === "ExportDefaultDeclaration") return "default export";
1072
+ if (statement.type === "ExportNamedDeclaration") return getNamedExportName(statement);
1073
+ if (statement.type === "FunctionDeclaration" && statement.id) return statement.id.name;
1074
+ if (statement.type === "VariableDeclaration")
1075
+ return getVarName(statement) ?? "top-level statement";
1076
+ return "top-level statement";
1077
+ }
1078
+ function getNamedExportName(statement) {
1079
+ const decl = statement.declaration;
1080
+ if (!decl) return "named export";
1081
+ if (decl.type === "FunctionDeclaration" && decl.id) return decl.id.name;
1082
+ if (decl.type === "VariableDeclaration") return getVarName(decl) ?? "named export";
1083
+ return "named export";
1084
+ }
1085
+ function getVarName(decl) {
1086
+ const [first] = decl.declarations;
1087
+ return first?.id.type === "Identifier" ? first.id.name : void 0;
1088
+ }
1089
+
1090
+ // src/rules/index.ts
1091
+ var rules_default = {
1092
+ "no-special-unicode": no_special_unicode_default,
1093
+ "no-unicode-escape": no_unicode_escape_default,
1094
+ "no-deep-imports": no_deep_imports_default,
1095
+ "no-false-sharing": no_false_sharing_default,
1096
+ "read-friendly-order": read_friendly_order_default
1097
+ };
1098
+
1099
+ // src/index.ts
1100
+ var plugin = {
1101
+ meta: {
1102
+ name: package_default.name,
1103
+ version: package_default.version
1104
+ },
1105
+ rules: rules_default,
1106
+ configs: {}
1107
+ };
1108
+ var recommended = {
1109
+ name: "unslop/recommended",
1110
+ plugins: { unslop: plugin },
1111
+ rules: {
1112
+ "unslop/no-special-unicode": "error",
1113
+ "unslop/no-unicode-escape": "error",
1114
+ "unslop/no-deep-imports": "error"
1115
+ }
1116
+ };
1117
+ plugin.configs["recommended"] = recommended;
1118
+ var index_default = plugin;
1119
+ //# sourceMappingURL=index.cjs.map