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 +5 -57
- package/dist/index.cjs +206 -82
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +206 -82
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
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 `"
|
|
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 (
|
|
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
|
|
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 = '
|
|
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.
|
|
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
|
|
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
|
|
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
|
|
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")
|
|
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
|
|
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
|
-
|
|
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
|
|
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.
|
|
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,
|
|
1275
|
-
doReportAll({ ctx, violations,
|
|
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:
|
|
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
|
|
1308
|
-
const
|
|
1309
|
-
|
|
1310
|
-
|
|
1311
|
-
const
|
|
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) =>
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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 = {
|