eslint-plugin-unslop 0.5.1 → 0.5.3
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 +9 -1
- package/dist/index.cjs +97 -13
- package/dist/index.cjs.map +1 -1
- package/dist/index.js +97 -13
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -42,11 +42,12 @@ export default [
|
|
|
42
42
|
]
|
|
43
43
|
```
|
|
44
44
|
|
|
45
|
-
Architecture rules (`import-control`, `export-control`, `no-false-sharing`, `no-single-use-constants`) require a reachable `tsconfig.json`. Set `compilerOptions.rootDir`, and if you use aliases, configure `compilerOptions.paths`.
|
|
45
|
+
Architecture rules (`import-control`, `no-whitebox-testing`, `export-control`, `no-false-sharing`, `no-single-use-constants`) require a reachable `tsconfig.json`. Set `compilerOptions.rootDir`, and if you use aliases, configure `compilerOptions.paths`.
|
|
46
46
|
|
|
47
47
|
| Rule | What it does |
|
|
48
48
|
| -------------------------------- | ---------------------------------------------------------------------- |
|
|
49
49
|
| `unslop/import-control` | Enforces module boundaries and forbids local namespace imports |
|
|
50
|
+
| `unslop/no-whitebox-testing` | Keeps tests on module entrypoints instead of same-folder internals |
|
|
50
51
|
| `unslop/export-control` | Restricts export patterns and forbids `export *` in module entrypoints |
|
|
51
52
|
| `unslop/no-false-sharing` | Flags shared entrypoint symbols with fewer than two consumer groups |
|
|
52
53
|
| `unslop/no-single-use-constants` | Flags module-scope constants used once or never across the project |
|
|
@@ -71,6 +72,7 @@ All architecture rules read from `settings.unslop.architecture`. Each key is a m
|
|
|
71
72
|
{
|
|
72
73
|
imports?: string[] // module matchers this module may import from; '*' allows all
|
|
73
74
|
exports?: string[] // regex patterns symbols exported from index.ts/types.ts must match
|
|
75
|
+
entrypoints?: string[] // public files allowed for external and test imports
|
|
74
76
|
shared?: boolean // marks module as shared; enables no-false-sharing
|
|
75
77
|
}
|
|
76
78
|
```
|
|
@@ -92,6 +94,12 @@ Deny-by-default for cross-module imports, so forgetting to declare a dependency
|
|
|
92
94
|
|
|
93
95
|
Alias imports are resolved via `compilerOptions.paths` from `tsconfig.json`.
|
|
94
96
|
|
|
97
|
+
### `unslop/no-whitebox-testing`
|
|
98
|
+
|
|
99
|
+
Keeps test files black-boxed. When a recognized test file lives beside a module's implementation, it must import that module through its public entrypoint (`.`, `./index`, or a configured `entrypoints` file) instead of reaching into sibling files like `./model.ts`.
|
|
100
|
+
|
|
101
|
+
This rule only checks recognized test filenames (`*.test.*`, `*.spec.*`, `*.*-test.*`, `*.*-spec.*`). Child submodule imports and cross-module imports are left to `unslop/import-control`.
|
|
102
|
+
|
|
95
103
|
### `unslop/export-control`
|
|
96
104
|
|
|
97
105
|
The customs declaration form for the other direction: what are you actually exporting from your module's public entrypoints?
|
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.5.
|
|
40
|
+
version: "0.5.3",
|
|
41
41
|
description: "ESLint plugin with rules for reducing AI-generated code smells",
|
|
42
42
|
repository: {
|
|
43
43
|
type: "git",
|
|
@@ -523,8 +523,13 @@ function parseModulePolicy(rawPolicy) {
|
|
|
523
523
|
if (!isRecord(rawPolicy)) return void 0;
|
|
524
524
|
const imports = readStringList(rawPolicy.imports);
|
|
525
525
|
const exports2 = readStringList(rawPolicy.exports);
|
|
526
|
+
const entrypoints = readEntrypoints(rawPolicy.entrypoints);
|
|
526
527
|
const shared = rawPolicy.shared === true;
|
|
527
|
-
return { imports, exports: exports2, shared };
|
|
528
|
+
return { imports, exports: exports2, entrypoints, shared };
|
|
529
|
+
}
|
|
530
|
+
function readEntrypoints(value) {
|
|
531
|
+
const configured = readStringList(value);
|
|
532
|
+
return configured.length > 0 ? configured : ["index.ts"];
|
|
528
533
|
}
|
|
529
534
|
function readStringList(value) {
|
|
530
535
|
if (!Array.isArray(value)) return [];
|
|
@@ -556,7 +561,7 @@ function makeDefaultModule(relativePath) {
|
|
|
556
561
|
return {
|
|
557
562
|
matcher: key,
|
|
558
563
|
instance: key,
|
|
559
|
-
policy: { imports: [], exports: [], shared: false },
|
|
564
|
+
policy: { imports: [], exports: [], entrypoints: ["index.ts"], shared: false },
|
|
560
565
|
order: 0
|
|
561
566
|
};
|
|
562
567
|
}
|
|
@@ -716,7 +721,7 @@ function reportUnsharedSymbols(context, node, options) {
|
|
|
716
721
|
for (const symbol of exportedSymbols) {
|
|
717
722
|
const consumerGroups = consumerGroupsBySymbol.get(symbol.exportedName);
|
|
718
723
|
if (consumerGroups === void 0) continue;
|
|
719
|
-
if (consumerGroups.size >=
|
|
724
|
+
if (consumerGroups.size >= 2) continue;
|
|
720
725
|
context.report({
|
|
721
726
|
node,
|
|
722
727
|
messageId: "notTrulyShared",
|
|
@@ -924,7 +929,6 @@ function getSingleConsumerGroup(groups) {
|
|
|
924
929
|
const [single] = [...groups];
|
|
925
930
|
return ` (group: ${single})`;
|
|
926
931
|
}
|
|
927
|
-
var MIN_CONSUMER_GROUPS = 2;
|
|
928
932
|
|
|
929
933
|
// src/rules/read-friendly-order/ast-utils.ts
|
|
930
934
|
function getDeclName(node) {
|
|
@@ -1620,13 +1624,13 @@ var import_control_default = {
|
|
|
1620
1624
|
meta: {
|
|
1621
1625
|
type: "problem",
|
|
1622
1626
|
docs: {
|
|
1623
|
-
description: "Enforce module import boundaries and
|
|
1627
|
+
description: "Enforce module import boundaries and configured entrypoint imports",
|
|
1624
1628
|
recommended: false
|
|
1625
1629
|
},
|
|
1626
1630
|
schema: [],
|
|
1627
1631
|
messages: {
|
|
1628
1632
|
notAllowed: "Import denied: module {{from}} cannot import module {{to}}.",
|
|
1629
|
-
nonEntrypoint: "Import denied: cross-module imports must target
|
|
1633
|
+
nonEntrypoint: "Import denied: cross-module imports must target a configured module entrypoint (offending import: {{specifier}}).",
|
|
1630
1634
|
namespaceLocalForbidden: "Import denied: local cross-module namespace imports are not allowed.",
|
|
1631
1635
|
tooDeep: "Import denied: same-module imports can only go one level deeper."
|
|
1632
1636
|
}
|
|
@@ -1687,7 +1691,7 @@ function checkModuleEdge(options) {
|
|
|
1687
1691
|
context.report({ node, messageId: "namespaceLocalForbidden" });
|
|
1688
1692
|
return;
|
|
1689
1693
|
}
|
|
1690
|
-
if (isShallowRelativeEntrypoint(specifier, targetFile)) return;
|
|
1694
|
+
if (isShallowRelativeEntrypoint(specifier, targetFile, importee.policy)) return;
|
|
1691
1695
|
if (!allowsImport(importer.policy, importee.matcher)) {
|
|
1692
1696
|
context.report({
|
|
1693
1697
|
node,
|
|
@@ -1696,15 +1700,22 @@ function checkModuleEdge(options) {
|
|
|
1696
1700
|
});
|
|
1697
1701
|
return;
|
|
1698
1702
|
}
|
|
1699
|
-
if (
|
|
1700
|
-
context.report({
|
|
1703
|
+
if (isAllowedModuleEntrypoint(targetFile, importee.policy)) return;
|
|
1704
|
+
context.report({
|
|
1705
|
+
node,
|
|
1706
|
+
messageId: "nonEntrypoint",
|
|
1707
|
+
data: { specifier }
|
|
1708
|
+
});
|
|
1701
1709
|
}
|
|
1702
1710
|
function isLocalNamespaceImport(node) {
|
|
1703
1711
|
if (node.type !== "ImportDeclaration") return false;
|
|
1704
1712
|
return node.specifiers.some((specifier) => specifier.type === "ImportNamespaceSpecifier");
|
|
1705
1713
|
}
|
|
1706
|
-
function isShallowRelativeEntrypoint(specifier, targetFile) {
|
|
1707
|
-
return !isRelativeTooDeep(specifier) && specifier.startsWith("./") &&
|
|
1714
|
+
function isShallowRelativeEntrypoint(specifier, targetFile, policy) {
|
|
1715
|
+
return !isRelativeTooDeep(specifier) && specifier.startsWith("./") && isAllowedModuleEntrypoint(targetFile, policy);
|
|
1716
|
+
}
|
|
1717
|
+
function isAllowedModuleEntrypoint(targetFile, policy) {
|
|
1718
|
+
return policy.entrypoints.includes(import_node_path5.default.basename(targetFile));
|
|
1708
1719
|
}
|
|
1709
1720
|
function reportDeepSameModuleImport(context, node, importerFile, targetFile) {
|
|
1710
1721
|
if (!isSameModuleImportTooDeep(importerFile, targetFile)) return;
|
|
@@ -1731,7 +1742,78 @@ function isRelativeTooDeep(specifier) {
|
|
|
1731
1742
|
return depth > 1;
|
|
1732
1743
|
}
|
|
1733
1744
|
function allowsImport(policy, targetMatcher) {
|
|
1734
|
-
|
|
1745
|
+
if (policy.imports.includes("*")) return true;
|
|
1746
|
+
return policy.imports.some((pattern) => importPatternMatches(pattern, targetMatcher));
|
|
1747
|
+
}
|
|
1748
|
+
function importPatternMatches(pattern, target) {
|
|
1749
|
+
const patternSegs = pattern.split("/").filter(Boolean);
|
|
1750
|
+
const targetSegs = target.split("/").filter(Boolean);
|
|
1751
|
+
if (patternSegs.length !== targetSegs.length) return false;
|
|
1752
|
+
return patternSegs.every((seg, i) => seg === "*" || seg === targetSegs[i]);
|
|
1753
|
+
}
|
|
1754
|
+
|
|
1755
|
+
// src/rules/no-whitebox-testing/index.ts
|
|
1756
|
+
var import_node_path6 = __toESM(require("path"), 1);
|
|
1757
|
+
var no_whitebox_testing_default = {
|
|
1758
|
+
meta: {
|
|
1759
|
+
type: "problem",
|
|
1760
|
+
docs: {
|
|
1761
|
+
description: "Require tests to import a module through its public entrypoint",
|
|
1762
|
+
recommended: false
|
|
1763
|
+
},
|
|
1764
|
+
schema: [],
|
|
1765
|
+
messages: {
|
|
1766
|
+
usePublicEntrypoint: "White-box test import denied: tests must import this module through its public entrypoint (offending import: {{specifier}})."
|
|
1767
|
+
}
|
|
1768
|
+
},
|
|
1769
|
+
create(context) {
|
|
1770
|
+
if (!isRecognizedTestFile(context.filename)) return {};
|
|
1771
|
+
const state = getArchitectureRuleState(context);
|
|
1772
|
+
if (state === void 0) return {};
|
|
1773
|
+
return {
|
|
1774
|
+
ImportDeclaration(node) {
|
|
1775
|
+
checkImportDeclaration(context, node, state);
|
|
1776
|
+
}
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
};
|
|
1780
|
+
function checkImportDeclaration(context, node, state) {
|
|
1781
|
+
const resolvedImport = resolveImport(node, state);
|
|
1782
|
+
if (resolvedImport === void 0) return;
|
|
1783
|
+
if (resolvedImport.targetModule.instance !== state.moduleMatch.instance) return;
|
|
1784
|
+
if (!isSameDirectoryImport(state.filename, resolvedImport.targetFile)) return;
|
|
1785
|
+
if (isAllowedModuleEntrypoint2(resolvedImport.targetFile, state.moduleMatch.policy.entrypoints))
|
|
1786
|
+
return;
|
|
1787
|
+
context.report({
|
|
1788
|
+
node,
|
|
1789
|
+
messageId: "usePublicEntrypoint",
|
|
1790
|
+
data: { specifier: resolvedImport.specifier }
|
|
1791
|
+
});
|
|
1792
|
+
}
|
|
1793
|
+
function resolveImport(node, state) {
|
|
1794
|
+
const specifier = getSpecifier2(node);
|
|
1795
|
+
if (specifier === void 0) return void 0;
|
|
1796
|
+
const resolvedTarget = resolveImportTarget(state.filename, state.policy.projectContext, specifier);
|
|
1797
|
+
if (resolvedTarget === void 0) return void 0;
|
|
1798
|
+
const targetFile = normalizeResolvedPath2(resolvedTarget);
|
|
1799
|
+
const targetModule = matchFileToArchitectureModule(targetFile, state.policy);
|
|
1800
|
+
if (targetModule === void 0) return void 0;
|
|
1801
|
+
return { specifier, targetFile, targetModule };
|
|
1802
|
+
}
|
|
1803
|
+
function getSpecifier2(node) {
|
|
1804
|
+
const value = node.source.value;
|
|
1805
|
+
return typeof value === "string" ? value : void 0;
|
|
1806
|
+
}
|
|
1807
|
+
function isRecognizedTestFile(filename) {
|
|
1808
|
+
if (filename.length === 0) return false;
|
|
1809
|
+
const basename = import_node_path6.default.basename(filename);
|
|
1810
|
+
return /^.+\.test\..+$/.test(basename) || /^.+\.spec\..+$/.test(basename) || /^.+\.[^.]+-test\..+$/.test(basename) || /^.+\.[^.]+-spec\..+$/.test(basename);
|
|
1811
|
+
}
|
|
1812
|
+
function isSameDirectoryImport(importerFile, targetFile) {
|
|
1813
|
+
return isSamePath(import_node_path6.default.dirname(importerFile), import_node_path6.default.dirname(targetFile));
|
|
1814
|
+
}
|
|
1815
|
+
function isAllowedModuleEntrypoint2(targetFile, entrypoints) {
|
|
1816
|
+
return entrypoints.includes(import_node_path6.default.basename(targetFile));
|
|
1735
1817
|
}
|
|
1736
1818
|
|
|
1737
1819
|
// src/rules/export-control/index.ts
|
|
@@ -2024,6 +2106,7 @@ var rules_default = {
|
|
|
2024
2106
|
"no-false-sharing": no_false_sharing_default,
|
|
2025
2107
|
"read-friendly-order": read_friendly_order_default,
|
|
2026
2108
|
"import-control": import_control_default,
|
|
2109
|
+
"no-whitebox-testing": no_whitebox_testing_default,
|
|
2027
2110
|
"export-control": export_control_default,
|
|
2028
2111
|
"no-single-use-constants": no_single_use_constants_default
|
|
2029
2112
|
};
|
|
@@ -2052,6 +2135,7 @@ var full = {
|
|
|
2052
2135
|
"unslop/no-special-unicode": "error",
|
|
2053
2136
|
"unslop/no-unicode-escape": "error",
|
|
2054
2137
|
"unslop/import-control": "error",
|
|
2138
|
+
"unslop/no-whitebox-testing": "error",
|
|
2055
2139
|
"unslop/export-control": "error",
|
|
2056
2140
|
"unslop/no-false-sharing": "error",
|
|
2057
2141
|
"unslop/no-single-use-constants": "error",
|