eslint-plugin-unslop 0.4.0 → 0.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.
package/README.md CHANGED
@@ -51,7 +51,7 @@ This turns on:
51
51
  | `unslop/export-control` | error | Restricts export patterns and forbids `export *` in module entrypoints |
52
52
  | `unslop/no-false-sharing` | error | Flags shared entrypoint symbols with fewer than two consumer groups |
53
53
  | `unslop/no-special-unicode` | error | Catches smart quotes, invisible spaces, and other unicode impostors |
54
- | `unslop/no-unicode-escape` | error | Prefers `"(c)"` over `"\u00A9"` |
54
+ | `unslop/no-unicode-escape` | error | Prefers `"©"` over `"\u00A9"` |
55
55
  | `unslop/read-friendly-order` | error | Enforces top-down, dependency-friendly declaration order |
56
56
 
57
57
  The `configs.minimal` config contains only the zero-config symbol fixers (`no-special-unicode` and `no-unicode-escape`). It is included automatically within `configs.full`, or can be used standalone for projects that don't need architecture enforcement:
@@ -245,7 +245,7 @@ function assertCorrect(value) {
245
245
 
246
246
  ### `unslop/no-special-unicode`
247
247
 
248
- Disallows special unicode punctuation and whitespace characters in string literals and template literals. LLMs love to sprinkle in smart quotes (`"like this"`), non-breaking spaces, and other invisible gremlins that look fine in a PR review but cause fun bugs at runtime.
248
+ Disallows special unicode punctuation and whitespace characters in string literals and template literals. LLMs love to sprinkle in smart quotes (`“like this”`), non-breaking spaces, and other invisible gremlins that look fine in a PR review but cause fun bugs at runtime.
249
249
 
250
250
  Caught characters include: left/right smart quotes (`“” ‘’`), non-breaking space, en/em dash, horizontal ellipsis, zero-width space, and various other exotic whitespace.
251
251
 
@@ -265,7 +265,7 @@ the problem this rule catches.
265
265
 
266
266
  ### `unslop/no-unicode-escape`
267
267
 
268
- Prefers actual characters over `\uXXXX` escape sequences. If your string says `\u00A9`, just write `(c)` - your coworkers will thank you. LLM-generated code sometimes encodes characters as escape sequences for no good reason.
268
+ Prefers actual characters over `\uXXXX` escape sequences. If your string says `\u00A9`, just write `©` - your coworkers will thank you. LLM-generated code sometimes encodes characters as escape sequences for no good reason.
269
269
 
270
270
  ```js
271
271
  // Bad
@@ -273,8 +273,8 @@ const copyright = '\u00A9 2025'
273
273
  const arrow = '\u2192'
274
274
 
275
275
  // Good
276
- const copyright = '(c) 2025'
277
- const arrow = '->'
276
+ const copyright = '© 2025'
277
+ const arrow = ''
278
278
  ```
279
279
 
280
280
  ## A Note on Provenance
@@ -283,58 +283,6 @@ Yes, a fair amount of this was vibe-coded with LLM assistance - which is fitting
283
283
 
284
284
  The project also dogfoods itself: `eslint-plugin-unslop` is linted using `eslint-plugin-unslop`.
285
285
 
286
- ## Maintainer: Main Branch Protection
287
-
288
- This repository treats `main` as a protected branch with pull-request-only merges.
289
-
290
- Baseline policy:
291
-
292
- - Require pull requests before merge
293
- - Require at least 1 approving review
294
- - Dismiss stale approvals when new commits are pushed
295
- - Require branch to be up to date before merge
296
- - Require status check: `PR Gate`
297
- - Apply restrictions to admins too (`enforce_admins`)
298
-
299
- The required check is produced by `.github/workflows/test.yml` (workflow/job name: `PR Gate`) and runs:
300
-
301
- 1. `npm run verify`
302
- 2. `npm run test`
303
-
304
- ### Safe Workflow Renames
305
-
306
- If you rename the workflow or job that produces `PR Gate`, update branch protection required checks immediately to match the new check context.
307
-
308
- Recommended update command:
309
-
310
- ```bash
311
- gh api -X PUT repos/skhoroshavin/eslint-plugin-unslop/branches/main/protection \
312
- -H "Accept: application/vnd.github+json" \
313
- -F 'required_status_checks[strict]=true' \
314
- -F 'required_status_checks[contexts][]=PR Gate' \
315
- -F 'enforce_admins=true' \
316
- -F 'required_pull_request_reviews[dismiss_stale_reviews]=true' \
317
- -F 'required_pull_request_reviews[require_code_owner_reviews]=false' \
318
- -F 'required_pull_request_reviews[required_approving_review_count]=1' \
319
- -F 'restrictions=null'
320
- ```
321
-
322
- ### Branch Protection Audit
323
-
324
- Run these checks periodically:
325
-
326
- ```bash
327
- gh api repos/skhoroshavin/eslint-plugin-unslop/branches/main/protection
328
- gh run list -workflow "Test" -limit 5
329
- ```
330
-
331
- Expected audit outcomes:
332
-
333
- - `required_status_checks.contexts` includes `Test`
334
- - `required_pull_request_reviews.required_approving_review_count` is `1` or greater
335
- - `required_pull_request_reviews.dismiss_stale_reviews` is `true`
336
- - `enforce_admins.enabled` is `true`
337
-
338
286
  ## Contributing
339
287
 
340
288
  See [AGENTS.md](./AGENTS.md) for development setup and guidelines.
package/dist/index.cjs CHANGED
@@ -37,7 +37,7 @@ module.exports = __toCommonJS(index_exports);
37
37
  // package.json
38
38
  var package_default = {
39
39
  name: "eslint-plugin-unslop",
40
- version: "0.4.0",
40
+ version: "0.4.2",
41
41
  description: "ESLint plugin with rules for reducing AI-generated code smells",
42
42
  repository: {
43
43
  type: "git",
@@ -106,7 +106,7 @@ var package_default = {
106
106
  };
107
107
 
108
108
  // src/rules/no-special-unicode/index.ts
109
- var rule = {
109
+ var no_special_unicode_default = {
110
110
  meta: {
111
111
  type: "suggestion",
112
112
  docs: {
@@ -216,10 +216,9 @@ function isSafe(entry, wrapper) {
216
216
  function formatCode(char) {
217
217
  return char.codePointAt(0).toString(16).toUpperCase().padStart(4, "0");
218
218
  }
219
- var no_special_unicode_default = rule;
220
219
 
221
220
  // src/rules/no-unicode-escape/index.ts
222
- var rule2 = {
221
+ var no_unicode_escape_default = {
223
222
  meta: {
224
223
  type: "suggestion",
225
224
  docs: {
@@ -288,18 +287,17 @@ function replaceMatches(raw, matches) {
288
287
  return result;
289
288
  }
290
289
  var ESCAPE_RE = /\\u([0-9A-Fa-f]{4})/g;
290
+ function isUnsafe(codePoint, wrapper) {
291
+ if (codePoint < 32) return true;
292
+ if (ALWAYS_UNSAFE.has(codePoint)) return true;
293
+ return codePoint === WRAPPER_UNSAFE[wrapper];
294
+ }
291
295
  var ALWAYS_UNSAFE = /* @__PURE__ */ new Set([92]);
292
296
  var WRAPPER_UNSAFE = {
293
297
  '"': 34,
294
298
  "'": 39,
295
299
  "`": 96
296
300
  };
297
- function isUnsafe(codePoint, wrapper) {
298
- if (codePoint < 32) return true;
299
- if (ALWAYS_UNSAFE.has(codePoint)) return true;
300
- return codePoint === WRAPPER_UNSAFE[wrapper];
301
- }
302
- var no_unicode_escape_default = rule2;
303
301
 
304
302
  // src/rules/no-false-sharing/index.ts
305
303
  var import_node_fs2 = __toESM(require("fs"), 1);
@@ -570,9 +568,7 @@ function hasStringName(value) {
570
568
  }
571
569
 
572
570
  // src/rules/no-false-sharing/index.ts
573
- var MIN_CONSUMER_GROUPS = 2;
574
- var LOCAL_SOURCE_RE = /(?:import|export)\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
575
- var rule3 = {
571
+ var no_false_sharing_default = {
576
572
  meta: {
577
573
  type: "suggestion",
578
574
  docs: {
@@ -632,6 +628,7 @@ function reportUnsharedSymbols(context, node, options) {
632
628
  });
633
629
  }
634
630
  }
631
+ var MIN_CONSUMER_GROUPS = 2;
635
632
  function collectExportedSymbols(program) {
636
633
  const symbols = /* @__PURE__ */ new Set();
637
634
  for (const statement of program.body) {
@@ -674,6 +671,26 @@ function findImporters(entrypointFile, sourceDir, sourceRoot, exportedSymbols) {
674
671
  scanDir(sourceDir, options);
675
672
  return options.importers;
676
673
  }
674
+ function getConsumerGroup(importerPath, sourceDir) {
675
+ const rel = normalizePath(import_node_path2.default.relative(sourceDir, importerPath));
676
+ const parts = rel.split("/");
677
+ if (parts.length <= 1) return rel;
678
+ return parts.slice(0, -1).join("/");
679
+ }
680
+ function getSingleConsumerGroup(groups) {
681
+ if (groups.size === 0) return " (no consumers found)";
682
+ if (groups.size !== 1) return "";
683
+ const [single] = [...groups];
684
+ return ` (group: ${single})`;
685
+ }
686
+ function deriveProjectRoot(filename, sourceRoot) {
687
+ const normalized = normalizePath(filename);
688
+ const marker = `/${sourceRoot}/`;
689
+ const index = normalized.indexOf(marker);
690
+ if (index === -1) return void 0;
691
+ return normalized.slice(0, index);
692
+ }
693
+ var LOCAL_SOURCE_RE = /(?:import|export)\s+(?:type\s+)?([\s\S]*?)\s+from\s+['"]([^'"]+)['"]/g;
677
694
  function scanDir(dir, options) {
678
695
  let entries;
679
696
  try {
@@ -760,26 +777,6 @@ function parseNamedSymbol(raw) {
760
777
  const normalized = left.trim();
761
778
  return normalized.length > 0 ? normalized : void 0;
762
779
  }
763
- function getConsumerGroup(importerPath, sourceDir) {
764
- const rel = normalizePath(import_node_path2.default.relative(sourceDir, importerPath));
765
- const parts = rel.split("/");
766
- if (parts.length <= 1) return rel;
767
- return parts.slice(0, -1).join("/");
768
- }
769
- function getSingleConsumerGroup(groups) {
770
- if (groups.size === 0) return " (no consumers found)";
771
- if (groups.size !== 1) return "";
772
- const [single] = [...groups];
773
- return ` (group: ${single})`;
774
- }
775
- function deriveProjectRoot(filename, sourceRoot) {
776
- const normalized = normalizePath(filename);
777
- const marker = `/${sourceRoot}/`;
778
- const index = normalized.indexOf(marker);
779
- if (index === -1) return void 0;
780
- return normalized.slice(0, index);
781
- }
782
- var no_false_sharing_default = rule3;
783
780
 
784
781
  // src/rules/read-friendly-order/ast-utils.ts
785
782
  function prop(obj, key) {
@@ -903,9 +900,55 @@ function walkNodeChildren(node, visit) {
903
900
  }
904
901
  }
905
902
  }
903
+ function getDeclKind(node) {
904
+ const t = node.type;
905
+ if (t === "TSInterfaceDeclaration" || t === "TSTypeAliasDeclaration") {
906
+ return "type";
907
+ }
908
+ if (t === "ExportNamedDeclaration") {
909
+ const decl = prop(node, "declaration");
910
+ if (decl && typeof decl === "object") {
911
+ const declType = strProp(decl, "type");
912
+ if (declType === "TSInterfaceDeclaration" || declType === "TSTypeAliasDeclaration") {
913
+ return "type";
914
+ }
915
+ if (declType === "VariableDeclaration" || declType === "FunctionDeclaration") {
916
+ return getDeclKind(decl);
917
+ }
918
+ }
919
+ return "other";
920
+ }
921
+ if (t === "FunctionDeclaration") {
922
+ return "function";
923
+ }
924
+ if (t === "VariableDeclaration") {
925
+ const decls = prop(node, "declarations");
926
+ if (!Array.isArray(decls) || decls.length === 0) return "other";
927
+ const name = idName(decls[0]);
928
+ if (name && /^[A-Z][A-Z_0-9]*$/.test(name)) {
929
+ return "constant";
930
+ }
931
+ const initType = strProp(prop(decls[0], "init"), "type");
932
+ if (initType === "FunctionExpression" || initType === "ArrowFunctionExpression") {
933
+ return "function";
934
+ }
935
+ return "other";
936
+ }
937
+ return "other";
938
+ }
939
+ function isLocalPublicExport(node) {
940
+ if (node.type === "ExportNamedDeclaration") {
941
+ return !!prop(node, "declaration");
942
+ }
943
+ return false;
944
+ }
906
945
  function isEagerInit(node) {
907
946
  const t = node.type;
908
947
  if (t === "ExpressionStatement" || t === "IfStatement") return true;
948
+ if (t === "ExportDefaultDeclaration") {
949
+ const declType = strProp(prop(node, "declaration"), "type");
950
+ return declType === "CallExpression" || declType === "NewExpression";
951
+ }
909
952
  const inner = t === "ExportNamedDeclaration" ? prop(node, "declaration") : node;
910
953
  if (strProp(inner, "type") !== "VariableDeclaration") return false;
911
954
  const decls = prop(inner, "declarations");
@@ -916,12 +959,26 @@ function isEagerInit(node) {
916
959
  }
917
960
  function isReexportNode(node) {
918
961
  if (node.type === "ExportNamedDeclaration") {
962
+ if (prop(node, "source")) return true;
963
+ return false;
964
+ }
965
+ if (node.type === "ExportAllDeclaration") {
966
+ return true;
967
+ }
968
+ return false;
969
+ }
970
+ function isLocalExportList(node) {
971
+ if (node.type === "ExportNamedDeclaration") {
972
+ if (prop(node, "source")) return false;
919
973
  if (prop(node, "declaration")) return false;
920
974
  const specs = prop(node, "specifiers");
921
975
  return Array.isArray(specs) && specs.length > 0;
922
976
  }
977
+ return false;
978
+ }
979
+ function isLocalExportDefault(node) {
923
980
  if (node.type === "ExportDefaultDeclaration") {
924
- return strProp(prop(node, "declaration"), "type") === "Identifier";
981
+ return strProp(prop(node, "declaration"), "type") !== "Identifier";
925
982
  }
926
983
  return false;
927
984
  }
@@ -956,14 +1013,37 @@ function checkFieldOrder(ctx, members, classBody, hasComputed) {
956
1013
  }
957
1014
  function checkMethodOrder(ctx, members, classBody, hasComputed) {
958
1015
  const methods = members.filter((m) => m.kind === "method");
1016
+ const cyclic = findCyclicMethods(methods);
959
1017
  for (const m of methods) {
960
- if (!m.name) continue;
1018
+ if (!m.name || cyclic.has(m.name)) continue;
961
1019
  if (methods.some((o) => o !== m && o.thisDeps.has(m.name) && o.idx > m.idx)) {
962
1020
  report(ctx, members, classBody, hasComputed, "moveMemberBelow", m);
963
1021
  return;
964
1022
  }
965
1023
  }
966
1024
  }
1025
+ function findCyclicMethods(methods) {
1026
+ const byName = new Map(methods.filter((m) => m.name).map((m) => [m.name, m]));
1027
+ const inCycle = /* @__PURE__ */ new Set();
1028
+ for (const [name] of byName) {
1029
+ if (methodReachesSelf(name, name, byName, /* @__PURE__ */ new Set())) {
1030
+ inCycle.add(name);
1031
+ }
1032
+ }
1033
+ return inCycle;
1034
+ }
1035
+ function methodReachesSelf(target, current, byName, visited) {
1036
+ const entry = byName.get(current);
1037
+ if (!entry) return false;
1038
+ for (const dep of entry.thisDeps) {
1039
+ if (!byName.has(dep)) continue;
1040
+ if (dep === target) return true;
1041
+ if (visited.has(dep)) continue;
1042
+ visited.add(dep);
1043
+ if (methodReachesSelf(target, dep, byName, visited)) return true;
1044
+ }
1045
+ return false;
1046
+ }
967
1047
  function report(ctx, members, classBody, hasComputed, messageId, target) {
968
1048
  const input = { ctx, members, classBody, hasComputed, messageId, target };
969
1049
  doReport(input);
@@ -1134,7 +1214,7 @@ function buildPhaseOrder(p, calls) {
1134
1214
  }
1135
1215
 
1136
1216
  // src/rules/read-friendly-order/index.ts
1137
- var rule4 = {
1217
+ var read_friendly_order_default = {
1138
1218
  meta: {
1139
1219
  type: "suggestion",
1140
1220
  docs: {
@@ -1166,11 +1246,25 @@ var rule4 = {
1166
1246
  };
1167
1247
  }
1168
1248
  };
1249
+ function checkTopLevel(ctx, p) {
1250
+ const entries = collectEntries(p);
1251
+ const decls = entries.filter((e) => !e.isImport && !e.isExternalReexport);
1252
+ filterDepsToLocal(decls);
1253
+ const eager = buildEagerSet(decls);
1254
+ const cyclic = findCyclic(decls);
1255
+ const violations = findViolations(decls, eager, cyclic);
1256
+ if (violations.length === 0) return;
1257
+ reportAll(ctx, violations, p, entries);
1258
+ }
1169
1259
  function collectEntries(p) {
1170
1260
  const entries = [];
1171
1261
  for (let i = 0; i < p.body.length; i++) {
1172
1262
  const stmt = p.body[i];
1173
1263
  const name = getDeclName(stmt);
1264
+ const isExternalReexport = isReexportNode(stmt);
1265
+ const isLocalExport = isLocalExportList(stmt);
1266
+ const isExportDefault = isLocalExportDefault(stmt);
1267
+ const isPublicExport = isLocalPublicExport(stmt);
1174
1268
  entries.push({
1175
1269
  node: stmt,
1176
1270
  idx: i,
@@ -1178,7 +1272,10 @@ function collectEntries(p) {
1178
1272
  deps: collectDeps(stmt, name),
1179
1273
  eager: isEagerInit(stmt),
1180
1274
  isImport: stmt.type === "ImportDeclaration",
1181
- isReexport: isReexportNode(stmt)
1275
+ isExternalReexport,
1276
+ isLocalExportList: isLocalExport,
1277
+ isLocalExportDefault: isExportDefault,
1278
+ isLocalPublicExport: isPublicExport
1182
1279
  });
1183
1280
  }
1184
1281
  return entries;
@@ -1236,17 +1333,6 @@ function doReachesSelf(args) {
1236
1333
  }
1237
1334
  return false;
1238
1335
  }
1239
- function checkTopLevel(ctx, p) {
1240
- const entries = collectEntries(p);
1241
- const decls = entries.filter((e) => !e.isImport && !e.isReexport);
1242
- filterDepsToLocal(decls);
1243
- const eager = buildEagerSet(decls);
1244
- const cyclic = findCyclic(decls);
1245
- const violations = findViolations(decls, eager, cyclic);
1246
- if (violations.length === 0) return;
1247
- const safe = isFixSafe(ctx, entries);
1248
- reportAll(ctx, violations, safe, p, entries);
1249
- }
1250
1336
  function filterDepsToLocal(decls) {
1251
1337
  const localNames = new Set(decls.filter((e) => e.name).map((e) => e.name));
1252
1338
  for (const e of decls) {
@@ -1258,21 +1344,32 @@ function findViolations(decls, eager, cyclic) {
1258
1344
  for (const e of decls) {
1259
1345
  if (!e.name || eager.has(e.name) || cyclic.has(e.name)) continue;
1260
1346
  const consumer = firstConsumer(e.name, decls);
1261
- if (consumer && e.idx < consumer.idx) violations.push(e);
1347
+ if (!consumer) continue;
1348
+ const eBand = getBand(e);
1349
+ const consumerBand = getBand(consumer);
1350
+ if (e.idx < consumer.idx && eBand >= consumerBand) {
1351
+ violations.push(e);
1352
+ }
1262
1353
  }
1263
1354
  return violations;
1264
1355
  }
1356
+ function getBand(entry) {
1357
+ if (entry.isImport) return 1;
1358
+ if (entry.isExternalReexport) return 2;
1359
+ if (entry.isLocalExportList || entry.isLocalExportDefault || entry.isLocalPublicExport) return 3;
1360
+ return 4;
1361
+ }
1265
1362
  function firstConsumer(name, decls) {
1266
1363
  let best;
1267
1364
  for (const e of decls) {
1268
- if (e.name === name || e.isReexport) continue;
1365
+ if (e.name === name || e.isExternalReexport || e.isLocalExportList) continue;
1269
1366
  if (!e.deps.has(name)) continue;
1270
1367
  if (!best || e.idx < best.idx) best = e;
1271
1368
  }
1272
1369
  return best;
1273
1370
  }
1274
- function reportAll(ctx, violations, safe, p, entries) {
1275
- doReportAll({ ctx, violations, safe, p, entries });
1371
+ function reportAll(ctx, violations, p, entries) {
1372
+ doReportAll({ ctx, violations, p, entries });
1276
1373
  }
1277
1374
  function doReportAll(args) {
1278
1375
  for (let i = 0; i < args.violations.length; i++) {
@@ -1281,37 +1378,52 @@ function doReportAll(args) {
1281
1378
  node: v.node,
1282
1379
  messageId: isConst(v.name) ? "moveConstantBelow" : "moveHelperBelow",
1283
1380
  data: { name: v.name },
1284
- fix: args.safe && i === 0 ? buildTopFix(args.ctx, args.p, args.entries) : null
1381
+ fix: i === 0 ? buildTopFix(args.ctx, args.p, args.entries) : null
1285
1382
  });
1286
1383
  }
1287
1384
  }
1288
- function isFixSafe(ctx, entries) {
1289
- const src = ctx.sourceCode;
1290
- const stmts = entries.filter((e) => !e.isImport);
1291
- for (let i = 0; i < stmts.length - 1; i++) {
1292
- const cur = stmts[i].node;
1293
- const nxt = stmts[i + 1].node;
1294
- for (const c of src.getCommentsBefore(nxt)) {
1295
- if (c.range[0] > cur.range[1]) return false;
1296
- }
1297
- }
1298
- return true;
1299
- }
1300
1385
  function isConst(name) {
1301
1386
  return /^[A-Z][A-Z_0-9]+$/.test(name);
1302
1387
  }
1303
1388
  function buildTopFix(ctx, p, entries) {
1304
1389
  return (fixer) => {
1305
1390
  const src = ctx.sourceCode;
1391
+ const nodeTexts = buildNodeTexts(src, entries);
1306
1392
  const imports = entries.filter((e) => e.isImport);
1307
- const reexports = entries.filter((e) => e.isReexport);
1308
- const decls = entries.filter((e) => !e.isImport && !e.isReexport);
1309
- const sorted = kahnsSort(decls);
1310
- const all = [...imports, ...sorted, ...reexports];
1311
- const text = all.map((e) => src.getText(e.node)).join("\n\n");
1393
+ const externalReexports = entries.filter((e) => e.isExternalReexport);
1394
+ const localPublicApi = entries.filter(
1395
+ (e) => e.isLocalExportList || e.isLocalExportDefault || e.isLocalPublicExport
1396
+ );
1397
+ const sortedPublicApi = kahnsSort(localPublicApi);
1398
+ const exportDefaultEntry = sortedPublicApi.find((e) => e.isLocalExportDefault);
1399
+ const otherPublicApi = sortedPublicApi.filter((e) => !e.isLocalExportDefault);
1400
+ const prioritizedPublicApi = exportDefaultEntry ? [exportDefaultEntry, ...otherPublicApi] : otherPublicApi;
1401
+ const privateDecls = entries.filter(
1402
+ (e) => !e.isImport && !e.isExternalReexport && !e.isLocalExportList && !e.isLocalExportDefault && !e.isLocalPublicExport
1403
+ );
1404
+ const sortedPrivate = kahnsSort(privateDecls);
1405
+ const all = [...imports, ...externalReexports, ...prioritizedPublicApi, ...sortedPrivate];
1406
+ const text = all.map((e) => nodeTexts.get(e)).join("\n\n");
1312
1407
  return fixer.replaceTextRange([p.range[0], p.range[1]], text);
1313
1408
  };
1314
1409
  }
1410
+ function buildNodeTexts(src, entries) {
1411
+ const result = /* @__PURE__ */ new Map();
1412
+ for (let i = 0; i < entries.length; i++) {
1413
+ const e = entries[i];
1414
+ const comments = src.getCommentsBefore(e.node);
1415
+ const prevEnd = i > 0 ? entries[i - 1].node.range[1] : e.node.range[0];
1416
+ const leadingComments = comments.filter((c) => c.range[0] >= prevEnd);
1417
+ if (leadingComments.length > 0) {
1418
+ const commentStart = leadingComments[0].range[0];
1419
+ const fullText = src.getText().slice(commentStart, e.node.range[1]);
1420
+ result.set(e, fullText);
1421
+ } else {
1422
+ result.set(e, src.getText(e.node));
1423
+ }
1424
+ }
1425
+ return result;
1426
+ }
1315
1427
  function kahnsSort(decls) {
1316
1428
  const byName = new Map(decls.filter((e) => e.name).map((e) => [e.name, e]));
1317
1429
  const inDeg = buildInDegrees(decls, byName);
@@ -1331,8 +1443,25 @@ function drainKahns2(decls, inDeg, byName) {
1331
1443
  const queue = decls.filter((e) => !e.name || inDeg.get(e.name) === 0);
1332
1444
  const result = [];
1333
1445
  const placed = /* @__PURE__ */ new Set();
1446
+ const kindPriority = (e) => {
1447
+ const kind = getDeclKind(e.node);
1448
+ switch (kind) {
1449
+ case "constant":
1450
+ return 0;
1451
+ case "type":
1452
+ return 1;
1453
+ case "function":
1454
+ return 2;
1455
+ default:
1456
+ return 3;
1457
+ }
1458
+ };
1334
1459
  while (queue.length > 0) {
1335
- queue.sort((a, b) => a.idx - b.idx);
1460
+ queue.sort((a, b) => {
1461
+ const priorityDiff = kindPriority(a) - kindPriority(b);
1462
+ if (priorityDiff !== 0) return priorityDiff;
1463
+ return a.idx - b.idx;
1464
+ });
1336
1465
  const e = queue.shift();
1337
1466
  result.push(e);
1338
1467
  if (e.name) placed.add(e.name);
@@ -1350,11 +1479,10 @@ function drainKahns2(decls, inDeg, byName) {
1350
1479
  }
1351
1480
  return result;
1352
1481
  }
1353
- var read_friendly_order_default = rule4;
1354
1482
 
1355
1483
  // src/rules/import-control/index.ts
1356
1484
  var import_node_path3 = __toESM(require("path"), 1);
1357
- var rule5 = {
1485
+ var import_control_default = {
1358
1486
  meta: {
1359
1487
  type: "problem",
1360
1488
  docs: {
@@ -1475,10 +1603,9 @@ function isRelativeTooDeep(specifier) {
1475
1603
  function allowsImport(policy, targetMatcher) {
1476
1604
  return policy.imports.includes("*") || policy.imports.includes(targetMatcher);
1477
1605
  }
1478
- var import_control_default = rule5;
1479
1606
 
1480
1607
  // src/rules/export-control/index.ts
1481
- var rule6 = {
1608
+ var export_control_default = {
1482
1609
  meta: {
1483
1610
  type: "problem",
1484
1611
  docs: {
@@ -1487,7 +1614,7 @@ var rule6 = {
1487
1614
  },
1488
1615
  schema: [],
1489
1616
  messages: {
1490
- exportAllForbidden: "Export denied: export * is not allowed in module entrypoints.",
1617
+ exportAllForbidden: "Export denied: export * is not allowed.",
1491
1618
  symbolDenied: 'Export denied: symbol "{{symbol}}" does not match configured exports patterns.',
1492
1619
  invalidExportRegex: 'Configuration error: invalid exports pattern "{{pattern}}"'
1493
1620
  }
@@ -1496,8 +1623,7 @@ var rule6 = {
1496
1623
  const filename = context.filename;
1497
1624
  if (!filename) return {};
1498
1625
  const state = buildRuleState(context, filename);
1499
- if (state === void 0) return {};
1500
- if (state.invalidPattern !== void 0) {
1626
+ if (state?.invalidPattern !== void 0) {
1501
1627
  const root = context.getSourceCode().ast;
1502
1628
  context.report({
1503
1629
  node: root,
@@ -1508,14 +1634,13 @@ var rule6 = {
1508
1634
  }
1509
1635
  return {
1510
1636
  ExportNamedDeclaration(node) {
1511
- checkNamedExport(context, node, state.patterns, state.shouldForbidExportAll);
1637
+ checkNamedExport(context, node, state?.patterns, state?.shouldForbidExportAll ?? true);
1512
1638
  },
1513
1639
  ExportAllDeclaration(node) {
1514
- if (!state.shouldForbidExportAll) return;
1515
1640
  checkExportAll(context, node);
1516
1641
  },
1517
1642
  ExportDefaultDeclaration(node) {
1518
- if (state.patterns === void 0) return;
1643
+ if (state?.patterns === void 0) return;
1519
1644
  checkDefaultExport(context, node, state.patterns);
1520
1645
  }
1521
1646
  };
@@ -1583,7 +1708,6 @@ function checkExportAll(context, node) {
1583
1708
  function matchesAnyPattern(symbol, patterns) {
1584
1709
  return patterns.some((pattern) => pattern.test(symbol));
1585
1710
  }
1586
- var export_control_default = rule6;
1587
1711
 
1588
1712
  // src/rules/index.ts
1589
1713
  var rules_default = {