@starci/hfs 4.0.10 → 4.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 (275) hide show
  1. package/CHANGELOG.md +5 -1
  2. package/emit/operations.mjs +1 -1
  3. package/emit/type-schema.mjs +2 -1
  4. package/lint/run.mjs +65 -24
  5. package/package.json +1 -1
  6. package/report/order.mjs +2 -0
  7. package/runtime/config.example.yaml +194 -0
  8. package/runtime/engine/by-code-unit.mjs +12 -0
  9. package/runtime/engine/config.mjs +222 -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/release-config.mjs +28 -0
  14. package/runtime/engine/removed-vocabulary.mjs +28 -0
  15. package/runtime/engine/resources-config.mjs +52 -0
  16. package/runtime/engine/runtime-root.mjs +17 -0
  17. package/runtime/engine/secrets.mjs +35 -31
  18. package/runtime/engine/sonar-config.mjs +21 -0
  19. package/runtime/engine/temp-root.mjs +32 -0
  20. package/runtime/knowledge/hfs/canon-pins.yaml +14 -14
  21. package/runtime/knowledge/hfs/peer-integrations.yaml +2 -3
  22. package/runtime/knowledge/hfs/rules.yaml +161 -65
  23. package/runtime/knowledge/hfs/slots.yaml +11 -14
  24. package/runtime/knowledge/patterns/be/api.yaml +7 -24
  25. package/runtime/knowledge/patterns/be/cli.yaml +3 -12
  26. package/runtime/knowledge/patterns/be/realtime.yaml +1 -14
  27. package/runtime/knowledge/patterns/be/webhooks.yaml +0 -19
  28. package/runtime/modules/kernel/failure-codes.yaml +1 -41
  29. package/runtime/modules/kernel/removed-vocabulary.yaml +268 -0
  30. package/runtime/modules/models/registry.yaml +25 -359
  31. package/runtime/modules/models/runtimes.yaml +126 -265
  32. package/runtime/modules/models/tiers.yaml +66 -0
  33. package/runtime/scripts/api/fs/claim-file.mjs +30 -0
  34. package/runtime/scripts/api/fs/ensure-temp-root.mjs +27 -0
  35. package/runtime/scripts/api/fs/forbidden-root.mjs +2 -1
  36. package/runtime/scripts/api/fs/make-temp-dir.mjs +11 -0
  37. package/runtime/scripts/api/fs/safe-remove.mjs +52 -35
  38. package/runtime/scripts/api/git/lib.mjs +34 -1
  39. package/runtime/scripts/api/process/resolve-real-tool.mjs +14 -8
  40. package/runtime/scripts/api/process/run-program.mjs +2 -1
  41. package/runtime/scripts/api/sops/lib.mjs +198 -120
  42. package/runtime/scripts/hfs/allows.mjs +3 -3
  43. package/runtime/scripts/hfs/architecture/ast-walks.mjs +63 -50
  44. package/runtime/scripts/hfs/architecture/automatic-gates.mjs +96 -0
  45. package/runtime/scripts/hfs/architecture/backend.mjs +261 -175
  46. package/runtime/scripts/hfs/architecture/background-unowned.mjs +69 -34
  47. package/runtime/scripts/hfs/architecture/client-reaches-server.mjs +55 -54
  48. package/runtime/scripts/hfs/architecture/clones.mjs +179 -106
  49. package/runtime/scripts/hfs/architecture/config-unread.mjs +5 -22
  50. package/runtime/scripts/hfs/architecture/config.mjs +95 -80
  51. package/runtime/scripts/hfs/architecture/connection-map.mjs +233 -141
  52. package/runtime/scripts/hfs/architecture/constructor-deps.mjs +15 -9
  53. package/runtime/scripts/hfs/architecture/context-coupling.mjs +76 -52
  54. package/runtime/scripts/hfs/architecture/context-map.mjs +215 -141
  55. package/runtime/scripts/hfs/architecture/context-owner.mjs +39 -30
  56. package/runtime/scripts/hfs/architecture/context-platform-tables.mjs +31 -17
  57. package/runtime/scripts/hfs/architecture/context-transaction.mjs +38 -25
  58. package/runtime/scripts/hfs/architecture/contract-fixture-guard.mjs +86 -40
  59. package/runtime/scripts/hfs/architecture/contracts-readonly.mjs +342 -0
  60. package/runtime/scripts/hfs/architecture/contracts.mjs +263 -455
  61. package/runtime/scripts/hfs/architecture/cross-app-duplicate.mjs +43 -34
  62. package/runtime/scripts/hfs/architecture/dead-exports.mjs +201 -154
  63. package/runtime/scripts/hfs/architecture/default-deny.mjs +118 -93
  64. package/runtime/scripts/hfs/architecture/doc-language.mjs +2 -1
  65. package/runtime/scripts/hfs/architecture/entrypoint.mjs +45 -30
  66. package/runtime/scripts/hfs/architecture/error-codes.mjs +19 -12
  67. package/runtime/scripts/hfs/architecture/error-masked.mjs +39 -26
  68. package/runtime/scripts/hfs/architecture/fe-slot-allows.mjs +2 -1
  69. package/runtime/scripts/hfs/architecture/feature-shape.mjs +26 -20
  70. package/runtime/scripts/hfs/architecture/framework-pinned.mjs +1 -1
  71. package/runtime/scripts/hfs/architecture/frontend-grammar.mjs +190 -0
  72. package/runtime/scripts/hfs/architecture/frontend-routing.mjs +186 -0
  73. package/runtime/scripts/hfs/architecture/frontend-world-render.mjs +311 -0
  74. package/runtime/scripts/hfs/architecture/frontend-world.mjs +328 -0
  75. package/runtime/scripts/hfs/architecture/frontend.mjs +109 -829
  76. package/runtime/scripts/hfs/architecture/hfs-graph.mjs +14 -2
  77. package/runtime/scripts/hfs/architecture/hfs.mjs +120 -288
  78. package/runtime/scripts/hfs/architecture/hooks-are-hooks.mjs +46 -26
  79. package/runtime/scripts/hfs/architecture/i18n-keys.mjs +114 -76
  80. package/runtime/scripts/hfs/architecture/index.mjs +164 -104
  81. package/runtime/scripts/hfs/architecture/injection-token-exported.mjs +33 -28
  82. package/runtime/scripts/hfs/architecture/machine-ast.mjs +180 -144
  83. package/runtime/scripts/hfs/architecture/managed-scripts.mjs +8 -2
  84. package/runtime/scripts/hfs/architecture/module-per-transport.mjs +150 -101
  85. package/runtime/scripts/hfs/architecture/next-data-contract.mjs +51 -43
  86. package/runtime/scripts/hfs/architecture/next-data-key.mjs +338 -0
  87. package/runtime/scripts/hfs/architecture/next-data.mjs +179 -427
  88. package/runtime/scripts/hfs/architecture/owners.mjs +27 -13
  89. package/runtime/scripts/hfs/architecture/package-shape.mjs +51 -23
  90. package/runtime/scripts/hfs/architecture/presentation.mjs +165 -0
  91. package/runtime/scripts/hfs/architecture/reachability.mjs +127 -61
  92. package/runtime/scripts/hfs/architecture/register-once.mjs +112 -63
  93. package/runtime/scripts/hfs/architecture/registration.mjs +187 -110
  94. package/runtime/scripts/hfs/architecture/required-files.mjs +121 -70
  95. package/runtime/scripts/hfs/architecture/route-files-thin.mjs +83 -58
  96. package/runtime/scripts/hfs/architecture/schema-owner.mjs +182 -108
  97. package/runtime/scripts/hfs/architecture/source-names-shape.mjs +318 -0
  98. package/runtime/scripts/hfs/architecture/source-names.mjs +83 -248
  99. package/runtime/scripts/hfs/architecture/sql-owner.mjs +175 -118
  100. package/runtime/scripts/hfs/architecture/sql-returning.mjs +39 -28
  101. package/runtime/scripts/hfs/architecture/sql-tokens.mjs +302 -172
  102. package/runtime/scripts/hfs/architecture/supabase-ast.mjs +1 -1
  103. package/runtime/scripts/hfs/architecture/supabase-be.mjs +55 -45
  104. package/runtime/scripts/hfs/architecture/supabase-results.mjs +182 -0
  105. package/runtime/scripts/hfs/architecture/supabase-tables.mjs +20 -12
  106. package/runtime/scripts/hfs/architecture/supabase.mjs +191 -263
  107. package/runtime/scripts/hfs/architecture/symbols.mjs +175 -99
  108. package/runtime/scripts/hfs/architecture/test-world-files.mjs +82 -41
  109. package/runtime/scripts/hfs/architecture/tiers.mjs +120 -69
  110. package/runtime/scripts/hfs/architecture/transport-owner.mjs +84 -57
  111. package/runtime/scripts/hfs/architecture/type-context.mjs +282 -0
  112. package/runtime/scripts/hfs/architecture/typescript.mjs +116 -276
  113. package/runtime/scripts/hfs/architecture/unit-spec-providers.mjs +84 -62
  114. package/runtime/scripts/hfs/architecture.mjs +3 -4
  115. package/runtime/scripts/hfs/catalog-once.mjs +15 -0
  116. package/runtime/scripts/hfs/check.mjs +63 -71
  117. package/runtime/scripts/hfs/coverage-scope.mjs +84 -38
  118. package/runtime/scripts/hfs/declaration-shape.mjs +77 -36
  119. package/runtime/scripts/hfs/declaration-slots.mjs +1 -1
  120. package/runtime/scripts/hfs/edition-slots.mjs +5 -3
  121. package/runtime/scripts/hfs/linear-text.mjs +30 -0
  122. package/runtime/scripts/hfs/manifest-shape.mjs +164 -63
  123. package/runtime/scripts/hfs/path-findings.mjs +58 -38
  124. package/runtime/scripts/hfs/pin-findings.mjs +31 -0
  125. package/runtime/scripts/hfs/repo-identity.mjs +21 -4
  126. package/runtime/scripts/hfs/rule-catalog.mjs +194 -0
  127. package/runtime/scripts/hfs/rule-params-shape.mjs +33 -27
  128. package/runtime/scripts/hfs/rules/cli.mjs +5 -1
  129. package/runtime/scripts/hfs/rules/contract-compat.mjs +40 -22
  130. package/runtime/scripts/hfs/rules/contract.mjs +35 -27
  131. package/runtime/scripts/hfs/rules/database-config.mjs +57 -32
  132. package/runtime/scripts/hfs/rules/database-migrations.mjs +52 -31
  133. package/runtime/scripts/hfs/rules/database-plpgsql.mjs +106 -91
  134. package/runtime/scripts/hfs/rules/database-sql.mjs +254 -173
  135. package/runtime/scripts/hfs/rules/database.mjs +36 -23
  136. package/runtime/scripts/hfs/rules/deps.mjs +64 -32
  137. package/runtime/scripts/hfs/rules/docker.mjs +113 -65
  138. package/runtime/scripts/hfs/rules/edition.mjs +118 -111
  139. package/runtime/scripts/hfs/rules/event-bus.mjs +91 -45
  140. package/runtime/scripts/hfs/rules/fe-contract-documents.mjs +17 -16
  141. package/runtime/scripts/hfs/rules/fe-no-tests.mjs +36 -16
  142. package/runtime/scripts/hfs/rules/frontend-tree.mjs +4 -3
  143. package/runtime/scripts/hfs/rules/integration-specs.mjs +75 -41
  144. package/runtime/scripts/hfs/rules/kinds.mjs +59 -25
  145. package/runtime/scripts/hfs/rules/lint-suppression.mjs +9 -3
  146. package/runtime/scripts/hfs/rules/monorepo.mjs +82 -45
  147. package/runtime/scripts/hfs/rules/peer-integrations.mjs +24 -3
  148. package/runtime/scripts/hfs/rules/pipeline.mjs +58 -9
  149. package/runtime/scripts/hfs/rules/proof-commands.mjs +14 -8
  150. package/runtime/scripts/hfs/rules/repo-local-checks.mjs +11 -6
  151. package/runtime/scripts/hfs/rules/saga.mjs +128 -61
  152. package/runtime/scripts/hfs/rules/secrets.mjs +4 -3
  153. package/runtime/scripts/hfs/rules/services.mjs +34 -16
  154. package/runtime/scripts/hfs/rules/stacks.mjs +21 -14
  155. package/runtime/scripts/hfs/rules/supabase-secrets.mjs +32 -23
  156. package/runtime/scripts/hfs/rules/test-topology.mjs +25 -11
  157. package/runtime/scripts/hfs/secret.mjs +59 -48
  158. package/runtime/scripts/hfs/slot-app-view.mjs +102 -0
  159. package/runtime/scripts/hfs/slot-classify.mjs +65 -0
  160. package/runtime/scripts/hfs/slot-errors.mjs +11 -0
  161. package/runtime/scripts/hfs/slot-imports.mjs +76 -0
  162. package/runtime/scripts/hfs/slot-manifest-shape.mjs +73 -0
  163. package/runtime/scripts/hfs/slot-match.mjs +153 -0
  164. package/runtime/scripts/hfs/slot-path.mjs +8 -0
  165. package/runtime/scripts/hfs/slot-required.mjs +76 -0
  166. package/runtime/scripts/hfs/slot-semantic-problems.mjs +148 -0
  167. package/runtime/scripts/hfs/slot-side-problems.mjs +28 -0
  168. package/runtime/scripts/hfs/slots.mjs +74 -695
  169. package/runtime/scripts/hfs/sql/pg-parse.mjs +5 -2
  170. package/runtime/scripts/hfs/trailing-slashes.mjs +8 -0
  171. package/runtime/scripts/hfs/tree.mjs +19 -14
  172. package/runtime/scripts/hfs/typescript-programs.mjs +6 -4
  173. package/runtime/scripts/hfs/view.mjs +1 -1
  174. package/runtime/scripts/lib/dockerfile.mjs +56 -33
  175. package/runtime/scripts/lib/env.mjs +10 -0
  176. package/runtime/scripts/lib/event-contract.mjs +17 -13
  177. package/runtime/scripts/lib/git.mjs +2 -2
  178. package/runtime/scripts/lib/graphql-contract.mjs +158 -322
  179. package/runtime/scripts/lib/graphql-sdl.mjs +262 -0
  180. package/runtime/scripts/lib/i18n.mjs +3 -2
  181. package/runtime/scripts/lib/in-order.mjs +71 -0
  182. package/runtime/scripts/lib/language.mjs +3 -3
  183. package/runtime/scripts/lib/list.mjs +8 -1
  184. package/runtime/scripts/lib/path-key.mjs +38 -5
  185. package/runtime/scripts/lib/pid-alive.mjs +7 -0
  186. package/runtime/scripts/lib/regex.mjs +1 -1
  187. package/runtime/scripts/lib/same-text.mjs +1 -1
  188. package/runtime/scripts/lib/secret-patterns.mjs +6 -2
  189. package/runtime/scripts/lib/sleep-sync.mjs +2 -2
  190. package/runtime/scripts/lib/sops-envelope.mjs +95 -48
  191. package/runtime/scripts/lib/stack-services.mjs +1 -2
  192. package/runtime/scripts/lib/ts-ast.mjs +9 -0
  193. package/runtime/scripts/lib/walk.mjs +6 -1
  194. package/runtime/scripts/lib/yaml-cached.mjs +14 -0
  195. package/scaffold/add-cli-lite.mjs +2 -1
  196. package/scaffold/add-table.mjs +2 -2
  197. package/scaffold/add.mjs +3 -2
  198. package/scaffold/app.mjs +4 -2
  199. package/scaffold/edition-gate.mjs +25 -25
  200. package/scaffold/lite-exports.mjs +2 -1
  201. package/scaffold/service.mjs +5 -4
  202. package/sync/hygiene.mjs +15 -9
  203. package/sync/index.mjs +7 -3
  204. package/templates/app/ci-workflows/github/workflows/ci.yml +4 -4
  205. package/templates/app/ci-workflows/github/workflows/e2e.yml +3 -3
  206. package/templates/app/ci-workflows/github/workflows/images.yml +3 -3
  207. package/templates/app/ci-workflows/sonar-steps.yml +2 -2
  208. package/templates/app/ci-workflows-lite/github/workflows/ci.yml +6 -6
  209. package/templates/app/ci-workflows-lite/github/workflows/db-deploy.yml +3 -3
  210. package/templates/app/ci-workflows-lite/github/workflows/images.yml +3 -3
  211. package/templates/app/starciwork.gitignore +1 -1
  212. package/templates/be/image/api/Dockerfile +1 -1
  213. package/templates/be/image/cli/Dockerfile +1 -1
  214. package/templates/be/image/worker/Dockerfile +1 -1
  215. package/templates/be/patterns/cli/group.cli.spec.ts.tpl +1 -1
  216. package/templates/be/patterns/cli/group.cli.ts.tpl +2 -2
  217. package/templates/be/patterns/event-bus/platform/event-runner.service.spec.ts.tpl +13 -0
  218. package/templates/be/patterns/event-bus/platform/event-runner.service.ts.tpl +7 -2
  219. package/templates/be/patterns/event-bus/platform/event.policy.ts.tpl +4 -1
  220. package/templates/be/patterns/event-bus/platform/kafka-event-transport.client.ts.tpl +2 -1
  221. package/templates/be/patterns/outbox/platform/outbox-relay.policy.ts.tpl +24 -9
  222. package/templates/be/patterns/queues/platform/queue-relay.service.ts.tpl +3 -2
  223. package/templates/be/patterns/queues/platform/queue-worker.service.ts.tpl +8 -5
  224. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.spec.ts +1 -1
  225. package/templates/be/skeleton/src/features/cli/migrate/migrate.cli.ts +2 -2
  226. package/templates/be/skeleton/src/features/cli/seed/seed.cli.spec.ts +1 -1
  227. package/templates/be/skeleton/src/features/cli/seed/seed.cli.ts +2 -2
  228. package/templates/be/skeleton/src/modules/platform/database/migrate-connections.client.ts +3 -2
  229. package/templates/be/skeleton/src/modules/platform/database/seed-connections.client.ts +5 -4
  230. package/templates/be/skeleton/src/modules/platform/primitives/index.ts +1 -0
  231. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.spec.ts +47 -0
  232. package/templates/be/skeleton/src/modules/platform/primitives/sequence.policy.ts +16 -0
  233. package/templates/be/skeleton-lite/apps/api/Dockerfile +1 -0
  234. package/templates/be/skeleton-lite/src/modules/platform/primitives/index.ts +2 -0
  235. package/templates/fe/image/next/Dockerfile +2 -1
  236. package/templates/fe/skeleton/apps/app/src/app/[locale]/page.tsx +2 -2
  237. package/templates/fe/skeleton/apps/landing/src/app/[locale]/page.tsx +2 -2
  238. package/templates/fe/skeleton/packages/__project__-i18n/src/index.ts +1 -2
  239. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/layout.tsx +1 -2
  240. package/templates/fe/skeleton-lite/apps/web/src/app/[locale]/page.tsx +2 -2
  241. package/templates/fe/skeleton-lite/apps/web/src/components/blocks/SignInForm/index.tsx +7 -1
  242. package/templates/fe/skeleton-lite/apps/web/src/modules/db/auth/write-sign-out.ts +1 -1
  243. package/templates/fe/skeleton-lite/apps/web/src/modules/db/validation/validation.mapper.ts +10 -2
  244. package/templates/fe/skeleton-lite/apps/web/src/modules/i18n/request.ts +10 -3
  245. package/upgrade/index.mjs +9 -2
  246. package/runtime/engine/admission.mjs +0 -308
  247. package/runtime/engine/canonical-json.mjs +0 -10
  248. package/runtime/engine/db/blob.mjs +0 -315
  249. package/runtime/engine/db/ledger-paths.mjs +0 -83
  250. package/runtime/engine/db/ledger.mjs +0 -1118
  251. package/runtime/engine/db/machine-connection.mjs +0 -135
  252. package/runtime/engine/db/machine-schema.mjs +0 -84
  253. package/runtime/engine/db/machine.mjs +0 -1367
  254. package/runtime/engine/db/migrations/machine/0001-init.sql +0 -924
  255. package/runtime/engine/db/migrations/runtime/0001-init.sql +0 -1108
  256. package/runtime/engine/db/provider-reservations.mjs +0 -101
  257. package/runtime/engine/digest.mjs +0 -16
  258. package/runtime/engine/refuse.mjs +0 -11
  259. package/runtime/modules/ops/_labels.yaml +0 -53
  260. package/runtime/scripts/api/node/lib.mjs +0 -14
  261. package/runtime/scripts/api/node/spawn-node.mjs +0 -6
  262. package/runtime/scripts/api/process/lib.mjs +0 -111
  263. package/runtime/scripts/api/process/owned-process.mjs +0 -78
  264. package/runtime/scripts/api/process/stop-owned-process.mjs +0 -9
  265. package/runtime/scripts/connectors/lib.mjs +0 -488
  266. package/runtime/scripts/lib/clip.mjs +0 -19
  267. package/runtime/scripts/lib/display-names.mjs +0 -259
  268. package/runtime/scripts/lib/example-refs.mjs +0 -158
  269. package/runtime/scripts/lib/json-schema.mjs +0 -52
  270. package/runtime/scripts/lib/process-identity.mjs +0 -6
  271. package/runtime/scripts/lib/read-yaml.mjs +0 -18
  272. package/runtime/scripts/lib/redact.mjs +0 -161
  273. package/runtime/scripts/lib/sleep.mjs +0 -7
  274. package/runtime/scripts/lib/source-phrases.mjs +0 -34
  275. package/runtime/scripts/lib/sqlite.mjs +0 -21
@@ -2,9 +2,10 @@ import path from 'node:path';
2
2
  import { canonical, isInside } from './config.mjs';
3
3
  import { parseContract, installedSWR } from './next-data-contract.mjs';
4
4
  import { relativePath, unwrapExpression } from './typescript.mjs';
5
- import { anyDescendant, normalizedSymbol, normalizedSymbolValue, selectedNode, valueSymbol as sharedValueSymbol } from './ast-walks.mjs';
6
- import { sourceLocation } from '../../lib/ts-ast.mjs';
7
-
5
+ import { anyDescendant, normalizedSymbol, normalizedSymbolValue, valueSymbol as sharedValueSymbol } from './ast-walks.mjs';
6
+ import { isConstVariable, sourceLocation } from '../../lib/ts-ast.mjs';
7
+ import { byCodeUnit } from '../../lib/list.mjs';
8
+ import { createNextDataKeyInspector } from './next-data-key.mjs';
8
9
  export const SWR_KEY_RULE_ID = 'FE_SWR_KEY_IDENTITY';
9
10
  export const SWR_MUTATION_RULE_ID = 'FE_SWR_MUTATION_RESOURCE_IDENTITY';
10
11
  export const SWR_DATA_RULE_IDS = [SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID];
@@ -20,6 +21,47 @@ function declarationsInside(symbol, root) {
20
21
  return declarations.length > 0 && declarations.every(declaration => isInside(root, canonical(declaration.getSourceFile().fileName)));
21
22
  }
22
23
 
24
+ function selectedImportBindings(ts, statement, selected) {
25
+ if (!ts.isImportDeclaration(statement) || !statement.importClause) return 0;
26
+ const clause = statement.importClause;
27
+ let count = clause.name && selected.has('default') ? 1 : 0;
28
+ const bindings = clause.namedBindings;
29
+ if (bindings && ts.isNamespaceImport(bindings)) count += 1;
30
+ if (bindings && ts.isNamedImports(bindings)) count += bindings.elements
31
+ .filter(element => selected.has(element.propertyName?.text ?? element.name.text)).length;
32
+ return count;
33
+ }
34
+
35
+ function addOfficialExports(official, checker, statement, selected, targets) {
36
+ const { config, context, installed, reasons } = official;
37
+ const moduleSymbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
38
+ if (!moduleSymbol) {
39
+ reasons.push(`${relativePath(config.root, statement.getSourceFile().fileName)} cannot resolve ${statement.moduleSpecifier.text} through the target TypeScript program`);
40
+ return false;
41
+ }
42
+ const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
43
+ for (const [name, kind] of selected) {
44
+ const target = exports.get(name);
45
+ if (!target) continue;
46
+ if (!declarationsInside(target, installed.root)) {
47
+ reasons.push(`${relativePath(config.root, statement.getSourceFile().fileName)} resolves ${statement.moduleSpecifier.text}#${name} outside installed swr ${installed.version}`);
48
+ continue;
49
+ }
50
+ targets.set(target, kind);
51
+ }
52
+ return true;
53
+ }
54
+
55
+ function addOfficialStatement(official, wanted, checker, statement, targets) {
56
+ const ts = official.context.ts;
57
+ if (!ts.isImportDeclaration(statement) && !ts.isExportDeclaration(statement)) return 0;
58
+ if (!statement.moduleSpecifier || !ts.isStringLiteralLike(statement.moduleSpecifier)) return 0;
59
+ const selected = wanted.get(statement.moduleSpecifier.text);
60
+ if (!selected) return 0;
61
+ if (!addOfficialExports(official, checker, statement, selected, targets)) return 0;
62
+ return selectedImportBindings(ts, statement, selected);
63
+ }
64
+
23
65
  function officialTargets(config, context, installed, reasons) {
24
66
  const wanted = new Map([
25
67
  ['swr', new Map([['default', 'query'], ['mutate', 'global-mutate'], ['useSWRConfig', 'config']])],
@@ -27,38 +69,11 @@ function officialTargets(config, context, installed, reasons) {
27
69
  ['swr/mutation', new Map([['default', 'mutation'], ['useSWRMutation', 'mutation']])],
28
70
  ]);
29
71
  const targets = new Map();
72
+ const official = { config, context, installed, reasons };
30
73
  let selectedBindings = 0;
31
74
  for (const source of context.files) {
32
75
  const checker = context.checkerFor(source.fileName);
33
- for (const statement of source.statements) {
34
- if (!context.ts.isImportDeclaration(statement) && !context.ts.isExportDeclaration(statement)) continue;
35
- if (!statement.moduleSpecifier || !context.ts.isStringLiteralLike(statement.moduleSpecifier)) continue;
36
- const selected = wanted.get(statement.moduleSpecifier.text);
37
- if (!selected) continue;
38
- const moduleSymbol = checker.getSymbolAtLocation(statement.moduleSpecifier);
39
- if (!moduleSymbol) {
40
- reasons.push(`${relativePath(config.root, source.fileName)} cannot resolve ${statement.moduleSpecifier.text} through the target TypeScript program`);
41
- continue;
42
- }
43
- const exports = new Map(checker.getExportsOfModule(moduleSymbol).map(symbol => [symbol.getName(), normalizedSymbolValue(context.ts, checker, symbol)]));
44
- for (const [name, kind] of selected) {
45
- const target = exports.get(name);
46
- if (!target) continue;
47
- if (!declarationsInside(target, installed.root)) {
48
- reasons.push(`${relativePath(config.root, source.fileName)} resolves ${statement.moduleSpecifier.text}#${name} outside installed swr ${installed.version}`);
49
- continue;
50
- }
51
- targets.set(target, kind);
52
- }
53
- if (context.ts.isImportDeclaration(statement) && statement.importClause) {
54
- const clause = statement.importClause;
55
- if (clause.name && selected.has('default')) selectedBindings += 1;
56
- const bindings = clause.namedBindings;
57
- if (bindings && context.ts.isNamespaceImport(bindings)) selectedBindings += 1;
58
- if (bindings && context.ts.isNamedImports(bindings)) selectedBindings += bindings.elements
59
- .filter(element => selected.has(element.propertyName?.text ?? element.name.text)).length;
60
- }
61
- }
76
+ for (const statement of source.statements) selectedBindings += addOfficialStatement(official, wanted, checker, statement, targets);
62
77
  }
63
78
  return { targets, selectedBindings };
64
79
  }
@@ -95,21 +110,28 @@ function nearestFunction(ts, node) {
95
110
  return null;
96
111
  }
97
112
 
113
+ function configElementProperty(ts, element) {
114
+ return element.propertyName && ts.isIdentifier(element.propertyName) ? element.propertyName.text
115
+ : (ts.isIdentifier(element.name) && element.name.text) || null;
116
+ }
117
+
118
+ function addConfigMutateSymbols(ts, checker, node, targets, symbols) {
119
+ if (!(ts.isVariableDeclaration(node) && ts.isObjectBindingPattern(node.name) && node.initializer
120
+ && ts.isCallExpression(unwrapExpression(ts, node.initializer))
121
+ && callKind(ts, checker, unwrapExpression(ts, node.initializer), targets) === 'config')) return;
122
+ for (const element of node.name.elements) {
123
+ if (configElementProperty(ts, element) === 'mutate' && ts.isIdentifier(element.name)) symbols.add(normalizedSymbol(ts, checker, element.name));
124
+ }
125
+ }
126
+
98
127
  function configMutateSymbols(config, context, targets) {
128
+ const { ts } = context;
99
129
  const symbols = new Set();
100
130
  for (const source of context.files) {
101
131
  const checker = context.checkerFor(source.fileName);
102
132
  const visit = node => {
103
- if (context.ts.isVariableDeclaration(node) && context.ts.isObjectBindingPattern(node.name) && node.initializer
104
- && context.ts.isCallExpression(unwrapExpression(context.ts, node.initializer))
105
- && callKind(context.ts, checker, unwrapExpression(context.ts, node.initializer), targets) === 'config') {
106
- for (const element of node.name.elements) {
107
- const property = element.propertyName && context.ts.isIdentifier(element.propertyName) ? element.propertyName.text
108
- : context.ts.isIdentifier(element.name) ? element.name.text : null;
109
- if (property === 'mutate' && context.ts.isIdentifier(element.name)) symbols.add(normalizedSymbol(context.ts, checker, element.name));
110
- }
111
- }
112
- context.ts.forEachChild(node, visit);
133
+ addConfigMutateSymbols(ts, checker, node, targets, symbols);
134
+ ts.forEachChild(node, visit);
113
135
  };
114
136
  visit(source);
115
137
  }
@@ -161,25 +183,25 @@ function exportedHook(config, context, entry, reasons) {
161
183
  return { source, checker, fn: functions[0] };
162
184
  }
163
185
 
186
+ function addBindingName(ts, checker, found, name) {
187
+ if (ts.isIdentifier(name)) {
188
+ const symbol = normalizedSymbol(ts, checker, name);
189
+ if (!symbol) return;
190
+ if (!found.has(name.text)) found.set(name.text, new Set());
191
+ found.get(name.text).add(symbol);
192
+ return;
193
+ }
194
+ if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) for (const element of name.elements) {
195
+ if (element && !ts.isOmittedExpression(element)) addBindingName(ts, checker, found, element.name);
196
+ }
197
+ }
198
+
164
199
  function bindingSymbols(ts, checker, fn) {
165
200
  const found = new Map();
166
- const addName = name => {
167
- if (ts.isIdentifier(name)) {
168
- const symbol = normalizedSymbol(ts, checker, name);
169
- if (symbol) {
170
- if (!found.has(name.text)) found.set(name.text, new Set());
171
- found.get(name.text).add(symbol);
172
- }
173
- return;
174
- }
175
- if (ts.isObjectBindingPattern(name) || ts.isArrayBindingPattern(name)) for (const element of name.elements) {
176
- if (element && !ts.isOmittedExpression(element)) addName(element.name);
177
- }
178
- };
179
- for (const parameter of fn.parameters) addName(parameter.name);
201
+ for (const parameter of fn.parameters) addBindingName(ts, checker, found, parameter.name);
180
202
  const visit = node => {
181
203
  if (node !== fn && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
182
- if (ts.isVariableDeclaration(node)) addName(node.name);
204
+ if (ts.isVariableDeclaration(node)) addBindingName(ts, checker, found, node.name);
183
205
  ts.forEachChild(node, visit);
184
206
  };
185
207
  if (fn.body) visit(fn.body);
@@ -201,249 +223,26 @@ function resolvedIdentities(ts, checker, fn, entry, reasons) {
201
223
  return identities.length === entry.identities.length ? identities : null;
202
224
  }
203
225
 
204
- function accessPath(ts, checker, node) {
205
- node = unwrapExpression(ts, node);
206
- if (ts.isIdentifier(node)) return { symbol: normalizedSymbol(ts, checker, node), parts: [node.text] };
207
- if (ts.isPropertyAccessExpression(node)) {
208
- const base = accessPath(ts, checker, node.expression);
209
- return base ? { symbol: base.symbol, parts: [...base.parts, node.name.text] } : null;
210
- }
211
- if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
212
- const base = accessPath(ts, checker, node.expression);
213
- return base ? { symbol: base.symbol, parts: [...base.parts, node.argumentExpression.text] } : null;
214
- }
215
- return null;
216
- }
217
-
218
- function expandedAccessPath(ts, checker, node, seen = new Set(), depth = 0) {
219
- if (!node || depth > 10) return null;
220
- node = unwrapExpression(ts, node);
221
- if (ts.isIdentifier(node)) {
222
- const symbol = normalizedSymbol(ts, checker, node);
223
- if (!symbol || seen.has(symbol)) return symbol ? { symbol, parts: [node.text] } : null;
224
- const initializer = constInitializer(ts, symbol);
225
- return initializer ? expandedAccessPath(ts, checker, initializer, new Set(seen).add(symbol), depth + 1)
226
- ?? { symbol, parts: [node.text] } : { symbol, parts: [node.text] };
227
- }
228
- if (ts.isPropertyAccessExpression(node)) {
229
- const base = expandedAccessPath(ts, checker, node.expression, seen, depth + 1);
230
- return base ? { symbol: base.symbol, parts: [...base.parts, node.name.text] } : null;
231
- }
232
- if (ts.isElementAccessExpression(node) && ts.isStringLiteralLike(node.argumentExpression)) {
233
- const base = expandedAccessPath(ts, checker, node.expression, seen, depth + 1);
234
- return base ? { symbol: base.symbol, parts: [...base.parts, node.argumentExpression.text] } : null;
235
- }
236
- return null;
237
- }
238
-
239
- function matchesIdentityAccess(ts, checker, node, identity) {
240
- const matches = access => access?.symbol === identity.symbol && access.parts.length >= identity.parts.length
241
- && identity.parts.every((part, index) => access.parts[index] === part);
242
- return matches(accessPath(ts, checker, node)) || matches(expandedAccessPath(ts, checker, node));
243
- }
244
-
245
- const expressionReferences = (ts, checker, expression, identity) =>
246
- anyDescendant(ts, expression, (node) => matchesIdentityAccess(ts, checker, node, identity));
247
-
248
226
  function constInitializer(ts, symbol) {
249
227
  const declarations = symbol?.getDeclarations?.() ?? [];
250
228
  if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
251
- || !(ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return null;
229
+ || !isConstVariable(ts, declarations[0])) return null;
252
230
  return declarations[0].initializer;
253
231
  }
254
232
 
255
- function propertyKey(ts, name) {
256
- if (ts.isIdentifier(name) || ts.isStringLiteralLike(name) || ts.isNumericLiteral(name)) return name.text;
257
- return null;
258
- }
259
-
260
- function effectiveObjectValues(ts, expression) {
261
- const selected = new Map();
262
- for (let index = expression.properties.length - 1; index >= 0; index -= 1) {
263
- const property = expression.properties[index];
264
- if (ts.isSpreadAssignment(property) || ts.isMethodDeclaration(property) || ts.isGetAccessorDeclaration(property)
265
- || ts.isSetAccessorDeclaration(property)) return null;
266
- const key = propertyKey(ts, property.name);
267
- if (key === null) return null;
268
- if (!selected.has(key)) selected.set(key, ts.isShorthandPropertyAssignment(property) ? property.name : property.initializer);
269
- }
270
- return [...selected.entries()].sort(([left], [right]) => left.localeCompare(right));
271
- }
272
-
273
- function identityContribution(ts, checker, expression, identity, seen = new Set(), depth = 0) {
274
- if (!expression || depth > 12) return 'unproven';
275
- expression = unwrapExpression(ts, expression);
276
- if (matchesIdentityAccess(ts, checker, expression, identity)) return 'yes';
277
- if (ts.isIdentifier(expression)) {
278
- const symbol = normalizedSymbol(ts, checker, expression);
279
- if (!symbol || seen.has(symbol)) return 'no';
280
- const initializer = constInitializer(ts, symbol);
281
- return initializer ? identityContribution(ts, checker, initializer, identity, new Set(seen).add(symbol), depth + 1) : 'no';
282
- }
283
- if (ts.isConditionalExpression(expression)) {
284
- const whenTrue = identityContribution(ts, checker, expression.whenTrue, identity, seen, depth + 1);
285
- const whenFalse = identityContribution(ts, checker, expression.whenFalse, identity, seen, depth + 1);
286
- if (whenTrue === 'yes' && whenFalse === 'yes') return 'yes';
287
- if (whenTrue === 'unproven' || whenFalse === 'unproven') return 'unproven';
288
- return 'no';
289
- }
290
- if (ts.isBinaryExpression(expression)) {
291
- if (expression.operatorToken.kind === ts.SyntaxKind.CommaToken) return identityContribution(ts, checker, expression.right, identity, seen, depth + 1);
292
- if (expression.operatorToken.kind !== ts.SyntaxKind.QuestionQuestionToken) {
293
- return expressionReferences(ts, checker, expression, identity) ? 'unproven' : 'no';
294
- }
295
- const left = identityContribution(ts, checker, expression.left, identity, seen, depth + 1);
296
- const right = identityContribution(ts, checker, expression.right, identity, seen, depth + 1);
297
- return left === 'yes' || right === 'yes' ? 'yes' : left === 'unproven' || right === 'unproven' ? 'unproven' : 'no';
298
- }
299
- if (ts.isPrefixUnaryExpression(expression)) return expressionReferences(ts, checker, expression, identity) ? 'unproven' : 'no';
300
- if (ts.isTemplateExpression(expression)) {
301
- const values = expression.templateSpans.map(span => identityContribution(ts, checker, span.expression, identity, seen, depth + 1));
302
- return values.includes('yes') ? 'yes' : values.includes('unproven') ? 'unproven' : 'no';
303
- }
304
- if (ts.isArrayLiteralExpression(expression)) {
305
- if (expression.elements.some(element => ts.isSpreadElement(element))) return 'unproven';
306
- const values = expression.elements.map(element => identityContribution(ts, checker, element, identity, seen, depth + 1));
307
- return values.includes('yes') ? 'yes' : values.includes('unproven') ? 'unproven' : 'no';
308
- }
309
- if (ts.isObjectLiteralExpression(expression)) {
310
- const values = effectiveObjectValues(ts, expression);
311
- if (!values || values.length !== expression.properties.length) return 'unproven';
312
- const states = values.map(([, value]) => identityContribution(ts, checker, value, identity, seen, depth + 1));
313
- return states.includes('yes') ? 'yes' : states.includes('unproven') ? 'unproven' : 'no';
314
- }
315
- if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) return 'no';
316
- if (ts.isCallExpression(expression) || ts.isNewExpression(expression) || ts.isAwaitExpression(expression)) return 'unproven';
317
- return 'no';
318
- }
319
-
320
- function returnedExpression(ts, fn) {
321
- if (ts.isArrowFunction(fn) && !ts.isBlock(fn.body)) return fn.body;
322
- if (!fn.body || !ts.isBlock(fn.body)) return null;
323
- const returns = [];
324
- const visit = node => {
325
- if (node !== fn.body && (ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) || ts.isMethodDeclaration(node))) return;
326
- if (ts.isReturnStatement(node) && node.expression) returns.push(node.expression);
327
- else ts.forEachChild(node, visit);
328
- };
329
- visit(fn.body);
330
- return returns.length === 1 ? returns[0] : null;
331
- }
332
-
333
- function rootKeyExpression(ts, checker, expression, identities, seen = new Set(), depth = 0) {
334
- if (!expression || depth > 10) return null;
335
- expression = unwrapExpression(ts, expression);
336
- if (ts.isArrowFunction(expression) || ts.isFunctionExpression(expression)) {
337
- const returned = returnedExpression(ts, expression);
338
- return returned ? rootKeyExpression(ts, checker, returned, identities, seen, depth + 1) : null;
339
- }
340
- if (ts.isIdentifier(expression) && !identities.some(identity => identity.symbol === normalizedSymbol(ts, checker, expression))) {
341
- const symbol = normalizedSymbol(ts, checker, expression);
342
- if (symbol && !seen.has(symbol)) {
343
- const declarations = symbol.getDeclarations?.() ?? [];
344
- if (declarations.length === 1 && ts.isVariableDeclaration(declarations[0]) && declarations[0].initializer
345
- && (ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) {
346
- return rootKeyExpression(ts, checker, declarations[0].initializer, identities, new Set(seen).add(symbol), depth + 1);
347
- }
348
- }
349
- }
350
- return expression;
351
- }
352
-
353
- function staticKeyExpression(ts, checker, expression, identities, seen = new Set(), depth = 0) {
354
- if (!expression || depth > 12) return false;
355
- expression = unwrapExpression(ts, expression);
356
- if (expression.kind === ts.SyntaxKind.NullKeyword || expression.kind === ts.SyntaxKind.TrueKeyword
357
- || expression.kind === ts.SyntaxKind.FalseKeyword || ts.isStringLiteralLike(expression)
358
- || ts.isNumericLiteral(expression) || ts.isBigIntLiteral(expression)) return true;
359
- if (ts.isNoSubstitutionTemplateLiteral(expression)) return true;
360
- if (ts.isTemplateExpression(expression)) return expression.templateSpans.every(span => staticKeyExpression(ts, checker, span.expression, identities, seen, depth + 1));
361
- if (ts.isArrayLiteralExpression(expression)) return expression.elements.every(element => !ts.isSpreadElement(element)
362
- && staticKeyExpression(ts, checker, element, identities, seen, depth + 1));
363
- if (ts.isObjectLiteralExpression(expression)) {
364
- const values = effectiveObjectValues(ts, expression);
365
- return values !== null && values.length === expression.properties.length
366
- && values.every(([, value]) => staticKeyExpression(ts, checker, value, identities, seen, depth + 1));
367
- }
368
- if (ts.isConditionalExpression(expression)) return staticKeyExpression(ts, checker, expression.condition, identities, seen, depth + 1)
369
- && staticKeyExpression(ts, checker, expression.whenTrue, identities, seen, depth + 1)
370
- && staticKeyExpression(ts, checker, expression.whenFalse, identities, seen, depth + 1);
371
- if (ts.isBinaryExpression(expression)) return staticKeyExpression(ts, checker, expression.left, identities, seen, depth + 1)
372
- && staticKeyExpression(ts, checker, expression.right, identities, seen, depth + 1);
373
- if (ts.isPrefixUnaryExpression(expression)) return staticKeyExpression(ts, checker, expression.operand, identities, seen, depth + 1);
374
- if (ts.isPropertyAccessExpression(expression) || ts.isElementAccessExpression(expression)) {
375
- if (identities.some(identity => expressionReferences(ts, checker, expression, identity))) return true;
376
- return false;
377
- }
378
- if (ts.isIdentifier(expression)) {
379
- if (expression.text === 'undefined' || identities.some(identity => identity.symbol === normalizedSymbol(ts, checker, expression))) return true;
380
- const symbol = normalizedSymbol(ts, checker, expression);
381
- if (!symbol || seen.has(symbol)) return false;
382
- const declarations = symbol.getDeclarations?.() ?? [];
383
- if (declarations.length !== 1 || !ts.isVariableDeclaration(declarations[0]) || !declarations[0].initializer
384
- || !(ts.getCombinedNodeFlags(declarations[0].parent) & ts.NodeFlags.Const)) return false;
385
- return staticKeyExpression(ts, checker, declarations[0].initializer, identities, new Set(seen).add(symbol), depth + 1);
386
- }
387
- return false;
388
- }
389
-
390
- function keyLeaves(ts, checker, expression, identities, decisions = [], depth = 0) {
391
- if (depth > 12) return null;
392
- expression = rootKeyExpression(ts, checker, expression, identities);
393
- if (!expression) return null;
394
- if (ts.isConditionalExpression(expression)) {
395
- if (!staticKeyExpression(ts, checker, expression.condition, identities)) return null;
396
- const whenTrue = keyLeaves(ts, checker, expression.whenTrue, identities, [...decisions, { condition: expression.condition, branch: true }], depth + 1);
397
- const whenFalse = keyLeaves(ts, checker, expression.whenFalse, identities, [...decisions, { condition: expression.condition, branch: false }], depth + 1);
398
- return whenTrue && whenFalse ? [...whenTrue, ...whenFalse] : null;
399
- }
400
- if (expression.kind === ts.SyntaxKind.NullKeyword) return [{ kind: 'null', expression, decisions }];
401
- return staticKeyExpression(ts, checker, expression, identities) ? [{ kind: 'active', expression, decisions }] : null;
402
- }
403
-
404
- function nullish(ts, checker, expression) {
405
- expression = unwrapExpression(ts, expression);
406
- if (expression.kind === ts.SyntaxKind.NullKeyword) return true;
407
- if (!ts.isIdentifier(expression) || expression.text !== 'undefined') return false;
408
- const symbol = checker.getSymbolAtLocation(expression);
409
- return !symbol || (symbol.getDeclarations?.() ?? []).every(declaration => declaration.getSourceFile().isDeclarationFile);
410
- }
411
-
412
- /** True when taking `branch` of `condition` proves the declared identity is available. */
413
- function branchMeansAvailable(ts, checker, condition, identity, branch) {
414
- condition = unwrapExpression(ts, condition);
415
- if (expressionReferences(ts, checker, condition, identity) && (ts.isIdentifier(condition)
416
- || ts.isPropertyAccessExpression(condition) || ts.isElementAccessExpression(condition))) return branch;
417
- if (ts.isPrefixUnaryExpression(condition) && condition.operator === ts.SyntaxKind.ExclamationToken) {
418
- return branchMeansAvailable(ts, checker, condition.operand, identity, !branch);
419
- }
420
- if (ts.isBinaryExpression(condition)) {
421
- const operator = condition.operatorToken.kind;
422
- if (operator === ts.SyntaxKind.AmpersandAmpersandToken || operator === ts.SyntaxKind.BarBarToken) {
423
- // `a && b` is true only when both sides are true; `a || b` is false only when both sides are false.
424
- const decisive = operator === ts.SyntaxKind.AmpersandAmpersandToken ? branch : !branch;
425
- return decisive && [condition.left, condition.right]
426
- .some(side => branchMeansAvailable(ts, checker, side, identity, branch));
427
- }
428
- const leftIdentity = expressionReferences(ts, checker, condition.left, identity) && nullish(ts, checker, condition.right);
429
- const rightIdentity = expressionReferences(ts, checker, condition.right, identity) && nullish(ts, checker, condition.left);
430
- if (!leftIdentity && !rightIdentity) return false;
431
- if ([ts.SyntaxKind.ExclamationEqualsToken, ts.SyntaxKind.ExclamationEqualsEqualsToken].includes(operator)) return branch;
432
- if ([ts.SyntaxKind.EqualsEqualsToken, ts.SyntaxKind.EqualsEqualsEqualsToken].includes(operator)) return !branch;
433
- }
434
- return false;
435
- }
436
-
437
233
  function violation(config, call, ruleId, message, extra = {}) {
438
234
  return { ruleId, path: relativePath(config.root, call.source.fileName), ...sourceLocation(call.source, call.node), message, ...extra };
439
235
  }
440
236
 
237
+ const inspectKey = createNextDataKeyInspector({ anyDescendant, normalizedSymbol, unwrapExpression, constInitializer, sourceLocation, violation,
238
+ SWR_KEY_RULE_ID, SWR_MUTATION_RULE_ID });
239
+
441
240
  function isUnshadowedCommonJsRequire(ts, checker, expression, seen = new Set(), depth = 0) {
442
241
  expression = unwrapExpression(ts, expression);
443
242
  if (!ts.isIdentifier(expression) || depth > 10) return false;
444
243
  const symbol = checker.getSymbolAtLocation(expression);
445
244
  const declarations = symbol?.getDeclarations?.() ?? [];
446
- if (expression.text === 'require' && (!symbol || declarations.length === 0
245
+ if (expression.text === 'require' && (!symbol
447
246
  || declarations.every(declaration => declaration.getSourceFile().isDeclarationFile))) return true;
448
247
  const normalized = symbolOfRequireAlias(ts, checker, expression);
449
248
  if (!normalized || seen.has(normalized)) return false;
@@ -456,145 +255,59 @@ function symbolOfRequireAlias(ts, checker, expression) {
456
255
  return normalizedSymbolValue(ts, checker, symbol);
457
256
  }
458
257
 
258
+ function isSwrModuleDeclaration(ts, node) {
259
+ return (ts.isImportDeclaration(node) || ts.isExportDeclaration(node)) && node.moduleSpecifier
260
+ && ts.isStringLiteralLike(node.moduleSpecifier) && SWR_SPECIFIERS.has(node.moduleSpecifier.text);
261
+ }
262
+
263
+ /** The kind of unsupported SWR binding the node declares, else null. */
264
+ function unsupportedSwrBinding(ts, checker, node) {
265
+ if (ts.isImportEqualsDeclaration(node) && ts.isExternalModuleReference(node.moduleReference)
266
+ && node.moduleReference.expression && ts.isStringLiteralLike(node.moduleReference.expression)
267
+ && SWR_SPECIFIERS.has(node.moduleReference.expression.text)) return 'import-equals binding';
268
+ if (ts.isCallExpression(node) && node.arguments.length >= 1 && ts.isStringLiteralLike(node.arguments[0])
269
+ && SWR_SPECIFIERS.has(node.arguments[0].text)) {
270
+ if (node.expression.kind === ts.SyntaxKind.ImportKeyword) return 'dynamic import binding';
271
+ if (isUnshadowedCommonJsRequire(ts, checker, node.expression)) return 'CommonJS require binding';
272
+ }
273
+ return null;
274
+ }
275
+
459
276
  function swrReferences(config, context) {
277
+ const { ts } = context;
460
278
  let found = false;
461
279
  const unsupported = [];
462
280
  for (const source of context.files) {
463
281
  const checker = context.checkerFor(source.fileName);
464
- const report = (node, kind) => {
465
- found = true;
466
- unsupported.push(`${relativePath(config.root, source.fileName)}:${sourceLocation(source, node).line} uses unsupported ${kind} for SWR`);
467
- };
468
282
  const visit = node => {
469
- if ((context.ts.isImportDeclaration(node) || context.ts.isExportDeclaration(node)) && node.moduleSpecifier
470
- && context.ts.isStringLiteralLike(node.moduleSpecifier) && SWR_SPECIFIERS.has(node.moduleSpecifier.text)) found = true;
471
- if (context.ts.isImportEqualsDeclaration(node) && context.ts.isExternalModuleReference(node.moduleReference)
472
- && node.moduleReference.expression && context.ts.isStringLiteralLike(node.moduleReference.expression)
473
- && SWR_SPECIFIERS.has(node.moduleReference.expression.text)) report(node, 'import-equals binding');
474
- if (context.ts.isCallExpression(node) && node.arguments.length >= 1 && context.ts.isStringLiteralLike(node.arguments[0])
475
- && SWR_SPECIFIERS.has(node.arguments[0].text)) {
476
- if (node.expression.kind === context.ts.SyntaxKind.ImportKeyword) report(node, 'dynamic import binding');
477
- else if (isUnshadowedCommonJsRequire(context.ts, checker, node.expression)) report(node, 'CommonJS require binding');
283
+ if (isSwrModuleDeclaration(ts, node)) found = true;
284
+ const kind = unsupportedSwrBinding(ts, checker, node);
285
+ if (kind) {
286
+ found = true;
287
+ unsupported.push(`${relativePath(config.root, source.fileName)}:${sourceLocation(source, node).line} uses unsupported ${kind} for SWR`);
478
288
  }
479
- context.ts.forEachChild(node, visit);
289
+ ts.forEachChild(node, visit);
480
290
  };
481
291
  visit(source);
482
292
  }
483
293
  return { found, unsupported };
484
294
  }
485
295
 
486
- function nonNullFalsy(ts, expression) {
487
- expression = unwrapExpression(ts, expression);
488
- return expression.kind === ts.SyntaxKind.FalseKeyword || ts.isStringLiteralLike(expression) && expression.text === ''
489
- || ts.isNumericLiteral(expression) && Number(expression.text) === 0
490
- || ts.isIdentifier(expression) && expression.text === 'undefined';
491
- }
492
-
493
- function containerSymbolsFromKey(ts, checker, expression, found = new Set(), seen = new Set(), depth = 0) {
494
- if (!expression || depth > 12) return found;
495
- expression = unwrapExpression(ts, expression);
496
- if (ts.isIdentifier(expression)) {
497
- const symbol = normalizedSymbol(ts, checker, expression);
498
- if (!symbol || seen.has(symbol)) return found;
499
- const initializer = constInitializer(ts, symbol);
500
- if (!initializer) return found;
501
- const value = unwrapExpression(ts, initializer);
502
- if (ts.isArrayLiteralExpression(value) || ts.isObjectLiteralExpression(value)) found.add(symbol);
503
- containerSymbolsFromKey(ts, checker, initializer, found, new Set(seen).add(symbol), depth + 1);
504
- return found;
505
- }
506
- ts.forEachChild(expression, child => { containerSymbolsFromKey(ts, checker, child, found, seen, depth + 1); });
507
- return found;
508
- }
509
-
510
- function keyContainerRisks(ts, checker, key, owner) {
511
- const symbols = containerSymbolsFromKey(ts, checker, key);
512
- if (!symbols.size || !owner?.body) return [];
513
- const declarationNames = new Set([...symbols].flatMap(symbol => (symbol.getDeclarations?.() ?? [])
514
- .filter(ts.isVariableDeclaration).map(declaration => declaration.name)));
515
- const risks = [];
516
- const visit = node => {
517
- if (ts.isIdentifier(node) && symbols.has(normalizedSymbol(ts, checker, node)) && !declarationNames.has(node)
518
- && !(node.pos >= key.pos && node.end <= key.end)) risks.push(node);
519
- ts.forEachChild(node, visit);
520
- };
521
- visit(owner.body);
522
- return risks;
523
- }
524
-
525
- function inspectKey(config, context, entry, call, identities, violations, reasons) {
526
- const key = call.node.arguments[0];
527
- if (!key) {
528
- reasons.push(`${entry.path}#${entry.export} has an SWR call without a key`);
529
- return;
530
- }
531
- const containerRisks = keyContainerRisks(context.ts, call.checker, key, call.owner);
532
- if (containerRisks.length) {
533
- const locations = containerRisks.map(node => sourceLocation(call.source, node).line).filter((line, index, all) => all.indexOf(line) === index);
534
- reasons.push(`${entry.path}#${entry.export} key container is referenced outside its declaration and selected SWR key${locations.length ? ` (line${locations.length === 1 ? '' : 's'} ${locations.join(', ')})` : ''}`);
535
- return;
536
- }
537
- const leaves = keyLeaves(context.ts, call.checker, key, identities);
538
- if (!leaves) {
539
- reasons.push(`${entry.path}#${entry.export} uses a dynamic SWR key whose identity cannot be proved`);
540
- return;
541
- }
542
- const active = leaves.filter(leaf => leaf.kind === 'active');
543
- if (active.some(leaf => nonNullFalsy(context.ts, leaf.expression))) violations.push(violation(config, call, SWR_KEY_RULE_ID,
544
- `${entry.id} uses explicit null, rather than another falsy value, for a disabled SWR key.`, { lifecycle: entry.id }));
545
- for (const identity of identities) {
546
- if (!active.length) {
547
- violations.push(violation(config, call, entry.kind === 'mutation' && identity.resource ? SWR_MUTATION_RULE_ID : SWR_KEY_RULE_ID,
548
- `${entry.id} has no active key path carrying declared identity ${identity.id} (${identity.binding}).`,
549
- { lifecycle: entry.id, identity: identity.id }));
550
- continue;
551
- }
552
- const contributions = active.map(leaf => identityContribution(context.ts, call.checker, leaf.expression, identity));
553
- if (contributions.includes('unproven')) {
554
- reasons.push(`${entry.path}#${entry.export} key value contribution for identity ${identity.id} (${identity.binding}) cannot be proved on every active path`);
555
- } else if (contributions.includes('no')) {
556
- const ruleId = entry.kind === 'mutation' && identity.resource ? SWR_MUTATION_RULE_ID : SWR_KEY_RULE_ID;
557
- violations.push(violation(config, call, ruleId,
558
- `${entry.id} key must include declared ${identity.resource ? 'resource ' : ''}identity ${identity.id} (${identity.binding}) on every active key path.`,
559
- { lifecycle: entry.id, identity: identity.id }));
560
- }
561
- if (identity.gatesRequest) {
562
- const gated = active.every(leaf => leaf.decisions.some(decision => {
563
- return branchMeansAvailable(context.ts, call.checker, decision.condition, identity, decision.branch);
564
- }));
565
- if (!gated) violations.push(violation(config, call, SWR_KEY_RULE_ID,
566
- `${entry.id} must produce an explicit null key when ${identity.id} (${identity.binding}) is unavailable.`,
567
- { lifecycle: entry.id, identity: identity.id }));
568
- }
569
- }
296
+ function unavailableDataLifecycle(ruleIds, details) {
297
+ return { violations: [], coverage: { status: 'unavailable', ruleIds, details } };
570
298
  }
571
299
 
572
- /** Check explicitly declared SWR lifecycle keys against the target program and installed SWR identity. */
573
- export function checkFrontendDataLifecycle(config, context) {
574
- const ruleIds = [...SWR_DATA_RULE_IDS];
575
- const reasons = [];
576
- const violations = [];
577
- const references = swrReferences(config, context);
578
- let contract;
579
- try { contract = parseContract(config); } catch (error) {
580
- return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
581
- }
582
- if (!contract && !references.found) return { violations, coverage: { status: 'not-applicable', ruleIds } };
583
- if (!contract) return { violations, coverage: { status: 'unavailable', ruleIds,
584
- details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
585
- reasons.push(...references.unsupported);
586
- let installed;
587
- try { installed = installedSWR(config.packageRoot ?? config.root); } catch (error) {
588
- return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
589
- }
590
- if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
300
+ function lifecycleCallSets(config, context, installed, reasons) {
591
301
  const official = officialTargets(config, context, installed, reasons);
592
302
  const calls = discoverCalls(config, context, official.targets);
593
303
  const lifecycleCalls = calls.filter(call => call.kind === 'query' || call.kind === 'mutation');
594
304
  const globalMutates = calls.filter(call => call.kind === 'global-mutate');
595
305
  if (official.selectedBindings && lifecycleCalls.length === 0) reasons.push('selected SWR hook imports have no statically resolved lifecycle call');
596
306
  for (const call of globalMutates) reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} uses global mutate; resource matching or filter semantics are not statically declared`);
307
+ return { lifecycleCalls };
308
+ }
597
309
 
310
+ function hookEntries(config, context, contract, reasons) {
598
311
  const hookCache = new Map();
599
312
  const entriesByHook = new Map();
600
313
  for (const entry of contract.hooks) {
@@ -603,7 +316,32 @@ export function checkFrontendDataLifecycle(config, context) {
603
316
  entriesByHook.get(key).push(entry);
604
317
  if (!hookCache.has(key)) hookCache.set(key, exportedHook(config, context, entry, reasons));
605
318
  }
319
+ return { hookCache, entriesByHook };
320
+ }
321
+
322
+ function inspectLifecycleEntry(scope, entry, hookCalls, mapped) {
323
+ const { config, context, violations, reasons } = scope;
324
+ const { entries, owned, hook } = hookCalls;
325
+ let candidates;
326
+ if (entry.resultBinding !== undefined) candidates = owned.filter(call => call.bindings.includes(entry.resultBinding));
327
+ else candidates = owned.length === 1 && entries.length === 1 ? owned : [];
328
+ if (candidates.length !== 1) {
329
+ reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} must select exactly one SWR call${owned.length > 1 ? ' with resultBinding' : ''}`);
330
+ return;
331
+ }
332
+ const call = candidates[0];
333
+ mapped.set(call, (mapped.get(call) ?? 0) + 1);
334
+ if (call.kind !== entry.kind) {
335
+ reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} declares ${entry.kind} but resolves ${call.kind}`);
336
+ return;
337
+ }
338
+ const identities = resolvedIdentities(context.ts, hook.checker, hook.fn, entry, reasons);
339
+ if (identities) inspectKey(config, context, entry, call, identities, violations, reasons);
340
+ }
341
+
342
+ function inspectHookLifecycle(config, context, lifecycleCalls, hookCache, entriesByHook, violations, reasons) {
606
343
  const mapped = new Map();
344
+ const scope = { config, context, violations, reasons };
607
345
  for (const [key, entries] of entriesByHook) {
608
346
  const hook = hookCache.get(key);
609
347
  if (!hook) continue;
@@ -612,37 +350,51 @@ export function checkFrontendDataLifecycle(config, context) {
612
350
  reasons.push(`${entries[0].path}#${entries[0].export} contains no statically resolved SWR lifecycle call`);
613
351
  continue;
614
352
  }
615
- for (const entry of entries) {
616
- let candidates;
617
- if (entry.resultBinding !== undefined) candidates = owned.filter(call => call.bindings.includes(entry.resultBinding));
618
- else candidates = owned.length === 1 && entries.length === 1 ? owned : [];
619
- if (candidates.length !== 1) {
620
- reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} must select exactly one SWR call${owned.length > 1 ? ' with resultBinding' : ''}`);
621
- continue;
622
- }
623
- const call = candidates[0];
624
- mapped.set(call, (mapped.get(call) ?? 0) + 1);
625
- if (call.kind !== entry.kind) {
626
- reasons.push(`${entry.path}#${entry.export} lifecycle ${entry.id} declares ${entry.kind} but resolves ${call.kind}`);
627
- continue;
628
- }
629
- const identities = resolvedIdentities(context.ts, hook.checker, hook.fn, entry, reasons);
630
- if (identities) inspectKey(config, context, entry, call, identities, violations, reasons);
631
- }
353
+ for (const entry of entries) inspectLifecycleEntry(scope, entry, { entries, owned, hook }, mapped);
632
354
  for (const call of owned) if ((mapped.get(call) ?? 0) !== 1) {
633
355
  reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} SWR call is not matched by exactly one declared lifecycle`);
634
356
  }
635
357
  }
358
+ return mapped;
359
+ }
360
+
361
+ function addUndeclaredLifecycleReasons(config, lifecycleCalls, hookCache, reasons) {
636
362
  const declaredFunctions = new Set([...hookCache.values()].filter(Boolean).map(item => item.fn));
637
363
  for (const call of lifecycleCalls) if (!declaredFunctions.has(call.owner)) {
638
364
  reasons.push(`${relativePath(config.root, call.source.fileName)}:${sourceLocation(call.source, call.node).line} SWR call is outside every declared lifecycle hook`);
639
365
  }
640
- const details = [...new Set(reasons)].sort();
641
- return {
642
- violations,
643
- coverage: details.length ? { status: 'unavailable', ruleIds, hooks: contract.hooks.length, calls: lifecycleCalls.length,
644
- swr: { name: installed.name, version: installed.version, major: installed.major }, details }
645
- : { status: 'checked', ruleIds, hooks: contract.hooks.length, calls: lifecycleCalls.length,
646
- swr: { name: installed.name, version: installed.version, major: installed.major } },
647
- };
366
+ }
367
+
368
+ function lifecycleCoverage(ruleIds, contract, installed, calls, details) {
369
+ return details.length ? { status: 'unavailable', ruleIds, hooks: contract.hooks.length, calls,
370
+ swr: { name: installed.name, version: installed.version, major: installed.major }, details }
371
+ : { status: 'checked', ruleIds, hooks: contract.hooks.length, calls,
372
+ swr: { name: installed.name, version: installed.version, major: installed.major } };
373
+ }
374
+
375
+ /** Check explicitly declared SWR lifecycle keys against the target program and installed SWR identity. */
376
+ export function checkFrontendDataLifecycle(config, context) {
377
+ const ruleIds = [...SWR_DATA_RULE_IDS];
378
+ const references = swrReferences(config, context);
379
+ let contract;
380
+ try { contract = parseContract(config); } catch (error) {
381
+ return unavailableDataLifecycle(ruleIds, [String(error.message ?? error)]);
382
+ }
383
+ if (!contract && !references.found) return { violations: [], coverage: { status: 'not-applicable', ruleIds } };
384
+ if (!contract) return { violations: [], coverage: { status: 'unavailable', ruleIds,
385
+ details: ['package.json#starci.codePatterns.next.dataLifecycle is required when production source selects SWR', ...references.unsupported] } };
386
+ const reasons = [];
387
+ const violations = [];
388
+ reasons.push(...references.unsupported);
389
+ let installed;
390
+ try { installed = installedSWR(config.packageRoot ?? config.root); } catch (error) {
391
+ return { violations, coverage: { status: 'unavailable', ruleIds, details: [String(error.message ?? error)] } };
392
+ }
393
+ if (installed.major !== contract.swr.major) reasons.push(`installed swr ${installed.version} does not match declared major ${contract.swr.major}`);
394
+ const { lifecycleCalls } = lifecycleCallSets(config, context, installed, reasons);
395
+ const { hookCache, entriesByHook } = hookEntries(config, context, contract, reasons);
396
+ inspectHookLifecycle(config, context, lifecycleCalls, hookCache, entriesByHook, violations, reasons);
397
+ addUndeclaredLifecycleReasons(config, lifecycleCalls, hookCache, reasons);
398
+ const details = [...new Set(reasons)].sort(byCodeUnit);
399
+ return { violations, coverage: lifecycleCoverage(ruleIds, contract, installed, lifecycleCalls.length, details) };
648
400
  }