@geonosis/oxlint-plugin-biological-architecture 3.0.0 → 3.2.0

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.
Files changed (239) hide show
  1. package/CHANGELOG.md +50 -0
  2. package/README.md +48 -5
  3. package/RULES.md +201 -98
  4. package/corpus/.oxlintrc.json +6 -0
  5. package/corpus/data/bad-dynamic-orm.ts +5 -0
  6. package/corpus/data/bad-type-only-orm.ts +3 -0
  7. package/corpus/e2e/bad-list-response.spec.ts +5 -0
  8. package/corpus/e2e/bad-timeout.spec.ts +5 -0
  9. package/corpus/e2e/good-detail-response.spec.ts +5 -0
  10. package/corpus/e2e/good-state.spec.ts +5 -0
  11. package/corpus/icon-controls/bad-unnamed-icon-button.tsx +8 -0
  12. package/corpus/icon-controls/good-named-icon-button.tsx +19 -0
  13. package/corpus/manifest.json +3 -0
  14. package/corpus/tissues/bad-awaits-data.tsx +8 -0
  15. package/corpus/tissues/bad-fetches-data.tsx +7 -0
  16. package/corpus/tissues/good-takes-products.tsx +5 -0
  17. package/dist/discover-atoms.d.ts +2 -37
  18. package/dist/discover-atoms.js +1 -104
  19. package/dist/index.d.ts +3 -12
  20. package/dist/index.js +19 -174
  21. package/dist/presets.d.ts +11 -19
  22. package/dist/presets.js +89 -3
  23. package/dist/registry.d.ts +2 -0
  24. package/dist/registry.js +172 -0
  25. package/dist/rules/atom-no-deps.d.ts +1 -0
  26. package/dist/rules/atom-no-deps.js +10 -11
  27. package/dist/rules/boundary-time-is-primitive.d.ts +0 -7
  28. package/dist/rules/cell-must-be-stateful.d.ts +2 -2
  29. package/dist/rules/cell-must-be-stateful.js +24 -23
  30. package/dist/rules/cell-must-not-compose-cell.d.ts +1 -0
  31. package/dist/rules/cell-must-not-compose-cell.js +10 -3
  32. package/dist/rules/cell-no-cross-feature-organelles.d.ts +1 -0
  33. package/dist/rules/cell-no-cross-feature-organelles.js +7 -0
  34. package/dist/rules/cell-no-tissues.d.ts +1 -0
  35. package/dist/rules/cell-no-tissues.js +11 -7
  36. package/dist/rules/cells-folder-index-is-barrel.d.ts +12 -10
  37. package/dist/rules/cells-folder-index-is-barrel.js +66 -34
  38. package/dist/rules/compound-must-be-stateless.d.ts +2 -0
  39. package/dist/rules/compound-must-be-stateless.js +24 -26
  40. package/dist/rules/compound-no-stores.d.ts +1 -0
  41. package/dist/rules/compound-no-stores.js +12 -10
  42. package/dist/rules/constants-in-constants-file.d.ts +2 -8
  43. package/dist/rules/constants-in-constants-file.js +5 -6
  44. package/dist/rules/dialect-through-the-seam.d.ts +0 -13
  45. package/dist/rules/durable-body-reads-through-steps.d.ts +0 -14
  46. package/dist/rules/durable-body-reads-through-steps.js +8 -3
  47. package/dist/rules/effect-hook-naming.d.ts +9 -11
  48. package/dist/rules/effect-hook-naming.js +33 -31
  49. package/dist/rules/emit-declares-attempts.d.ts +0 -17
  50. package/dist/rules/emit-declares-attempts.js +5 -10
  51. package/dist/rules/font-roles-only.d.ts +0 -18
  52. package/dist/rules/font-roles-only.js +19 -22
  53. package/dist/rules/hermetic-e2e-no-wall-clock-waits.d.ts +53 -0
  54. package/dist/rules/hermetic-e2e-no-wall-clock-waits.js +100 -0
  55. package/dist/rules/icon-control-has-name.d.ts +53 -0
  56. package/dist/rules/icon-control-has-name.js +71 -0
  57. package/dist/rules/layer-walls.d.ts +0 -25
  58. package/dist/rules/layer-walls.js +7 -13
  59. package/dist/rules/lib/atom-map.d.ts +24 -0
  60. package/dist/rules/lib/atom-map.js +16 -0
  61. package/dist/rules/lib/cells-barrel.d.ts +0 -1
  62. package/dist/rules/lib/colors.d.ts +0 -19
  63. package/dist/rules/lib/colors.js +10 -243
  64. package/dist/rules/lib/comments.d.ts +19 -0
  65. package/dist/rules/lib/comments.js +5 -0
  66. package/dist/rules/lib/context.d.ts +0 -10
  67. package/dist/rules/lib/direction-rule.d.ts +3 -21
  68. package/dist/rules/lib/direction-rule.js +12 -3
  69. package/dist/rules/lib/doors.d.ts +2 -18
  70. package/dist/rules/lib/doors.js +79 -41
  71. package/dist/rules/lib/hooks.d.ts +2 -17
  72. package/dist/rules/lib/hooks.js +14 -9
  73. package/dist/rules/lib/imports.d.ts +2 -23
  74. package/dist/rules/lib/imports.js +7 -2
  75. package/dist/rules/lib/jsx.d.ts +31 -0
  76. package/dist/rules/lib/jsx.js +11 -0
  77. package/dist/rules/lib/landing.d.ts +0 -40
  78. package/dist/rules/lib/literal-call.d.ts +1 -0
  79. package/dist/rules/lib/literal-call.js +2 -0
  80. package/dist/rules/lib/module-text.d.ts +3 -27
  81. package/dist/rules/lib/module-text.js +19 -4
  82. package/dist/rules/lib/mutations.d.ts +0 -14
  83. package/dist/rules/lib/nodes.d.ts +1 -18
  84. package/dist/rules/lib/nodes.js +1 -0
  85. package/dist/rules/lib/objects.d.ts +2 -11
  86. package/dist/rules/lib/objects.js +21 -0
  87. package/dist/rules/lib/options.d.ts +2 -52
  88. package/dist/rules/lib/options.js +44 -18
  89. package/dist/rules/lib/package-imports.d.ts +0 -5
  90. package/dist/rules/lib/package-imports.js +4 -10
  91. package/dist/rules/lib/paths.d.ts +15 -41
  92. package/dist/rules/lib/paths.js +34 -1
  93. package/dist/rules/lib/pattern-match.d.ts +2 -12
  94. package/dist/rules/lib/pattern-match.js +5 -1
  95. package/dist/rules/lib/probe.d.ts +0 -43
  96. package/dist/rules/lib/reexports.d.ts +0 -18
  97. package/dist/rules/lib/render-tracking.d.ts +0 -8
  98. package/dist/rules/lib/shared-features.d.ts +0 -8
  99. package/dist/rules/lib/shared-features.js +2 -9
  100. package/dist/rules/lib/state-sources.d.ts +8 -0
  101. package/dist/rules/lib/state-sources.js +55 -0
  102. package/dist/rules/lib/steps.d.ts +3 -5
  103. package/dist/rules/lib/steps.js +6 -1
  104. package/dist/rules/lib/styling.d.ts +9 -42
  105. package/dist/rules/lib/styling.js +36 -10
  106. package/dist/rules/lib/tiers.d.ts +11 -24
  107. package/dist/rules/lib/tiers.js +41 -1
  108. package/dist/rules/lib/tissue-entrypoint.d.ts +0 -1
  109. package/dist/rules/lib/tissue-entrypoint.js +4 -5
  110. package/dist/rules/lib/tsconfig-paths.d.ts +0 -4
  111. package/dist/rules/lib/tsconfig-paths.js +47 -15
  112. package/dist/rules/lib/type-members.d.ts +6 -0
  113. package/dist/rules/lib/type-members.js +165 -0
  114. package/dist/rules/lib/visitors.d.ts +0 -22
  115. package/dist/rules/lib/workspace-members.d.ts +2 -0
  116. package/dist/rules/lib/workspace-members.js +115 -0
  117. package/dist/rules/list-take-without-order.js +2 -8
  118. package/dist/rules/max-comment-density.d.ts +3 -18
  119. package/dist/rules/max-comment-density.js +3 -3
  120. package/dist/rules/molecule-atoms-only.d.ts +1 -0
  121. package/dist/rules/molecule-atoms-only.js +11 -7
  122. package/dist/rules/molecule-must-compose.d.ts +3 -1
  123. package/dist/rules/molecule-must-compose.js +17 -21
  124. package/dist/rules/mutating-step-declares-retry.d.ts +2 -7
  125. package/dist/rules/mutating-step-declares-retry.js +29 -11
  126. package/dist/rules/named-when-block.d.ts +0 -2
  127. package/dist/rules/named-when-block.js +2 -8
  128. package/dist/rules/next-route-segment-is-thin-delegate.d.ts +4 -19
  129. package/dist/rules/next-route-segment-is-thin-delegate.js +47 -71
  130. package/dist/rules/no-brand-names.d.ts +0 -9
  131. package/dist/rules/no-brand-names.js +1 -1
  132. package/dist/rules/no-card-shaped-div.d.ts +4 -42
  133. package/dist/rules/no-card-shaped-div.js +12 -4
  134. package/dist/rules/no-cross-feature-stores.d.ts +1 -0
  135. package/dist/rules/no-cross-feature-stores.js +9 -5
  136. package/dist/rules/no-d1-transaction.d.ts +2 -5
  137. package/dist/rules/no-d1-transaction.js +7 -0
  138. package/dist/rules/no-date-string-compare.d.ts +2 -0
  139. package/dist/rules/no-date-string-compare.js +14 -8
  140. package/dist/rules/no-duplicate-jsx-patterns.d.ts +2 -0
  141. package/dist/rules/no-duplicate-jsx-patterns.js +16 -1
  142. package/dist/rules/no-generic-utility-package.d.ts +48 -2
  143. package/dist/rules/no-generic-utility-package.js +48 -9
  144. package/dist/rules/no-hook-in-component-disguise.d.ts +27 -13
  145. package/dist/rules/no-hook-in-component-disguise.js +38 -30
  146. package/dist/rules/no-index-count-as-exact.d.ts +0 -3
  147. package/dist/rules/no-inert-hidden-jsx.d.ts +4 -21
  148. package/dist/rules/no-inert-hidden-jsx.js +13 -12
  149. package/dist/rules/no-inline-data-in-jsx.d.ts +2 -0
  150. package/dist/rules/no-inline-data-in-jsx.js +17 -2
  151. package/dist/rules/no-invalid-feature-folders.d.ts +2 -0
  152. package/dist/rules/no-invalid-feature-folders.js +12 -4
  153. package/dist/rules/no-loader-side-effect-in-index-file.d.ts +0 -7
  154. package/dist/rules/no-logic-in-component-files.d.ts +6 -5
  155. package/dist/rules/no-logic-in-component-files.js +52 -31
  156. package/dist/rules/no-mock-db-in-integration.d.ts +0 -15
  157. package/dist/rules/no-mock-db-in-integration.js +3 -4
  158. package/dist/rules/no-orm-outside-db.d.ts +1 -0
  159. package/dist/rules/no-orm-outside-db.js +9 -7
  160. package/dist/rules/no-raw-html-atoms.d.ts +3 -22
  161. package/dist/rules/no-raw-html-atoms.js +5 -17
  162. package/dist/rules/no-raw-sql-outside-allowed.d.ts +2 -0
  163. package/dist/rules/no-raw-sql-outside-allowed.js +10 -15
  164. package/dist/rules/no-react-namespace.d.ts +2 -0
  165. package/dist/rules/no-react-namespace.js +13 -0
  166. package/dist/rules/no-renamed-html-props.d.ts +2 -0
  167. package/dist/rules/no-renamed-html-props.js +11 -2
  168. package/dist/rules/no-render-prop-reader.d.ts +4 -20
  169. package/dist/rules/no-render-prop-reader.js +13 -5
  170. package/dist/rules/no-runtime-dead-affordance.d.ts +3 -26
  171. package/dist/rules/no-runtime-dead-affordance.js +7 -20
  172. package/dist/rules/no-swallowed-catch.js +2 -8
  173. package/dist/rules/no-trivial-wrapper-component.d.ts +4 -19
  174. package/dist/rules/no-trivial-wrapper-component.js +24 -33
  175. package/dist/rules/no-ts-in-bio-folders.d.ts +2 -13
  176. package/dist/rules/no-ts-in-bio-folders.js +9 -7
  177. package/dist/rules/no-type-definitions-in-components.d.ts +2 -0
  178. package/dist/rules/no-type-definitions-in-components.js +12 -1
  179. package/dist/rules/no-unregistered-plugin-dir.d.ts +0 -41
  180. package/dist/rules/no-unregistered-plugin-dir.js +1 -9
  181. package/dist/rules/no-void-port.d.ts +2 -4
  182. package/dist/rules/no-void-port.js +7 -0
  183. package/dist/rules/organelle-dependency.d.ts +1 -0
  184. package/dist/rules/organelle-dependency.js +10 -8
  185. package/dist/rules/organelle-single-source.d.ts +2 -0
  186. package/dist/rules/organelle-single-source.js +58 -9
  187. package/dist/rules/plugin-route-namespaced.d.ts +0 -11
  188. package/dist/rules/plugin-route-namespaced.js +2 -2
  189. package/dist/rules/queries-require-org-scope.d.ts +0 -3
  190. package/dist/rules/queries-require-org-scope.js +3 -10
  191. package/dist/rules/queue-loop-is-the-library.js +3 -2
  192. package/dist/rules/route-no-inline-mutation.d.ts +1 -25
  193. package/dist/rules/route-no-inline-mutation.js +2 -7
  194. package/dist/rules/router-schema-parity.d.ts +0 -9
  195. package/dist/rules/router-schema-parity.js +5 -9
  196. package/dist/rules/service-name-not-reserved.d.ts +0 -3
  197. package/dist/rules/service-name-not-reserved.js +1 -34
  198. package/dist/rules/ssot-no-inline-facts.js +2 -5
  199. package/dist/rules/ssot-no-process-env.js +4 -12
  200. package/dist/rules/step-declares-compensation-or-none.d.ts +2 -1
  201. package/dist/rules/step-declares-compensation-or-none.js +17 -1
  202. package/dist/rules/step-opens-its-own-cell.d.ts +2 -25
  203. package/dist/rules/step-opens-its-own-cell.js +8 -13
  204. package/dist/rules/store-route-scopes-tenant-data.js +8 -26
  205. package/dist/rules/subpath-over-barrel.d.ts +2 -10
  206. package/dist/rules/subpath-over-barrel.js +48 -9
  207. package/dist/rules/subscriber-declares-id.d.ts +0 -2
  208. package/dist/rules/subscriber-declares-id.js +2 -8
  209. package/dist/rules/tables-declare-their-plane.d.ts +0 -2
  210. package/dist/rules/tables-declare-their-plane.js +4 -10
  211. package/dist/rules/tenant-tables-carry-org-id.d.ts +1 -4
  212. package/dist/rules/tenant-tables-carry-org-id.js +5 -12
  213. package/dist/rules/test-no-clock-polling-for-promise-flips.d.ts +2 -0
  214. package/dist/rules/test-no-clock-polling-for-promise-flips.js +18 -1
  215. package/dist/rules/time-through-the-door.d.ts +0 -24
  216. package/dist/rules/time-through-the-door.js +5 -5
  217. package/dist/rules/tissue-must-compose.d.ts +4 -2
  218. package/dist/rules/tissue-must-compose.js +25 -20
  219. package/dist/rules/tissue-no-data-props.d.ts +56 -0
  220. package/dist/rules/tissue-no-data-props.js +114 -32
  221. package/dist/rules/tissue-no-fetch.d.ts +26 -0
  222. package/dist/rules/tissue-no-fetch.js +57 -0
  223. package/dist/rules/tissue-no-hooks.d.ts +20 -22
  224. package/dist/rules/tissue-no-hooks.js +22 -4
  225. package/dist/rules/tissue-no-organelles.d.ts +1 -0
  226. package/dist/rules/tissue-no-organelles.js +9 -1
  227. package/dist/rules/tissue-no-stores.d.ts +1 -0
  228. package/dist/rules/tissue-no-stores.js +9 -1
  229. package/dist/rules/todo-names-its-plan.d.ts +1 -28
  230. package/dist/rules/todo-names-its-plan.js +2 -2
  231. package/dist/rules/tokens-only-colors.d.ts +2 -37
  232. package/dist/rules/tokens-only-colors.js +33 -18
  233. package/dist/rules/unique-query-step-name.d.ts +0 -7
  234. package/dist/rules/unique-query-step-name.js +6 -10
  235. package/dist/rules/worker-handles-are-scoped.d.ts +11 -8
  236. package/dist/rules/worker-handles-are-scoped.js +12 -5
  237. package/dist/rules/zustand-v5-best-practices.d.ts +8 -18
  238. package/dist/rules/zustand-v5-best-practices.js +32 -56
  239. package/package.json +2 -2
@@ -1,11 +1,3 @@
1
- /**
2
- * The `create(context)` parameter every rule hand-typed for itself.
3
- *
4
- * Fifty-six rules wrote out `{ filename: string; report: (descriptor: { data, messageId, node }) =>
5
- * void }` themselves — sometimes with the descriptor's fields in a different order, sometimes with
6
- * the parameter named `d` instead of `descriptor` — because oxlint's plugin API hands `create` a
7
- * plain object and names nothing a rule can import. One shape, named once.
8
- */
9
1
  type ReportDescriptor = {
10
2
  data: Record<string, string>;
11
3
  messageId: string;
@@ -15,11 +7,9 @@ export type RuleContext = {
15
7
  filename: string;
16
8
  report: (descriptor: ReportDescriptor) => void;
17
9
  };
18
- /** A rule whose schema is read, so oxlint hands it whatever the config named for it. */
19
10
  export type RuleContextWithOptions<Options> = RuleContext & {
20
11
  options?: [Options?];
21
12
  };
22
- /** A rule that reads its own options bag through `bagOf`, rather than a typed positional tuple. */
23
13
  export type RuleContextWithRawOptions = RuleContext & {
24
14
  options: readonly unknown[];
25
15
  };
@@ -1,42 +1,23 @@
1
+ import { type Probe, type ProbeTree } from './probe.js';
1
2
  import { type Tier } from './tiers.js';
2
3
  import { type Visitor } from './visitors.js';
3
- /**
4
- * Ten of this plugin's rules said the same sentence: files HERE may not import things THERE. Each
5
- * one wrote its own scope test, its own source parse, its own visitor set and its own loop, and the
6
- * differences between them were not decisions — they were the order the rules happened to be
7
- * written in. One rule watched three node types and its neighbour four; one resolved a relative
8
- * source and its neighbour was blind to it; two counted type-only imports and the rest did not.
9
- *
10
- * So a direction rule is DATA now: which files it polices, which targets are forbidden, and what it
11
- * says. Everything mechanical — reading the filename, reading the source, which doors to watch —
12
- * happens once, here, where a fix reaches all ten.
13
- */
14
- /** A file or an import source, said in the terms a direction rule judges by. */
15
4
  type Classified = {
16
- /** The last segment, as a diagnostic names it. */
17
5
  file: string;
18
- /** The feature it belongs to, if the layout has features. */
19
6
  feature: string | undefined;
20
- /** The path it is at — normalised, and for a relative source, resolved against the importer. */
21
7
  path: string;
22
- /** The source exactly as it was written, for the message and for a rule that matches on it. */
23
8
  source: string;
24
- /** The tier it sits in, if any. */
25
9
  tier: Tier | undefined;
26
10
  };
27
11
  export declare const classify: (path: string, source?: string) => Classified;
28
12
  type Declaration<Options> = {
29
- /** The message data, when `{ file, source }` is not what the message names. */
30
13
  data?: (target: Classified, importer: Classified) => Record<string, string>;
31
14
  description: string;
32
15
  fixShape: string;
33
- /** True when this import is a breach. */
34
16
  forbidden: (target: Classified, importer: Classified, options: Options) => boolean;
35
- /** The id of the message reported; the only one, unless the declaration says otherwise. */
36
17
  messageId?: string;
18
+ probe?: (options: readonly unknown[]) => Probe | ProbeTree;
37
19
  messages: Record<string, string>;
38
20
  optionsFrom?: (raw: unknown) => Options;
39
- /** Which files the rule polices: a tier, or a predicate for a scope no tier name describes. */
40
21
  scope: Tier | ((importer: Classified) => boolean);
41
22
  schema?: unknown[];
42
23
  typeOnly?: 'skip' | 'visit';
@@ -53,6 +34,7 @@ type Context = {
53
34
  export declare const directionRule: <Options = undefined>(declaration: Declaration<Options>) => {
54
35
  create(context: Context): Visitor;
55
36
  fixShape: string;
37
+ probe?: ((options: readonly unknown[]) => Probe | ProbeTree) | undefined;
56
38
  meta: {
57
39
  docs: {
58
40
  description: string;
@@ -3,6 +3,7 @@ import { dirname, relative } from "node:path";
3
3
  import { resolveRelative } from "./imports.js";
4
4
  import { exportedNamesOf, landingsOf, namesATierFolder, rootOf } from "./landing.js";
5
5
  import { propertyNameOf } from "./nodes.js";
6
+ import { destructuredNamesOf, withoutTypes } from "./objects.js";
6
7
  import { fileOf, normalise } from "./paths.js";
7
8
  import { featureOf, inTier, tierOf } from "./tiers.js";
8
9
  import { onImportSource } from "./visitors.js";
@@ -13,7 +14,6 @@ var classify = (path, source = path) => ({
13
14
  source,
14
15
  tier: tierOf(path)
15
16
  });
16
- var FORBIDDEN = "forbidden";
17
17
  var placeAndSource = (target, importer) => ({
18
18
  file: importer.file,
19
19
  source: target.source
@@ -51,7 +51,7 @@ var targetsOf = (filename, importer, source, taken) => {
51
51
  var directionRule = (declaration) => {
52
52
  const {
53
53
  data = placeAndSource,
54
- messageId = FORBIDDEN,
54
+ messageId = "forbidden",
55
55
  optionsFrom,
56
56
  scope,
57
57
  typeOnly = "skip"
@@ -89,7 +89,8 @@ var directionRule = (declaration) => {
89
89
  reachedThrough.set(local, reached);
90
90
  namespaces.push({ node, reached, source });
91
91
  }, { typeOnly });
92
- const whole = onImportSource((source, node) => judge(source, node, WHOLE), { typeOnly });
92
+ const destructured = new WeakMap;
93
+ const whole = onImportSource((source, node) => judge(source, node, destructured.get(node) ?? WHOLE), { typeOnly });
93
94
  return {
94
95
  CallExpression: whole.CallExpression,
95
96
  ExportAllDeclaration: whole.ExportAllDeclaration,
@@ -103,6 +104,13 @@ var directionRule = (declaration) => {
103
104
  MemberExpression(node) {
104
105
  reach(node.object, propertyNameOf(node));
105
106
  },
107
+ VariableDeclarator(node) {
108
+ const names = destructuredNamesOf(node.id);
109
+ const init = withoutTypes(node.init);
110
+ const call = init?.type === "AwaitExpression" ? withoutTypes(init.argument) : init;
111
+ if (names !== null && call !== undefined)
112
+ destructured.set(call, names);
113
+ },
106
114
  "Program:exit"() {
107
115
  for (const { node, reached, source } of namespaces) {
108
116
  judge(source, node, reached.size === 0 ? WHOLE : [...reached]);
@@ -111,6 +119,7 @@ var directionRule = (declaration) => {
111
119
  };
112
120
  },
113
121
  fixShape: declaration.fixShape,
122
+ ...declaration.probe === undefined ? {} : { probe: declaration.probe },
114
123
  meta: {
115
124
  docs: { description: declaration.description },
116
125
  messages: declaration.messages,
@@ -1,26 +1,10 @@
1
- /**
2
- * A package's doors, read off its own `package.json` `exports`: each subpath, and the names the
3
- * file behind it exports — the map `subpath-over-barrel` needs, which one consumer kept by hand.
4
- *
5
- * Read with the same regexes three rules already read a re-export shim with, never a parser: a
6
- * door's entry is `export … from` lines and declarations, and `export *` is followed inside the
7
- * package to a small fixed depth. A door written as a pattern (`./icons/*`) names no file to read
8
- * and is left out; a name two doors both export is placed under neither, because the rule could
9
- * not tell a reader which one to open.
10
- */
11
1
  export type Doors = {
12
2
  name: string;
13
- /** exported name → the door (subpath, no `./`) it lives under */ homes: Map<string, string>;
3
+ homes: Map<string, string>;
14
4
  };
15
- /** The doors a manifest declares, each with the names behind it. */
16
5
  export declare const doorsOf: (manifestPath: string) => Doors | undefined;
17
- /**
18
- * Where a bare specifier resolves from a file: the nearest `node_modules/<name>/package.json`
19
- * walking up. `own` says whether it is the repo's: linked in from outside any `node_modules` (a
20
- * workspace link — pnpm links a dependency too, but into its store's `node_modules/<name>`) or
21
- * private (npm refuses to publish one, so under `node_modules` it is the repo's).
22
- */
23
6
  export declare const resolvePackage: (from: string, name: string) => {
24
7
  manifest: string;
25
8
  own: boolean;
26
9
  } | undefined;
10
+ export declare const nodeModulesAbove: (from: string) => boolean;
@@ -1,48 +1,55 @@
1
1
  // src/rules/lib/doors.ts
2
- import { existsSync, lstatSync, readFileSync, realpathSync } from "node:fs";
2
+ import { existsSync, lstatSync, readdirSync, realpathSync } from "node:fs";
3
3
  import { dirname, join } from "node:path";
4
- import { conditionTargetOf, exportTableOf, fileFor } from "./module-text.js";
5
- var DEPTH = 8;
6
- var exportsOf = (file, seen, depth) => {
7
- if (depth > DEPTH || seen.has(file))
8
- return [];
9
- seen.add(file);
10
- const table = exportTableOf(file);
11
- if (table === undefined)
4
+ import { exportedNamesOf } from "./landing.js";
5
+ import { conditionTargetOf, fileFor, readManifest, recordOf } from "./module-text.js";
6
+ import { workspaceMemberNamed } from "./workspace-members.js";
7
+ var LISTED_FILES = 4000;
8
+ var filesBelow = (dir, budget) => {
9
+ let entries;
10
+ try {
11
+ entries = readdirSync(dir, { withFileTypes: true });
12
+ } catch {
12
13
  return [];
13
- const names = [...table.declared, ...table.reexports.keys()];
14
- for (const star of table.stars) {
15
- const next = fileFor(file, star);
16
- if (next !== undefined)
17
- names.push(...exportsOf(next, seen, depth + 1));
18
14
  }
19
- return names.filter((one) => one !== "default");
15
+ return entries.flatMap((entry) => {
16
+ if (budget.left <= 0 || entry.name === "node_modules")
17
+ return [];
18
+ if (entry.isDirectory()) {
19
+ return filesBelow(join(dir, entry.name), budget).map((one) => `${entry.name}/${one}`);
20
+ }
21
+ budget.left -= 1;
22
+ return [entry.name];
23
+ });
24
+ };
25
+ var expanded = (dir, key, target) => {
26
+ const star = target.indexOf("*");
27
+ const before = target.slice(0, star);
28
+ const suffix = target.slice(star + 1);
29
+ const folder = before.slice(0, before.lastIndexOf("/") + 1);
30
+ const stem = before.slice(folder.length);
31
+ return filesBelow(join(dir, folder), { left: LISTED_FILES }).flatMap((relative) => {
32
+ if (!relative.startsWith(stem) || !relative.endsWith(suffix))
33
+ return [];
34
+ const matched = relative.slice(stem.length, relative.length - suffix.length);
35
+ return matched === "" ? [] : [[key.replace("*", matched).slice(2), `${folder}${relative}`]];
36
+ });
20
37
  };
21
38
  var doorsOf = (manifestPath) => {
22
- let manifest;
23
- try {
24
- manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
25
- } catch {
26
- return;
27
- }
28
- if (typeof manifest.name !== "string")
29
- return;
30
- if (typeof manifest.exports !== "object" || manifest.exports === null)
39
+ const manifest = readManifest(manifestPath);
40
+ const exported = recordOf(manifest?.exports);
41
+ if (typeof manifest?.name !== "string" || exported === undefined)
31
42
  return;
32
43
  const dir = dirname(manifestPath);
33
44
  const placed = new Map;
34
45
  const contested = new Set;
35
- for (const [key, value] of Object.entries(manifest.exports)) {
36
- if (key === "." || !key.startsWith("./") || key.includes("*"))
37
- continue;
38
- const target = conditionTargetOf(value);
39
- if (target === undefined)
40
- continue;
46
+ const place = (door, target) => {
41
47
  const entry = fileFor(join(dir, "package.json"), target);
42
48
  if (entry === undefined)
43
- continue;
44
- const door = key.slice(2);
45
- for (const name of new Set(exportsOf(entry, new Set, 0))) {
49
+ return;
50
+ for (const name of exportedNamesOf(entry)) {
51
+ if (name === "default")
52
+ continue;
46
53
  if (contested.has(name))
47
54
  continue;
48
55
  const already = placed.get(name);
@@ -53,10 +60,25 @@ var doorsOf = (manifestPath) => {
53
60
  }
54
61
  placed.set(name, door);
55
62
  }
63
+ };
64
+ for (const [key, value] of Object.entries(exported)) {
65
+ if (key === "." || !key.startsWith("./"))
66
+ continue;
67
+ const target = conditionTargetOf(value);
68
+ if (target === undefined)
69
+ continue;
70
+ if (key.includes("*")) {
71
+ if (!target.includes("*"))
72
+ continue;
73
+ for (const [door, concrete] of expanded(dir, key, target))
74
+ place(door, concrete);
75
+ continue;
76
+ }
77
+ place(key.slice(2), target);
56
78
  }
57
79
  return { homes: placed, name: manifest.name };
58
80
  };
59
- var resolvePackage = (from, name) => {
81
+ var resolveInNodeModules = (from, name) => {
60
82
  let dir = dirname(from);
61
83
  for (;; ) {
62
84
  const packageDir = join(dir, "node_modules", name);
@@ -68,13 +90,8 @@ var resolvePackage = (from, name) => {
68
90
  } catch {
69
91
  own = false;
70
92
  }
71
- if (!own) {
72
- try {
73
- own = JSON.parse(readFileSync(manifest, "utf8")).private === true;
74
- } catch {
75
- own = false;
76
- }
77
- }
93
+ if (!own)
94
+ own = readManifest(manifest)?.private === true;
78
95
  return { manifest, own };
79
96
  }
80
97
  const parent = dirname(dir);
@@ -83,7 +100,28 @@ var resolvePackage = (from, name) => {
83
100
  dir = parent;
84
101
  }
85
102
  };
103
+ var resolvePackage = (from, name) => {
104
+ const found = resolveInNodeModules(from, name);
105
+ if (found?.own === true)
106
+ return found;
107
+ const member = workspaceMemberNamed(from, name);
108
+ if (member === undefined)
109
+ return found;
110
+ return { manifest: found?.manifest ?? member, own: true };
111
+ };
112
+ var nodeModulesAbove = (from) => {
113
+ let dir = dirname(from);
114
+ for (;; ) {
115
+ if (existsSync(join(dir, "node_modules")))
116
+ return true;
117
+ const parent = dirname(dir);
118
+ if (parent === dir)
119
+ return false;
120
+ dir = parent;
121
+ }
122
+ };
86
123
  export {
87
124
  doorsOf,
125
+ nodeModulesAbove,
88
126
  resolvePackage
89
127
  };
@@ -1,20 +1,5 @@
1
1
  import { type AnyNode } from './nodes.js';
2
- /**
3
- * React's own naming convention: a function called `use<Something>` is a hook.
4
- *
5
- * Exported because two other rules test a bare callee identifier against this exact pattern
6
- * without needing `hookCalledBy`'s member-expression reach — spelling the regex a second and a
7
- * third time drifted nothing here, but there is no reason to trust that forever.
8
- */
9
2
  export declare const HOOK_NAME: RegExp;
10
- /**
11
- * The hook a call names, spelled as the file spelled it — or null when the call is not a hook.
12
- *
13
- * Two stateless-tier rules read a call's callee as a bare identifier and nothing else, so
14
- * `React.useState(0)` and `hooks.useCart()` held state in a tissue and in a compound under both
15
- * rules at "error": the hook was there, the name was there, and the rule was looking one node too
16
- * high. A member call is read here too — the object is whatever the file imported the hook
17
- * through, and the property is the hook. A computed member (`hooks[name]()`) names nothing this
18
- * can read and is left alone, as every unreadable shape in this plugin is.
19
- */
3
+ export declare const STORE_HOOK_NAME: RegExp;
20
4
  export declare const hookCalledBy: (node: AnyNode) => string | null;
5
+ export declare const hookNameOf: (node: AnyNode) => string | null;
@@ -1,21 +1,26 @@
1
1
  // src/rules/lib/hooks.ts
2
2
  import { calleeNameOf, propertyNameOf } from "./nodes.js";
3
3
  var HOOK_NAME = /^use[A-Z]/;
4
+ var STORE_HOOK_NAME = /^use\w+Store$/;
4
5
  var hookCalledBy = (node) => {
6
+ const name = hookNameOf(node);
7
+ if (name === null || calleeNameOf(node) !== null)
8
+ return name;
9
+ const object = node.callee.object;
10
+ const through = object?.type === "Identifier" && typeof object.name === "string" ? object.name : null;
11
+ return through === null ? name : `${through}.${name}`;
12
+ };
13
+ var hookNameOf = (node) => {
5
14
  const bare = calleeNameOf(node);
6
15
  if (bare !== null)
7
16
  return HOOK_NAME.test(bare) ? bare : null;
8
17
  const callee = node.callee;
9
- if (callee === undefined)
10
- return null;
11
- const property = propertyNameOf(callee);
12
- if (property === null || !HOOK_NAME.test(property))
13
- return null;
14
- const object = callee.object;
15
- const through = object?.type === "Identifier" && typeof object.name === "string" ? object.name : null;
16
- return through === null ? property : `${through}.${property}`;
18
+ const property = callee === undefined ? null : propertyNameOf(callee);
19
+ return property !== null && HOOK_NAME.test(property) ? property : null;
17
20
  };
18
21
  export {
19
22
  HOOK_NAME,
20
- hookCalledBy
23
+ STORE_HOOK_NAME,
24
+ hookCalledBy,
25
+ hookNameOf
21
26
  };
@@ -1,9 +1,3 @@
1
- /**
2
- * How a file names another file, read once.
3
- *
4
- * Twenty-one rules parsed an import source themselves and each covered a different subset of the
5
- * five ways it can be written, because each was written by whoever needed one of them that day.
6
- */
7
1
  export type ModuleNode = {
8
2
  arguments?: {
9
3
  type?: string;
@@ -19,34 +13,19 @@ export type ModuleNode = {
19
13
  type?: string;
20
14
  value?: unknown;
21
15
  } | null;
22
- /** What the import brought in, for the rules that judge an import by its names. */
23
16
  specifiers?: {
24
17
  imported?: {
25
18
  name?: string;
26
19
  };
20
+ importKind?: string;
27
21
  local?: {
28
22
  name?: string;
29
23
  };
30
24
  type?: string;
31
25
  }[];
32
26
  };
33
- /**
34
- * The module a node names, or null when it names none — `export const x = 1` names nothing, a call
35
- * that is not a `require` names nothing, and `import(`./${name}`)` names something no rule can
36
- * read. A rule that guessed at the last one would report a finding whose source it cannot print.
37
- */
38
27
  export declare const sourceOf: (node: ModuleNode) => string | null;
39
- /** `import type X from`, `import { … } from` graded type, and `export type … from`. */
40
28
  export declare const isTypeOnly: (node: ModuleNode) => boolean;
41
- /** Whether a specifier is relative — walked from the importer, never resolved through an alias. */
42
29
  export declare const isRelative: (source: string) => boolean;
43
- /**
44
- * Where a relative source lands, as a path — and any other source unchanged.
45
- *
46
- * `../../billing/organelles/totals` carries no feature name and no tier name until it is resolved
47
- * against the file that wrote it. Two rules did that and the rest did not, so the same breach was a
48
- * finding or not depending on which rule happened to look at it. `@/features/x` is NOT relative: it
49
- * is an alias the bundler resolves, and joining it to the importer's directory would invent a path
50
- * that exists nowhere.
51
- */
52
30
  export declare const resolveRelative: (importer: string, source: string) => string;
31
+ export declare const isR3fImport: (node: ModuleNode) => boolean;
@@ -2,13 +2,13 @@
2
2
  import * as path from "node:path";
3
3
  import { literalNamedBy } from "./literal-call.js";
4
4
  import { normalise } from "./paths.js";
5
- var REQUIRE = "require";
5
+ var R3F_PACKAGES = new Set(["@react-three/drei", "@react-three/fiber"]);
6
6
  var sourceOf = (node) => {
7
7
  const source = node.source;
8
8
  if (source === undefined || source === null) {
9
9
  if (node.callee === undefined)
10
10
  return null;
11
- return literalNamedBy(node, REQUIRE);
11
+ return literalNamedBy(node, "require");
12
12
  }
13
13
  if (source.type !== undefined && source.type !== "Literal")
14
14
  return null;
@@ -17,7 +17,12 @@ var sourceOf = (node) => {
17
17
  var isTypeOnly = (node) => node.importKind === "type" || node.exportKind === "type";
18
18
  var isRelative = (source) => source === "." || source === ".." || source.startsWith("./") || source.startsWith("../");
19
19
  var resolveRelative = (importer, source) => isRelative(source) ? path.posix.normalize(path.posix.join(path.posix.dirname(normalise(importer)), source)) : source;
20
+ var isR3fImport = (node) => {
21
+ const source = sourceOf(node);
22
+ return source !== null && R3F_PACKAGES.has(source);
23
+ };
20
24
  export {
25
+ isR3fImport,
21
26
  isRelative,
22
27
  isTypeOnly,
23
28
  resolveRelative,
@@ -0,0 +1,31 @@
1
+ export interface JsxAttribute {
2
+ name?: {
3
+ name?: string;
4
+ type: string;
5
+ };
6
+ type: string;
7
+ value?: {
8
+ type: string;
9
+ value?: unknown;
10
+ } | null;
11
+ }
12
+ export interface JsxOpeningElement {
13
+ attributes?: JsxAttribute[];
14
+ loc?: unknown;
15
+ name: {
16
+ name?: string;
17
+ type: string;
18
+ };
19
+ type: 'JSXOpeningElement';
20
+ }
21
+ export declare const isCapitalised: (name: string) => boolean;
22
+ export declare const attributeNamed: (node: {
23
+ attributes?: readonly JsxAttribute[];
24
+ }, name: string) => JsxAttribute | undefined;
25
+ export declare const tagNameOf: (node: {
26
+ name?: JsxOpeningElement['name'];
27
+ } | undefined) => string | undefined;
28
+ export declare const meaningfulChildren: <Child extends {
29
+ type: string;
30
+ value?: string;
31
+ }>(children: readonly Child[] | undefined) => Child[];
@@ -0,0 +1,11 @@
1
+ // src/rules/lib/jsx.ts
2
+ var isCapitalised = (name) => /^[A-Z]/.test(name);
3
+ var attributeNamed = (node, name) => node.attributes?.find((attribute) => attribute?.type === "JSXAttribute" && attribute.name?.type === "JSXIdentifier" && attribute.name.name === name);
4
+ var tagNameOf = (node) => node?.name?.type === "JSXIdentifier" && node.name.name ? node.name.name : undefined;
5
+ var meaningfulChildren = (children) => (children ?? []).filter((child) => child.type !== "JSXText" || (child.value ?? "").trim() !== "");
6
+ export {
7
+ attributeNamed,
8
+ isCapitalised,
9
+ meaningfulChildren,
10
+ tagNameOf
11
+ };
@@ -1,50 +1,10 @@
1
- /**
2
- * Where an import LANDS, and where the code behind each name it takes LIVES.
3
- *
4
- * Two consumer issues were one blindness. A direction rule read the specifier as a string and
5
- * stopped, so `@/components/organelles` — a bare barrel, which names no file in a tier folder —
6
- * was invisible (#4), and a one-line `export { CustomCarousel } from '@/kit/organelles/carousel'`
7
- * sitting in `cells/` laundered an organelle into the cell tier for every rule that looked (#11).
8
- * The Store adoption that found #4 read 78 → 8 → 5 → 1 as tier fixes landed, which is to say its
9
- * early numbers measured the rules' blindness as much as the code.
10
- *
11
- * So the specifier is resolved on disk: relative to the importer, through the repo's own
12
- * `package.json` `imports` for a `#` subpath, or through its `tsconfig.json` (or `jsconfig.json`)
13
- * `paths` — never an alias this plugin knows (LAW 6). The file it lands on is the
14
- * DOOR. Each name the import takes is then followed through `export … from` and `export *` to the
15
- * file that declares it, the HOME, to a depth of eight and never twice through one file. A name
16
- * the door does not export and a bare package are left at the door; a specifier nothing on disk
17
- * answers lands nowhere, and the rule judges the spelling as before. An import that takes the
18
- * whole module — a namespace, `export *`, `import()`, `require()` — takes every name the door
19
- * carries (row 593), narrowed to the members the file reaches when it is a namespace.
20
- */
21
1
  type Landings = {
22
- /** The file the specifier resolves to, absolute — or nowhere. */
23
2
  door: string | undefined;
24
- /** Imported name → the absolute file whose code it is; the door when it could not be followed. */
25
3
  homes: Map<string, string>;
26
4
  };
27
- /** The absolute file a specifier names from a file, or undefined when nothing on disk answers. */
28
5
  export declare const resolveSource: (importer: string, source: string) => string | undefined;
29
- /**
30
- * Every name a module exports, its `export *` modules followed to the same depth as a home is. A
31
- * namespace import, an `export *`, `import()` and `require()` take the whole module (row 593), and
32
- * "the whole module" is this list — each name then lands where its code lives, as a named import's
33
- * would.
34
- */
35
6
  export declare const exportedNamesOf: (file: string) => Set<string>;
36
7
  export declare const landingsOf: (importer: string, source: string, names: readonly string[]) => Landings;
37
- /**
38
- * Whether a specifier's last segment IS a tier folder — `@/components/organelles`, `../cells`.
39
- * A folder named `organelles` is the organelles tier whichever alias reaches it, so when nothing
40
- * on disk answers the spelling is still judged as the folder it names.
41
- */
42
8
  export declare const namesATierFolder: (source: string) => boolean;
43
- /**
44
- * The root a landed path is judged from: the nearest directory above the importer holding a
45
- * `package.json` or a `tsconfig.json`, else the importer's own. An absolute path would be judged
46
- * with every directory above the repo in it, and a checkout under `/app/` or `/home/cells/` would
47
- * make every resolved import a breach.
48
- */
49
9
  export declare const rootOf: (file: string) => string;
50
10
  export {};
@@ -8,4 +8,5 @@ export type LiteralCall = {
8
8
  value?: unknown;
9
9
  }[];
10
10
  };
11
+ export declare const TABLE_CALLEES: string[];
11
12
  export declare const literalNamedBy: (node: LiteralCall, callee: string) => string | null;
@@ -1,4 +1,5 @@
1
1
  // src/rules/lib/literal-call.ts
2
+ var TABLE_CALLEES = ["sqliteTable", "pgTable"];
2
3
  var literalNamedBy = (node, callee) => {
3
4
  if (node.callee.type !== "Identifier" || node.callee.name !== callee)
4
5
  return null;
@@ -6,5 +7,6 @@ var literalNamedBy = (node, callee) => {
6
7
  return name?.type === "Literal" && typeof name.value === "string" ? name.value : null;
7
8
  };
8
9
  export {
10
+ TABLE_CALLEES,
9
11
  literalNamedBy
10
12
  };
@@ -1,43 +1,19 @@
1
- /**
2
- * What a module's TEXT says it exports, read with regexes and never a parser — the same reading
3
- * `doors.ts` did for a package's `exports` map, shared now with the landing resolver, because a
4
- * barrel and a re-export shim are the same shape read for a different question.
5
- *
6
- * A table is read once per file per process: `create` runs once per FILE, and the tree does not
7
- * change under a lint run.
8
- */
9
1
  type ExportTable = {
10
- /** Names whose code is HERE: declarations, `export { A as B }` with no `from`, `default`. */
11
2
  declared: Set<string>;
12
- /** Exported name → the name and module it is taken from: `export { A as B } from './x'`. */
13
3
  reexports: Map<string, {
14
4
  name: string;
15
5
  source: string;
16
6
  }>;
17
- /** The modules `export * from` hands through, in order. */
18
7
  stars: string[];
19
8
  };
20
9
  export declare const EXTENSIONS: string[];
21
- /**
22
- * Source with its comments blanked to spaces, strings stepped over whole — a glob like `'src/*'`
23
- * opens no comment and a URL keeps its `//`. Blanked, not cut: the tokens either side of a comment
24
- * stay apart and every line keeps its number. Every text reader in the plugin reads through it.
25
- */
26
10
  export declare const stripComments: (source: string) => string;
27
- /** The file at a base path: itself, with an extension, or its index — or nothing on disk. */
28
11
  export declare const fileAt: (base: string) => string | undefined;
29
- /** A relative module specifier as the file it names, or undefined when nothing on disk answers. */
30
12
  export declare const fileFor: (from: string, specifier: string) => string | undefined;
31
- /** The export table of a file, or undefined when it cannot be read. */
13
+ export declare const readSource: (file: string) => string | undefined;
14
+ export declare const recordOf: (value: unknown) => Record<string, unknown> | undefined;
15
+ export declare const readManifest: (file: string) => Record<string, unknown> | undefined;
32
16
  export declare const exportTableOf: (file: string) => ExportTable | undefined;
33
- /**
34
- * The file an `exports` or `imports` value points at, whatever conditions it is wrapped in — a
35
- * string, an array of alternatives, or a conditions object read in the order a bundler would.
36
- */
37
17
  export declare const conditionTargetOf: (value: unknown) => string | undefined;
38
- /**
39
- * JSON as `tsconfig.json` writes it: comments and trailing commas allowed. `JSON.parse` refuses
40
- * both, and a reader that stripped `//` blindly would cut `"$schema": "https://…"` in half.
41
- */
42
18
  export declare const parseLooseJson: (text: string) => unknown;
43
19
  export {};
@@ -49,14 +49,26 @@ var entriesOf = (list) => list.split(",").map((one) => one.trim().replace(/^type
49
49
  const [taken, exported] = one.split(/\s+as\s+/);
50
50
  return { exported: exported ?? taken ?? "", taken: taken ?? "" };
51
51
  });
52
- var tables = new Map;
53
- var readTable = (file) => {
54
- let source;
52
+ var readSource = (file) => {
55
53
  try {
56
- source = stripComments(readFileSync(file, "utf8"));
54
+ return stripComments(readFileSync(file, "utf8"));
57
55
  } catch {
58
56
  return;
59
57
  }
58
+ };
59
+ var recordOf = (value) => typeof value === "object" && value !== null && !Array.isArray(value) ? Object.fromEntries(Object.entries(value)) : undefined;
60
+ var readManifest = (file) => {
61
+ try {
62
+ return recordOf(JSON.parse(readFileSync(file, "utf8")));
63
+ } catch {
64
+ return;
65
+ }
66
+ };
67
+ var tables = new Map;
68
+ var readTable = (file) => {
69
+ const source = readSource(file);
70
+ if (source === undefined)
71
+ return;
60
72
  const declared = new Set;
61
73
  const reexports = new Map;
62
74
  const stars = [];
@@ -119,5 +131,8 @@ export {
119
131
  fileAt,
120
132
  fileFor,
121
133
  parseLooseJson,
134
+ readManifest,
135
+ readSource,
136
+ recordOf,
122
137
  stripComments
123
138
  };