eslint-plugin-unslop 0.6.0 → 0.6.1

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
@@ -85,13 +85,16 @@ Each value is a policy object:
85
85
 
86
86
  ```ts
87
87
  {
88
- imports?: string[] // exact module, direct child via /*, self-or-child via /+, or '*' for all
89
- exports?: string[] // regex patterns symbols exported from entrypoints must match
90
- entrypoints?: string[] // public files allowed for external and test imports
91
- shared?: boolean // marks module as shared; enables no-false-sharing
88
+ imports?: string[] // exact module, direct child via /*, self-or-child via /+, or '*' for all
89
+ typeImports?: string[] // same patterns as imports, but only for type-only imports
90
+ exports?: string[] // regex patterns symbols exported from entrypoints must match
91
+ entrypoints?: string[] // public files allowed for external and test imports
92
+ shared?: boolean // marks module as shared; enables no-false-sharing
92
93
  }
93
94
  ```
94
95
 
96
+ `typeImports` defaults to `[]` when omitted. Type-only imports are also allowed when the target matches `imports`, so `typeImports` is only needed for modules you want to allow type access to without allowing value imports.
97
+
95
98
  Architecture keys and import allowlists use different matching rules:
96
99
 
97
100
  - keys assign ownership to subtrees
@@ -99,7 +102,7 @@ Architecture keys and import allowlists use different matching rules:
99
102
  - `imports: ['models/*']` allows only direct children like `models/user`
100
103
  - `imports: ['models/+']` allows `models` and direct children like `models/user`
101
104
 
102
- When multiple keys cover the same canonical module path, the winner is chosen by nearest owner first, then exact named path over wildcard path at the same depth, then longer selector path, then declaration order. Unmatched canonical module paths become anonymous modules with empty `imports`, empty `exports`, `shared: false`, and default `entrypoints: ['index.ts']`.
105
+ When multiple keys cover the same canonical module path, the winner is chosen by nearest owner first, then exact named path over wildcard path at the same depth, then longer selector path, then declaration order. Unmatched canonical module paths become anonymous modules with empty `imports`, empty `typeImports`, empty `exports`, `shared: false`, and default `entrypoints: ['index.ts']`.
103
106
 
104
107
  All architecture rules take no options. Policy comes entirely from this shared settings block.
105
108
 
@@ -112,6 +115,7 @@ Customs control for your modules: you declare which modules are allowed to impor
112
115
  Deny-by-default for cross-module imports, so forgetting to declare a dependency is a loud error rather than a silent free-for-all. It also enforces:
113
116
 
114
117
  - cross-module imports must arrive through the public gate (configured entrypoints)
118
+ - type-only imports can be separately allowed via `typeImports` (value imports from those modules remain forbidden)
115
119
  - local cross-module namespace imports are forbidden (`import * as X from '<local-module>'`)
116
120
  - same-module relative imports can only go one level deeper - no tunnelling into internals
117
121
  - files that don't match any declared module become anonymous modules and are denied by default
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.6.0",
40
+ version: "0.6.1",
41
41
  description: "ESLint plugin with rules for reducing AI-generated code smells",
42
42
  repository: {
43
43
  type: "git",
@@ -632,11 +632,13 @@ function getUnsupportedArchitectureKeyDetails(matcher) {
632
632
  function parseModulePolicy(rawPolicy) {
633
633
  if (!isRecord(rawPolicy)) return void 0;
634
634
  const imports = readStringList(rawPolicy.imports);
635
+ const typeImports = readStringList(rawPolicy.typeImports);
635
636
  const exports2 = readStringList(rawPolicy.exports);
636
637
  const entrypoints = readStringList(rawPolicy.entrypoints);
637
638
  const shared = rawPolicy.shared === true;
638
639
  return {
639
640
  imports,
641
+ typeImports,
640
642
  exports: exports2,
641
643
  entrypoints: entrypoints.length > 0 ? entrypoints : ["index.ts"],
642
644
  shared
@@ -704,7 +706,13 @@ function makeAnonymousModule(canonicalPath) {
704
706
  canonicalPath,
705
707
  ownerKey: canonicalPath,
706
708
  ownerPath: canonicalPath,
707
- policy: { imports: [], exports: [], entrypoints: ["index.ts"], shared: false },
709
+ policy: {
710
+ imports: [],
711
+ typeImports: [],
712
+ exports: [],
713
+ entrypoints: ["index.ts"],
714
+ shared: false
715
+ },
708
716
  order: 0,
709
717
  anonymous: true,
710
718
  ownerDepth: depth,
@@ -1802,7 +1810,7 @@ function checkModuleEdge(options) {
1802
1810
  return;
1803
1811
  }
1804
1812
  if (isShallowRelativeEntrypoint(specifier, targetFile, importee.policy)) return;
1805
- if (!allowsImport(importer.policy, importee.canonicalPath)) {
1813
+ if (!allowsCrossModuleDeclaration(importer.policy, importee.canonicalPath, node)) {
1806
1814
  context.report({
1807
1815
  node,
1808
1816
  messageId: "notAllowed",
@@ -1849,9 +1857,48 @@ function isRelativeDepthTooDeep(specifier) {
1849
1857
  const parts = specifier.slice(2).split("/").filter(Boolean);
1850
1858
  return parts.length > 2;
1851
1859
  }
1852
- function allowsImport(policy, targetMatcher) {
1853
- if (policy.imports.includes("*")) return true;
1854
- return policy.imports.some((pattern) => importPatternMatches(pattern, targetMatcher));
1860
+ function allowsCrossModuleDeclaration(policy, targetMatcher, node) {
1861
+ if (allowsImportPattern(policy.imports, targetMatcher)) return true;
1862
+ return isTypeOnlyDeclaration(node) && allowsImportPattern(policy.typeImports, targetMatcher);
1863
+ }
1864
+ function isTypeOnlyDeclaration(node) {
1865
+ if (node.type === "ImportDeclaration") return isTypeOnlyImportDeclaration(node);
1866
+ if (node.type === "ExportAllDeclaration") return getExportKind(node) === "type";
1867
+ return isTypeOnlyExportNamedDeclaration(node);
1868
+ }
1869
+ function isTypeOnlyImportDeclaration(node) {
1870
+ if (getImportKind(node) === "type") return true;
1871
+ if (node.specifiers.length === 0) return false;
1872
+ return node.specifiers.every(isTypeOnlyImportSpecifier);
1873
+ }
1874
+ function isTypeOnlyImportSpecifier(specifier) {
1875
+ return specifier.type === "ImportSpecifier" && getImportKind(specifier) === "type";
1876
+ }
1877
+ function isTypeOnlyExportNamedDeclaration(node) {
1878
+ if (getExportKind(node) === "type") return true;
1879
+ if (node.specifiers.length === 0) return false;
1880
+ return node.specifiers.every(isTypeOnlyExportSpecifier);
1881
+ }
1882
+ function isTypeOnlyExportSpecifier(specifier) {
1883
+ return getExportKind(specifier) === "type";
1884
+ }
1885
+ function getImportKind(value) {
1886
+ return getDeclarationKind(value, "importKind");
1887
+ }
1888
+ function getExportKind(value) {
1889
+ return getDeclarationKind(value, "exportKind");
1890
+ }
1891
+ function getDeclarationKind(value, key) {
1892
+ if (!isRecord2(value)) return void 0;
1893
+ const kind = value[key];
1894
+ return kind === "type" || kind === "value" ? kind : void 0;
1895
+ }
1896
+ function isRecord2(value) {
1897
+ return typeof value === "object" && value !== null && !Array.isArray(value);
1898
+ }
1899
+ function allowsImportPattern(patterns, targetMatcher) {
1900
+ if (patterns.includes("*")) return true;
1901
+ return patterns.some((pattern) => importPatternMatches(pattern, targetMatcher));
1855
1902
  }
1856
1903
  function importPatternMatches(pattern, target) {
1857
1904
  if (pattern === target) return true;