@starci/hfs 4.0.10 → 4.1.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 (271) hide show
  1. package/CHANGELOG.md +1 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +3 -2
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +187 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +221 -239
  10. package/runtime/engine/invalid-config.mjs +5 -5
  11. package/runtime/engine/model-config.mjs +90 -0
  12. package/runtime/engine/orca-config.mjs +3 -1
  13. package/runtime/engine/removed-vocabulary.mjs +28 -0
  14. package/runtime/engine/resources-config.mjs +52 -0
  15. package/runtime/engine/runtime-root.mjs +17 -0
  16. package/runtime/engine/secrets.mjs +35 -31
  17. package/runtime/engine/sonar-config.mjs +21 -0
  18. package/runtime/engine/temp-root.mjs +32 -0
  19. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  20. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  21. package/runtime/knowledge/hfs/rules.yaml +98 -65
  22. package/runtime/knowledge/hfs/slots.yaml +11 -14
  23. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  24. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  25. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  26. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  27. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  28. package/runtime/modules/kernel/removed-vocabulary.yaml +168 -0
  29. package/runtime/modules/models/registry.yaml +25 -359
  30. package/runtime/modules/models/runtimes.yaml +92 -256
  31. package/runtime/modules/models/tiers.yaml +65 -0
  32. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  33. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  34. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  35. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  36. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  37. package/runtime/scripts/api/git/lib.mjs +34 -1
  38. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  39. package/runtime/scripts/api/process/run-program.mjs +2 -1
  40. package/runtime/scripts/api/sops/lib.mjs +198 -120
  41. package/runtime/scripts/hfs/allows.mjs +3 -3
  42. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  43. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  44. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  45. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  46. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  47. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  48. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  49. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  50. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  51. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  52. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  53. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  54. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  55. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  56. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  57. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  58. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  59. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  60. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  61. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  62. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  63. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  64. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  65. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  66. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  67. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  68. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  69. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  70. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  71. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  72. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  74. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  75. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  76. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  77. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  78. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  79. package/runtime/scripts/hfs/architecture/index.mjs +158 -102
  80. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  81. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  82. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  83. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  84. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  85. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  86. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  87. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  88. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  89. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  90. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  91. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  92. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  93. package/runtime/scripts/hfs/architecture/required-files.mjs +100 -68
  94. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  95. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  96. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  97. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  98. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  99. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  100. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  101. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  102. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  103. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  104. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  105. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  106. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  107. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  108. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  109. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  110. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  111. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  112. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  113. package/runtime/scripts/hfs/architecture.mjs +3 -4
  114. package/runtime/scripts/hfs/check.mjs +61 -70
  115. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  116. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  117. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  118. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  119. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  120. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  121. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  122. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  123. package/runtime/scripts/hfs/repo-identity.mjs +5 -2
  124. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  125. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  126. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  127. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  128. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  129. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  130. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  131. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  132. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  133. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  134. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  135. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  136. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  137. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  138. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  139. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  140. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  141. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  142. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  143. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  144. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  145. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  146. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  147. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  148. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  149. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  150. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  151. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  152. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  153. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  154. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  155. package/runtime/scripts/hfs/secret.mjs +59 -48
  156. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  157. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  158. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  159. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  160. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  161. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  162. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  163. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  164. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  165. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  166. package/runtime/scripts/hfs/slots.mjs +72 -693
  167. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  168. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  169. package/runtime/scripts/hfs/tree.mjs +19 -14
  170. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  171. package/runtime/scripts/hfs/view.mjs +1 -1
  172. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  173. package/runtime/scripts/lib/event-contract.mjs +17 -13
  174. package/runtime/scripts/lib/git.mjs +2 -2
  175. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  176. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  177. package/runtime/scripts/lib/i18n.mjs +3 -2
  178. package/runtime/scripts/lib/in-order.mjs +71 -0
  179. package/runtime/scripts/lib/language.mjs +3 -3
  180. package/runtime/scripts/lib/list.mjs +8 -1
  181. package/runtime/scripts/lib/path-key.mjs +38 -5
  182. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  183. package/runtime/scripts/lib/regex.mjs +1 -1
  184. package/runtime/scripts/lib/same-text.mjs +1 -1
  185. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  186. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  187. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  188. package/runtime/scripts/lib/stack-services.mjs +1 -2
  189. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  190. package/runtime/scripts/lib/walk.mjs +6 -1
  191. package/scaffold/add-cli-lite.mjs +2 -1
  192. package/scaffold/add-table.mjs +2 -2
  193. package/scaffold/add.mjs +3 -2
  194. package/scaffold/app.mjs +4 -2
  195. package/scaffold/edition-gate.mjs +25 -25
  196. package/scaffold/lite-exports.mjs +2 -1
  197. package/scaffold/service.mjs +5 -4
  198. package/sync/hygiene.mjs +15 -9
  199. package/sync/index.mjs +7 -3
  200. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  201. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  202. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  203. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  204. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  205. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  206. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  207. package/templates/app/starciwork.gitignore +1 -1
  208. package/templates/be/image/api/Dockerfile +1 -1
  209. package/templates/be/image/cli/Dockerfile +1 -1
  210. package/templates/be/image/worker/Dockerfile +1 -1
  211. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  212. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  213. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  214. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  215. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  216. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  217. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  218. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  219. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  220. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  221. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  222. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  223. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  224. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  225. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  226. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  227. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  228. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  229. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  230. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  231. package/templates/fe/image/next/Dockerfile +2 -1
  232. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  233. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  234. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  235. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  236. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  238. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  239. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  241. package/upgrade/index.mjs +9 -2
  242. package/runtime/engine/admission.mjs +0 -308
  243. package/runtime/engine/canonical-json.mjs +0 -10
  244. package/runtime/engine/db/blob.mjs +0 -315
  245. package/runtime/engine/db/ledger-paths.mjs +0 -83
  246. package/runtime/engine/db/ledger.mjs +0 -1118
  247. package/runtime/engine/db/machine-connection.mjs +0 -135
  248. package/runtime/engine/db/machine-schema.mjs +0 -84
  249. package/runtime/engine/db/machine.mjs +0 -1367
  250. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  251. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  252. package/runtime/engine/db/provider-reservations.mjs +0 -101
  253. package/runtime/engine/digest.mjs +0 -16
  254. package/runtime/engine/refuse.mjs +0 -11
  255. package/runtime/modules/ops/_labels.yaml +0 -53
  256. package/runtime/scripts/api/node/lib.mjs +0 -14
  257. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  258. package/runtime/scripts/api/process/lib.mjs +0 -111
  259. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  260. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  261. package/runtime/scripts/connectors/lib.mjs +0 -488
  262. package/runtime/scripts/lib/clip.mjs +0 -19
  263. package/runtime/scripts/lib/display-names.mjs +0 -259
  264. package/runtime/scripts/lib/example-refs.mjs +0 -158
  265. package/runtime/scripts/lib/json-schema.mjs +0 -52
  266. package/runtime/scripts/lib/process-identity.mjs +0 -6
  267. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  268. package/runtime/scripts/lib/redact.mjs +0 -161
  269. package/runtime/scripts/lib/sleep.mjs +0 -7
  270. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  271. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -3,6 +3,7 @@ import path from 'node:path';
3
3
  import { checkerScope } from './required-files.mjs';
4
4
  import { allowsFile } from '../allows.mjs';
5
5
  import { unwrapEach } from './ast-walks.mjs';
6
+ import { byCodeUnit } from '../../lib/list.mjs';
6
7
 
7
8
  /**
8
9
  * R47 `contract-fixture-guard` (BE_CONTRACT_UNGUARDED). A fake at the network edge serves payload fixtures; a fixture drifts
@@ -37,51 +38,87 @@ function walk(ts, root, visit) {
37
38
  step(root);
38
39
  }
39
40
 
40
- /** Facts one contract spec establishes: the providers whose fixtures it references and whether it compares shapes. */
41
- function readSpec(ts, sourceFile, helper, providers) {
41
+ const importsFakesOf = (from, provider) => from.includes(`${FAKES_DIRECTORY}${provider}/`) || from.endsWith(`${FAKES_DIRECTORY}${provider}`);
42
+
43
+ const shapeNamesOf = (ts, statement, helper) => {
44
+ const named = statement.importClause?.namedBindings;
45
+ if (!named || !ts.isNamedImports(named)) return [];
46
+ return named.elements.filter(element => (element.propertyName ?? element.name).text === helper).map(element => element.name.text);
47
+ };
48
+
49
+ const importsOfSpec = (ts, sourceFile, helper, providers) => {
42
50
  const shapeNames = new Set();
43
51
  const referenced = new Set();
44
52
  for (const statement of sourceFile.statements) {
45
53
  if (!ts.isImportDeclaration(statement)) continue;
46
54
  const from = statement.moduleSpecifier.text;
47
- for (const provider of providers) if (from.includes(`${FAKES_DIRECTORY}${provider}/`) || from.endsWith(`${FAKES_DIRECTORY}${provider}`)) referenced.add(provider);
48
- const named = statement.importClause?.namedBindings;
49
- if (!named || !ts.isNamedImports(named)) continue;
50
- for (const element of named.elements) if ((element.propertyName ?? element.name).text === helper) shapeNames.add(element.name.text);
55
+ for (const provider of providers) if (importsFakesOf(from, provider)) referenced.add(provider);
56
+ for (const name of shapeNamesOf(ts, statement, helper)) shapeNames.add(name);
57
+ }
58
+ return { shapeNames, referenced };
59
+ };
60
+
61
+ const callsShape = (ts, node, shapeNames) => {
62
+ let found = false;
63
+ walk(ts, node, inner => { if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression) && shapeNames.has(inner.expression.text)) found = true; });
64
+ return found;
65
+ };
66
+
67
+ const TEST_BLOCKS = new Set(['describe', 'it', 'test']);
68
+
69
+ const referenceProviders = (ts, node, providers, referenced) => {
70
+ if (ts.isStringLiteralLike(node)) for (const provider of providers) if (node.text.includes(`${FAKES_DIRECTORY}${provider}/`)) referenced.add(provider);
71
+ if (!ts.isCallExpression(node)) return;
72
+ const named = ts.isIdentifier(node.expression) ? node.expression.text : null;
73
+ for (const argument of node.arguments) {
74
+ const value = unwrap(ts, argument);
75
+ if (value && ts.isStringLiteralLike(value) && providers.has(value.text) && !TEST_BLOCKS.has(named)) referenced.add(value.text);
51
76
  }
52
- const callsShape = node => {
53
- let found = false;
54
- walk(ts, node, inner => { if (ts.isCallExpression(inner) && ts.isIdentifier(inner.expression) && shapeNames.has(inner.expression.text)) found = true; });
55
- return found;
56
- };
77
+ };
78
+
79
+ /** The `expect(...)` call a shape matcher call hangs off (through `.not` / `.resolves`), or null. */
80
+ const expectReceiverOf = (ts, callee) => {
81
+ let receiver = unwrap(ts, callee.expression);
82
+ while (receiver && ts.isPropertyAccessExpression(receiver)) receiver = unwrap(ts, receiver.expression);
83
+ return receiver && ts.isCallExpression(receiver) && ts.isIdentifier(receiver.expression) && receiver.expression.text === 'expect' ? receiver : null;
84
+ };
85
+
86
+ const comparesShapes = (ts, node, shapeNames) => {
87
+ if (!ts.isCallExpression(node)) return false;
88
+ const callee = node.expression;
89
+ if (!ts.isPropertyAccessExpression(callee) || !SHAPE_MATCHERS.has(callee.name.text)) return false;
90
+ const receiver = expectReceiverOf(ts, callee);
91
+ if (!receiver) return false;
92
+ const [actual] = receiver.arguments;
93
+ const [expected] = node.arguments;
94
+ return Boolean(actual && expected && callsShape(ts, actual, shapeNames) && callsShape(ts, expected, shapeNames));
95
+ };
96
+
97
+ const referenceAndCompare = (ts, node, providers, referenced, shapeNames) => {
98
+ referenceProviders(ts, node, providers, referenced);
99
+ return comparesShapes(ts, node, shapeNames);
100
+ };
101
+
102
+ /** Facts one contract spec establishes: the providers whose fixtures it references and whether it compares shapes. */
103
+ function readSpec(ts, sourceFile, helper, providers) {
104
+ const { shapeNames, referenced } = importsOfSpec(ts, sourceFile, helper, providers);
57
105
  let compares = false;
58
- walk(ts, sourceFile, node => {
59
- if (ts.isStringLiteralLike(node)) for (const provider of providers) if (node.text.includes(`${FAKES_DIRECTORY}${provider}/`)) referenced.add(provider);
60
- if (!ts.isCallExpression(node)) return;
61
- const named = ts.isIdentifier(node.expression) ? node.expression.text : null;
62
- for (const argument of node.arguments) {
63
- const value = unwrap(ts, argument);
64
- if (value && ts.isStringLiteralLike(value) && providers.has(value.text) && named !== 'describe' && named !== 'it' && named !== 'test') referenced.add(value.text);
65
- }
66
- const callee = node.expression;
67
- if (!ts.isPropertyAccessExpression(callee) || !SHAPE_MATCHERS.has(callee.name.text)) return;
68
- let receiver = unwrap(ts, callee.expression);
69
- while (receiver && ts.isPropertyAccessExpression(receiver)) receiver = unwrap(ts, receiver.expression); // .not / .resolves
70
- if (!receiver || !ts.isCallExpression(receiver) || !ts.isIdentifier(receiver.expression) || receiver.expression.text !== 'expect') return;
71
- const [actual] = receiver.arguments;
72
- const [expected] = node.arguments;
73
- if (actual && expected && callsShape(actual) && callsShape(expected)) compares = true;
74
- });
106
+ walk(ts, sourceFile, node => { if (referenceAndCompare(ts, node, providers, referenced, shapeNames)) compares = true; });
75
107
  return { referenced, compares };
76
108
  }
77
109
 
78
- export function checkContractFixtureGuard(input) {
79
- const { config, graph, ts, resolver, tree } = checkerScope(input);
80
- const { contractShape } = resolver.ruleParams();
81
- const violations = [];
110
+ /** The payload fixture a file of the world slot is, as `[provider, file]`, or null. */
111
+ const payloadFixtureOf = (resolver, file) => {
112
+ const verdict = allowsFile(resolver, file);
113
+ if (verdict?.slot !== WORLD_SLOT || !verdict.relative.startsWith(FAKES_DIRECTORY)) return null;
114
+ const match = PAYLOADS.exec(verdict.relative.slice(FAKES_DIRECTORY.length));
115
+ return match ? [match[1], file] : null;
116
+ };
117
+
118
+ const fixtureFilesOf = (tree, resolver) => {
82
119
  const fixtures = new Map(); // provider -> first payload fixture file
83
120
  const specs = new Map(); // provider -> [contract spec file]
84
- for (const file of [...tree.files].sort()) {
121
+ for (const file of [...tree.files].sort(byCodeUnit)) {
85
122
  const classified = resolver.classifyPath(file);
86
123
  if (classified.status !== 'owned') continue;
87
124
  if (classified.slot === CONTRACT_SLOT) {
@@ -89,14 +126,13 @@ export function checkContractFixtureGuard(input) {
89
126
  if (provider && file.endsWith('.contract-spec.ts')) specs.set(provider, [...(specs.get(provider) ?? []), file]);
90
127
  continue;
91
128
  }
92
- const verdict = classified.slot === WORLD_SLOT ? allowsFile(resolver, file) : null;
93
- if (verdict?.slot === WORLD_SLOT) {
94
- if (!verdict.relative.startsWith(FAKES_DIRECTORY)) continue;
95
- const match = PAYLOADS.exec(verdict.relative.slice(FAKES_DIRECTORY.length));
96
- if (match && !fixtures.has(match[1])) fixtures.set(match[1], file);
97
- }
129
+ const fixture = classified.slot === WORLD_SLOT ? payloadFixtureOf(resolver, file) : null;
130
+ if (fixture && !fixtures.has(fixture[0])) fixtures.set(...fixture);
98
131
  }
99
- const providers = new Set(fixtures.keys());
132
+ return { fixtures, specs };
133
+ };
134
+
135
+ const guardedProvidersOf = (config, ts, contractShape, providers, specs) => {
100
136
  const guarded = new Set();
101
137
  for (const [provider, files] of specs) {
102
138
  for (const file of files) {
@@ -107,6 +143,16 @@ export function checkContractFixtureGuard(input) {
107
143
  if (facts.compares && facts.referenced.has(provider)) guarded.add(provider);
108
144
  }
109
145
  }
146
+ return guarded;
147
+ };
148
+
149
+ export function checkContractFixtureGuard(input) {
150
+ const { config, ts, resolver, tree } = checkerScope(input);
151
+ const { contractShape } = resolver.ruleParams();
152
+ const violations = [];
153
+ const { fixtures, specs } = fixtureFilesOf(tree, resolver);
154
+ const providers = new Set(fixtures.keys());
155
+ const guarded = guardedProvidersOf(config, ts, contractShape, providers, specs);
110
156
  for (const [provider, file] of [...fixtures].sort(([a], [b]) => a.localeCompare(b))) {
111
157
  if (guarded.has(provider)) continue;
112
158
  const has = specs.get(provider)?.length ?? 0;
@@ -0,0 +1,342 @@
1
+ import path from 'node:path';
2
+ import { relativePath, unwrapExpression } from './typescript.mjs';
3
+ import { anyDescendant, nodeDecorators, normalizedSymbol, normalizedSymbolValue, valueSymbol, violation } from './ast-walks.mjs';
4
+ import { hasAnyFlag, isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
5
+
6
+ export const READONLY_BOUNDARY_RULE_ID = 'BE_READONLY_BOUNDARY';
7
+
8
+ export function canonical(file) {
9
+ return path.resolve(file);
10
+ }
11
+
12
+ export function modifiers(ts, node) {
13
+ return ts.canHaveModifiers(node) ? ts.getModifiers(node) ?? [] : node.modifiers ?? [];
14
+ }
15
+
16
+ export function hasModifier(ts, node, kind) {
17
+ return modifiers(ts, node).some(modifier => modifier.kind === kind);
18
+ }
19
+
20
+ export function isPublicMember(ts, node) {
21
+ return !hasModifier(ts, node, ts.SyntaxKind.PrivateKeyword) && !hasModifier(ts, node, ts.SyntaxKind.ProtectedKeyword);
22
+ }
23
+
24
+ function readonlyMember(ts, node) {
25
+ return hasModifier(ts, node, ts.SyntaxKind.ReadonlyKeyword);
26
+ }
27
+
28
+ function propertyName(ts, node) {
29
+ const name = node.name;
30
+ if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isPrivateIdentifier(name)) return name.text;
31
+ return null;
32
+ }
33
+
34
+ function thisOriginStatus(ts, checker, expression, seen = new Set(), depth = 0) {
35
+ if (!expression || depth > 8) return 'no';
36
+ expression = unwrapExpression(ts, expression);
37
+ if (expression.kind === ts.SyntaxKind.ThisKeyword) return 'yes';
38
+ const symbol = normalizedSymbol(ts, checker, expression);
39
+ if (!symbol || seen.has(symbol)) return 'no';
40
+ const declarations = symbol.getDeclarations?.() ?? [];
41
+ if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer) return 'no';
42
+ const origin = thisOriginStatus(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
43
+ if (origin === 'no') return 'no';
44
+ return isConstVariable(ts, declarations[0]) ? origin : 'unavailable';
45
+ }
46
+
47
+ function instanceAssignmentTarget(ts, checker, expression) {
48
+ expression = unwrapExpression(ts, expression);
49
+ if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return { instance: false };
50
+ const owner = thisOriginStatus(ts, checker, expression.expression);
51
+ if (owner === 'no') return { instance: false };
52
+ if (owner === 'unavailable') return { instance: true, name: null, supported: false };
53
+ if (ts.isPropertyAccessExpression(expression)) return { instance: true, name: expression.name.text, supported: true };
54
+ if (ts.isStringLiteralLike(expression.argumentExpression)) return { instance: true, name: expression.argumentExpression.text, supported: true };
55
+ return { instance: true, name: null, supported: false };
56
+ }
57
+
58
+ function expressionOrigin(ts, checker, expression, seen = new Set(), depth = 0) {
59
+ if (!expression || depth > 8) return null;
60
+ expression = unwrapExpression(ts, expression);
61
+ const shorthand = ts.isShorthandPropertyAssignment(expression) ? checker.getShorthandAssignmentValueSymbol?.(expression) : null;
62
+ const symbol = normalizedSymbolValue(ts, checker, shorthand) ?? normalizedSymbol(ts, checker, expression);
63
+ if (!symbol || seen.has(symbol)) return symbol;
64
+ const declarations = symbol.getDeclarations?.() ?? [];
65
+ if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
66
+ && isConstVariable(ts, declarations[0])) {
67
+ return expressionOrigin(ts, checker, declarations[0].initializer, new Set(seen).add(symbol), depth + 1);
68
+ }
69
+ return symbol;
70
+ }
71
+
72
+ const expressionReferencesOrigin = (ts, checker, expression, expected) =>
73
+ anyDescendant(ts, expression, node => expressionOrigin(ts, checker, node) === expected);
74
+
75
+ function isSafePrimitiveProjection(ts, checker, expression, expected) {
76
+ expression = unwrapExpression(ts, expression);
77
+ if (!ts.isPropertyAccessExpression(expression) && !ts.isElementAccessExpression(expression)) return false;
78
+ if (expressionOrigin(ts, checker, expression.expression) !== expected) return false;
79
+ const type = checker.getTypeAtLocation(expression);
80
+ const primitive = ts.TypeFlags.StringLike | ts.TypeFlags.NumberLike | ts.TypeFlags.BooleanLike
81
+ | ts.TypeFlags.BigIntLike | ts.TypeFlags.ESSymbolLike | ts.TypeFlags.Null | ts.TypeFlags.Undefined
82
+ | ts.TypeFlags.Void | ts.TypeFlags.Never;
83
+ return hasAnyFlag(type.flags, primitive) && !hasAnyFlag(type.flags, ts.TypeFlags.Any | ts.TypeFlags.Unknown);
84
+ }
85
+
86
+ function assignmentPatternHasInstanceTarget(ts, checker, expression) {
87
+ let found = false;
88
+ const visit = node => {
89
+ if (found) return;
90
+ const target = instanceAssignmentTarget(ts, checker, node);
91
+ if (target.instance) {
92
+ found = true;
93
+ return;
94
+ }
95
+ ts.forEachChild(node, visit);
96
+ };
97
+ visit(expression);
98
+ return found;
99
+ }
100
+
101
+ function globalLibraryIdentifier(ts, checker, node, expected) {
102
+ if (!ts.isIdentifier(node) || node.text !== expected) return false;
103
+ const symbol = normalizedSymbol(ts, checker, node);
104
+ const declarations = symbol?.getDeclarations?.() ?? [];
105
+ return declarations.length > 0 && declarations.every(declaration => declaration.getSourceFile().isDeclarationFile
106
+ && /(?:^|[/\\])lib\.[^/\\]+\.d\.ts$/i.test(declaration.getSourceFile().fileName));
107
+ }
108
+
109
+ function instanceMutationCall(ts, checker, node) {
110
+ if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression)) return null;
111
+ const owner = node.expression.expression;
112
+ const method = node.expression.name.text;
113
+ const known = (method === 'assign' || method === 'defineProperty') && globalLibraryIdentifier(ts, checker, owner, 'Object')
114
+ || method === 'set' && globalLibraryIdentifier(ts, checker, owner, 'Reflect');
115
+ if (!known || thisOriginStatus(ts, checker, node.arguments[0]) === 'no') return null;
116
+ return { node, values: node.arguments.slice(method === 'assign' ? 1 : 2) };
117
+ }
118
+
119
+ const isFunctionBoundary = (ts, node) => ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node)
120
+ || ts.isArrowFunction(node) || ts.isMethodDeclaration(node);
121
+
122
+ /** The instance assignment a `<left> = <origin-derived>` expression performs, or null when its target is no instance member. */
123
+ function instanceAssignment(ts, checker, node) {
124
+ const target = instanceAssignmentTarget(ts, checker, node.left);
125
+ if (target.instance) {
126
+ return { node, name: target.name, supported: target.supported && node.operatorToken.kind === ts.SyntaxKind.EqualsToken };
127
+ }
128
+ return assignmentPatternHasInstanceTarget(ts, checker, node.left) ? { node, name: null, supported: false } : null;
129
+ }
130
+
131
+ /** The assignment or mutation call at `node` that stores the constructor parameter on the instance, or null. */
132
+ function constructorFinding(ts, checker, node, parameterSymbol) {
133
+ if (ts.isBinaryExpression(node) && expressionReferencesOrigin(ts, checker, node.right, parameterSymbol)
134
+ && !isSafePrimitiveProjection(ts, checker, node.right, parameterSymbol)) {
135
+ return instanceAssignment(ts, checker, node);
136
+ }
137
+ const mutation = instanceMutationCall(ts, checker, node);
138
+ return mutation?.values.some(argument => expressionReferencesOrigin(ts, checker, argument, parameterSymbol))
139
+ ? { node, name: null, supported: false }
140
+ : null;
141
+ }
142
+
143
+ function constructorAssignments(ts, checker, constructor, parameter) {
144
+ if (!constructor.body) return [];
145
+ const parameterSymbol = normalizedSymbol(ts, checker, parameter.name);
146
+ const assignments = [];
147
+ const visit = node => {
148
+ if (node !== constructor.body && isFunctionBoundary(ts, node)) return;
149
+ const finding = constructorFinding(ts, checker, node, parameterSymbol);
150
+ if (finding) assignments.push(finding);
151
+ else ts.forEachChild(node, visit);
152
+ };
153
+ visit(constructor.body);
154
+ return assignments;
155
+ }
156
+
157
+ function checkReadonlyProperty(config, context, sourceFile, node, label, violations) {
158
+ if (!readonlyMember(context.ts, node)) violations.push(violation(config, sourceFile, node.name ?? node, READONLY_BOUNDARY_RULE_ID,
159
+ `${label} must be readonly; execution state must not mutate an injected dependency or received message.`));
160
+ }
161
+
162
+ function classValuesForSymbol(ts, symbol, localFiles) {
163
+ return (symbol?.getDeclarations?.() ?? []).flatMap(declaration => {
164
+ if (ts.isClassDeclaration(declaration)) return [declaration];
165
+ if (ts.isVariableDeclaration(declaration) && declaration.initializer) {
166
+ const initializer = unwrapExpression(ts, declaration.initializer);
167
+ if (ts.isClassExpression(initializer)) return [initializer];
168
+ }
169
+ return [];
170
+ }).filter(declaration => localFiles.has(canonical(declaration.getSourceFile().fileName)));
171
+ }
172
+
173
+ export function messageClass(config, context, checker, decorator, localFiles, reasons) {
174
+ const expression = decorator.expression;
175
+ if (!context.ts.isCallExpression(expression) || expression.arguments.length < 1) {
176
+ reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} has a handler decorator without a static message class`);
177
+ return null;
178
+ }
179
+ const symbol = valueSymbol(context.ts, checker, expression.arguments[0]);
180
+ const declarations = classValuesForSymbol(context.ts, symbol, localFiles);
181
+ if (declarations.length !== 1) {
182
+ reasons.push(`${relativePath(config.root, decorator.getSourceFile().fileName)} cannot resolve one local message class from its handler decorator`);
183
+ return null;
184
+ }
185
+ return declarations[0];
186
+ }
187
+
188
+ function inspectMessageConstructor(state, constructor) {
189
+ const { config, context, checker, sourceFile, violations, reasons } = state;
190
+ const ts = context.ts;
191
+ for (const parameter of constructor.parameters) {
192
+ if (modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword,
193
+ ts.SyntaxKind.PrivateKeyword, ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind))) {
194
+ checkReadonlyProperty(config, context, sourceFile, parameter, `Message field ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
195
+ }
196
+ }
197
+ if (!constructor.body) return;
198
+ const inspectAssign = node => {
199
+ if (node !== constructor.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node))) return;
200
+ if (instanceMutationCall(ts, checker, node)) {
201
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance mutation`);
202
+ } else if (ts.isBinaryExpression(node) && assignmentPatternHasInstanceTarget(ts, checker, node.left)
203
+ && !instanceAssignmentTarget(ts, checker, node.left).supported) {
204
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} constructs message fields through an unsupported instance assignment`);
205
+ } else ts.forEachChild(node, inspectAssign);
206
+ };
207
+ inspectAssign(constructor.body);
208
+ }
209
+
210
+ function inspectMessageMembers(state) {
211
+ const { config, context, declaration, violations, reasons } = state;
212
+ const ts = context.ts;
213
+ const sourceFile = declaration.getSourceFile();
214
+ const classState = { ...state, sourceFile };
215
+ for (const member of declaration.members) {
216
+ if (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword)) {
217
+ checkReadonlyProperty(config, context, sourceFile, member, `Message field ${propertyName(ts, member) ?? '(computed)'}`, violations);
218
+ }
219
+ if (ts.isSetAccessorDeclaration(member)) violations.push(violation(config, sourceFile, member.name, READONLY_BOUNDARY_RULE_ID,
220
+ `Message field ${propertyName(ts, member) ?? '(computed)'} cannot expose a setter.`));
221
+ if (ts.isIndexSignatureDeclaration(member)) reasons.push(`${relativePath(config.root, sourceFile.fileName)} has a message index signature whose mutation boundary cannot be proved`);
222
+ if (ts.isConstructorDeclaration(member)) inspectMessageConstructor(classState, member);
223
+ }
224
+ }
225
+
226
+ function classCarriesState(ts, declaration) {
227
+ return declaration.members.some(member => (ts.isPropertyDeclaration(member) && !hasModifier(ts, member, ts.SyntaxKind.StaticKeyword))
228
+ || ts.isSetAccessorDeclaration(member) || ts.isIndexSignatureDeclaration(member)
229
+ || (ts.isConstructorDeclaration(member) && member.parameters.length > 0));
230
+ }
231
+
232
+ function classInstanceType(ts, checker, declaration) {
233
+ const symbol = declaration.name ? normalizedSymbol(ts, checker, declaration.name) : null;
234
+ if (symbol && ts.isClassDeclaration(declaration)) return checker.getDeclaredTypeOfSymbol(symbol);
235
+ const value = checker.getTypeAtLocation(declaration);
236
+ const constructor = checker.getSignaturesOfType(value, ts.SignatureKind.Construct)[0];
237
+ return constructor ? checker.getReturnTypeOfSignature(constructor) : value;
238
+ }
239
+
240
+ function inspectMessageBases(state) {
241
+ const { config, context, checker, declaration, localFiles, sourceFile, reasons } = state;
242
+ const ts = context.ts;
243
+ const type = classInstanceType(ts, checker, declaration);
244
+ for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
245
+ const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
246
+ if (declarations.length !== 1) {
247
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message fields from a class outside the checked production program`);
248
+ } else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
249
+ visitMessageClass({ ...state, declaration: declarations[0], sourceFile: declarations[0].getSourceFile() });
250
+ } else if (classCarriesState(ts, declarations[0])) {
251
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits message state from a class outside the checked production program`);
252
+ }
253
+ }
254
+ }
255
+
256
+ function visitMessageClass(state) {
257
+ const { declaration, visited } = state;
258
+ const key = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
259
+ if (visited.has(key)) return;
260
+ visited.add(key);
261
+ const classState = { ...state, sourceFile: declaration.getSourceFile() };
262
+ inspectMessageMembers(classState);
263
+ inspectMessageBases(classState);
264
+ }
265
+
266
+ export function checkMessageReadonly(config, context, checker, declaration, localFiles, violations, reasons) {
267
+ visitMessageClass({ config, context, checker, declaration, localFiles, visited: new Set(), violations, reasons });
268
+ }
269
+
270
+ function inspectInjectedParameter(state, member, parameter) {
271
+ const { config, context, checker, sourceFile, classKind, properties, framework, violations, reasons } = state;
272
+ const ts = context.ts;
273
+ const explicitInject = nodeDecorators(ts, parameter).some(decorator => framework.decoratorKind(ts, checker, decorator, framework.targets) === 'Inject');
274
+ if (!classKind && !explicitInject) return;
275
+ const parameterProperty = modifiers(ts, parameter).some(modifier => [ts.SyntaxKind.PublicKeyword, ts.SyntaxKind.PrivateKeyword,
276
+ ts.SyntaxKind.ProtectedKeyword, ts.SyntaxKind.ReadonlyKeyword].includes(modifier.kind));
277
+ if (parameterProperty) {
278
+ checkReadonlyProperty(config, context, sourceFile, parameter, `Injected dependency ${propertyName(ts, parameter) ?? '(computed)'}`, violations);
279
+ return;
280
+ }
281
+ for (const assignment of constructorAssignments(ts, checker, member, parameter)) {
282
+ if (!assignment.supported || !assignment.name) {
283
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)}:${sourceLocation(sourceFile, assignment.node).line} stores an injected dependency through an unsupported instance assignment`);
284
+ continue;
285
+ }
286
+ const property = properties.get(assignment.name);
287
+ if (!property) reasons.push(`${relativePath(config.root, sourceFile.fileName)} assigns injected dependency ${assignment.name} without a declared field`);
288
+ else checkReadonlyProperty(config, context, sourceFile, property, `Injected dependency ${assignment.name}`, violations);
289
+ }
290
+ }
291
+
292
+ function inspectInjectedConstructor(state, member) {
293
+ for (const parameter of member.parameters) {
294
+ inspectInjectedParameter(state, member, parameter);
295
+ }
296
+ }
297
+
298
+ function inspectInjectedMembers(state) {
299
+ const { config, context, checker, declaration, framework, violations } = state;
300
+ const ts = context.ts;
301
+ const sourceFile = declaration.getSourceFile();
302
+ const classState = { ...state, sourceFile };
303
+ for (const member of declaration.members) {
304
+ if (ts.isPropertyDeclaration(member)) {
305
+ const injected = nodeDecorators(ts, member).some(decorator => framework.decoratorKind(ts, checker, decorator, framework.targets) === 'Inject');
306
+ if (injected) checkReadonlyProperty(config, context, sourceFile, member, `Injected property ${propertyName(ts, member) ?? '(computed)'}`, violations);
307
+ continue;
308
+ }
309
+ if (!ts.isConstructorDeclaration(member)) continue;
310
+ inspectInjectedConstructor(classState, member);
311
+ }
312
+ }
313
+
314
+ function inspectInjectedBases(state) {
315
+ const { config, context, checker, declaration, localFiles, reasons } = state;
316
+ const ts = context.ts;
317
+ const sourceFile = declaration.getSourceFile();
318
+ const type = classInstanceType(ts, checker, declaration);
319
+ for (const base of type && checker.getBaseTypes ? checker.getBaseTypes(type) : []) {
320
+ const declarations = (base.symbol?.getDeclarations?.() ?? []).filter(item => ts.isClassDeclaration(item)) ?? [];
321
+ if (declarations.length !== 1) {
322
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
323
+ } else if (localFiles.has(canonical(declarations[0].getSourceFile().fileName))) {
324
+ checkInjectedClass({ ...state, declaration: declarations[0] });
325
+ } else if (classCarriesState(ts, declarations[0])) {
326
+ reasons.push(`${relativePath(config.root, sourceFile.fileName)} inherits an injected constructor from a class outside the checked production program`);
327
+ }
328
+ }
329
+ }
330
+
331
+ export function checkInjectedClass({ config, context, checker, declaration, classKind, targets, decoratorKind, localFiles, violations, reasons, visited = new Set() }) {
332
+ const ts = context.ts;
333
+ const classKey = `${canonical(declaration.getSourceFile().fileName)}:${declaration.pos}`;
334
+ if (visited.has(classKey)) return;
335
+ visited.add(classKey);
336
+ const properties = new Map(declaration.members.filter(member => ts.isPropertyDeclaration(member))
337
+ .map(member => [propertyName(ts, member), member]).filter(([name]) => name));
338
+ const state = { config, context, checker, declaration, classKind, properties, targets, decoratorKind,
339
+ framework: { targets, decoratorKind }, localFiles, violations, reasons, visited };
340
+ inspectInjectedMembers(state);
341
+ if (classKind) inspectInjectedBases(state);
342
+ }