@endora-commerce/cli 0.100.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.
- package/LICENSE +21 -0
- package/README.md +37 -0
- package/dist/bin/endora.d.ts +28 -0
- package/dist/bin/endora.d.ts.map +1 -0
- package/dist/bin/endora.js +926 -0
- package/dist/bin/endora.js.map +1 -0
- package/dist/check/estate.d.ts +189 -0
- package/dist/check/estate.d.ts.map +1 -0
- package/dist/check/estate.js +1037 -0
- package/dist/check/estate.js.map +1 -0
- package/dist/check/hosts.d.ts +25 -0
- package/dist/check/hosts.d.ts.map +1 -0
- package/dist/check/hosts.js +1347 -0
- package/dist/check/hosts.js.map +1 -0
- package/dist/check/index.d.ts +60 -0
- package/dist/check/index.d.ts.map +1 -0
- package/dist/check/index.js +111 -0
- package/dist/check/index.js.map +1 -0
- package/dist/check/layout.d.ts +136 -0
- package/dist/check/layout.d.ts.map +1 -0
- package/dist/check/layout.js +262 -0
- package/dist/check/layout.js.map +1 -0
- package/dist/check/ledger.d.ts +98 -0
- package/dist/check/ledger.d.ts.map +1 -0
- package/dist/check/ledger.js +173 -0
- package/dist/check/ledger.js.map +1 -0
- package/dist/check/peer-owners.d.ts +116 -0
- package/dist/check/peer-owners.d.ts.map +1 -0
- package/dist/check/peer-owners.js +225 -0
- package/dist/check/peer-owners.js.map +1 -0
- package/dist/check/report.d.ts +33 -0
- package/dist/check/report.d.ts.map +1 -0
- package/dist/check/report.js +107 -0
- package/dist/check/report.js.map +1 -0
- package/dist/check/run.d.ts +147 -0
- package/dist/check/run.d.ts.map +1 -0
- package/dist/check/run.js +111 -0
- package/dist/check/run.js.map +1 -0
- package/dist/checks.d.ts +17 -0
- package/dist/checks.d.ts.map +1 -0
- package/dist/checks.js +17 -0
- package/dist/checks.js.map +1 -0
- package/dist/dev/index.d.ts +83 -0
- package/dist/dev/index.d.ts.map +1 -0
- package/dist/dev/index.js +298 -0
- package/dist/dev/index.js.map +1 -0
- package/dist/generate/divergence.d.ts +38 -0
- package/dist/generate/divergence.d.ts.map +1 -0
- package/dist/generate/divergence.js +237 -0
- package/dist/generate/divergence.js.map +1 -0
- package/dist/generate/index.d.ts +90 -0
- package/dist/generate/index.d.ts.map +1 -0
- package/dist/generate/index.js +369 -0
- package/dist/generate/index.js.map +1 -0
- package/dist/index.d.ts +23 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +35 -0
- package/dist/index.js.map +1 -0
- package/dist/inputs/declaration.d.ts +18 -0
- package/dist/inputs/declaration.d.ts.map +1 -0
- package/dist/inputs/declaration.js +64 -0
- package/dist/inputs/declaration.js.map +1 -0
- package/dist/inputs/env-file.d.ts +73 -0
- package/dist/inputs/env-file.d.ts.map +1 -0
- package/dist/inputs/env-file.js +134 -0
- package/dist/inputs/env-file.js.map +1 -0
- package/dist/inputs/prompt.d.ts +21 -0
- package/dist/inputs/prompt.d.ts.map +1 -0
- package/dist/inputs/prompt.js +59 -0
- package/dist/inputs/prompt.js.map +1 -0
- package/dist/inputs/resolve.d.ts +163 -0
- package/dist/inputs/resolve.d.ts.map +1 -0
- package/dist/inputs/resolve.js +290 -0
- package/dist/inputs/resolve.js.map +1 -0
- package/dist/install/host.d.ts +27 -0
- package/dist/install/host.d.ts.map +1 -0
- package/dist/install/host.js +90 -0
- package/dist/install/host.js.map +1 -0
- package/dist/install/index.d.ts +173 -0
- package/dist/install/index.d.ts.map +1 -0
- package/dist/install/index.js +793 -0
- package/dist/install/index.js.map +1 -0
- package/dist/install/wizard.d.ts +144 -0
- package/dist/install/wizard.d.ts.map +1 -0
- package/dist/install/wizard.js +362 -0
- package/dist/install/wizard.js.map +1 -0
- package/dist/lib/admin-artefacts.d.ts +70 -0
- package/dist/lib/admin-artefacts.d.ts.map +1 -0
- package/dist/lib/admin-artefacts.js +354 -0
- package/dist/lib/admin-artefacts.js.map +1 -0
- package/dist/lib/admin-surfaces.d.ts +298 -0
- package/dist/lib/admin-surfaces.d.ts.map +1 -0
- package/dist/lib/admin-surfaces.js +669 -0
- package/dist/lib/admin-surfaces.js.map +1 -0
- package/dist/lib/delegated-composer.d.ts +85 -0
- package/dist/lib/delegated-composer.d.ts.map +1 -0
- package/dist/lib/delegated-composer.js +241 -0
- package/dist/lib/delegated-composer.js.map +1 -0
- package/dist/lib/divergence-artefacts.d.ts +305 -0
- package/dist/lib/divergence-artefacts.d.ts.map +1 -0
- package/dist/lib/divergence-artefacts.js +828 -0
- package/dist/lib/divergence-artefacts.js.map +1 -0
- package/dist/lib/divergence.d.ts +337 -0
- package/dist/lib/divergence.d.ts.map +1 -0
- package/dist/lib/divergence.js +1005 -0
- package/dist/lib/divergence.js.map +1 -0
- package/dist/lib/docs-artefacts.d.ts +395 -0
- package/dist/lib/docs-artefacts.d.ts.map +1 -0
- package/dist/lib/docs-artefacts.js +781 -0
- package/dist/lib/docs-artefacts.js.map +1 -0
- package/dist/lib/emitted-exports.d.ts +21 -0
- package/dist/lib/emitted-exports.d.ts.map +1 -0
- package/dist/lib/emitted-exports.js +96 -0
- package/dist/lib/emitted-exports.js.map +1 -0
- package/dist/lib/emitted-freshness.d.ts +112 -0
- package/dist/lib/emitted-freshness.d.ts.map +1 -0
- package/dist/lib/emitted-freshness.js +288 -0
- package/dist/lib/emitted-freshness.js.map +1 -0
- package/dist/lib/entity-index-artefact.d.ts +95 -0
- package/dist/lib/entity-index-artefact.d.ts.map +1 -0
- package/dist/lib/entity-index-artefact.js +210 -0
- package/dist/lib/entity-index-artefact.js.map +1 -0
- package/dist/lib/instance-build-inputs.d.ts +108 -0
- package/dist/lib/instance-build-inputs.d.ts.map +1 -0
- package/dist/lib/instance-build-inputs.js +165 -0
- package/dist/lib/instance-build-inputs.js.map +1 -0
- package/dist/lib/module-docs.d.ts +473 -0
- package/dist/lib/module-docs.d.ts.map +1 -0
- package/dist/lib/module-docs.js +711 -0
- package/dist/lib/module-docs.js.map +1 -0
- package/dist/lib/module-package-subpaths.d.ts +56 -0
- package/dist/lib/module-package-subpaths.d.ts.map +1 -0
- package/dist/lib/module-package-subpaths.js +223 -0
- package/dist/lib/module-package-subpaths.js.map +1 -0
- package/dist/lib/module-packages.d.ts +200 -0
- package/dist/lib/module-packages.d.ts.map +1 -0
- package/dist/lib/module-packages.js +580 -0
- package/dist/lib/module-packages.js.map +1 -0
- package/dist/lib/module-population.d.ts +129 -0
- package/dist/lib/module-population.d.ts.map +1 -0
- package/dist/lib/module-population.js +172 -0
- package/dist/lib/module-population.js.map +1 -0
- package/dist/lib/module-roots.d.ts +337 -0
- package/dist/lib/module-roots.d.ts.map +1 -0
- package/dist/lib/module-roots.js +586 -0
- package/dist/lib/module-roots.js.map +1 -0
- package/dist/lib/nested-checkouts.d.ts +33 -0
- package/dist/lib/nested-checkouts.d.ts.map +1 -0
- package/dist/lib/nested-checkouts.js +160 -0
- package/dist/lib/nested-checkouts.js.map +1 -0
- package/dist/lib/platform-root.d.ts +43 -0
- package/dist/lib/platform-root.d.ts.map +1 -0
- package/dist/lib/platform-root.js +134 -0
- package/dist/lib/platform-root.js.map +1 -0
- package/dist/lib/platform-surface.d.ts +235 -0
- package/dist/lib/platform-surface.d.ts.map +1 -0
- package/dist/lib/platform-surface.js +393 -0
- package/dist/lib/platform-surface.js.map +1 -0
- package/dist/lib/port-registrations.d.ts +223 -0
- package/dist/lib/port-registrations.d.ts.map +1 -0
- package/dist/lib/port-registrations.js +532 -0
- package/dist/lib/port-registrations.js.map +1 -0
- package/dist/lib/read-size.d.ts +154 -0
- package/dist/lib/read-size.d.ts.map +1 -0
- package/dist/lib/read-size.js +182 -0
- package/dist/lib/read-size.js.map +1 -0
- package/dist/lib/registration-owners.d.ts +79 -0
- package/dist/lib/registration-owners.d.ts.map +1 -0
- package/dist/lib/registration-owners.js +77 -0
- package/dist/lib/registration-owners.js.map +1 -0
- package/dist/lib/release-index.d.ts +53 -0
- package/dist/lib/release-index.d.ts.map +1 -0
- package/dist/lib/release-index.js +162 -0
- package/dist/lib/release-index.js.map +1 -0
- package/dist/lib/repeating-timers.d.ts +79 -0
- package/dist/lib/repeating-timers.d.ts.map +1 -0
- package/dist/lib/repeating-timers.js +189 -0
- package/dist/lib/repeating-timers.js.map +1 -0
- package/dist/lib/source-text.d.ts +34 -0
- package/dist/lib/source-text.d.ts.map +1 -0
- package/dist/lib/source-text.js +80 -0
- package/dist/lib/source-text.js.map +1 -0
- package/dist/lib/specifiers.d.ts +20 -0
- package/dist/lib/specifiers.d.ts.map +1 -0
- package/dist/lib/specifiers.js +130 -0
- package/dist/lib/specifiers.js.map +1 -0
- package/dist/lib/sql-tables.d.ts +166 -0
- package/dist/lib/sql-tables.d.ts.map +1 -0
- package/dist/lib/sql-tables.js +464 -0
- package/dist/lib/sql-tables.js.map +1 -0
- package/dist/lib/switchable-modules.d.ts +54 -0
- package/dist/lib/switchable-modules.d.ts.map +1 -0
- package/dist/lib/switchable-modules.js +104 -0
- package/dist/lib/switchable-modules.js.map +1 -0
- package/dist/lib/tailwind-sources.d.ts +136 -0
- package/dist/lib/tailwind-sources.d.ts.map +1 -0
- package/dist/lib/tailwind-sources.js +307 -0
- package/dist/lib/tailwind-sources.js.map +1 -0
- package/dist/lib/ui-layer.d.ts +54 -0
- package/dist/lib/ui-layer.d.ts.map +1 -0
- package/dist/lib/ui-layer.js +57 -0
- package/dist/lib/ui-layer.js.map +1 -0
- package/dist/lib/workspace-packages.d.ts +186 -0
- package/dist/lib/workspace-packages.d.ts.map +1 -0
- package/dist/lib/workspace-packages.js +351 -0
- package/dist/lib/workspace-packages.js.map +1 -0
- package/dist/new-instance/deploy.d.ts +211 -0
- package/dist/new-instance/deploy.d.ts.map +1 -0
- package/dist/new-instance/deploy.js +1381 -0
- package/dist/new-instance/deploy.js.map +1 -0
- package/dist/new-instance/docs-toolchain.d.ts +66 -0
- package/dist/new-instance/docs-toolchain.d.ts.map +1 -0
- package/dist/new-instance/docs-toolchain.js +69 -0
- package/dist/new-instance/docs-toolchain.js.map +1 -0
- package/dist/new-instance/host.d.ts +124 -0
- package/dist/new-instance/host.d.ts.map +1 -0
- package/dist/new-instance/host.js +276 -0
- package/dist/new-instance/host.js.map +1 -0
- package/dist/new-instance/index.d.ts +118 -0
- package/dist/new-instance/index.d.ts.map +1 -0
- package/dist/new-instance/index.js +567 -0
- package/dist/new-instance/index.js.map +1 -0
- package/dist/new-instance/modules.d.ts +188 -0
- package/dist/new-instance/modules.d.ts.map +1 -0
- package/dist/new-instance/modules.js +392 -0
- package/dist/new-instance/modules.js.map +1 -0
- package/dist/new-instance/template.d.ts +505 -0
- package/dist/new-instance/template.d.ts.map +1 -0
- package/dist/new-instance/template.js +1886 -0
- package/dist/new-instance/template.js.map +1 -0
- package/dist/new-module/emit.d.ts +67 -0
- package/dist/new-module/emit.d.ts.map +1 -0
- package/dist/new-module/emit.js +1393 -0
- package/dist/new-module/emit.js.map +1 -0
- package/dist/new-module/host.d.ts +67 -0
- package/dist/new-module/host.d.ts.map +1 -0
- package/dist/new-module/host.js +224 -0
- package/dist/new-module/host.js.map +1 -0
- package/dist/new-module/index.d.ts +32 -0
- package/dist/new-module/index.d.ts.map +1 -0
- package/dist/new-module/index.js +193 -0
- package/dist/new-module/index.js.map +1 -0
- package/dist/new-module/spec.d.ts +188 -0
- package/dist/new-module/spec.d.ts.map +1 -0
- package/dist/new-module/spec.js +404 -0
- package/dist/new-module/spec.js.map +1 -0
- package/dist/new-module/text.d.ts +15 -0
- package/dist/new-module/text.d.ts.map +1 -0
- package/dist/new-module/text.js +22 -0
- package/dist/new-module/text.js.map +1 -0
- package/dist/new-storefront/dockerfile.d.ts +20 -0
- package/dist/new-storefront/dockerfile.d.ts.map +1 -0
- package/dist/new-storefront/dockerfile.js +131 -0
- package/dist/new-storefront/dockerfile.js.map +1 -0
- package/dist/new-storefront/gitignore.d.ts +23 -0
- package/dist/new-storefront/gitignore.d.ts.map +1 -0
- package/dist/new-storefront/gitignore.js +39 -0
- package/dist/new-storefront/gitignore.js.map +1 -0
- package/dist/new-storefront/index.d.ts +93 -0
- package/dist/new-storefront/index.d.ts.map +1 -0
- package/dist/new-storefront/index.js +329 -0
- package/dist/new-storefront/index.js.map +1 -0
- package/dist/new-storefront/npmrc.d.ts +98 -0
- package/dist/new-storefront/npmrc.d.ts.map +1 -0
- package/dist/new-storefront/npmrc.js +217 -0
- package/dist/new-storefront/npmrc.js.map +1 -0
- package/dist/new-storefront/reference.d.ts +189 -0
- package/dist/new-storefront/reference.d.ts.map +1 -0
- package/dist/new-storefront/reference.js +430 -0
- package/dist/new-storefront/reference.js.map +1 -0
- package/dist/new-storefront/rewrite.d.ts +171 -0
- package/dist/new-storefront/rewrite.d.ts.map +1 -0
- package/dist/new-storefront/rewrite.js +701 -0
- package/dist/new-storefront/rewrite.js.map +1 -0
- package/dist/release-index.json +284 -0
- package/dist/rules/action-route-permissions.d.ts +141 -0
- package/dist/rules/action-route-permissions.d.ts.map +1 -0
- package/dist/rules/action-route-permissions.js +556 -0
- package/dist/rules/action-route-permissions.js.map +1 -0
- package/dist/rules/bundle-pairing.d.ts +74 -0
- package/dist/rules/bundle-pairing.d.ts.map +1 -0
- package/dist/rules/bundle-pairing.js +281 -0
- package/dist/rules/bundle-pairing.js.map +1 -0
- package/dist/rules/channel-resolution.d.ts +17 -0
- package/dist/rules/channel-resolution.d.ts.map +1 -0
- package/dist/rules/channel-resolution.js +382 -0
- package/dist/rules/channel-resolution.js.map +1 -0
- package/dist/rules/command-coverage.d.ts +210 -0
- package/dist/rules/command-coverage.d.ts.map +1 -0
- package/dist/rules/command-coverage.js +714 -0
- package/dist/rules/command-coverage.js.map +1 -0
- package/dist/rules/container-imports.d.ts +60 -0
- package/dist/rules/container-imports.d.ts.map +1 -0
- package/dist/rules/container-imports.js +158 -0
- package/dist/rules/container-imports.js.map +1 -0
- package/dist/rules/default-language-prose.d.ts +212 -0
- package/dist/rules/default-language-prose.d.ts.map +1 -0
- package/dist/rules/default-language-prose.js +710 -0
- package/dist/rules/default-language-prose.js.map +1 -0
- package/dist/rules/diacritic-folds.d.ts +238 -0
- package/dist/rules/diacritic-folds.d.ts.map +1 -0
- package/dist/rules/diacritic-folds.js +681 -0
- package/dist/rules/diacritic-folds.js.map +1 -0
- package/dist/rules/entity-tenant-classification.d.ts +171 -0
- package/dist/rules/entity-tenant-classification.d.ts.map +1 -0
- package/dist/rules/entity-tenant-classification.js +323 -0
- package/dist/rules/entity-tenant-classification.js.map +1 -0
- package/dist/rules/entry-presence.d.ts +142 -0
- package/dist/rules/entry-presence.d.ts.map +1 -0
- package/dist/rules/entry-presence.js +339 -0
- package/dist/rules/entry-presence.js.map +1 -0
- package/dist/rules/entry-scope.d.ts +91 -0
- package/dist/rules/entry-scope.d.ts.map +1 -0
- package/dist/rules/entry-scope.js +404 -0
- package/dist/rules/entry-scope.js.map +1 -0
- package/dist/rules/env-inputs.d.ts +222 -0
- package/dist/rules/env-inputs.d.ts.map +1 -0
- package/dist/rules/env-inputs.js +951 -0
- package/dist/rules/env-inputs.js.map +1 -0
- package/dist/rules/kernel-boundary.d.ts +37 -0
- package/dist/rules/kernel-boundary.d.ts.map +1 -0
- package/dist/rules/kernel-boundary.js +195 -0
- package/dist/rules/kernel-boundary.js.map +1 -0
- package/dist/rules/nul-bytes.d.ts +233 -0
- package/dist/rules/nul-bytes.d.ts.map +1 -0
- package/dist/rules/nul-bytes.js +332 -0
- package/dist/rules/nul-bytes.js.map +1 -0
- package/dist/rules/platform-surface.d.ts +479 -0
- package/dist/rules/platform-surface.d.ts.map +1 -0
- package/dist/rules/platform-surface.js +749 -0
- package/dist/rules/platform-surface.js.map +1 -0
- package/dist/rules/port-catches.d.ts +225 -0
- package/dist/rules/port-catches.d.ts.map +1 -0
- package/dist/rules/port-catches.js +1374 -0
- package/dist/rules/port-catches.js.map +1 -0
- package/dist/rules/port-shape.d.ts +213 -0
- package/dist/rules/port-shape.d.ts.map +1 -0
- package/dist/rules/port-shape.js +670 -0
- package/dist/rules/port-shape.js.map +1 -0
- package/dist/rules/queue-names.d.ts +108 -0
- package/dist/rules/queue-names.d.ts.map +1 -0
- package/dist/rules/queue-names.js +395 -0
- package/dist/rules/queue-names.js.map +1 -0
- package/dist/rules/singleton-identity.d.ts +205 -0
- package/dist/rules/singleton-identity.d.ts.map +1 -0
- package/dist/rules/singleton-identity.js +830 -0
- package/dist/rules/singleton-identity.js.map +1 -0
- package/dist/rules/subscribe-seam.d.ts +121 -0
- package/dist/rules/subscribe-seam.d.ts.map +1 -0
- package/dist/rules/subscribe-seam.js +594 -0
- package/dist/rules/subscribe-seam.js.map +1 -0
- package/dist/rules/transaction-context.d.ts +40 -0
- package/dist/rules/transaction-context.d.ts.map +1 -0
- package/dist/rules/transaction-context.js +294 -0
- package/dist/rules/transaction-context.js.map +1 -0
- package/package.json +59 -0
|
@@ -0,0 +1,1374 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A `catch` around a gated-port call may not swallow the module's presence
|
|
3
|
+
* answer (issue #84; the deferred half of feature 072's D-43).
|
|
4
|
+
*
|
|
5
|
+
* `lazyPort` resolves the name inside the *forwarded call*, so a switched-off
|
|
6
|
+
* owner surfaces as `ModuleDisabledError` at the call site rather than at wiring
|
|
7
|
+
* time. A `try { … } catch { return null }` around one therefore converts
|
|
8
|
+
* fail-closed into fail-open, and does it silently: the caller answers "no data"
|
|
9
|
+
* where the truthful answer is "this capability is off", and the operator reads a
|
|
10
|
+
* working screen that is lying to them.
|
|
11
|
+
*
|
|
12
|
+
* **The whole of the reasoning is in `backend/scripts/check-port-catches.ts`**,
|
|
13
|
+
* the repository-scope host, whose header it has always been: the three shapes
|
|
14
|
+
* the measured 51 sites turned out to be, the two spellings one predicate judges,
|
|
15
|
+
* the `async`-boundary bound, the alias fixpoint and its exclusions, the one hop
|
|
16
|
+
* backwards through a class, the scoping correction of issue #278, what counts as
|
|
17
|
+
* handling it, and `OWNER LOCKED`. None of it is restated here.
|
|
18
|
+
*
|
|
19
|
+
* `specs/101-endora-check/contracts/package-scope-layout.md` §6 — one analysis,
|
|
20
|
+
* two hosts.
|
|
21
|
+
*
|
|
22
|
+
* ## Two things changed on the way across, and both are load-bearing
|
|
23
|
+
*
|
|
24
|
+
* **1. `portOwners` can be seeded from outside, and it had to be.** The owner map
|
|
25
|
+
* is built from `di.providePort` in a file that declares `registerModule` — the
|
|
26
|
+
* **owner's** file. `isProxyCall` then admits `lazyPort(ctx, '<name>')` only when
|
|
27
|
+
* `<name>` is already in that map. So over one package in isolation a consumer's
|
|
28
|
+
* resolutions seed nothing and every `catch` around one reads clean.
|
|
29
|
+
*
|
|
30
|
+
* Measured over this repository's module packages before the seam existed: the
|
|
31
|
+
* whole-tree run attributes **226** sites to 41 packages, the same analysis over
|
|
32
|
+
* each package alone finds **19** in 10 packages, and **31 of the 41 lose every
|
|
33
|
+
* site** — including all four PIM connectors and `product_feeds`, which is to say
|
|
34
|
+
* the integration-heavy modules the package host exists for: the ones whose ports
|
|
35
|
+
* are almost all somebody else's. {@link PortCatchInput.peerOwners} is what a
|
|
36
|
+
* package-scope host supplies instead, out of its installed and workspace peers.
|
|
37
|
+
*
|
|
38
|
+
* **2. {@link resolvedPortNames} is new, and it is the denominator.** A rule that
|
|
39
|
+
* reads an owner map has to say how much of it it managed to resolve, or a short
|
|
40
|
+
* map is indistinguishable from a clean tree — and the estate's idiom for *"I
|
|
41
|
+
* read n of the m things I needed"* is the `read:` line's `sources=` token, not a
|
|
42
|
+
* sentence. This collects every `lazyPort(ctx, '<literal>')` name the sources
|
|
43
|
+
* write **whether or not an owner is known**, so a host can print
|
|
44
|
+
* `sources=owners:<attributed>/<written>` and refuse when the numerator is zero.
|
|
45
|
+
* Nothing else in the analysis can supply it: everywhere else, an unowned name is
|
|
46
|
+
* already gone.
|
|
47
|
+
*
|
|
48
|
+
* **3. `checkPortCatches`'s ledger argument loses its default.** A host states
|
|
49
|
+
* which exemptions it is judging against; `PORT_CATCHES_TO_DRAIN` is this
|
|
50
|
+
* repository's and stays in `backend/scripts/`, where `check:lock-claims`'
|
|
51
|
+
* population can also still see it.
|
|
52
|
+
*/
|
|
53
|
+
import { existsSync, readdirSync, statSync } from 'node:fs';
|
|
54
|
+
import { join, posix } from 'node:path';
|
|
55
|
+
import ts from 'typescript';
|
|
56
|
+
import { NO_HOST_RESIDENT_MODULES, } from '../lib/module-population.js';
|
|
57
|
+
import { declaresRegisterModule } from '../lib/module-roots.js';
|
|
58
|
+
import { moduleOf, providedPortNames } from '../lib/port-registrations.js';
|
|
59
|
+
import { lockedOwners, } from '../lib/switchable-modules.js';
|
|
60
|
+
/** A file that belongs to no module — a composition root, `http/`, `db/`. */
|
|
61
|
+
const ROOT = '(root)';
|
|
62
|
+
/**
|
|
63
|
+
* The bindings a lexical scope declares **directly**, by the name a bare
|
|
64
|
+
* identifier inside it would resolve to (issue #278).
|
|
65
|
+
*
|
|
66
|
+
* Only the shapes the check can *judge* are collected: a `const`/`let`/`var`
|
|
67
|
+
* with an identifier name, and a function or constructor parameter with one.
|
|
68
|
+
* A destructuring binding (`const { promotion } = deps`), an import binding and
|
|
69
|
+
* a `catch` variable are deliberately absent — the check cannot tell what they
|
|
70
|
+
* hold, and treating one as a shadow would silence a real finding, which is the
|
|
71
|
+
* one direction a narrowing may not fail in. A nested `function` declaration is
|
|
72
|
+
* absent for the same reason: it binds a name, but not to anything this analysis
|
|
73
|
+
* has an opinion about.
|
|
74
|
+
*/
|
|
75
|
+
const SCOPE_BINDINGS = new WeakMap();
|
|
76
|
+
function directBindings(scope) {
|
|
77
|
+
const cached = SCOPE_BINDINGS.get(scope);
|
|
78
|
+
if (cached !== undefined)
|
|
79
|
+
return cached;
|
|
80
|
+
const found = new Map();
|
|
81
|
+
const addList = (list) => {
|
|
82
|
+
for (const declaration of list.declarations) {
|
|
83
|
+
if (ts.isIdentifier(declaration.name))
|
|
84
|
+
found.set(declaration.name.text, declaration);
|
|
85
|
+
}
|
|
86
|
+
};
|
|
87
|
+
const addStatements = (statements) => {
|
|
88
|
+
for (const statement of statements) {
|
|
89
|
+
if (ts.isVariableStatement(statement))
|
|
90
|
+
addList(statement.declarationList);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
if (ts.isSourceFile(scope) || ts.isBlock(scope) || ts.isModuleBlock(scope)) {
|
|
94
|
+
addStatements(scope.statements);
|
|
95
|
+
}
|
|
96
|
+
else if (ts.isCaseBlock(scope)) {
|
|
97
|
+
for (const clause of scope.clauses)
|
|
98
|
+
addStatements(clause.statements);
|
|
99
|
+
}
|
|
100
|
+
else if (ts.isForStatement(scope) || ts.isForInStatement(scope) || ts.isForOfStatement(scope)) {
|
|
101
|
+
const { initializer } = scope;
|
|
102
|
+
if (initializer !== undefined && ts.isVariableDeclarationList(initializer))
|
|
103
|
+
addList(initializer);
|
|
104
|
+
}
|
|
105
|
+
else if (ts.isFunctionLike(scope)) {
|
|
106
|
+
for (const parameter of scope.parameters) {
|
|
107
|
+
if (ts.isIdentifier(parameter.name))
|
|
108
|
+
found.set(parameter.name.text, parameter);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
SCOPE_BINDINGS.set(scope, found);
|
|
112
|
+
return found;
|
|
113
|
+
}
|
|
114
|
+
/**
|
|
115
|
+
* The declaration a bare identifier `name`, written at `at`, resolves to — or
|
|
116
|
+
* `null` when nothing in scope binds it and the alias table therefore answers.
|
|
117
|
+
*
|
|
118
|
+
* Walks the parent chain, so an inner block wins over an outer one and a
|
|
119
|
+
* parameter wins over a file-level `const` of the same spelling. That ordering
|
|
120
|
+
* is the whole point: `catalog`'s `queue` case, which the file-scoping rule
|
|
121
|
+
* already handles across files, is the same collision one level down.
|
|
122
|
+
*/
|
|
123
|
+
function nearestBinding(at, name) {
|
|
124
|
+
for (let node = at.parent; node !== undefined; node = node.parent) {
|
|
125
|
+
const binding = directBindings(node).get(name);
|
|
126
|
+
if (binding !== undefined)
|
|
127
|
+
return binding;
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
/**
|
|
132
|
+
* Can the check say **positively** that this expression is not a port?
|
|
133
|
+
*
|
|
134
|
+
* The distinction this function exists to keep is the one a narrowing gets
|
|
135
|
+
* wrong: {@link Analysis.readsAsPort}'s carriage analysis deliberately
|
|
136
|
+
* under-approximates — a call's *result* is data, so `carries` answers "no" both
|
|
137
|
+
* for a value that is genuinely not a port and for one it simply cannot follow.
|
|
138
|
+
* "Cannot tell" is not "not a port", and using the first as a shadow silences a
|
|
139
|
+
* real finding. It was measured: `catalog` binds
|
|
140
|
+
* `const customFields = this.#requireCustomFields()` — a call the analysis
|
|
141
|
+
* cannot follow, holding `custom_fields`' gated definition port — and treating
|
|
142
|
+
* that binding as a shadow took six `catch` sites in `attribute-commands.ts` out
|
|
143
|
+
* of the population.
|
|
144
|
+
*
|
|
145
|
+
* So this is a small, syntactic allow-list of values that cannot be a port
|
|
146
|
+
* however the analysis is extended: a literal, an object or array of
|
|
147
|
+
* non-ports, a `new` whose every argument is one of those, a primitive-valued
|
|
148
|
+
* operator. A **call**, an **identifier**, a **property access**, a **closure**
|
|
149
|
+
* and a parameter with no default are all "cannot tell", and none of them
|
|
150
|
+
* shadows anything. `catalog`'s `const queue = new Queue('catalog-bulk', {…})`,
|
|
151
|
+
* the collision the file-scoping rule was originally written for, is on the
|
|
152
|
+
* allow-list; `const transitionService = requireTransitionService()` is not, and
|
|
153
|
+
* does not need to be — issue #278's own case is fixed by scoping the parameter
|
|
154
|
+
* to its declaring file, which is where the binding actually is.
|
|
155
|
+
*/
|
|
156
|
+
function manifestlyNotAPort(node) {
|
|
157
|
+
if (ts.isStringLiteralLike(node) ||
|
|
158
|
+
ts.isNumericLiteral(node) ||
|
|
159
|
+
ts.isBigIntLiteral(node) ||
|
|
160
|
+
ts.isRegularExpressionLiteral(node) ||
|
|
161
|
+
ts.isTemplateExpression(node) ||
|
|
162
|
+
node.kind === ts.SyntaxKind.TrueKeyword ||
|
|
163
|
+
node.kind === ts.SyntaxKind.FalseKeyword ||
|
|
164
|
+
node.kind === ts.SyntaxKind.NullKeyword ||
|
|
165
|
+
ts.isObjectLiteralExpression(node) ||
|
|
166
|
+
ts.isTypeOfExpression(node) ||
|
|
167
|
+
ts.isVoidExpression(node) ||
|
|
168
|
+
ts.isPrefixUnaryExpression(node)) {
|
|
169
|
+
return true;
|
|
170
|
+
}
|
|
171
|
+
if (ts.isArrayLiteralExpression(node))
|
|
172
|
+
return node.elements.every(manifestlyNotAPort);
|
|
173
|
+
if (ts.isNewExpression(node))
|
|
174
|
+
return (node.arguments ?? []).every(manifestlyNotAPort);
|
|
175
|
+
if (ts.isParenthesizedExpression(node) ||
|
|
176
|
+
ts.isAwaitExpression(node) ||
|
|
177
|
+
ts.isNonNullExpression(node) ||
|
|
178
|
+
ts.isAsExpression(node) ||
|
|
179
|
+
ts.isSatisfiesExpression(node)) {
|
|
180
|
+
return manifestlyNotAPort(node.expression);
|
|
181
|
+
}
|
|
182
|
+
if (ts.isConditionalExpression(node)) {
|
|
183
|
+
return manifestlyNotAPort(node.whenTrue) && manifestlyNotAPort(node.whenFalse);
|
|
184
|
+
}
|
|
185
|
+
if (ts.isBinaryExpression(node)) {
|
|
186
|
+
const operator = node.operatorToken.kind;
|
|
187
|
+
// `??`, `||` and `&&` yield one of their operands; every other operator
|
|
188
|
+
// yields a primitive, whatever its operands were.
|
|
189
|
+
if (operator !== ts.SyntaxKind.QuestionQuestionToken &&
|
|
190
|
+
operator !== ts.SyntaxKind.BarBarToken &&
|
|
191
|
+
operator !== ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
return manifestlyNotAPort(node.left) && manifestlyNotAPort(node.right);
|
|
195
|
+
}
|
|
196
|
+
return false;
|
|
197
|
+
}
|
|
198
|
+
/**
|
|
199
|
+
* Does the binding `name` resolves to here hide a wider alias of that spelling?
|
|
200
|
+
*
|
|
201
|
+
* Three answers, and only the third is a shadow: no binding at all (the alias
|
|
202
|
+
* table answers), a binding the alias table itself introduced (it **is** the
|
|
203
|
+
* alias), or a binding whose value is manifestly not a port.
|
|
204
|
+
*/
|
|
205
|
+
function shadowsAlias(at, name, carriers) {
|
|
206
|
+
const binding = nearestBinding(at, name);
|
|
207
|
+
if (binding === null || carriers.has(binding))
|
|
208
|
+
return false;
|
|
209
|
+
// A parameter with no default is "cannot tell": a caller this analysis did
|
|
210
|
+
// not walk may hand it a port, and the alias table is how that is found out.
|
|
211
|
+
if (ts.isParameter(binding)) {
|
|
212
|
+
return binding.initializer !== undefined && manifestlyNotAPort(binding.initializer);
|
|
213
|
+
}
|
|
214
|
+
if (!ts.isVariableDeclaration(binding) || binding.initializer === undefined)
|
|
215
|
+
return false;
|
|
216
|
+
return manifestlyNotAPort(binding.initializer);
|
|
217
|
+
}
|
|
218
|
+
/** Every alias is visible here: a gated port's own name, outside its owner. */
|
|
219
|
+
const EVERYWHERE = '*';
|
|
220
|
+
/**
|
|
221
|
+
* `<file>:<port>` — the ledger key, and the identity of a site, with `#promise`
|
|
222
|
+
* appended for the promise form.
|
|
223
|
+
*
|
|
224
|
+
* The key is deliberately not line-based: moving code inside a file must not
|
|
225
|
+
* invalidate an entry, and re-opening the hole must not silently inherit one.
|
|
226
|
+
* The discriminator is the same rule one granularity across. A file can hold
|
|
227
|
+
* both spellings over one alias — `product_feeds` does — and a single key would
|
|
228
|
+
* let an entry written for the `try` absorb a `.catch` added later, which is
|
|
229
|
+
* the inheritance the line-independence was chosen to avoid. It is a **suffix**
|
|
230
|
+
* so that every entry standing when the promise form landed keeps its key.
|
|
231
|
+
*/
|
|
232
|
+
export function keyOf(found) {
|
|
233
|
+
const base = `${found.file}:${found.port}`;
|
|
234
|
+
return found.form === 'promise' ? `${base}#promise` : base;
|
|
235
|
+
}
|
|
236
|
+
export { lockedOwners };
|
|
237
|
+
/**
|
|
238
|
+
* The awilix resolver-builder chain. `ctx.asFunction(f).singleton()` is the
|
|
239
|
+
* factory `f` in another wrapper, so whatever `f` carries the registration
|
|
240
|
+
* carries — unlike an ordinary method call on a value, whose result is data.
|
|
241
|
+
*/
|
|
242
|
+
const RESOLVER_BUILDERS = new Set(['singleton', 'scoped', 'transient', 'inject', 'disposer']);
|
|
243
|
+
/** Where such a chain starts — the registration wrapping a factory. */
|
|
244
|
+
const RESOLVER_ENTRIES = new Set(['asFunction', 'asValue', 'asClass']);
|
|
245
|
+
/**
|
|
246
|
+
* A call that claims a **container** name — `registerValues(container, …)`,
|
|
247
|
+
* `composedModules.contribute(…)`, `ctx.di.register(…)`,
|
|
248
|
+
* `ctx.di.providePort(…)`.
|
|
249
|
+
*
|
|
250
|
+
* `contribute` is the same claim as `registerValues` made inside D-45's window
|
|
251
|
+
* (issue #52), and it has to be read here for the same reason: a root moving 72
|
|
252
|
+
* contributions onto the method would otherwise take every one of those names
|
|
253
|
+
* out of this analysis, and the check would go quiet without anything changing
|
|
254
|
+
* about the tree.
|
|
255
|
+
*
|
|
256
|
+
* Deliberately narrow. Matching a bare `.register(` would also match Fastify's
|
|
257
|
+
* `app.register(plugin, options)` and turn every option key in the tree into an
|
|
258
|
+
* alias. The caller narrows it further: only a **root's** registration key is
|
|
259
|
+
* published everywhere, because a root belongs to no module and a module's own
|
|
260
|
+
* internal names (`mailer`, `client`, `settings`) collide with half the tree.
|
|
261
|
+
*/
|
|
262
|
+
function isContainerRegistration(node) {
|
|
263
|
+
if (ts.isIdentifier(node.expression))
|
|
264
|
+
return node.expression.text === 'registerValues';
|
|
265
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
266
|
+
return false;
|
|
267
|
+
const method = node.expression.name.text;
|
|
268
|
+
if (method === 'contribute')
|
|
269
|
+
return true;
|
|
270
|
+
if (method !== 'register' && method !== 'providePort')
|
|
271
|
+
return false;
|
|
272
|
+
return tailName(node.expression.expression) === 'di';
|
|
273
|
+
}
|
|
274
|
+
/**
|
|
275
|
+
* Which names stand for a gated port, and where each one may be read.
|
|
276
|
+
*
|
|
277
|
+
* A **fixpoint**, because the shapes chain without bound: a `lazyPort` is bound
|
|
278
|
+
* to a local, the local passed to a constructor, the constructed holder given a
|
|
279
|
+
* deps-object key, that key registered on the container by a root, and the
|
|
280
|
+
* container name resolved by a module three files away. Each round can only add
|
|
281
|
+
* aliases, and a round that adds none is the last.
|
|
282
|
+
*/
|
|
283
|
+
function analyze(sources, hostResident = NO_HOST_RESIDENT_MODULES, peerOwners = new Map()) {
|
|
284
|
+
const parsed = new Map();
|
|
285
|
+
for (const [file, text] of sources) {
|
|
286
|
+
parsed.set(file, ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true));
|
|
287
|
+
}
|
|
288
|
+
const portOwners = new Map();
|
|
289
|
+
// Peers first, so the walk's own registrations overwrite them: a package that
|
|
290
|
+
// provides a port is its owner whatever a peer's artefact also claims.
|
|
291
|
+
for (const [name, owner] of peerOwners)
|
|
292
|
+
portOwners.set(name, owner);
|
|
293
|
+
for (const [file, sf] of parsed) {
|
|
294
|
+
// A module's composition entry point, by the **marker** rather than by a
|
|
295
|
+
// filename (feature 080, T040b).
|
|
296
|
+
//
|
|
297
|
+
// This read `file.endsWith('backend.ts')`, which a module package's entry
|
|
298
|
+
// point is not: it lives wherever the `exports` map's `./backend` subpath
|
|
299
|
+
// points, and for every package in this repository that is
|
|
300
|
+
// `src/backend/index.ts`. So a packaged owner provided no port as far as
|
|
301
|
+
// this analysis was concerned, and every `catch` around one of its gates
|
|
302
|
+
// read clean — fail-open, and the same defect !920 found in
|
|
303
|
+
// `check-port-dependencies` under the same assumption. `webhooks` is where
|
|
304
|
+
// it surfaced when its module became a package: the `LEDGER-PERMANENT`
|
|
305
|
+
// self-edge below went *stale*, which is the loud half of a blindness whose
|
|
306
|
+
// other half is silent.
|
|
307
|
+
//
|
|
308
|
+
// `declaresRegisterModule` is `generate-composer.ts`'s own marker, so the
|
|
309
|
+
// composer and this check cannot disagree about which file composes a
|
|
310
|
+
// module.
|
|
311
|
+
if (!declaresRegisterModule(sf.getFullText()))
|
|
312
|
+
continue;
|
|
313
|
+
const owner = moduleOf(`/src/${file}`, hostResident);
|
|
314
|
+
if (owner === null)
|
|
315
|
+
continue;
|
|
316
|
+
for (const name of providedPortNames(sf.getFullText(), file))
|
|
317
|
+
portOwners.set(name, owner);
|
|
318
|
+
}
|
|
319
|
+
const aliases = new Map();
|
|
320
|
+
/** Alias → scope → the gated port names it carries there; see {@link Analysis.gatesAt}. */
|
|
321
|
+
const gatesOf = new Map();
|
|
322
|
+
/**
|
|
323
|
+
* The declarations the alias table itself introduced — a `const` bound to a
|
|
324
|
+
* carrying value, a parameter the port was passed as (issue #278).
|
|
325
|
+
*
|
|
326
|
+
* A binding in here is the alias rather than a shadow of one, which is what
|
|
327
|
+
* keeps `const cartService = lazyPort(…)` readable as a port in the very file
|
|
328
|
+
* that binds it while an unrelated `const transitionService = …` two modules
|
|
329
|
+
* away is not.
|
|
330
|
+
*/
|
|
331
|
+
const carrierBindings = new Set();
|
|
332
|
+
/** True when the alias is new — which is what keeps the fixpoint running. */
|
|
333
|
+
let grew = false;
|
|
334
|
+
const markCarrier = (binding) => {
|
|
335
|
+
if (carrierBindings.has(binding))
|
|
336
|
+
return;
|
|
337
|
+
carrierBindings.add(binding);
|
|
338
|
+
// A lifted shadow is as much a change as a new alias: the round that lifts
|
|
339
|
+
// it may add nothing else, and the next round is where the reads it unblocks
|
|
340
|
+
// are seen.
|
|
341
|
+
grew = true;
|
|
342
|
+
};
|
|
343
|
+
const addAlias = (name, scope, where, gates) => {
|
|
344
|
+
const scopes = aliases.get(name) ?? new Set();
|
|
345
|
+
aliases.set(name, scopes);
|
|
346
|
+
if (!scopes.has(scope)) {
|
|
347
|
+
scopes.add(scope);
|
|
348
|
+
grew = true;
|
|
349
|
+
if (process.env.PORT_CATCH_WHY)
|
|
350
|
+
console.error(`ALIAS ${name} @${scope} <- ${where ?? '-'}`);
|
|
351
|
+
}
|
|
352
|
+
if (gates === undefined)
|
|
353
|
+
return;
|
|
354
|
+
// Gates keep the fixpoint running on their own: a holder can be bound
|
|
355
|
+
// before the round that discovers what it was built from.
|
|
356
|
+
const byScope = gatesOf.get(name) ?? new Map();
|
|
357
|
+
gatesOf.set(name, byScope);
|
|
358
|
+
const carried = byScope.get(scope) ?? new Set();
|
|
359
|
+
byScope.set(scope, carried);
|
|
360
|
+
for (const gate of gates) {
|
|
361
|
+
if (carried.has(gate))
|
|
362
|
+
continue;
|
|
363
|
+
carried.add(gate);
|
|
364
|
+
grew = true;
|
|
365
|
+
if (process.env.PORT_CATCH_WHY)
|
|
366
|
+
console.error(`GATE ${name} <- ${gate} @${where ?? '-'}`);
|
|
367
|
+
}
|
|
368
|
+
};
|
|
369
|
+
// A gated port's own name reads as one everywhere except inside its owner,
|
|
370
|
+
// where the identical identifier is normally the module's own instance.
|
|
371
|
+
for (const name of portOwners.keys())
|
|
372
|
+
addAlias(name, EVERYWHERE, undefined, new Set([name]));
|
|
373
|
+
const readsAsPort = (name, moduleId, file, at) => {
|
|
374
|
+
const scopes = aliases.get(name);
|
|
375
|
+
if (scopes === undefined)
|
|
376
|
+
return false;
|
|
377
|
+
// Lexical resolution first (issue #278): a nearer binding that manifestly
|
|
378
|
+
// holds no port is what the identifier denotes here, whatever a wider alias
|
|
379
|
+
// of the same spelling says.
|
|
380
|
+
if (at !== undefined && shadowsAlias(at, name, carrierBindings))
|
|
381
|
+
return false;
|
|
382
|
+
if (scopes.has(moduleId) || scopes.has(file))
|
|
383
|
+
return true;
|
|
384
|
+
return scopes.has(EVERYWHERE) && portOwners.get(name) !== moduleId;
|
|
385
|
+
};
|
|
386
|
+
const gatesAt = (name, moduleId, file) => {
|
|
387
|
+
const found = new Set();
|
|
388
|
+
for (const [scope, gates] of gatesOf.get(name) ?? []) {
|
|
389
|
+
const visible = scope === moduleId ||
|
|
390
|
+
scope === file ||
|
|
391
|
+
(scope === EVERYWHERE && portOwners.get(name) !== moduleId);
|
|
392
|
+
if (visible)
|
|
393
|
+
for (const gate of gates)
|
|
394
|
+
found.add(gate);
|
|
395
|
+
}
|
|
396
|
+
return found;
|
|
397
|
+
};
|
|
398
|
+
/** `lazyPort<T>(ctx, 'gatedName')`, and only a gated one. */
|
|
399
|
+
const isProxyCall = (node) => {
|
|
400
|
+
if (!ts.isCallExpression(node) ||
|
|
401
|
+
!ts.isIdentifier(node.expression) ||
|
|
402
|
+
node.expression.text !== 'lazyPort') {
|
|
403
|
+
return false;
|
|
404
|
+
}
|
|
405
|
+
const [, nameArgument] = node.arguments;
|
|
406
|
+
return (nameArgument !== undefined &&
|
|
407
|
+
ts.isStringLiteralLike(nameArgument) &&
|
|
408
|
+
portOwners.has(nameArgument.text));
|
|
409
|
+
};
|
|
410
|
+
const declarations = collectDeclarations(parsed);
|
|
411
|
+
const resolveClass = (file, name) => resolveDeclaration(declarations, (perFile) => perFile.classes, file, name);
|
|
412
|
+
const resolveFunction = (file, name) => resolveDeclaration(declarations, (perFile) => perFile.functions, file, name);
|
|
413
|
+
/**
|
|
414
|
+
* Every spelling some file declares a function under — what the factory
|
|
415
|
+
* clause in `carries` asks. Deliberately still by name: see that clause.
|
|
416
|
+
*/
|
|
417
|
+
const functionNames = new Set();
|
|
418
|
+
for (const perFile of declarations.values()) {
|
|
419
|
+
for (const name of perFile.functions.keys())
|
|
420
|
+
functionNames.add(name);
|
|
421
|
+
}
|
|
422
|
+
/**
|
|
423
|
+
* One round over every file. Reads the alias table as it stands and adds to
|
|
424
|
+
* it; `grew` says whether another round can find anything new.
|
|
425
|
+
*/
|
|
426
|
+
const round = () => {
|
|
427
|
+
for (const [file, sf] of parsed) {
|
|
428
|
+
const scope = moduleOf(`/src/${file}`, hostResident) ?? ROOT;
|
|
429
|
+
const reads = (name, at) => readsAsPort(name, scope, file, at);
|
|
430
|
+
const at = (node) => `${file}:${sf.getLineAndCharacterOfPosition(node.getStart(sf)).line + 1}`;
|
|
431
|
+
/**
|
|
432
|
+
* Does evaluating this expression yield a value that still reaches the
|
|
433
|
+
* gate?
|
|
434
|
+
*
|
|
435
|
+
* Calls are the deliberate exception: `proxy.applyToCart(…)` *is* the
|
|
436
|
+
* gated call, but its **result** is data, so carriage does not survive a
|
|
437
|
+
* call. What does survive is construction — a holder given the proxy
|
|
438
|
+
* keeps forwarding to it — and a closure, which has not run yet.
|
|
439
|
+
*/
|
|
440
|
+
const carries = (node) => {
|
|
441
|
+
if (isProxyCall(node))
|
|
442
|
+
return true;
|
|
443
|
+
// A bare identifier resolves lexically first (issue #278); a property
|
|
444
|
+
// name below does not, having no lexical binding to resolve against.
|
|
445
|
+
if (ts.isIdentifier(node))
|
|
446
|
+
return reads(node.text, node);
|
|
447
|
+
// `cradle().promotionService`, `this.deps.promotion` — the **trailing**
|
|
448
|
+
// name is what the gate answers for. A field read *off* a port
|
|
449
|
+
// (`proxy.rows`, `p.attributeValues[k]`) is data, and treating it as a
|
|
450
|
+
// carrier cascaded through every local it was ever assigned to.
|
|
451
|
+
if (ts.isPropertyAccessExpression(node))
|
|
452
|
+
return reads(node.name.text);
|
|
453
|
+
if (ts.isParenthesizedExpression(node) ||
|
|
454
|
+
ts.isAwaitExpression(node) ||
|
|
455
|
+
ts.isNonNullExpression(node) ||
|
|
456
|
+
ts.isAsExpression(node) ||
|
|
457
|
+
ts.isSatisfiesExpression(node)) {
|
|
458
|
+
return carries(node.expression);
|
|
459
|
+
}
|
|
460
|
+
if (ts.isConditionalExpression(node)) {
|
|
461
|
+
return carries(node.whenTrue) || carries(node.whenFalse);
|
|
462
|
+
}
|
|
463
|
+
// `port ?? fallback`, `flag && port` — a comparison yields a boolean.
|
|
464
|
+
if (ts.isBinaryExpression(node)) {
|
|
465
|
+
const operator = node.operatorToken.kind;
|
|
466
|
+
if (operator !== ts.SyntaxKind.QuestionQuestionToken &&
|
|
467
|
+
operator !== ts.SyntaxKind.BarBarToken &&
|
|
468
|
+
operator !== ts.SyntaxKind.AmpersandAmpersandToken) {
|
|
469
|
+
return false;
|
|
470
|
+
}
|
|
471
|
+
return carries(node.left) || carries(node.right);
|
|
472
|
+
}
|
|
473
|
+
// An object literal deliberately does **not** carry, however many ports
|
|
474
|
+
// it holds: a deps bag is a record, and tainting the bag makes every
|
|
475
|
+
// `this.deps.anythingAtAll()` in the receiving class read as a port
|
|
476
|
+
// call. Its carrying keys become aliases one by one instead, which is
|
|
477
|
+
// where the port is actually reached.
|
|
478
|
+
//
|
|
479
|
+
// So a holder **built from** a bag does not carry either, and that
|
|
480
|
+
// blind spot is known and kept (feature 134 D18 §2, half 2 withdrawn).
|
|
481
|
+
// Letting `new C({ k: <carrier> })` carry was measured: it made the
|
|
482
|
+
// `product_feeds` inline-delivery chain visible, and it also added
|
|
483
|
+
// fourteen unhandled sites, among them `catch` blocks around holder
|
|
484
|
+
// methods that never touch the port — `connections.getOrFail()` reads a
|
|
485
|
+
// row, yet the holder carries `credentials` as a whole, because gates
|
|
486
|
+
// are attributed per value rather than per method. A site the analysis
|
|
487
|
+
// cannot tell apart from a real swallow has no honest classification,
|
|
488
|
+
// so the widening waits for per-method attribution.
|
|
489
|
+
if (ts.isArrayLiteralExpression(node))
|
|
490
|
+
return node.elements.some(carries);
|
|
491
|
+
if (ts.isArrowFunction(node) || ts.isFunctionExpression(node))
|
|
492
|
+
return bodyReads(node, reads);
|
|
493
|
+
if (ts.isNewExpression(node))
|
|
494
|
+
return (node.arguments ?? []).some(carries);
|
|
495
|
+
if (ts.isCallExpression(node)) {
|
|
496
|
+
// A **factory** hands the port on, and `ctx.asFunction(f).singleton()`
|
|
497
|
+
// is `f` in a wrapper. Every other call returns data — including
|
|
498
|
+
// `proxy.applyToCart(…)`, which *is* the gated call but whose result
|
|
499
|
+
// is a discount, and `JSON.stringify(payloadFromAPort)`, which is a
|
|
500
|
+
// string. Carriage through an arbitrary callee turned every local
|
|
501
|
+
// downstream of a port into an alias.
|
|
502
|
+
const callee = node.expression;
|
|
503
|
+
const argumentsCarry = () => (node.arguments ?? []).some(carries);
|
|
504
|
+
if (ts.isPropertyAccessExpression(callee)) {
|
|
505
|
+
if (RESOLVER_BUILDERS.has(callee.name.text))
|
|
506
|
+
return carries(callee.expression);
|
|
507
|
+
return RESOLVER_ENTRIES.has(callee.name.text) && argumentsCarry();
|
|
508
|
+
}
|
|
509
|
+
if (!ts.isIdentifier(callee))
|
|
510
|
+
return false;
|
|
511
|
+
return ((RESOLVER_ENTRIES.has(callee.text) || functionNames.has(callee.text)) && argumentsCarry());
|
|
512
|
+
}
|
|
513
|
+
return false;
|
|
514
|
+
};
|
|
515
|
+
/**
|
|
516
|
+
* **Which** gates a carrying expression reaches — the input to D-63's
|
|
517
|
+
* `OWNER LOCKED` derivation.
|
|
518
|
+
*
|
|
519
|
+
* A blunt walk of the whole expression rather than a mirror of
|
|
520
|
+
* {@link carries}: it is only ever asked about a value that already
|
|
521
|
+
* carries, and over-collecting can only add owners, which can only make
|
|
522
|
+
* the "every owner is locked" test harder to pass. Under-collecting is
|
|
523
|
+
* the error that would matter, because it retires a site whose gate an
|
|
524
|
+
* operator can still close.
|
|
525
|
+
*
|
|
526
|
+
* Which is why the lexical narrowing of issue #278 stops here: `reads` is
|
|
527
|
+
* called **without** a node, so a shadowed local still contributes its
|
|
528
|
+
* gates. Shadowing decides whether a site *exists*; it may not decide
|
|
529
|
+
* which owners a site that does exist rests on.
|
|
530
|
+
*/
|
|
531
|
+
const gatesIn = (node) => {
|
|
532
|
+
const found = new Set();
|
|
533
|
+
const record = (name) => {
|
|
534
|
+
if (portOwners.has(name))
|
|
535
|
+
found.add(name);
|
|
536
|
+
for (const gate of gatesAt(name, scope, file))
|
|
537
|
+
found.add(gate);
|
|
538
|
+
};
|
|
539
|
+
const scan = (inner) => {
|
|
540
|
+
if (ts.isTypeNode(inner))
|
|
541
|
+
return;
|
|
542
|
+
if (isProxyCall(inner) && ts.isCallExpression(inner)) {
|
|
543
|
+
const [, nameArgument] = inner.arguments;
|
|
544
|
+
if (nameArgument !== undefined && ts.isStringLiteralLike(nameArgument)) {
|
|
545
|
+
found.add(nameArgument.text);
|
|
546
|
+
}
|
|
547
|
+
return;
|
|
548
|
+
}
|
|
549
|
+
if (ts.isPropertyAccessExpression(inner)) {
|
|
550
|
+
if (reads(inner.name.text))
|
|
551
|
+
record(inner.name.text);
|
|
552
|
+
scan(inner.expression);
|
|
553
|
+
return;
|
|
554
|
+
}
|
|
555
|
+
if (ts.isIdentifier(inner) && reads(inner.text))
|
|
556
|
+
record(inner.text);
|
|
557
|
+
inner.forEachChild(scan);
|
|
558
|
+
};
|
|
559
|
+
scan(node);
|
|
560
|
+
return found;
|
|
561
|
+
};
|
|
562
|
+
const visit = (node) => {
|
|
563
|
+
// `const cartService = lazyPort(…)`, `const recompute = new X(port)`.
|
|
564
|
+
// Scoped to the **file**, because a `const` is: `catalog` renames its
|
|
565
|
+
// bulk-operation service to `queue` in one file and holds a BullMQ
|
|
566
|
+
// queue under the same spelling in another, and a module-wide binding
|
|
567
|
+
// read the second as a port.
|
|
568
|
+
if (ts.isVariableDeclaration(node) &&
|
|
569
|
+
ts.isIdentifier(node.name) &&
|
|
570
|
+
node.initializer &&
|
|
571
|
+
carries(node.initializer)) {
|
|
572
|
+
addAlias(node.name.text, file, at(node), gatesIn(node.initializer));
|
|
573
|
+
// This binding *is* the alias, so it must not read as a shadow of one.
|
|
574
|
+
markCarrier(node);
|
|
575
|
+
}
|
|
576
|
+
// `{ promotion: lazyPort(ctx, 'promotionService') }` — a deps-object key.
|
|
577
|
+
if (ts.isPropertyAssignment(node) && ts.isIdentifier(node.name)) {
|
|
578
|
+
if (carries(node.initializer)) {
|
|
579
|
+
addAlias(node.name.text, scope, at(node), gatesIn(node.initializer));
|
|
580
|
+
}
|
|
581
|
+
}
|
|
582
|
+
if (ts.isShorthandPropertyAssignment(node) && reads(node.name.text)) {
|
|
583
|
+
addAlias(node.name.text, scope, at(node), gatesIn(node.name));
|
|
584
|
+
}
|
|
585
|
+
if (ts.isCallExpression(node) || ts.isNewExpression(node)) {
|
|
586
|
+
// `new CartService(emFactory, lazyPort(…))` — the parameter it lands on.
|
|
587
|
+
const callee = ts.isIdentifier(node.expression) ? node.expression.text : null;
|
|
588
|
+
if (callee !== null && node.arguments) {
|
|
589
|
+
node.arguments.forEach((argument, index) => {
|
|
590
|
+
if (!carries(argument))
|
|
591
|
+
return;
|
|
592
|
+
// The two lookups are kept apart rather than merged into one
|
|
593
|
+
// ternary: `classes` and `functions` hold different declaration
|
|
594
|
+
// shapes, and merging them costs two casts for no gain.
|
|
595
|
+
let declared;
|
|
596
|
+
if (ts.isNewExpression(node)) {
|
|
597
|
+
const owner = resolveClass(file, callee);
|
|
598
|
+
const constructor = owner?.declaration.members.find(ts.isConstructorDeclaration);
|
|
599
|
+
if (owner !== undefined && constructor !== undefined) {
|
|
600
|
+
declared = { file: owner.file, declaration: constructor };
|
|
601
|
+
}
|
|
602
|
+
}
|
|
603
|
+
else {
|
|
604
|
+
declared = resolveFunction(file, callee);
|
|
605
|
+
}
|
|
606
|
+
if (declared === undefined)
|
|
607
|
+
return;
|
|
608
|
+
const parameter = declared.declaration.parameters[index];
|
|
609
|
+
if (parameter && ts.isIdentifier(parameter.name)) {
|
|
610
|
+
// Issue #278 — the **declaring** file, which is where the
|
|
611
|
+
// parameter is in scope, rather than the module the `new`
|
|
612
|
+
// happens to sit in, where it is not in scope at all.
|
|
613
|
+
addAlias(parameter.name.text, declared.file, at(node), gatesIn(argument));
|
|
614
|
+
markCarrier(parameter);
|
|
615
|
+
}
|
|
616
|
+
});
|
|
617
|
+
}
|
|
618
|
+
// A name a **root** contributes is global: it belongs to no module,
|
|
619
|
+
// and whichever module resolves it gets the port behind it. That is
|
|
620
|
+
// the shape the five e-mail notifiers arrived by (issue #113). A
|
|
621
|
+
// module's own `ctx.di.register` key stays module-scoped — it is
|
|
622
|
+
// either a port, and already global by the rule above, or an internal
|
|
623
|
+
// name whose spelling (`mailer`, `client`, `settings`) collides with
|
|
624
|
+
// half the tree.
|
|
625
|
+
if (scope === ROOT && ts.isCallExpression(node) && isContainerRegistration(node)) {
|
|
626
|
+
for (const argument of node.arguments) {
|
|
627
|
+
if (!ts.isObjectLiteralExpression(argument))
|
|
628
|
+
continue;
|
|
629
|
+
for (const property of argument.properties) {
|
|
630
|
+
if (ts.isPropertyAssignment(property) &&
|
|
631
|
+
ts.isIdentifier(property.name) &&
|
|
632
|
+
carries(property.initializer)) {
|
|
633
|
+
addAlias(property.name.text, EVERYWHERE, at(property), gatesIn(property.initializer));
|
|
634
|
+
}
|
|
635
|
+
if (ts.isShorthandPropertyAssignment(property) && reads(property.name.text)) {
|
|
636
|
+
addAlias(property.name.text, EVERYWHERE, at(property), gatesIn(property.name));
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
node.forEachChild(visit);
|
|
643
|
+
};
|
|
644
|
+
sf.forEachChild(visit);
|
|
645
|
+
}
|
|
646
|
+
};
|
|
647
|
+
// Bounded so a pathological tree cannot spin: each round can only add
|
|
648
|
+
// aliases, and the deepest chain the tree holds today is five hops.
|
|
649
|
+
for (let pass = 0; pass < 24; pass += 1) {
|
|
650
|
+
grew = false;
|
|
651
|
+
round();
|
|
652
|
+
if (!grew)
|
|
653
|
+
break;
|
|
654
|
+
}
|
|
655
|
+
return { portOwners, aliases, gatesAt, readsAsPort, parsed };
|
|
656
|
+
}
|
|
657
|
+
/**
|
|
658
|
+
* The file a relative specifier names, among the files read — or `null` for a
|
|
659
|
+
* bare specifier, which names a package and is never another module's private
|
|
660
|
+
* source (D18), or for a path the walk does not hold.
|
|
661
|
+
*/
|
|
662
|
+
function resolveRelative(fromFile, specifier, parsed) {
|
|
663
|
+
if (!specifier.startsWith('./') && !specifier.startsWith('../'))
|
|
664
|
+
return null;
|
|
665
|
+
const base = posix.normalize(posix.join(posix.dirname(fromFile), specifier));
|
|
666
|
+
const stem = base.replace(/\.(?:js|ts|mjs|mts)$/, '');
|
|
667
|
+
for (const candidate of [`${stem}.ts`, `${stem}.tsx`, `${stem}.mts`, `${stem}/index.ts`]) {
|
|
668
|
+
if (parsed.has(candidate))
|
|
669
|
+
return candidate;
|
|
670
|
+
}
|
|
671
|
+
return null;
|
|
672
|
+
}
|
|
673
|
+
function collectDeclarations(parsed) {
|
|
674
|
+
const all = new Map();
|
|
675
|
+
for (const [file, sf] of parsed) {
|
|
676
|
+
const own = {
|
|
677
|
+
classes: new Map(),
|
|
678
|
+
functions: new Map(),
|
|
679
|
+
imports: new Map(),
|
|
680
|
+
reExports: [],
|
|
681
|
+
};
|
|
682
|
+
const collect = (node) => {
|
|
683
|
+
if (ts.isClassDeclaration(node) && node.name)
|
|
684
|
+
own.classes.set(node.name.text, node);
|
|
685
|
+
if (ts.isFunctionDeclaration(node) && node.name)
|
|
686
|
+
own.functions.set(node.name.text, node);
|
|
687
|
+
if (ts.isVariableDeclaration(node) &&
|
|
688
|
+
ts.isIdentifier(node.name) &&
|
|
689
|
+
node.initializer &&
|
|
690
|
+
ts.isArrowFunction(node.initializer)) {
|
|
691
|
+
own.functions.set(node.name.text, node.initializer);
|
|
692
|
+
}
|
|
693
|
+
node.forEachChild(collect);
|
|
694
|
+
};
|
|
695
|
+
sf.forEachChild(collect);
|
|
696
|
+
for (const statement of sf.statements) {
|
|
697
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
698
|
+
const from = resolveRelative(file, statement.moduleSpecifier.text, parsed);
|
|
699
|
+
const bindings = statement.importClause?.namedBindings;
|
|
700
|
+
if (from === null || bindings === undefined || !ts.isNamedImports(bindings))
|
|
701
|
+
continue;
|
|
702
|
+
for (const element of bindings.elements) {
|
|
703
|
+
own.imports.set(element.name.text, {
|
|
704
|
+
from,
|
|
705
|
+
name: (element.propertyName ?? element.name).text,
|
|
706
|
+
});
|
|
707
|
+
}
|
|
708
|
+
}
|
|
709
|
+
if (ts.isExportDeclaration(statement) &&
|
|
710
|
+
statement.moduleSpecifier !== undefined &&
|
|
711
|
+
ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
712
|
+
const from = resolveRelative(file, statement.moduleSpecifier.text, parsed);
|
|
713
|
+
if (from === null)
|
|
714
|
+
continue;
|
|
715
|
+
const clause = statement.exportClause;
|
|
716
|
+
if (clause === undefined) {
|
|
717
|
+
own.reExports.push({ from, names: null });
|
|
718
|
+
}
|
|
719
|
+
else if (ts.isNamedExports(clause)) {
|
|
720
|
+
const names = new Map();
|
|
721
|
+
for (const element of clause.elements) {
|
|
722
|
+
names.set(element.name.text, (element.propertyName ?? element.name).text);
|
|
723
|
+
}
|
|
724
|
+
own.reExports.push({ from, names });
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
}
|
|
728
|
+
all.set(file, own);
|
|
729
|
+
}
|
|
730
|
+
return all;
|
|
731
|
+
}
|
|
732
|
+
/**
|
|
733
|
+
* The declaration `name`, written in `file`, resolves to: the file's own, or
|
|
734
|
+
* the one a relative import names, followed through relative re-exports.
|
|
735
|
+
* Never a declaration some unrelated file happens to spell the same way.
|
|
736
|
+
*/
|
|
737
|
+
function resolveDeclaration(declarations, pick, file, name) {
|
|
738
|
+
const own = (at, exported) => {
|
|
739
|
+
const perFile = declarations.get(at);
|
|
740
|
+
const declaration = perFile === undefined ? undefined : pick(perFile).get(exported);
|
|
741
|
+
return declaration === undefined ? undefined : { file: at, declaration };
|
|
742
|
+
};
|
|
743
|
+
const seen = new Set();
|
|
744
|
+
const exportedFrom = (at, exported) => {
|
|
745
|
+
const visit = `${at}#${exported}`;
|
|
746
|
+
if (seen.has(visit))
|
|
747
|
+
return undefined;
|
|
748
|
+
seen.add(visit);
|
|
749
|
+
const direct = own(at, exported);
|
|
750
|
+
if (direct !== undefined)
|
|
751
|
+
return direct;
|
|
752
|
+
const perFile = declarations.get(at);
|
|
753
|
+
if (perFile === undefined)
|
|
754
|
+
return undefined;
|
|
755
|
+
// `import { x } from './y.js'; export { x };` is not followed: no file in
|
|
756
|
+
// the tree re-exports that way, and following it would be one more shape
|
|
757
|
+
// nothing exercises.
|
|
758
|
+
for (const reExport of perFile.reExports) {
|
|
759
|
+
if (reExport.names === null) {
|
|
760
|
+
const found = exportedFrom(reExport.from, exported);
|
|
761
|
+
if (found !== undefined)
|
|
762
|
+
return found;
|
|
763
|
+
}
|
|
764
|
+
else {
|
|
765
|
+
const original = reExport.names.get(exported);
|
|
766
|
+
if (original !== undefined) {
|
|
767
|
+
const found = exportedFrom(reExport.from, original);
|
|
768
|
+
if (found !== undefined)
|
|
769
|
+
return found;
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
return undefined;
|
|
774
|
+
};
|
|
775
|
+
const local = own(file, name);
|
|
776
|
+
if (local !== undefined)
|
|
777
|
+
return local;
|
|
778
|
+
const imported = declarations.get(file)?.imports.get(name);
|
|
779
|
+
return imported === undefined ? undefined : exportedFrom(imported.from, imported.name);
|
|
780
|
+
}
|
|
781
|
+
/**
|
|
782
|
+
* Does a closure's body reach the gate? A `() => cradle().portName()` has not
|
|
783
|
+
* resolved anything yet, so the closure itself carries and every name it is
|
|
784
|
+
* bound to is an alias.
|
|
785
|
+
*
|
|
786
|
+
* Property *names* and parameter *names* are skipped: `{ promotion: 1 }`
|
|
787
|
+
* mentions an alias without reading one.
|
|
788
|
+
*/
|
|
789
|
+
function bodyReads(fn, reads) {
|
|
790
|
+
let hit = false;
|
|
791
|
+
const scan = (node) => {
|
|
792
|
+
if (hit || ts.isTypeNode(node))
|
|
793
|
+
return;
|
|
794
|
+
if (ts.isPropertyAccessExpression(node)) {
|
|
795
|
+
if (reads(node.name.text)) {
|
|
796
|
+
hit = true;
|
|
797
|
+
return;
|
|
798
|
+
}
|
|
799
|
+
scan(node.expression);
|
|
800
|
+
return;
|
|
801
|
+
}
|
|
802
|
+
if (ts.isPropertyAssignment(node)) {
|
|
803
|
+
scan(node.initializer);
|
|
804
|
+
return;
|
|
805
|
+
}
|
|
806
|
+
if (ts.isParameter(node)) {
|
|
807
|
+
if (node.initializer)
|
|
808
|
+
scan(node.initializer);
|
|
809
|
+
return;
|
|
810
|
+
}
|
|
811
|
+
// Issue #278 — a bare identifier inside the closure resolves lexically, so
|
|
812
|
+
// a `const` the closure binds itself does not make the closure a carrier.
|
|
813
|
+
if (ts.isIdentifier(node) && reads(node.text, node)) {
|
|
814
|
+
hit = true;
|
|
815
|
+
return;
|
|
816
|
+
}
|
|
817
|
+
node.forEachChild(scan);
|
|
818
|
+
};
|
|
819
|
+
scan(fn.body);
|
|
820
|
+
return hit;
|
|
821
|
+
}
|
|
822
|
+
/** The trailing identifier of a receiver: `this.deps.x` → `x`, `y` → `y`. */
|
|
823
|
+
function tailName(node) {
|
|
824
|
+
if (ts.isIdentifier(node))
|
|
825
|
+
return node.text;
|
|
826
|
+
if (ts.isPropertyAccessExpression(node))
|
|
827
|
+
return node.name.text;
|
|
828
|
+
if (ts.isNonNullExpression(node) || ts.isParenthesizedExpression(node)) {
|
|
829
|
+
return tailName(node.expression);
|
|
830
|
+
}
|
|
831
|
+
return null;
|
|
832
|
+
}
|
|
833
|
+
/**
|
|
834
|
+
* Functions the `catch` may hand the error to and still be handling it.
|
|
835
|
+
*
|
|
836
|
+
* Two shapes exist in the tree, and both are correct code the literal rule read
|
|
837
|
+
* as a violation: `toCatalogHttpError(err, key): never`, whose last statement is
|
|
838
|
+
* `throw err`, and `ReturnEmailNotifier#contained(rc, kind, error)`, which calls
|
|
839
|
+
* `rethrowIfModuleDisabled` on the caller's behalf. The requirement is narrow on
|
|
840
|
+
* purpose — the delegate must **end** by re-throwing its own parameter, or name
|
|
841
|
+
* the kernel's narrowing. A helper that ends by throwing something it built
|
|
842
|
+
* itself converts the presence answer and does not qualify.
|
|
843
|
+
*/
|
|
844
|
+
function collectRethrowDelegates(parsed) {
|
|
845
|
+
const delegates = new Set();
|
|
846
|
+
const qualifies = (parameters, body) => {
|
|
847
|
+
if (body === undefined || !ts.isBlock(body))
|
|
848
|
+
return false;
|
|
849
|
+
let names = false;
|
|
850
|
+
const scan = (node) => {
|
|
851
|
+
if (ts.isIdentifier(node) &&
|
|
852
|
+
(node.text === 'rethrowIfModuleDisabled' || node.text === 'ModuleDisabledError')) {
|
|
853
|
+
names = true;
|
|
854
|
+
}
|
|
855
|
+
node.forEachChild(scan);
|
|
856
|
+
};
|
|
857
|
+
body.forEachChild(scan);
|
|
858
|
+
if (names)
|
|
859
|
+
return true;
|
|
860
|
+
const last = body.statements.at(-1);
|
|
861
|
+
if (!last || !ts.isThrowStatement(last) || !last.expression)
|
|
862
|
+
return false;
|
|
863
|
+
if (!ts.isIdentifier(last.expression))
|
|
864
|
+
return false;
|
|
865
|
+
const thrown = last.expression.text;
|
|
866
|
+
return parameters.some((parameter) => ts.isIdentifier(parameter.name) && parameter.name.text === thrown);
|
|
867
|
+
};
|
|
868
|
+
for (const sf of parsed) {
|
|
869
|
+
const collect = (node) => {
|
|
870
|
+
if ((ts.isFunctionDeclaration(node) || ts.isMethodDeclaration(node)) &&
|
|
871
|
+
node.name &&
|
|
872
|
+
ts.isIdentifier(node.name) &&
|
|
873
|
+
qualifies(node.parameters, node.body)) {
|
|
874
|
+
delegates.add(node.name.text);
|
|
875
|
+
}
|
|
876
|
+
if (ts.isVariableDeclaration(node) &&
|
|
877
|
+
ts.isIdentifier(node.name) &&
|
|
878
|
+
node.initializer &&
|
|
879
|
+
(ts.isArrowFunction(node.initializer) || ts.isFunctionExpression(node.initializer)) &&
|
|
880
|
+
qualifies(node.initializer.parameters, node.initializer.body)) {
|
|
881
|
+
delegates.add(node.name.text);
|
|
882
|
+
}
|
|
883
|
+
node.forEachChild(collect);
|
|
884
|
+
};
|
|
885
|
+
sf.forEachChild(collect);
|
|
886
|
+
}
|
|
887
|
+
return delegates;
|
|
888
|
+
}
|
|
889
|
+
/** The kernel's one-line narrowing, by the name every handler spells it. */
|
|
890
|
+
const NARROWING = 'rethrowIfModuleDisabled';
|
|
891
|
+
/**
|
|
892
|
+
* Does a handler body let `ModuleDisabledError` through?
|
|
893
|
+
*
|
|
894
|
+
* One predicate for both forms, deliberately: a `catch` block and a
|
|
895
|
+
* `.catch(handler)` body answer the same question, and two implementations of
|
|
896
|
+
* it are two answers waiting to disagree — which is how the promise form came
|
|
897
|
+
* to be unjudged in the first place.
|
|
898
|
+
*
|
|
899
|
+
* A **block** whose last statement is a `throw` re-throws unconditionally. An
|
|
900
|
+
* **expression** body (a concise arrow) cannot throw at all, so it qualifies
|
|
901
|
+
* only by naming the narrowing or a delegate. `catch (e) { if (rare) throw e }`
|
|
902
|
+
* and `.catch((e) => { if (rare) throw e })` both fail the last-statement test
|
|
903
|
+
* and both are violations: `ModuleDisabledError` is an `HttpError`, so a
|
|
904
|
+
* status-code test lets it through by accident rather than by decision.
|
|
905
|
+
*/
|
|
906
|
+
function bodyHandles(body, delegates) {
|
|
907
|
+
if (ts.isBlock(body)) {
|
|
908
|
+
const last = body.statements.at(-1);
|
|
909
|
+
if (last && ts.isThrowStatement(last))
|
|
910
|
+
return true;
|
|
911
|
+
}
|
|
912
|
+
let named = false;
|
|
913
|
+
const scan = (node) => {
|
|
914
|
+
if (ts.isIdentifier(node) && node.text === NARROWING)
|
|
915
|
+
named = true;
|
|
916
|
+
if (ts.isIdentifier(node) && node.text === 'ModuleDisabledError')
|
|
917
|
+
named = true;
|
|
918
|
+
if (ts.isCallExpression(node)) {
|
|
919
|
+
const callee = ts.isPropertyAccessExpression(node.expression)
|
|
920
|
+
? node.expression.name.text
|
|
921
|
+
: ts.isIdentifier(node.expression)
|
|
922
|
+
? node.expression.text
|
|
923
|
+
: null;
|
|
924
|
+
if (callee !== null && delegates.has(callee))
|
|
925
|
+
named = true;
|
|
926
|
+
}
|
|
927
|
+
node.forEachChild(scan);
|
|
928
|
+
};
|
|
929
|
+
scan(body);
|
|
930
|
+
return named;
|
|
931
|
+
}
|
|
932
|
+
/** Does this `catch` let `ModuleDisabledError` through? */
|
|
933
|
+
function handles(clause, delegates) {
|
|
934
|
+
return bodyHandles(clause.block, delegates);
|
|
935
|
+
}
|
|
936
|
+
function rejectionHandlerOf(node) {
|
|
937
|
+
if (!ts.isCallExpression(node) || !ts.isPropertyAccessExpression(node.expression))
|
|
938
|
+
return null;
|
|
939
|
+
const method = node.expression.name.text;
|
|
940
|
+
const guarded = node.expression.expression;
|
|
941
|
+
if (method === 'catch') {
|
|
942
|
+
const [handler] = node.arguments;
|
|
943
|
+
// `.catch()` with no argument consumes nothing; it is not a site.
|
|
944
|
+
return handler === undefined ? null : { guarded, handler };
|
|
945
|
+
}
|
|
946
|
+
if (method === 'then') {
|
|
947
|
+
const handler = node.arguments[1];
|
|
948
|
+
return handler === undefined ? null : { guarded, handler };
|
|
949
|
+
}
|
|
950
|
+
return null;
|
|
951
|
+
}
|
|
952
|
+
/**
|
|
953
|
+
* Does a promise-form handler let `ModuleDisabledError` through?
|
|
954
|
+
*
|
|
955
|
+
* Three ways, and the first two have no counterpart in the statement form
|
|
956
|
+
* because a `catch` block cannot be written as a reference: the narrowing
|
|
957
|
+
* handed over directly (`.catch(rethrowIfModuleDisabled)`), a re-throwing
|
|
958
|
+
* delegate handed over directly (`.catch(toCatalogHttpError)` — the same
|
|
959
|
+
* delegates the statement form accepts being *called*), and a function
|
|
960
|
+
* expression whose body qualifies under {@link bodyHandles}.
|
|
961
|
+
*
|
|
962
|
+
* Anything else — a bare identifier the analysis cannot see through, a logger,
|
|
963
|
+
* `() => undefined` — is a violation, in the direction the doubt has to fail.
|
|
964
|
+
*/
|
|
965
|
+
function handlerHandles(handler, delegates) {
|
|
966
|
+
if (ts.isArrowFunction(handler) || ts.isFunctionExpression(handler)) {
|
|
967
|
+
return bodyHandles(handler.body, delegates);
|
|
968
|
+
}
|
|
969
|
+
const named = tailName(handler);
|
|
970
|
+
if (named === null)
|
|
971
|
+
return false;
|
|
972
|
+
return named === NARROWING || delegates.has(named);
|
|
973
|
+
}
|
|
974
|
+
/**
|
|
975
|
+
* How many rounds the method fixpoint runs before it gives up.
|
|
976
|
+
*
|
|
977
|
+
* The same cap the alias table uses, for the same reason: a chain of private
|
|
978
|
+
* methods is bounded in practice, and a cap makes "did not converge" a bug
|
|
979
|
+
* report rather than a hang.
|
|
980
|
+
*/
|
|
981
|
+
const METHOD_HOP_ROUNDS = 12;
|
|
982
|
+
/** The name of a class member as `this.<name>` would spell it, or `null`. */
|
|
983
|
+
function memberName(name) {
|
|
984
|
+
if (name === undefined)
|
|
985
|
+
return null;
|
|
986
|
+
if (ts.isIdentifier(name) || ts.isPrivateIdentifier(name))
|
|
987
|
+
return name.text;
|
|
988
|
+
if (ts.isStringLiteral(name))
|
|
989
|
+
return name.text;
|
|
990
|
+
return null;
|
|
991
|
+
}
|
|
992
|
+
/** `this.<name>` / `this.#name`, as the receiver of a call — the name, or `null`. */
|
|
993
|
+
function thisMethodCalled(node) {
|
|
994
|
+
if (!ts.isPropertyAccessExpression(node.expression))
|
|
995
|
+
return null;
|
|
996
|
+
if (node.expression.expression.kind !== ts.SyntaxKind.ThisKeyword)
|
|
997
|
+
return null;
|
|
998
|
+
const name = node.expression.name;
|
|
999
|
+
return ts.isIdentifier(name) || ts.isPrivateIdentifier(name) ? name.text : null;
|
|
1000
|
+
}
|
|
1001
|
+
/** The class a node sits inside, by name — `null` for a class expression. */
|
|
1002
|
+
function enclosingClassName(node) {
|
|
1003
|
+
for (let at = node; at !== undefined; at = at.parent) {
|
|
1004
|
+
if (ts.isClassDeclaration(at))
|
|
1005
|
+
return at.name?.text ?? null;
|
|
1006
|
+
}
|
|
1007
|
+
return null;
|
|
1008
|
+
}
|
|
1009
|
+
/** `<file>#<class>#<method>` — a method's identity for the hop. */
|
|
1010
|
+
function methodKey(file, className, method) {
|
|
1011
|
+
return `${file}#${className}#${method}`;
|
|
1012
|
+
}
|
|
1013
|
+
/**
|
|
1014
|
+
* Which methods carry a gate into a `catch` around a call to them (D-88).
|
|
1015
|
+
*
|
|
1016
|
+
* Two passes and then a fixpoint. The first records, per class member, the port
|
|
1017
|
+
* or alias names its body reaches directly — the same three call shapes the
|
|
1018
|
+
* `try` scan reads, so a method and a `try` block agree about what a port call
|
|
1019
|
+
* looks like by construction. The second records `this.<name>(…)` edges inside
|
|
1020
|
+
* the class. The fixpoint then walks the edges backwards until nothing new
|
|
1021
|
+
* appears, which is what makes a private method calling a private method that
|
|
1022
|
+
* reaches a gate carry as well.
|
|
1023
|
+
*
|
|
1024
|
+
* Nothing crosses a class boundary here, and nothing crosses a file boundary:
|
|
1025
|
+
* that is the whole limit, and it is stated in the header because one hop is a
|
|
1026
|
+
* limit too.
|
|
1027
|
+
*/
|
|
1028
|
+
function collectCarryingMethods(parsed, readsAsPortIn, declaresMember) {
|
|
1029
|
+
/** method key → the port/alias names its body reaches. */
|
|
1030
|
+
const carries = new Map();
|
|
1031
|
+
/** method key → the method keys it calls through `this`. */
|
|
1032
|
+
const edges = new Map();
|
|
1033
|
+
for (const [file, sf] of parsed) {
|
|
1034
|
+
const reads = (name, at) => readsAsPortIn(file, name, at);
|
|
1035
|
+
const record = (className, method, body) => {
|
|
1036
|
+
const key = methodKey(file, className, method);
|
|
1037
|
+
const ports = carries.get(key) ?? new Set();
|
|
1038
|
+
const calls = edges.get(key) ?? new Set();
|
|
1039
|
+
const scan = (inner) => {
|
|
1040
|
+
if (ts.isCallExpression(inner)) {
|
|
1041
|
+
const hop = thisMethodCalled(inner);
|
|
1042
|
+
if (hop !== null && declaresMember(file, className, hop)) {
|
|
1043
|
+
calls.add(methodKey(file, className, hop));
|
|
1044
|
+
}
|
|
1045
|
+
else if (ts.isPropertyAccessExpression(inner.expression)) {
|
|
1046
|
+
const receiver = tailName(inner.expression.expression);
|
|
1047
|
+
// Issue #278 — the receiver resolves lexically when it is a bare
|
|
1048
|
+
// identifier (`transitionService.apply(…)`); `this.deps.promotion`
|
|
1049
|
+
// is a property chain and has no lexical binding to resolve.
|
|
1050
|
+
const receiverNode = ts.isIdentifier(inner.expression.expression)
|
|
1051
|
+
? inner.expression.expression
|
|
1052
|
+
: undefined;
|
|
1053
|
+
if (receiver !== null && reads(receiver, receiverNode))
|
|
1054
|
+
ports.add(receiver);
|
|
1055
|
+
else if (reads(inner.expression.name.text))
|
|
1056
|
+
ports.add(inner.expression.name.text);
|
|
1057
|
+
}
|
|
1058
|
+
if (ts.isIdentifier(inner.expression) &&
|
|
1059
|
+
inner.expression.text !== 'lazyPort' &&
|
|
1060
|
+
reads(inner.expression.text, inner.expression)) {
|
|
1061
|
+
ports.add(inner.expression.text);
|
|
1062
|
+
}
|
|
1063
|
+
if (ts.isIdentifier(inner.expression) && inner.expression.text === 'requireModuleEnabled') {
|
|
1064
|
+
ports.add('requireModuleEnabled');
|
|
1065
|
+
}
|
|
1066
|
+
}
|
|
1067
|
+
inner.forEachChild(scan);
|
|
1068
|
+
};
|
|
1069
|
+
body.forEachChild(scan);
|
|
1070
|
+
carries.set(key, ports);
|
|
1071
|
+
edges.set(key, calls);
|
|
1072
|
+
};
|
|
1073
|
+
for (const [className, members] of classesIn(sf)) {
|
|
1074
|
+
for (const [name, body] of members)
|
|
1075
|
+
record(className, name, body);
|
|
1076
|
+
}
|
|
1077
|
+
}
|
|
1078
|
+
for (let round = 0; round < METHOD_HOP_ROUNDS; round += 1) {
|
|
1079
|
+
let grew = false;
|
|
1080
|
+
for (const [key, calls] of edges) {
|
|
1081
|
+
const ports = carries.get(key);
|
|
1082
|
+
if (ports === undefined)
|
|
1083
|
+
continue;
|
|
1084
|
+
for (const callee of calls) {
|
|
1085
|
+
for (const port of carries.get(callee) ?? []) {
|
|
1086
|
+
if (!ports.has(port)) {
|
|
1087
|
+
ports.add(port);
|
|
1088
|
+
grew = true;
|
|
1089
|
+
}
|
|
1090
|
+
}
|
|
1091
|
+
}
|
|
1092
|
+
}
|
|
1093
|
+
if (!grew)
|
|
1094
|
+
break;
|
|
1095
|
+
}
|
|
1096
|
+
// A method that reaches nothing is not a carrier; dropping it here keeps the
|
|
1097
|
+
// lookup at the `try` site a presence test rather than a size test.
|
|
1098
|
+
for (const [key, ports] of [...carries])
|
|
1099
|
+
if (ports.size === 0)
|
|
1100
|
+
carries.delete(key);
|
|
1101
|
+
return carries;
|
|
1102
|
+
}
|
|
1103
|
+
/** Class name → its method-shaped members, by the name `this.<x>` spells. */
|
|
1104
|
+
function classesIn(sf) {
|
|
1105
|
+
const classes = new Map();
|
|
1106
|
+
const visit = (node) => {
|
|
1107
|
+
if (ts.isClassDeclaration(node) && node.name) {
|
|
1108
|
+
const members = classes.get(node.name.text) ?? new Map();
|
|
1109
|
+
for (const member of node.members) {
|
|
1110
|
+
const name = memberName(member.name);
|
|
1111
|
+
if (name === null)
|
|
1112
|
+
continue;
|
|
1113
|
+
if (ts.isMethodDeclaration(member) && member.body)
|
|
1114
|
+
members.set(name, member.body);
|
|
1115
|
+
else if (ts.isPropertyDeclaration(member) &&
|
|
1116
|
+
member.initializer &&
|
|
1117
|
+
(ts.isArrowFunction(member.initializer) || ts.isFunctionExpression(member.initializer))) {
|
|
1118
|
+
members.set(name, member.initializer.body);
|
|
1119
|
+
}
|
|
1120
|
+
}
|
|
1121
|
+
classes.set(node.name.text, members);
|
|
1122
|
+
}
|
|
1123
|
+
node.forEachChild(visit);
|
|
1124
|
+
};
|
|
1125
|
+
sf.forEachChild(visit);
|
|
1126
|
+
return classes;
|
|
1127
|
+
}
|
|
1128
|
+
/** Every `try` and every promise-form rejection handler that reaches a gated port. */
|
|
1129
|
+
export function findPortCatches(input) {
|
|
1130
|
+
return scanSources(input).found;
|
|
1131
|
+
}
|
|
1132
|
+
function scanSources(input) {
|
|
1133
|
+
const hostResident = input.hostResidentModules ?? NO_HOST_RESIDENT_MODULES;
|
|
1134
|
+
const analysis = analyze(input.sources, hostResident, input.peerOwners ?? new Map());
|
|
1135
|
+
const locked = lockedOwners(input.manifests ?? []);
|
|
1136
|
+
/**
|
|
1137
|
+
* D-63 — every gate this alias carries is owned by a module the platform
|
|
1138
|
+
* refuses to switch off, so the `catch` has no reachable presence answer to
|
|
1139
|
+
* swallow. An alias carrying no gate at all (`requireModuleEnabled`) is never
|
|
1140
|
+
* locked: nothing is known about what it answers.
|
|
1141
|
+
*/
|
|
1142
|
+
const isOwnerLocked = (gates) => gates.length > 0 &&
|
|
1143
|
+
gates.every((gate) => {
|
|
1144
|
+
const owner = analysis.portOwners.get(gate);
|
|
1145
|
+
return owner !== undefined && locked.has(owner);
|
|
1146
|
+
});
|
|
1147
|
+
const found = [];
|
|
1148
|
+
/** Every `.catch(h)` / `.then(ok, onErr)` the walk read — see {@link SiteScan}. */
|
|
1149
|
+
let rejectionHandlerSites = 0;
|
|
1150
|
+
// The analysis's own ASTs, not a second parse of the same text: the shadowing
|
|
1151
|
+
// rule (issue #278) records **declaration nodes** as carriers, and node
|
|
1152
|
+
// identity only holds across one parse.
|
|
1153
|
+
const { parsed } = analysis;
|
|
1154
|
+
const delegates = collectRethrowDelegates(parsed.values());
|
|
1155
|
+
/**
|
|
1156
|
+
* The classes each file declares, by member name.
|
|
1157
|
+
*
|
|
1158
|
+
* It decides a **shadowing** rule the pre-D-88 check got wrong in one
|
|
1159
|
+
* direction: `this.close(…)` where `close` is a method of the enclosing class
|
|
1160
|
+
* is that method, never a module-scoped alias that happens to share the
|
|
1161
|
+
* spelling. `product_feeds` holds both — a plugin closure bound to `close` and
|
|
1162
|
+
* a `TaxonomyRefreshService#close` that writes a check row — and without this
|
|
1163
|
+
* the hop reported the second as though it reached the first's twenty gates.
|
|
1164
|
+
*/
|
|
1165
|
+
const declaredMembers = new Map();
|
|
1166
|
+
for (const [file, sf] of parsed) {
|
|
1167
|
+
declaredMembers.set(file, new Map([...classesIn(sf)].map(([name, members]) => [name, new Set(members.keys())])));
|
|
1168
|
+
}
|
|
1169
|
+
const declaresMember = (file, className, name) => declaredMembers.get(file)?.get(className)?.has(name) === true;
|
|
1170
|
+
const carryingMethods = collectCarryingMethods(parsed, (file, name, at) => analysis.readsAsPort(name, moduleOf(`/src/${file}`, hostResident) ?? ROOT, file, at), declaresMember);
|
|
1171
|
+
for (const [file] of input.sources) {
|
|
1172
|
+
const moduleId = moduleOf(`/src/${file}`, hostResident) ?? ROOT;
|
|
1173
|
+
const sf = parsed.get(file);
|
|
1174
|
+
const readsAsPort = (name, at) => analysis.readsAsPort(name, moduleId, file, at);
|
|
1175
|
+
/**
|
|
1176
|
+
* One site, whichever spelling it is written in.
|
|
1177
|
+
*
|
|
1178
|
+
* `guardedRoots` are the expressions the handler would absorb a throw from
|
|
1179
|
+
* — the `try` block's statements, or the receiver chain a `.catch` hangs
|
|
1180
|
+
* off. Everything below that point is identical for the two forms by
|
|
1181
|
+
* construction, which is what stops the promise form drifting into a
|
|
1182
|
+
* second, weaker rule.
|
|
1183
|
+
*/
|
|
1184
|
+
const record = (guardedRoots, at, handled, form) => {
|
|
1185
|
+
/**
|
|
1186
|
+
* The name the site is reported under → the port/alias names it stands
|
|
1187
|
+
* for. Identity for a direct reach; for a D-88 method hop the key is the
|
|
1188
|
+
* method and the value is what its body reaches, so `gates` — and with
|
|
1189
|
+
* it the `OWNER LOCKED` derivation — comes out unchanged.
|
|
1190
|
+
*/
|
|
1191
|
+
const ports = new Map();
|
|
1192
|
+
const add = (name, via) => {
|
|
1193
|
+
const carried = ports.get(name) ?? new Set();
|
|
1194
|
+
for (const one of via)
|
|
1195
|
+
carried.add(one);
|
|
1196
|
+
ports.set(name, carried);
|
|
1197
|
+
};
|
|
1198
|
+
const className = enclosingClassName(at);
|
|
1199
|
+
/** D-88's shadowing rule — see {@link declaredMembers}. */
|
|
1200
|
+
const ownMethod = (inner) => {
|
|
1201
|
+
const method = className === null ? null : thisMethodCalled(inner);
|
|
1202
|
+
return method !== null && className !== null && declaresMember(file, className, method)
|
|
1203
|
+
? method
|
|
1204
|
+
: null;
|
|
1205
|
+
};
|
|
1206
|
+
const scan = (inner) => {
|
|
1207
|
+
if (ts.isCallExpression(inner) &&
|
|
1208
|
+
ts.isPropertyAccessExpression(inner.expression) &&
|
|
1209
|
+
ownMethod(inner) === null) {
|
|
1210
|
+
const receiver = tailName(inner.expression.expression);
|
|
1211
|
+
// Issue #278 — see the twin in `collectCarryingMethods`: a bare
|
|
1212
|
+
// identifier receiver resolves lexically, a property chain cannot.
|
|
1213
|
+
const receiverNode = ts.isIdentifier(inner.expression.expression)
|
|
1214
|
+
? inner.expression.expression
|
|
1215
|
+
: undefined;
|
|
1216
|
+
if (receiver !== null && readsAsPort(receiver, receiverNode)) {
|
|
1217
|
+
add(receiver, [receiver]);
|
|
1218
|
+
}
|
|
1219
|
+
else if (readsAsPort(inner.expression.name.text)) {
|
|
1220
|
+
// `this.deps.getTransactionalEmailSender()` — the alias is the
|
|
1221
|
+
// thing being called, not the object it hangs off.
|
|
1222
|
+
add(inner.expression.name.text, [inner.expression.name.text]);
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
if (ts.isCallExpression(inner) &&
|
|
1226
|
+
ts.isIdentifier(inner.expression) &&
|
|
1227
|
+
inner.expression.text !== 'lazyPort' &&
|
|
1228
|
+
readsAsPort(inner.expression.text, inner.expression)) {
|
|
1229
|
+
add(inner.expression.text, [inner.expression.text]);
|
|
1230
|
+
}
|
|
1231
|
+
// `requireModuleEnabled('x')` throws the same error, on purpose.
|
|
1232
|
+
if (ts.isCallExpression(inner) &&
|
|
1233
|
+
ts.isIdentifier(inner.expression) &&
|
|
1234
|
+
inner.expression.text === 'requireModuleEnabled') {
|
|
1235
|
+
add('requireModuleEnabled', ['requireModuleEnabled']);
|
|
1236
|
+
}
|
|
1237
|
+
// D-88 — one hop backwards, through `this` and nothing else.
|
|
1238
|
+
if (ts.isCallExpression(inner) && className !== null) {
|
|
1239
|
+
const method = ownMethod(inner);
|
|
1240
|
+
const carried = method === null ? undefined : carryingMethods.get(methodKey(file, className, method));
|
|
1241
|
+
if (method !== null && carried !== undefined)
|
|
1242
|
+
add(method, carried);
|
|
1243
|
+
}
|
|
1244
|
+
inner.forEachChild(scan);
|
|
1245
|
+
};
|
|
1246
|
+
for (const root of guardedRoots)
|
|
1247
|
+
scan(root);
|
|
1248
|
+
if (ports.size > 0) {
|
|
1249
|
+
const line = sf.getLineAndCharacterOfPosition(at.getStart(sf)).line + 1;
|
|
1250
|
+
for (const [port, via] of ports) {
|
|
1251
|
+
const gates = [
|
|
1252
|
+
...new Set([...via].flatMap((one) => [
|
|
1253
|
+
...(analysis.portOwners.has(one) ? [one] : []),
|
|
1254
|
+
...analysis.gatesAt(one, moduleId, file),
|
|
1255
|
+
])),
|
|
1256
|
+
].sort();
|
|
1257
|
+
found.push({
|
|
1258
|
+
file,
|
|
1259
|
+
line,
|
|
1260
|
+
moduleId,
|
|
1261
|
+
port,
|
|
1262
|
+
form,
|
|
1263
|
+
handled,
|
|
1264
|
+
gates,
|
|
1265
|
+
gateOwners: [
|
|
1266
|
+
...new Set(gates
|
|
1267
|
+
.map((gate) => analysis.portOwners.get(gate))
|
|
1268
|
+
.filter((owner) => owner !== undefined)),
|
|
1269
|
+
].sort(),
|
|
1270
|
+
ownerLocked: isOwnerLocked(gates),
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
}
|
|
1274
|
+
};
|
|
1275
|
+
const visit = (node) => {
|
|
1276
|
+
if (ts.isTryStatement(node) && node.catchClause) {
|
|
1277
|
+
record([...node.tryBlock.statements], node, handles(node.catchClause, delegates), 'statement');
|
|
1278
|
+
}
|
|
1279
|
+
const rejection = rejectionHandlerOf(node);
|
|
1280
|
+
if (rejection !== null) {
|
|
1281
|
+
// Counted before the port test, and over every rejection handler in the
|
|
1282
|
+
// tree: it is the population floor, and a floor derived from the
|
|
1283
|
+
// findings would be satisfied by a recogniser that had stopped working.
|
|
1284
|
+
rejectionHandlerSites += 1;
|
|
1285
|
+
record([rejection.guarded], node, handlerHandles(rejection.handler, delegates), 'promise');
|
|
1286
|
+
}
|
|
1287
|
+
node.forEachChild(visit);
|
|
1288
|
+
};
|
|
1289
|
+
sf.forEachChild(visit);
|
|
1290
|
+
}
|
|
1291
|
+
found.sort((a, b) => (a.file === b.file ? a.line - b.line : a.file.localeCompare(b.file)));
|
|
1292
|
+
return { found, rejectionHandlerSites };
|
|
1293
|
+
}
|
|
1294
|
+
export function checkPortCatches(input, ledger) {
|
|
1295
|
+
const { found: all, rejectionHandlerSites } = scanSources(input);
|
|
1296
|
+
const unhandled = all.filter((entry) => !entry.handled);
|
|
1297
|
+
const ownerLocked = unhandled.filter((entry) => entry.ownerLocked);
|
|
1298
|
+
const open = unhandled.filter((entry) => !entry.ownerLocked);
|
|
1299
|
+
// An `OWNER LOCKED` site is deliberately **not** in the key set: a ledger
|
|
1300
|
+
// entry over one therefore reads stale and has to go, which is what makes the
|
|
1301
|
+
// classification re-red the site if the lock is ever withdrawn (D-63).
|
|
1302
|
+
const keys = new Set(open.map(keyOf));
|
|
1303
|
+
return {
|
|
1304
|
+
total: all.length,
|
|
1305
|
+
violations: open.filter((entry) => ledger[keyOf(entry)] === undefined),
|
|
1306
|
+
ledgered: open.filter((entry) => ledger[keyOf(entry)] !== undefined),
|
|
1307
|
+
ownerLocked,
|
|
1308
|
+
stale: Object.keys(ledger).filter((key) => !keys.has(key)),
|
|
1309
|
+
rejectionHandlerSites,
|
|
1310
|
+
};
|
|
1311
|
+
}
|
|
1312
|
+
/**
|
|
1313
|
+
* Every source this rule reads under `roots`.
|
|
1314
|
+
*
|
|
1315
|
+
* One spelling, both hosts: the repository host supplies its source roots and the
|
|
1316
|
+
* package host supplies one package's, and neither gets to differ about which
|
|
1317
|
+
* files are in the population.
|
|
1318
|
+
*/
|
|
1319
|
+
export function collectPortCatchFiles(roots, out = []) {
|
|
1320
|
+
for (const root of roots) {
|
|
1321
|
+
if (!existsSync(root))
|
|
1322
|
+
continue;
|
|
1323
|
+
for (const name of readdirSync(root)) {
|
|
1324
|
+
const full = join(root, name);
|
|
1325
|
+
if (statSync(full).isDirectory()) {
|
|
1326
|
+
if (name === 'node_modules' || name === 'dist')
|
|
1327
|
+
continue;
|
|
1328
|
+
collectPortCatchFiles([full], out);
|
|
1329
|
+
}
|
|
1330
|
+
else if (name.endsWith('.ts') && !name.endsWith('.test.ts') && !name.endsWith('.d.ts')) {
|
|
1331
|
+
out.push(full);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
}
|
|
1335
|
+
return out;
|
|
1336
|
+
}
|
|
1337
|
+
/**
|
|
1338
|
+
* Every gated-port name the sources **write**, owned or not — the denominator.
|
|
1339
|
+
*
|
|
1340
|
+
* This is the one thing the rest of the analysis structurally cannot report. A
|
|
1341
|
+
* `lazyPort(ctx, '<name>')` whose `<name>` is in no owner map never becomes a
|
|
1342
|
+
* site, never becomes an alias and never becomes a finding: it disappears, and
|
|
1343
|
+
* the disappearance is indistinguishable from a package that resolves nothing.
|
|
1344
|
+
* So the count is taken here, before the owner map is consulted at all, and a
|
|
1345
|
+
* host prints `sources=owners:<attributed>/<written>` from it.
|
|
1346
|
+
*
|
|
1347
|
+
* Deliberately **not** gated on `portOwners`, and deliberately blind to whether
|
|
1348
|
+
* the name is really a port: what a host needs is *how many names this package
|
|
1349
|
+
* asked me to attribute*, and every `lazyPort` literal is one of those. A
|
|
1350
|
+
* non-literal name is not counted — it cannot be attributed by anyone, so
|
|
1351
|
+
* counting it would make the denominator permanently unreachable.
|
|
1352
|
+
*/
|
|
1353
|
+
export function resolvedPortNames(sources) {
|
|
1354
|
+
const names = new Set();
|
|
1355
|
+
for (const [file, text] of sources) {
|
|
1356
|
+
if (!text.includes('lazyPort'))
|
|
1357
|
+
continue;
|
|
1358
|
+
const sf = ts.createSourceFile(file, text, ts.ScriptTarget.Latest, true);
|
|
1359
|
+
const visit = (node) => {
|
|
1360
|
+
if (ts.isCallExpression(node) &&
|
|
1361
|
+
ts.isIdentifier(node.expression) &&
|
|
1362
|
+
node.expression.text === 'lazyPort') {
|
|
1363
|
+
const [, nameArgument] = node.arguments;
|
|
1364
|
+
if (nameArgument !== undefined && ts.isStringLiteralLike(nameArgument)) {
|
|
1365
|
+
names.add(nameArgument.text);
|
|
1366
|
+
}
|
|
1367
|
+
}
|
|
1368
|
+
node.forEachChild(visit);
|
|
1369
|
+
};
|
|
1370
|
+
sf.forEachChild(visit);
|
|
1371
|
+
}
|
|
1372
|
+
return names;
|
|
1373
|
+
}
|
|
1374
|
+
//# sourceMappingURL=port-catches.js.map
|