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 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.1",
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 >= MIN_CONSUMER_GROUPS) continue;
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 public entrypoint imports",
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 index.ts or types.ts.",
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 (isPublicEntrypoint(targetFile)) return;
1700
- context.report({ node, messageId: "nonEntrypoint" });
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("./") && isPublicEntrypoint(targetFile);
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
- return policy.imports.includes("*") || policy.imports.includes(targetMatcher);
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",