okengine 0.21.1 → 0.23.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/AGENTS.md +11 -4
- package/LICENSE +202 -0
- package/NOTICE +2 -0
- package/README.md +15 -9
- package/manifest.v1.schema.json +54 -0
- package/package.json +53 -38
- package/site/content/docs/ai/mcp.mdx +13 -9
- package/site/content/docs/ai/skills.mdx +2 -2
- package/site/content/docs/client/calling.mdx +27 -19
- package/site/content/docs/client/index.mdx +1 -1
- package/site/content/docs/elements/ai/agents.mdx +61 -13
- package/site/content/docs/elements/ai/decide.mdx +536 -0
- package/site/content/docs/elements/ai/events.mdx +373 -0
- package/site/content/docs/elements/ai/index.mdx +9 -3
- package/site/content/docs/elements/ai/meta.json +1 -1
- package/site/content/docs/elements/ai/prompts.mdx +4 -2
- package/site/content/docs/elements/channel/email.mdx +1 -1
- package/site/content/docs/elements/channel/index.mdx +1 -1
- package/site/content/docs/elements/flow/http.mdx +4 -4
- package/site/content/docs/elements/flow/index.mdx +3 -3
- package/site/content/docs/elements/flow/meta.json +1 -1
- package/site/content/docs/elements/gate/auth.mdx +4 -1
- package/site/content/docs/elements/gate/tenancy.mdx +1 -1
- package/site/content/docs/elements/store/index.mdx +1 -1
- package/site/content/docs/elements/store/kv.mdx +5 -5
- package/site/content/docs/elements/vault/rotation.mdx +15 -0
- package/site/content/docs/plugins/otp.mdx +31 -31
- package/site/content/docs/recipes/dragonfly.mdx +5 -5
- package/site/content/docs/recipes/index.mdx +6 -6
- package/site/content/docs/recipes/mailpit.mdx +1 -1
- package/site/content/docs/recipes/meilisearch.mdx +1 -1
- package/site/content/docs/recipes/pgdog.mdx +1 -1
- package/site/content/docs/recipes/postgres.mdx +12 -8
- package/site/content/docs/recipes/redis.mdx +4 -4
- package/site/content/docs/reference/cli.mdx +2 -1
- package/site/content/docs/reference/configuration.mdx +11 -9
- package/site/content/docs/reference/errors.mdx +12 -2
- package/site/content/docs/reference/fx.mdx +35 -21
- package/site/content/docs/reference/idempotency.mdx +192 -0
- package/site/content/docs/reference/index.mdx +6 -1
- package/site/content/docs/reference/meta.json +2 -1
- package/site/content/docs/understand/meta.json +1 -1
- package/site/content/docs/{elements/flow → understand}/routing.mdx +11 -10
- package/site/content/docs/understand/the-architecture.mdx +14 -12
- package/site/content/docs/understand/try-it.mdx +3 -3
- package/src/auth/api-key-sql.test.ts +44 -0
- package/src/auth/api-key-sql.ts +1 -0
- package/src/auth/api-keys.ts +30 -12
- package/src/auth/config.ts +6 -2
- package/src/auth/gate-auth.test.ts +20 -0
- package/src/cli/agents-md.test.ts +25 -0
- package/src/cli/ask-vault-gaps.test.ts +38 -1
- package/src/cli/ask-vault-gaps.ts +35 -2
- package/src/cli/competitor-mention-removal.test.ts +3 -3
- package/src/cli/decide.ts +174 -0
- package/src/cli/decision-lock-watch.test.ts +45 -0
- package/src/cli/decision-lock-watch.ts +51 -0
- package/src/cli/dev-app-runner.ts +34 -8
- package/src/cli/dev-auth-secret.test.ts +49 -0
- package/src/cli/dev-auth-secret.ts +49 -0
- package/src/cli/dev-hot.test.ts +41 -0
- package/src/cli/dev-hot.ts +48 -0
- package/src/cli/dev.test.ts +1 -1
- package/src/cli/dev.ts +24 -6
- package/src/cli/docker-cli.test.ts +1 -1
- package/src/cli/eval.ts +142 -1
- package/src/cli/index.ts +14 -1
- package/src/cli/load-config.images.test.ts +12 -12
- package/src/cli/load-config.ts +2 -2
- package/src/cli/mcp-from-console.ts +48 -0
- package/src/cli/registry.ts +35 -1
- package/src/cli/vault-cmd.test.ts +23 -0
- package/src/cli/vault-cmd.ts +7 -0
- package/src/client/agent.ts +307 -0
- package/src/client/create.ts +18 -1
- package/src/client/transport.test.ts +245 -0
- package/src/client/transport.ts +133 -19
- package/src/client/types.ts +29 -3
- package/src/client-react/index.ts +7 -1
- package/src/client-react/use-agent-run.test.ts +271 -0
- package/src/client-react/use-agent-run.ts +193 -0
- package/src/compiler/ai-approval.test.ts +36 -0
- package/src/compiler/ai-repair.test.ts +22 -0
- package/src/compiler/decisions.extract.test.ts +164 -0
- package/src/compiler/effects-infer.ts +753 -17
- package/src/compiler/extract.test.ts +37 -0
- package/src/compiler/extract.ts +381 -24
- package/src/compiler/fixtures/skyport.expected.json +24 -0
- package/src/compiler/fx-follow.test.ts +164 -0
- package/src/compiler/fx-index.ts +399 -0
- package/src/compiler/response.ts +35 -2
- package/src/config/index.ts +21 -3
- package/src/console/server/ai-runs-flows.ts +269 -0
- package/src/console/server/ai-runs.test.ts +323 -0
- package/src/console/server/ai-runs.ts +562 -0
- package/src/console/server/ai.ts +12 -1
- package/src/console/server/app.ts +10 -1
- package/src/console/server/decisions-flows.ts +157 -0
- package/src/console/server/decisions.test.ts +66 -0
- package/src/console/server/decisions.ts +229 -0
- package/src/console/server/flows-invoke.test.ts +2 -2
- package/src/console/server/flows.ts +50 -2
- package/src/console/server/invoke-user-flow.ts +9 -0
- package/src/console/server/runs-ingest.ts +16 -0
- package/src/console/server/serve.ts +5 -0
- package/src/console/server/state.ts +25 -1
- package/src/console/server/store-stats.test.ts +47 -0
- package/src/console/server/store-stats.ts +92 -18
- package/src/console/ui-next/dist/assets/access-page-DDKkhT9v.js +4 -0
- package/src/console/ui-next/dist/assets/agent-disclosure-62Xts8Xi.js +1 -0
- package/src/console/ui-next/dist/assets/{cache-glyph-92uM5MO7.js → cache-glyph-DyeoKHKA.js} +1 -1
- package/src/console/ui-next/dist/assets/{call-pii-button-DtGVPtcs.js → call-pii-button-DvfpLXsU.js} +1 -1
- package/src/console/ui-next/dist/assets/{collapsible-DN7l6zmC.js → collapsible-BY03SeCg.js} +1 -1
- package/src/console/ui-next/dist/assets/confirm-sheet-DZfuJXP4.js +1 -0
- package/src/console/ui-next/dist/assets/copy-inline-button-BTjPVjRT.js +1 -0
- package/src/console/ui-next/dist/assets/decisions-page-CgkKlA56.js +1 -0
- package/src/console/ui-next/dist/assets/detail-header-Dk5LipF6.js +1 -0
- package/src/console/ui-next/dist/assets/dropdown-menu-CRGrj5JU.js +1 -0
- package/src/console/ui-next/dist/assets/duration-tone-Y6HLjyJ5.js +9 -0
- package/src/console/ui-next/dist/assets/element-icons-C6t8Vlml.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-chrome-Dslh2yE8.js +1 -0
- package/src/console/ui-next/dist/assets/explorer-empty-CIVg7Jgb.js +1 -0
- package/src/console/ui-next/dist/assets/flows-page-DLPfNy-_.js +1 -0
- package/src/console/ui-next/dist/assets/{highlighted-json-BlAEVgNW.js → highlighted-json-D7nNzpgB.js} +1 -1
- package/src/console/ui-next/dist/assets/{http-method-BDf7OAHv.js → http-method-DdL19zzh.js} +1 -1
- package/src/console/ui-next/dist/assets/index-BlrN49Hs.css +2 -0
- package/src/console/ui-next/dist/assets/index-C0lc_s9d.js +58 -0
- package/src/console/ui-next/dist/assets/observability-page-CRlcyLCC.js +8 -0
- package/src/console/ui-next/dist/assets/{replica-lag-B3GLNVfF.js → replica-lag-Bb3FWUu9.js} +7 -7
- package/src/console/ui-next/dist/assets/request-meta-DYBMWhX8.js +1 -0
- package/src/console/ui-next/dist/assets/shortcut-keys-DTE1sjTC.js +1 -0
- package/src/console/ui-next/dist/assets/store-page-CcE-SXC8.js +41 -0
- package/src/console/ui-next/dist/assets/trace-detail-sheet-CKMSEQ4s.js +2 -0
- package/src/console/ui-next/dist/assets/tree-expand-toggle-QOfk0M0H.js +55 -0
- package/src/console/ui-next/dist/assets/units-page-DKC1CDDd.js +1 -0
- package/src/console/ui-next/dist/assets/use-vault-list-BAyQG8Re.js +1 -0
- package/src/console/ui-next/dist/assets/vault-page-B54v4Yit.js +2 -0
- package/src/console/ui-next/dist/index.html +4 -3
- package/src/console/ui-next/seed-parked-approval.ts +126 -0
- package/src/console/ui-next/src/client.ts +224 -1
- package/src/console/ui-next/src/components/explorer/detail-header.tsx +9 -0
- package/src/console/ui-next/src/components/explorer/explorer-start-toggle.tsx +5 -1
- package/src/console/ui-next/src/components/ui/sheet-form.tsx +21 -12
- package/src/console/ui-next/src/features/flows/decisions/decisions-page.tsx +216 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/decisions/resolve.ts +111 -0
- package/src/console/ui-next/src/features/flows/flows-page.tsx +10 -0
- package/src/console/ui-next/src/features/flows/graph/element-map.ts +1 -0
- package/src/console/ui-next/src/features/flows/traces/effect-kind.ts +6 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.test.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/effect-summary.ts +24 -0
- package/src/console/ui-next/src/features/flows/traces/http-status.ts +58 -0
- package/src/console/ui-next/src/features/flows/traces/request-client-card.tsx +355 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.test.ts +114 -0
- package/src/console/ui-next/src/features/flows/traces/request-client.ts +423 -0
- package/src/console/ui-next/src/features/flows/traces/request-input-view.test.ts +52 -6
- package/src/console/ui-next/src/features/flows/traces/request-input-view.ts +85 -19
- package/src/console/ui-next/src/features/flows/traces/trace-detail-sheet.tsx +505 -80
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.test.ts +61 -0
- package/src/console/ui-next/src/features/flows/traces/trace-lanes.ts +78 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.test.ts +48 -0
- package/src/console/ui-next/src/features/flows/traces/trace-nav.ts +57 -0
- package/src/console/ui-next/src/features/flows/traces/trace-request-section.tsx +275 -96
- package/src/console/ui-next/src/features/flows/traces/traces-pane.tsx +16 -2
- package/src/console/ui-next/src/features/observability/detail/ai-runs.tsx +369 -0
- package/src/console/ui-next/src/features/observability/observability-page.tsx +31 -1
- package/src/console/ui-next/src/features/observability/state/observability-selection.ts +18 -3
- package/src/console/ui-next/src/features/store/performance/kv-performance-panel.tsx +12 -1
- package/src/console/ui-next/src/features/store/performance/performance-panel.tsx +14 -1
- package/src/console/ui-next/src/features/units/call/call-api-panel.tsx +1 -1
- package/src/console/ui-next/src/features/units/detail/flow-contract-panel.tsx +15 -2
- package/src/console/ui-next/src/features/units/explorer/units-tree.tsx +73 -18
- package/src/console/ui-next/src/features/units/lib/unit-tree.test.ts +21 -0
- package/src/console/ui-next/src/features/units/lib/unit-tree.ts +82 -0
- package/src/console/ui-next/src/features/units/units-page.tsx +25 -7
- package/src/console/ui-next/src/router.tsx +17 -0
- package/src/console/ui-next/ui-next-seed-runs.ts +23 -0
- package/src/console/ui-next/vite-console-kernel-plugin.ts +43 -3
- package/src/console/ui-next/vite.config.ts +9 -0
- package/src/docker/docker.test.ts +13 -13
- package/src/docker/images-config.test.ts +12 -12
- package/src/docker/recipes/dragonfly.ts +3 -2
- package/src/docker/recipes/redis.ts +1 -1
- package/src/docker/stack-id.test.ts +1 -1
- package/src/drivers/ai-anthropic.ts +212 -21
- package/src/drivers/ai-mock.ts +34 -1
- package/src/drivers/ai-openai-compatible.ts +67 -26
- package/src/drivers/ai-preconnect.ts +18 -0
- package/src/drivers/ai-stream.test.ts +62 -0
- package/src/drivers/ai-types.ts +20 -0
- package/src/drivers/journal-postgres.ts +455 -3
- package/src/drivers/meilisearch.ts +2 -0
- package/src/drivers/memory-ddl.test.ts +18 -0
- package/src/drivers/memory.ts +21 -1
- package/src/drivers/pg-rls.test.ts +31 -0
- package/src/drivers/pg-rls.ts +15 -0
- package/src/drivers/postgres.test.ts +9 -0
- package/src/drivers/postgres.ts +49 -11
- package/src/drivers/vault-builtin.test.ts +10 -0
- package/src/drivers/vault-builtin.ts +13 -1
- package/src/drivers/vault-remote-bag.ts +5 -1
- package/src/drivers/vault-types.ts +6 -1
- package/src/elements/ai/agent-event-slot.ts +27 -0
- package/src/elements/ai/approval-http.ts +207 -0
- package/src/elements/ai/approval.test.ts +741 -0
- package/src/elements/ai/approval.ts +228 -0
- package/src/elements/ai/decisions/bind.ts +163 -0
- package/src/elements/ai/decisions/certificate.test.ts +291 -0
- package/src/elements/ai/decisions/certificate.ts +377 -0
- package/src/elements/ai/decisions/certify.ts +314 -0
- package/src/elements/ai/decisions/decide.test.ts +953 -0
- package/src/elements/ai/decisions/e2e.test.ts +399 -0
- package/src/elements/ai/decisions/export.test.ts +120 -0
- package/src/elements/ai/decisions/export.ts +146 -0
- package/src/elements/ai/decisions/fixtures/openrouter-request.json +24 -0
- package/src/elements/ai/decisions/fixtures/openrouter-response.json +22 -0
- package/src/elements/ai/decisions/fixtures/typesafe-request.json +19 -0
- package/src/elements/ai/decisions/fixtures/typesafe-response.json +13 -0
- package/src/elements/ai/decisions/http.test.ts +154 -0
- package/src/elements/ai/decisions/http.ts +240 -0
- package/src/elements/ai/decisions/labels.test.ts +282 -0
- package/src/elements/ai/decisions/labels.ts +257 -0
- package/src/elements/ai/decisions/openrouter.live.test.ts +136 -0
- package/src/elements/ai/decisions/openrouter.ts +36 -0
- package/src/elements/ai/decisions/provider.ts +102 -0
- package/src/elements/ai/decisions/typesafe.ts +36 -0
- package/src/elements/ai/declare.ts +252 -1
- package/src/elements/ai/events.test.ts +379 -0
- package/src/elements/ai/events.ts +138 -0
- package/src/elements/ai/run-events.test.ts +461 -0
- package/src/elements/ai/run-events.ts +522 -0
- package/src/elements/ai/runtime.ts +1186 -105
- package/src/elements/ai/stream-turn.ts +214 -0
- package/src/elements/ai/stream.live.test.ts +76 -0
- package/src/elements/ai/subagent.test.ts +136 -0
- package/src/elements/ai.test.ts +497 -3
- package/src/elements/ai.ts +16 -1
- package/src/elements/clock/durable.ts +27 -1
- package/src/elements/store/emit-drizzle.ts +2 -2
- package/src/elements/store/live-default.test.ts +2 -0
- package/src/elements/store/live-http.test.ts +1 -0
- package/src/elements/store/resource-list-docs.test.ts +2 -2
- package/src/elements/store/resource.test.ts +3 -3
- package/src/elements/store/schema-decl.test.ts +1 -0
- package/src/elements/store/sql-condition.ts +24 -7
- package/src/elements/store/sql-session.test.ts +51 -0
- package/src/elements/store/sql-session.ts +111 -1
- package/src/elements/store/upsert-app.test.ts +1 -1
- package/src/elements/vault/audit.test.ts +137 -0
- package/src/elements/vault/audit.ts +106 -13
- package/src/elements/vault/boot-chain.ts +9 -1
- package/src/elements/vault/builtin-adapter.ts +33 -3
- package/src/i18n/catalogs/ar.ts +9 -0
- package/src/i18n/catalogs/en.ts +9 -0
- package/src/index.ts +2 -0
- package/src/kernel/abort-scope.ts +33 -2
- package/src/kernel/agent-event-store.ts +448 -0
- package/src/kernel/app.ts +294 -19
- package/src/kernel/auto-cache.test.ts +6 -6
- package/src/kernel/boot-bind/ai.ts +3 -0
- package/src/kernel/boot-bind/store.test.ts +26 -0
- package/src/kernel/boot-bind/store.ts +82 -6
- package/src/kernel/boot-bind/vault.ts +1 -0
- package/src/kernel/boot.ts +34 -5
- package/src/kernel/builtin-errors.ts +10 -0
- package/src/kernel/capability.ts +3 -0
- package/src/kernel/client-descriptor.ts +1 -0
- package/src/kernel/concurrency.test.ts +33 -0
- package/src/kernel/decision-label-store.ts +441 -0
- package/src/kernel/effects-stamping.test.ts +2 -2
- package/src/kernel/effects.test.ts +2 -1
- package/src/kernel/effects.ts +13 -1
- package/src/kernel/element-registries.ts +3 -0
- package/src/kernel/errors-compiler.ts +20 -0
- package/src/kernel/errors-text.ts +4 -0
- package/src/kernel/errors.ts +2 -0
- package/src/kernel/external-effects.test.ts +36 -0
- package/src/kernel/flow.test.ts +2 -3
- package/src/kernel/flow.ts +24 -0
- package/src/kernel/fx-decide.ts +761 -0
- package/src/kernel/fx-fetch.ts +5 -1
- package/src/kernel/fx.test.ts +12 -5
- package/src/kernel/fx.ts +316 -29
- package/src/kernel/http-frame.test.ts +94 -0
- package/src/kernel/http-frame.ts +203 -0
- package/src/kernel/idempotency-store.ts +578 -0
- package/src/kernel/idempotency.test.ts +556 -0
- package/src/kernel/idempotency.ts +304 -0
- package/src/kernel/index.ts +1 -0
- package/src/kernel/journal.ts +160 -0
- package/src/kernel/json-result.ts +10 -0
- package/src/kernel/pipeline.test.ts +45 -0
- package/src/kernel/sse-id.ts +32 -0
- package/src/manifest/diff.ts +1 -0
- package/src/manifest/types.ts +37 -2
- package/src/mcp/ai-tools.test.ts +202 -0
- package/src/mcp/authorization.ts +66 -0
- package/src/mcp/session.ts +3 -0
- package/src/mcp/tools.ts +168 -0
- package/src/plugins/auth-delivery.mailpit.integration.test.ts +1 -1
- package/src/plugins/otp.test.ts +65 -1
- package/src/plugins/otp.ts +59 -7
- package/src/release/absolute-regression.test.ts +63 -0
- package/src/release/http-graph.test.ts +27 -0
- package/src/release/http-graph.ts +95 -0
- package/src/release/index.ts +3 -0
- package/src/release/limits.ts +17 -2
- package/src/release/measure.ts +144 -28
- package/src/runs/collect.ts +4 -1
- package/src/runs/parquet.test.ts +27 -1
- package/src/runs/parquet.ts +19 -0
- package/src/runs/types.ts +31 -0
- package/src/runtime/json-code-block.test.ts +106 -47
- package/src/runtime/json-code-block.ts +1209 -46
- package/src/term.test.ts +13 -0
- package/src/term.ts +81 -49
- package/src/test/create-test-app.test.ts +3 -1
- package/src/test/create-test-app.ts +40 -0
- package/src/test/live-signals.test.ts +1 -0
- package/src/test/provisions.integration.test.ts +1 -0
- package/src/test/tenant-isolation.test.ts +1 -0
- package/src/console/ui-next/dist/assets/PlusSignIcon-CwG3nxfu.js +0 -1
- package/src/console/ui-next/dist/assets/access-page-Bgt9bq2r.js +0 -4
- package/src/console/ui-next/dist/assets/agent-disclosure-B43CXZZR.js +0 -1
- package/src/console/ui-next/dist/assets/copy-inline-button-CAYD18cr.js +0 -1
- package/src/console/ui-next/dist/assets/detail-header-DVWNjWjg.js +0 -1
- package/src/console/ui-next/dist/assets/dropdown-menu-4h2LOVXM.js +0 -1
- package/src/console/ui-next/dist/assets/duration-tone-BmIR9FV8.js +0 -9
- package/src/console/ui-next/dist/assets/element-icons-BI8cJgdh.js +0 -1
- package/src/console/ui-next/dist/assets/explorer-empty-CJs5A-wm.js +0 -1
- package/src/console/ui-next/dist/assets/flows-page-BMHs-IzK.js +0 -1
- package/src/console/ui-next/dist/assets/index-BKpaes3n.js +0 -63
- package/src/console/ui-next/dist/assets/index-VxoEz295.css +0 -2
- package/src/console/ui-next/dist/assets/observability-page-WnVLI-0j.js +0 -4
- package/src/console/ui-next/dist/assets/request-meta-DMbnAe3f.js +0 -1
- package/src/console/ui-next/dist/assets/shortcut-keys-3ILd8oGn.js +0 -1
- package/src/console/ui-next/dist/assets/store-page-KvFDingJ.js +0 -41
- package/src/console/ui-next/dist/assets/trace-detail-sheet-Htk8Cm9t.js +0 -2
- package/src/console/ui-next/dist/assets/tree-expand-toggle-CFMPWX4f.js +0 -55
- package/src/console/ui-next/dist/assets/units-page-C4NdNuxP.js +0 -1
- package/src/console/ui-next/dist/assets/use-vault-list-CT4-gajj.js +0 -1
- package/src/console/ui-next/dist/assets/vault-page-CBYl0LW_.js +0 -2
|
@@ -57,10 +57,12 @@ export interface InferBinding {
|
|
|
57
57
|
| "signal"
|
|
58
58
|
| "clock"
|
|
59
59
|
| "prompt"
|
|
60
|
+
| "agent"
|
|
60
61
|
| "secret"
|
|
61
62
|
| "template"
|
|
62
63
|
| "flow"
|
|
63
64
|
| "embed"
|
|
65
|
+
| "decision"
|
|
64
66
|
| "table"
|
|
65
67
|
| "mcp-server"
|
|
66
68
|
| "mcp-tool"
|
|
@@ -71,8 +73,16 @@ export interface InferBinding {
|
|
|
71
73
|
readonly facet?: "sql" | "kv" | "files" | "index";
|
|
72
74
|
/** Prompt version when kind is `prompt`. */
|
|
73
75
|
readonly version?: number;
|
|
76
|
+
/** Nested agent tools when kind is `agent`. */
|
|
77
|
+
readonly agentCalls?: readonly string[];
|
|
74
78
|
}
|
|
75
79
|
|
|
80
|
+
/** What a callee that is passed `fx` resolves to. */
|
|
81
|
+
export type FxCalleeResolution =
|
|
82
|
+
| { readonly kind: "function"; readonly fn: AstNode; readonly file?: string }
|
|
83
|
+
| { readonly kind: "intrinsic"; readonly name: "liveQuery" | "applySearchEmbedCdc" }
|
|
84
|
+
| { readonly kind: "unresolved"; readonly label: string };
|
|
85
|
+
|
|
76
86
|
/** Options for {@link inferEffects}. */
|
|
77
87
|
export interface InferEffectsOptions {
|
|
78
88
|
/** The `do` handler AST node (function / arrow). */
|
|
@@ -81,9 +91,25 @@ export interface InferEffectsOptions {
|
|
|
81
91
|
readonly bindings: ReadonlyMap<string, InferBinding>;
|
|
82
92
|
/**
|
|
83
93
|
* True when the flow options already declare an `effects` object.
|
|
84
|
-
* Required for `fx.raw` to stay cache-eligible.
|
|
94
|
+
* Required for `fx.raw` to stay cache-eligible. Opaque `fx` (a renamed
|
|
95
|
+
* parameter, an escaped chain alias, an unresolved callee) is allowed
|
|
96
|
+
* only when this is set; the declared set must still cover every effect
|
|
97
|
+
* the walk can see.
|
|
85
98
|
*/
|
|
86
99
|
readonly hasExplicitEffects: boolean;
|
|
100
|
+
/** Manifest flow name, used in OKE1900. */
|
|
101
|
+
readonly flowName?: string;
|
|
102
|
+
/**
|
|
103
|
+
* Resolve a callee in the flow's file. Absent only for unit tests that
|
|
104
|
+
* never pass `fx` into another function.
|
|
105
|
+
*/
|
|
106
|
+
/** File that contains `do`, so imported helpers resolve from there. */
|
|
107
|
+
readonly file?: string;
|
|
108
|
+
/**
|
|
109
|
+
* Resolve a callee as seen from `file`. Absent only for unit tests that
|
|
110
|
+
* never pass `fx` into another function.
|
|
111
|
+
*/
|
|
112
|
+
readonly resolveCallee?: (callee: AstNode, file: string) => FxCalleeResolution;
|
|
87
113
|
}
|
|
88
114
|
|
|
89
115
|
/** Result of effect inference for one flow body. */
|
|
@@ -168,13 +194,19 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
168
194
|
const secrets = new Set<SecretRef>();
|
|
169
195
|
const calls = new Set<FlowRef>();
|
|
170
196
|
const fetches = new Set<FetchHostRef>();
|
|
197
|
+
const decides = new Set<string>();
|
|
171
198
|
const steps: string[] = [];
|
|
172
199
|
let usesRaw = false;
|
|
173
200
|
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
201
|
+
const opaque: string[] = [];
|
|
202
|
+
const userIdRoots: AstNode[] = [options.doNode];
|
|
203
|
+
const intrinsics: Array<{
|
|
204
|
+
name: "liveQuery" | "applySearchEmbedCdc";
|
|
205
|
+
call: CallExpression;
|
|
206
|
+
}> = [];
|
|
207
|
+
const chains = collectFxChains(options, opaque, userIdRoots, intrinsics);
|
|
177
208
|
|
|
209
|
+
for (const { call, chain } of chains) {
|
|
178
210
|
if (chain.rootMethod === "raw") {
|
|
179
211
|
usesRaw = true;
|
|
180
212
|
continue;
|
|
@@ -210,6 +242,17 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
210
242
|
continue;
|
|
211
243
|
}
|
|
212
244
|
|
|
245
|
+
if (chain.rootMethod === "run" && call === chain.rootCall) {
|
|
246
|
+
const binding = resolveBinding(call.arguments[0], options.bindings);
|
|
247
|
+
const name =
|
|
248
|
+
stringArg(call.arguments[0]) ?? (binding?.kind === "agent" ? binding.ref : undefined);
|
|
249
|
+
if (name) asks.add(name);
|
|
250
|
+
if (binding?.kind === "agent") {
|
|
251
|
+
for (const child of binding.agentCalls ?? []) calls.add(child);
|
|
252
|
+
}
|
|
253
|
+
continue;
|
|
254
|
+
}
|
|
255
|
+
|
|
213
256
|
if (chain.rootMethod === "ask" && call === chain.rootCall) {
|
|
214
257
|
const ref = resolvePrompt(call.arguments[0], options.bindings);
|
|
215
258
|
if (ref) asks.add(ref);
|
|
@@ -241,6 +284,14 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
241
284
|
continue;
|
|
242
285
|
}
|
|
243
286
|
|
|
287
|
+
if (chain.rootMethod === "decide" && call === chain.rootCall) {
|
|
288
|
+
const binding = resolveBinding(call.arguments[0], options.bindings);
|
|
289
|
+
const name =
|
|
290
|
+
stringArg(call.arguments[0]) ?? (binding?.kind === "decision" ? binding.ref : undefined);
|
|
291
|
+
if (name) decides.add(name);
|
|
292
|
+
continue;
|
|
293
|
+
}
|
|
294
|
+
|
|
244
295
|
if (chain.rootMethod === "fetch" && call === chain.rootCall) {
|
|
245
296
|
// fx.fetch(url) — host becomes effects.fetches entry.
|
|
246
297
|
const lit = stringArg(call.arguments[0]);
|
|
@@ -301,14 +352,14 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
301
352
|
}
|
|
302
353
|
|
|
303
354
|
if (chain.rootMethod === "store") {
|
|
304
|
-
const resolved = storeResourceFromCall(call, options.bindings);
|
|
355
|
+
const resolved = storeResourceFromCall(call, options.bindings, chain);
|
|
305
356
|
if (!resolved) continue;
|
|
306
357
|
const op = classifyStoreMethods(resolved.methods);
|
|
307
358
|
if (op === "none") continue;
|
|
308
359
|
// Skip incomplete chains (`select` before `.from`, `insert` before table) —
|
|
309
360
|
// the sibling call that carries the table arg records the real resource.
|
|
310
361
|
const leaf = resolved.methods[resolved.methods.length - 1]!;
|
|
311
|
-
const hasTable = tableFromStoreChain(call, options.bindings) !== undefined;
|
|
362
|
+
const hasTable = tableFromStoreChain(call, options.bindings, chain) !== undefined;
|
|
312
363
|
if (!hasTable && (leaf === "select" || leaf === "insert" || leaf === "update")) {
|
|
313
364
|
continue;
|
|
314
365
|
}
|
|
@@ -323,6 +374,20 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
323
374
|
}
|
|
324
375
|
}
|
|
325
376
|
|
|
377
|
+
for (const item of intrinsics) {
|
|
378
|
+
noteIntrinsic(item.name, item.call, options.bindings, reads, writes, embeds, (detail) => {
|
|
379
|
+
opaque.push(detail);
|
|
380
|
+
});
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
if (opaque.length > 0 && !options.hasExplicitEffects) {
|
|
384
|
+
const flow = options.flowName ?? "flow";
|
|
385
|
+
const detail = opaque[0] ?? "hides fx from effect inference";
|
|
386
|
+
throw new Error(
|
|
387
|
+
`OKE1900: flow "${flow}" ${detail}. Name the parameter fx and keep chain aliases in the same function, or declare effects that include every effect inference can see.`,
|
|
388
|
+
);
|
|
389
|
+
}
|
|
390
|
+
|
|
326
391
|
const effects: Effects = {};
|
|
327
392
|
if (reads.size > 0) effects.reads = sortUnique([...reads]);
|
|
328
393
|
if (writes.size > 0) effects.writes = sortUnique([...writes]);
|
|
@@ -333,17 +398,51 @@ export function inferEffects(options: InferEffectsOptions): InferredEffects {
|
|
|
333
398
|
if (secrets.size > 0) effects.secrets = sortUnique([...secrets]);
|
|
334
399
|
if (calls.size > 0) effects.calls = sortUnique([...calls]);
|
|
335
400
|
if (fetches.size > 0) effects.fetches = sortUnique([...fetches]);
|
|
401
|
+
if (decides.size > 0) effects.decides = sortUnique([...decides]);
|
|
336
402
|
|
|
337
403
|
return {
|
|
338
404
|
effects,
|
|
339
405
|
steps,
|
|
340
406
|
usesRaw,
|
|
341
407
|
cacheIneligible: usesRaw && !options.hasExplicitEffects,
|
|
342
|
-
nondeterministic: asks.size > 0 || embeds.size > 0,
|
|
343
|
-
readsUserId: containsAuthUserId(
|
|
408
|
+
nondeterministic: asks.size > 0 || embeds.size > 0 || decides.size > 0,
|
|
409
|
+
readsUserId: userIdRoots.some((node) => containsAuthUserId(node)),
|
|
344
410
|
};
|
|
345
411
|
}
|
|
346
412
|
|
|
413
|
+
const EFFECT_KEYS = [
|
|
414
|
+
"reads",
|
|
415
|
+
"writes",
|
|
416
|
+
"emits",
|
|
417
|
+
"sends",
|
|
418
|
+
"asks",
|
|
419
|
+
"embeds",
|
|
420
|
+
"secrets",
|
|
421
|
+
"calls",
|
|
422
|
+
"fetches",
|
|
423
|
+
"decides",
|
|
424
|
+
] as const satisfies readonly (keyof Effects)[];
|
|
425
|
+
|
|
426
|
+
/**
|
|
427
|
+
* Keys in `inferred` that `declared` does not include.
|
|
428
|
+
*
|
|
429
|
+
* An explicit `effects` object is a floor: it may add keys inference
|
|
430
|
+
* cannot see, and it must include every key the walk can see.
|
|
431
|
+
*
|
|
432
|
+
* @param declared - Author-declared effects
|
|
433
|
+
* @param inferred - Effects the walk can see
|
|
434
|
+
*/
|
|
435
|
+
export function effectsMissingFromDeclaration(declared: Effects, inferred: Effects): string[] {
|
|
436
|
+
const missing: string[] = [];
|
|
437
|
+
for (const key of EFFECT_KEYS) {
|
|
438
|
+
const have = new Set<string>(declared[key] ?? []);
|
|
439
|
+
for (const item of inferred[key] ?? []) {
|
|
440
|
+
if (!have.has(item)) missing.push(`${key} ${item}`);
|
|
441
|
+
}
|
|
442
|
+
}
|
|
443
|
+
return missing;
|
|
444
|
+
}
|
|
445
|
+
|
|
347
446
|
/**
|
|
348
447
|
* Walk an AST and collect every CallExpression.
|
|
349
448
|
*
|
|
@@ -380,6 +479,8 @@ interface FxChain {
|
|
|
380
479
|
rootMethod: string;
|
|
381
480
|
methods: string[];
|
|
382
481
|
rootCall: CallExpression;
|
|
482
|
+
/** Outermost call of an alias init, when the continuation is a later statement. */
|
|
483
|
+
aliasLeaf?: CallExpression;
|
|
383
484
|
}
|
|
384
485
|
|
|
385
486
|
/**
|
|
@@ -451,15 +552,16 @@ export function fxMemberChain(call: CallExpression): FxChain | null {
|
|
|
451
552
|
export function storeResourceFromCall(
|
|
452
553
|
call: CallExpression,
|
|
453
554
|
bindings: ReadonlyMap<string, InferBinding>,
|
|
555
|
+
known?: FxChain,
|
|
454
556
|
): { resource: ResourceRef; methods: string[] } | undefined {
|
|
455
|
-
const chain = fxMemberChain(call);
|
|
557
|
+
const chain = known ?? fxMemberChain(call);
|
|
456
558
|
if (!chain || chain.rootMethod !== "store") return undefined;
|
|
457
559
|
|
|
458
560
|
const storeArg = chain.rootCall.arguments[0];
|
|
459
561
|
const storeBinding = resolveBinding(storeArg, bindings);
|
|
460
562
|
const facet = storeBinding?.facet ?? "sql";
|
|
461
563
|
|
|
462
|
-
const table = tableFromStoreChain(call, bindings);
|
|
564
|
+
const table = tableFromStoreChain(call, bindings, chain);
|
|
463
565
|
|
|
464
566
|
if (table) {
|
|
465
567
|
return {
|
|
@@ -506,18 +608,36 @@ export function storeResourceFromCall(
|
|
|
506
608
|
function tableFromStoreChain(
|
|
507
609
|
call: CallExpression,
|
|
508
610
|
bindings: ReadonlyMap<string, InferBinding>,
|
|
611
|
+
chain?: FxChain,
|
|
612
|
+
): string | undefined {
|
|
613
|
+
const found = tableFromStoreWalk(call, bindings, chain);
|
|
614
|
+
if (found) return found;
|
|
615
|
+
if (chain?.aliasLeaf && chain.aliasLeaf !== call) {
|
|
616
|
+
return tableFromStoreWalk(chain.aliasLeaf, bindings);
|
|
617
|
+
}
|
|
618
|
+
return undefined;
|
|
619
|
+
}
|
|
620
|
+
|
|
621
|
+
function tableFromStoreWalk(
|
|
622
|
+
call: CallExpression,
|
|
623
|
+
bindings: ReadonlyMap<string, InferBinding>,
|
|
624
|
+
chain?: FxChain,
|
|
509
625
|
): string | undefined {
|
|
510
626
|
let current: AstNode | undefined = call;
|
|
511
|
-
|
|
627
|
+
const seen = new Set<AstNode>();
|
|
628
|
+
while (current && current.type === "CallExpression" && !seen.has(current)) {
|
|
629
|
+
seen.add(current);
|
|
512
630
|
const c = current as CallExpression;
|
|
513
|
-
const link = fxMemberChain(c);
|
|
514
|
-
if (!link || link.rootMethod !== "store") break;
|
|
515
|
-
const leaf = link
|
|
631
|
+
const link = chain ? undefined : fxMemberChain(c);
|
|
632
|
+
if (!chain && (!link || link.rootMethod !== "store")) break;
|
|
633
|
+
const leaf = chain ? methodNameOfCall(c) : link?.methods[link.methods.length - 1];
|
|
634
|
+
if (!leaf) break;
|
|
516
635
|
if (TABLE_ARG_METHODS.has(leaf)) {
|
|
517
636
|
// KV / files / index take a key, not a table. Treating `delete(key)` as
|
|
518
637
|
// `delete(table)` stamps `kv:key` instead of `kv:drafts` and the runtime
|
|
519
638
|
// then throws OKE1002 on the real namespace.
|
|
520
|
-
const
|
|
639
|
+
const storeArg = chain?.rootCall.arguments[0] ?? link?.rootCall.arguments[0];
|
|
640
|
+
const storeBinding = resolveBinding(storeArg, bindings);
|
|
521
641
|
if (storeBinding?.facet !== undefined && storeBinding.facet !== "sql") {
|
|
522
642
|
// keep walking — no table on this link
|
|
523
643
|
} else {
|
|
@@ -530,14 +650,24 @@ function tableFromStoreChain(
|
|
|
530
650
|
}
|
|
531
651
|
const callee = c.callee;
|
|
532
652
|
if (callee.type === "MemberExpression") {
|
|
533
|
-
|
|
534
|
-
|
|
653
|
+
const obj = (callee as AstNode & { object: AstNode }).object;
|
|
654
|
+
if (obj.type === "CallExpression") {
|
|
655
|
+
current = obj;
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
535
658
|
}
|
|
536
659
|
break;
|
|
537
660
|
}
|
|
538
661
|
return undefined;
|
|
539
662
|
}
|
|
540
663
|
|
|
664
|
+
function methodNameOfCall(call: CallExpression): string | undefined {
|
|
665
|
+
const callee = call.callee;
|
|
666
|
+
if (callee.type !== "MemberExpression") return undefined;
|
|
667
|
+
const property = (callee as AstNode & { property?: AstNode }).property;
|
|
668
|
+
return identifierName(property);
|
|
669
|
+
}
|
|
670
|
+
|
|
541
671
|
function classifyStoreMethods(methods: string[]): "read" | "write" | "both" | "none" {
|
|
542
672
|
const ops = methods.slice(1);
|
|
543
673
|
let read = false;
|
|
@@ -751,3 +881,609 @@ function containsAuthUserId(root: AstNode): boolean {
|
|
|
751
881
|
});
|
|
752
882
|
return found;
|
|
753
883
|
}
|
|
884
|
+
|
|
885
|
+
interface ChainAlias {
|
|
886
|
+
readonly methods: readonly string[];
|
|
887
|
+
readonly rootMethod: string;
|
|
888
|
+
readonly rootCall: CallExpression;
|
|
889
|
+
readonly leafCall: CallExpression;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
interface ChainHit {
|
|
893
|
+
readonly call: CallExpression;
|
|
894
|
+
readonly chain: FxChain;
|
|
895
|
+
}
|
|
896
|
+
|
|
897
|
+
const VALUE_WRAPPERS = new Set([
|
|
898
|
+
"ParenthesizedExpression",
|
|
899
|
+
"TSAsExpression",
|
|
900
|
+
"TSTypeAssertion",
|
|
901
|
+
"TSSatisfiesExpression",
|
|
902
|
+
"TSNonNullExpression",
|
|
903
|
+
"AwaitExpression",
|
|
904
|
+
]);
|
|
905
|
+
|
|
906
|
+
function collectFxChains(
|
|
907
|
+
options: InferEffectsOptions,
|
|
908
|
+
opaque: string[],
|
|
909
|
+
userIdRoots: AstNode[],
|
|
910
|
+
intrinsics: Array<{ name: "liveQuery" | "applySearchEmbedCdc"; call: CallExpression }>,
|
|
911
|
+
): ChainHit[] {
|
|
912
|
+
const hits: ChainHit[] = [];
|
|
913
|
+
const seen = new Set<AstNode>();
|
|
914
|
+
|
|
915
|
+
const follow = (
|
|
916
|
+
fn: AstNode,
|
|
917
|
+
file: string,
|
|
918
|
+
aliases: ReadonlyMap<string, ChainAlias>,
|
|
919
|
+
locals: ReadonlyMap<string, AstNode>,
|
|
920
|
+
): void => {
|
|
921
|
+
analyzeFunction(fn, "fx", aliases, locals, file);
|
|
922
|
+
};
|
|
923
|
+
|
|
924
|
+
const analyzeFunction = (
|
|
925
|
+
fn: AstNode,
|
|
926
|
+
door: string,
|
|
927
|
+
outerAliases: ReadonlyMap<string, ChainAlias>,
|
|
928
|
+
outerLocals: ReadonlyMap<string, AstNode>,
|
|
929
|
+
file: string,
|
|
930
|
+
): void => {
|
|
931
|
+
if (seen.has(fn)) return;
|
|
932
|
+
seen.add(fn);
|
|
933
|
+
userIdRoots.push(fn);
|
|
934
|
+
const body = (fn as AstNode & { body?: AstNode }).body;
|
|
935
|
+
if (!body) return;
|
|
936
|
+
analyzeBody(body, door, outerAliases, outerLocals, file);
|
|
937
|
+
};
|
|
938
|
+
|
|
939
|
+
const analyzeBody = (
|
|
940
|
+
body: AstNode,
|
|
941
|
+
door: string,
|
|
942
|
+
outerAliases: ReadonlyMap<string, ChainAlias>,
|
|
943
|
+
outerLocals: ReadonlyMap<string, AstNode>,
|
|
944
|
+
file: string,
|
|
945
|
+
): void => {
|
|
946
|
+
const aliases = new Map(outerAliases);
|
|
947
|
+
const locals = new Map(outerLocals);
|
|
948
|
+
collectDirect(body, aliases, locals);
|
|
949
|
+
checkEscapes(body, door, new Set(aliases.keys()), (detail) => {
|
|
950
|
+
opaque.push(detail);
|
|
951
|
+
});
|
|
952
|
+
for (const call of callsSkippingNested(body)) {
|
|
953
|
+
const chain = chainFromCall(call, aliases);
|
|
954
|
+
if (chain) {
|
|
955
|
+
hits.push({ call, chain });
|
|
956
|
+
continue;
|
|
957
|
+
}
|
|
958
|
+
noteFxPass(call, door, aliases, locals, file, opaque, intrinsics, follow);
|
|
959
|
+
}
|
|
960
|
+
for (const nested of nestedFunctions(body)) {
|
|
961
|
+
if (seen.has(nested)) continue;
|
|
962
|
+
const shadows = functionParams(nested).some((param) => paramIdentifierName(param) === door);
|
|
963
|
+
if (shadows) continue;
|
|
964
|
+
analyzeFunction(nested, door, aliases, locals, file);
|
|
965
|
+
}
|
|
966
|
+
};
|
|
967
|
+
|
|
968
|
+
const startFile = options.file ?? "";
|
|
969
|
+
if (isFunctionNode(options.doNode)) {
|
|
970
|
+
const params = functionParams(options.doNode);
|
|
971
|
+
const second = params.length >= 2 ? params[1] : undefined;
|
|
972
|
+
if (second && paramIdentifierName(second) !== "fx") {
|
|
973
|
+
opaque.push("the do callback's second parameter is not named fx");
|
|
974
|
+
}
|
|
975
|
+
analyzeFunction(options.doNode, "fx", new Map(), new Map(), startFile);
|
|
976
|
+
} else {
|
|
977
|
+
analyzeBody(options.doNode, "fx", new Map(), new Map(), startFile);
|
|
978
|
+
}
|
|
979
|
+
|
|
980
|
+
return hits;
|
|
981
|
+
|
|
982
|
+
function noteFxPass(
|
|
983
|
+
call: CallExpression,
|
|
984
|
+
door: string,
|
|
985
|
+
aliases: ReadonlyMap<string, ChainAlias>,
|
|
986
|
+
locals: ReadonlyMap<string, AstNode>,
|
|
987
|
+
file: string,
|
|
988
|
+
reasons: string[],
|
|
989
|
+
foundIntrinsics: Array<{ name: "liveQuery" | "applySearchEmbedCdc"; call: CallExpression }>,
|
|
990
|
+
enter: (
|
|
991
|
+
fn: AstNode,
|
|
992
|
+
file: string,
|
|
993
|
+
aliases: ReadonlyMap<string, ChainAlias>,
|
|
994
|
+
locals: ReadonlyMap<string, AstNode>,
|
|
995
|
+
) => void,
|
|
996
|
+
): void {
|
|
997
|
+
const args = call.arguments ?? [];
|
|
998
|
+
for (let index = 0; index < args.length; index++) {
|
|
999
|
+
const arg = unwrapValue(args[index]);
|
|
1000
|
+
if (!arg || arg.type !== "Identifier") continue;
|
|
1001
|
+
const name = identifierName(arg);
|
|
1002
|
+
if (!name) continue;
|
|
1003
|
+
if (aliases.has(name)) {
|
|
1004
|
+
reasons.push(`passes chain alias "${name}" to a call`);
|
|
1005
|
+
continue;
|
|
1006
|
+
}
|
|
1007
|
+
if (name !== door) continue;
|
|
1008
|
+
const resolved = resolveCallee(call.callee, locals, options, file);
|
|
1009
|
+
if (resolved.kind === "function") {
|
|
1010
|
+
const param = functionParams(resolved.fn)[index];
|
|
1011
|
+
if (!param || paramIdentifierName(param) !== "fx") {
|
|
1012
|
+
reasons.push("passes fx to a parameter that is not named fx");
|
|
1013
|
+
continue;
|
|
1014
|
+
}
|
|
1015
|
+
const local = calleeLocal(call.callee, locals);
|
|
1016
|
+
enter(
|
|
1017
|
+
resolved.fn,
|
|
1018
|
+
resolved.file ?? file,
|
|
1019
|
+
local ? aliases : new Map(),
|
|
1020
|
+
local ? locals : new Map(),
|
|
1021
|
+
);
|
|
1022
|
+
continue;
|
|
1023
|
+
}
|
|
1024
|
+
if (resolved.kind === "intrinsic") {
|
|
1025
|
+
if (index !== 0) {
|
|
1026
|
+
reasons.push(`passes fx to ${resolved.name} in a position inference cannot read`);
|
|
1027
|
+
continue;
|
|
1028
|
+
}
|
|
1029
|
+
foundIntrinsics.push({ name: resolved.name, call });
|
|
1030
|
+
continue;
|
|
1031
|
+
}
|
|
1032
|
+
reasons.push(`passes fx to "${resolved.label}", which inference cannot resolve`);
|
|
1033
|
+
}
|
|
1034
|
+
}
|
|
1035
|
+
}
|
|
1036
|
+
|
|
1037
|
+
function calleeLocal(callee: AstNode, locals: ReadonlyMap<string, AstNode>): boolean {
|
|
1038
|
+
if (callee.type !== "Identifier") return false;
|
|
1039
|
+
const name = identifierName(callee);
|
|
1040
|
+
return name !== undefined && locals.has(name);
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
function resolveCallee(
|
|
1044
|
+
callee: AstNode,
|
|
1045
|
+
locals: ReadonlyMap<string, AstNode>,
|
|
1046
|
+
options: InferEffectsOptions,
|
|
1047
|
+
file: string,
|
|
1048
|
+
): FxCalleeResolution {
|
|
1049
|
+
if (callee.type === "Identifier") {
|
|
1050
|
+
const name = identifierName(callee);
|
|
1051
|
+
const local = name ? locals.get(name) : undefined;
|
|
1052
|
+
if (local) return { kind: "function", fn: local, file };
|
|
1053
|
+
}
|
|
1054
|
+
if (options.resolveCallee) return options.resolveCallee(callee, file);
|
|
1055
|
+
const label = identifierName(callee) ?? "callee";
|
|
1056
|
+
return { kind: "unresolved", label };
|
|
1057
|
+
}
|
|
1058
|
+
|
|
1059
|
+
function noteIntrinsic(
|
|
1060
|
+
name: "liveQuery" | "applySearchEmbedCdc",
|
|
1061
|
+
call: CallExpression,
|
|
1062
|
+
bindings: ReadonlyMap<string, InferBinding>,
|
|
1063
|
+
reads: Set<ResourceRef | SignalResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef>,
|
|
1064
|
+
writes: Set<ResourceRef | AuthApiKeysResourceRef | AuthTenantsResourceRef>,
|
|
1065
|
+
embeds: Set<EmbedRef>,
|
|
1066
|
+
opaque: (detail: string) => void,
|
|
1067
|
+
): void {
|
|
1068
|
+
if (name === "liveQuery") {
|
|
1069
|
+
const ref = sqlRefFromArg(call.arguments[1], bindings);
|
|
1070
|
+
if (!ref) {
|
|
1071
|
+
opaque("calls liveQuery with a table inference cannot resolve");
|
|
1072
|
+
return;
|
|
1073
|
+
}
|
|
1074
|
+
reads.add(ref as ResourceRef);
|
|
1075
|
+
return;
|
|
1076
|
+
}
|
|
1077
|
+
const sqlRef = stringArg(unwrapValueNode(call.arguments[5]));
|
|
1078
|
+
if (!sqlRef) {
|
|
1079
|
+
opaque("calls applySearchEmbedCdc with a sqlRef inference cannot resolve");
|
|
1080
|
+
return;
|
|
1081
|
+
}
|
|
1082
|
+
const ref = (sqlRef.includes(":") ? sqlRef : `sql:${sqlRef}`) as ResourceRef;
|
|
1083
|
+
reads.add(ref);
|
|
1084
|
+
writes.add(ref);
|
|
1085
|
+
const models = embedModelsFromColumns(call.arguments[3]);
|
|
1086
|
+
if (!models) {
|
|
1087
|
+
opaque("calls applySearchEmbedCdc with embed columns inference cannot resolve");
|
|
1088
|
+
return;
|
|
1089
|
+
}
|
|
1090
|
+
for (const model of models) embeds.add(model);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
function sqlRefFromArg(
|
|
1094
|
+
arg: AstNode | undefined,
|
|
1095
|
+
bindings: ReadonlyMap<string, InferBinding>,
|
|
1096
|
+
): string | undefined {
|
|
1097
|
+
if (!arg) return undefined;
|
|
1098
|
+
const node = unwrapValue(arg);
|
|
1099
|
+
const lit = stringArg(node);
|
|
1100
|
+
if (lit) return lit.includes(":") ? lit : `sql:${lit}`;
|
|
1101
|
+
const binding = resolveBinding(node, bindings);
|
|
1102
|
+
if (binding?.kind === "table") return sqlTableRef(binding.ref);
|
|
1103
|
+
if (binding?.kind === "store") return binding.ref;
|
|
1104
|
+
return undefined;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
function embedModelsFromColumns(arg: AstNode | undefined): string[] | null {
|
|
1108
|
+
if (!arg) return null;
|
|
1109
|
+
const node = unwrapValue(arg);
|
|
1110
|
+
if (!node || node.type !== "ArrayExpression") return null;
|
|
1111
|
+
const elements = (node as AstNode & { elements?: readonly (AstNode | null)[] }).elements ?? [];
|
|
1112
|
+
const models: string[] = [];
|
|
1113
|
+
for (const element of elements) {
|
|
1114
|
+
if (!element || element.type !== "ObjectExpression") return null;
|
|
1115
|
+
const model = objectStringProp(element, "model");
|
|
1116
|
+
if (model === undefined) {
|
|
1117
|
+
if (objectHasProp(element, "model")) return null;
|
|
1118
|
+
models.push("default");
|
|
1119
|
+
continue;
|
|
1120
|
+
}
|
|
1121
|
+
models.push(model);
|
|
1122
|
+
}
|
|
1123
|
+
return models;
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
function objectStringProp(obj: AstNode, key: string): string | undefined {
|
|
1127
|
+
for (const prop of objectProps(obj)) {
|
|
1128
|
+
if (propKeyName(prop) !== key) continue;
|
|
1129
|
+
return stringArg(unwrapValue((prop as AstNode & { value?: AstNode }).value));
|
|
1130
|
+
}
|
|
1131
|
+
return undefined;
|
|
1132
|
+
}
|
|
1133
|
+
|
|
1134
|
+
function objectHasProp(obj: AstNode, key: string): boolean {
|
|
1135
|
+
return objectProps(obj).some((prop) => propKeyName(prop) === key);
|
|
1136
|
+
}
|
|
1137
|
+
|
|
1138
|
+
function objectProps(obj: AstNode): AstNode[] {
|
|
1139
|
+
return ((obj as AstNode & { properties?: AstNode[] }).properties ?? []).filter(
|
|
1140
|
+
(prop) => prop.type === "Property" || prop.type === "ObjectProperty",
|
|
1141
|
+
);
|
|
1142
|
+
}
|
|
1143
|
+
|
|
1144
|
+
function propKeyName(prop: AstNode): string | undefined {
|
|
1145
|
+
const key = (prop as AstNode & { key?: AstNode }).key;
|
|
1146
|
+
return identifierName(key) ?? stringArg(key);
|
|
1147
|
+
}
|
|
1148
|
+
|
|
1149
|
+
function collectDirect(
|
|
1150
|
+
body: AstNode,
|
|
1151
|
+
aliases: Map<string, ChainAlias>,
|
|
1152
|
+
locals: Map<string, AstNode>,
|
|
1153
|
+
): void {
|
|
1154
|
+
walkStatements(body, (node) => {
|
|
1155
|
+
if (node.type === "FunctionDeclaration") {
|
|
1156
|
+
const name = identifierName((node as AstNode & { id?: AstNode }).id);
|
|
1157
|
+
if (name) locals.set(name, node);
|
|
1158
|
+
return;
|
|
1159
|
+
}
|
|
1160
|
+
if (node.type !== "VariableDeclarator") return;
|
|
1161
|
+
const name = identifierName((node as AstNode & { id?: AstNode }).id);
|
|
1162
|
+
const init = (node as AstNode & { init?: AstNode | null }).init;
|
|
1163
|
+
if (!name || !init) return;
|
|
1164
|
+
const value = unwrapValue(init);
|
|
1165
|
+
if (value && isFunctionNode(value)) {
|
|
1166
|
+
locals.set(name, value);
|
|
1167
|
+
return;
|
|
1168
|
+
}
|
|
1169
|
+
const alias = aliasFromInit(init, aliases);
|
|
1170
|
+
if (alias) aliases.set(name, alias);
|
|
1171
|
+
});
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
function aliasFromInit(init: AstNode, aliases: ReadonlyMap<string, ChainAlias>): ChainAlias | null {
|
|
1175
|
+
const node = unwrapValue(init);
|
|
1176
|
+
if (!node || node.type !== "CallExpression") return null;
|
|
1177
|
+
const chain = chainFromCall(node as CallExpression, aliases);
|
|
1178
|
+
if (!chain || chain.rootMethod !== "store") return null;
|
|
1179
|
+
// `const rows = await fx.store(db).select().from(table)` is the query
|
|
1180
|
+
// result. `const q = await fx.store(db)` is still the handle.
|
|
1181
|
+
if (expressionIsAwaited(init) && !isStoreHandle(chain)) return null;
|
|
1182
|
+
return {
|
|
1183
|
+
methods: chain.methods,
|
|
1184
|
+
rootMethod: chain.rootMethod,
|
|
1185
|
+
rootCall: chain.rootCall,
|
|
1186
|
+
leafCall: chain.aliasLeaf ?? (node as CallExpression),
|
|
1187
|
+
};
|
|
1188
|
+
}
|
|
1189
|
+
|
|
1190
|
+
function isStoreHandle(chain: FxChain): boolean {
|
|
1191
|
+
return chain.rootMethod === "store" && classifyStoreMethods(chain.methods) === "none";
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
function expressionIsAwaited(node: AstNode): boolean {
|
|
1195
|
+
let current: AstNode | undefined = node;
|
|
1196
|
+
for (let i = 0; i < 8 && current; i++) {
|
|
1197
|
+
if (current.type === "AwaitExpression") return true;
|
|
1198
|
+
if (!VALUE_WRAPPERS.has(current.type)) return false;
|
|
1199
|
+
current =
|
|
1200
|
+
(current as AstNode & { expression?: AstNode }).expression ??
|
|
1201
|
+
(current as AstNode & { argument?: AstNode }).argument;
|
|
1202
|
+
}
|
|
1203
|
+
return false;
|
|
1204
|
+
}
|
|
1205
|
+
|
|
1206
|
+
function chainFromCall(
|
|
1207
|
+
call: CallExpression,
|
|
1208
|
+
aliases: ReadonlyMap<string, ChainAlias>,
|
|
1209
|
+
): FxChain | null {
|
|
1210
|
+
const direct = fxMemberChain(call);
|
|
1211
|
+
if (direct) return direct;
|
|
1212
|
+
|
|
1213
|
+
let current: CallExpression | null = call;
|
|
1214
|
+
const methods: string[] = [];
|
|
1215
|
+
while (current) {
|
|
1216
|
+
const callee = current.callee;
|
|
1217
|
+
if (callee.type !== "MemberExpression") return null;
|
|
1218
|
+
const member = callee as AstNode & {
|
|
1219
|
+
object: AstNode;
|
|
1220
|
+
property: AstNode;
|
|
1221
|
+
computed?: boolean;
|
|
1222
|
+
};
|
|
1223
|
+
if (member.computed === true || member.property.type !== "Identifier") return null;
|
|
1224
|
+
methods.unshift((member.property as Identifier).name);
|
|
1225
|
+
if (member.object.type === "Identifier") {
|
|
1226
|
+
const alias = aliases.get((member.object as Identifier).name);
|
|
1227
|
+
if (!alias) return null;
|
|
1228
|
+
return {
|
|
1229
|
+
rootMethod: alias.rootMethod,
|
|
1230
|
+
methods: [...alias.methods, ...methods],
|
|
1231
|
+
rootCall: alias.rootCall,
|
|
1232
|
+
aliasLeaf: alias.leafCall,
|
|
1233
|
+
};
|
|
1234
|
+
}
|
|
1235
|
+
if (member.object.type === "CallExpression") {
|
|
1236
|
+
current = member.object as CallExpression;
|
|
1237
|
+
continue;
|
|
1238
|
+
}
|
|
1239
|
+
return null;
|
|
1240
|
+
}
|
|
1241
|
+
return null;
|
|
1242
|
+
}
|
|
1243
|
+
|
|
1244
|
+
function checkEscapes(
|
|
1245
|
+
root: AstNode,
|
|
1246
|
+
door: string,
|
|
1247
|
+
aliasNames: ReadonlySet<string>,
|
|
1248
|
+
report: (detail: string) => void,
|
|
1249
|
+
): void {
|
|
1250
|
+
const rec = (node: AstNode, parent: AstNode | undefined, inCallArg: boolean): void => {
|
|
1251
|
+
if (node !== root && isFunctionNode(node)) return;
|
|
1252
|
+
if (node.type === "Identifier") {
|
|
1253
|
+
const name = (node as Identifier).name;
|
|
1254
|
+
const isAlias = aliasNames.has(name);
|
|
1255
|
+
const isDoor = name === door;
|
|
1256
|
+
if (
|
|
1257
|
+
(isAlias || isDoor) &&
|
|
1258
|
+
!isTypePosition(parent) &&
|
|
1259
|
+
!(isDoor && inCallArg) &&
|
|
1260
|
+
!isAllowedFxUse(node, parent)
|
|
1261
|
+
) {
|
|
1262
|
+
report(describeFxEscape(name, parent, isAlias));
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
if (node.type === "CallExpression") {
|
|
1266
|
+
const call = node as CallExpression;
|
|
1267
|
+
rec(call.callee, node, false);
|
|
1268
|
+
for (const arg of call.arguments ?? []) {
|
|
1269
|
+
if (arg) rec(arg, node, true);
|
|
1270
|
+
}
|
|
1271
|
+
return;
|
|
1272
|
+
}
|
|
1273
|
+
if (VALUE_WRAPPERS.has(node.type) && inCallArg) {
|
|
1274
|
+
const inner =
|
|
1275
|
+
(node as AstNode & { expression?: AstNode }).expression ??
|
|
1276
|
+
(node as AstNode & { argument?: AstNode }).argument;
|
|
1277
|
+
if (inner) rec(inner, node, true);
|
|
1278
|
+
return;
|
|
1279
|
+
}
|
|
1280
|
+
for (const value of Object.values(node)) {
|
|
1281
|
+
if (Array.isArray(value)) {
|
|
1282
|
+
for (const item of value) {
|
|
1283
|
+
if (isAstNode(item)) rec(item, node, inCallArg);
|
|
1284
|
+
}
|
|
1285
|
+
} else if (isAstNode(value)) {
|
|
1286
|
+
rec(value, node, inCallArg);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
};
|
|
1290
|
+
rec(root, undefined, false);
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
function isAllowedFxUse(node: AstNode, parent: AstNode | undefined): boolean {
|
|
1294
|
+
if (!parent) return false;
|
|
1295
|
+
if (parent.type === "MemberExpression") {
|
|
1296
|
+
const member = parent as AstNode & { object?: AstNode; property?: AstNode; computed?: boolean };
|
|
1297
|
+
if (member.property === node && member.computed !== true) return true;
|
|
1298
|
+
if (member.object === node) return true;
|
|
1299
|
+
}
|
|
1300
|
+
if (parent.type === "VariableDeclarator" && (parent as AstNode & { id?: AstNode }).id === node) {
|
|
1301
|
+
return true;
|
|
1302
|
+
}
|
|
1303
|
+
if (
|
|
1304
|
+
(parent.type === "FunctionDeclaration" ||
|
|
1305
|
+
parent.type === "FunctionExpression" ||
|
|
1306
|
+
parent.type === "ArrowFunctionExpression") &&
|
|
1307
|
+
(parent as AstNode & { id?: AstNode }).id === node
|
|
1308
|
+
) {
|
|
1309
|
+
return true;
|
|
1310
|
+
}
|
|
1311
|
+
return false;
|
|
1312
|
+
}
|
|
1313
|
+
|
|
1314
|
+
function describeFxEscape(name: string, parent: AstNode | undefined, isAlias: boolean): string {
|
|
1315
|
+
if (isAlias) {
|
|
1316
|
+
if (parent?.type === "ReturnStatement") return `returns chain alias "${name}"`;
|
|
1317
|
+
if (parent?.type === "CallExpression") return `passes chain alias "${name}" to a call`;
|
|
1318
|
+
if (
|
|
1319
|
+
parent?.type === "ArrayExpression" ||
|
|
1320
|
+
parent?.type === "ObjectExpression" ||
|
|
1321
|
+
parent?.type === "Property" ||
|
|
1322
|
+
parent?.type === "ObjectProperty"
|
|
1323
|
+
) {
|
|
1324
|
+
return `puts chain alias "${name}" in an object or array`;
|
|
1325
|
+
}
|
|
1326
|
+
if (parent?.type === "AssignmentExpression") return `reassigns chain alias "${name}"`;
|
|
1327
|
+
return `lets chain alias "${name}" escape`;
|
|
1328
|
+
}
|
|
1329
|
+
if (parent?.type === "VariableDeclarator") return "aliases fx";
|
|
1330
|
+
if (
|
|
1331
|
+
parent?.type === "ObjectPattern" ||
|
|
1332
|
+
parent?.type === "ArrayPattern" ||
|
|
1333
|
+
parent?.type === "Property" ||
|
|
1334
|
+
parent?.type === "ObjectProperty" ||
|
|
1335
|
+
parent?.type === "RestElement"
|
|
1336
|
+
) {
|
|
1337
|
+
return "destructures fx";
|
|
1338
|
+
}
|
|
1339
|
+
return "lets fx escape";
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
function isTypePosition(parent: AstNode | undefined): boolean {
|
|
1343
|
+
if (!parent) return false;
|
|
1344
|
+
if (!parent.type.startsWith("TS")) return false;
|
|
1345
|
+
return !VALUE_WRAPPERS.has(parent.type);
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
function callsSkippingNested(body: AstNode): CallExpression[] {
|
|
1349
|
+
const out: CallExpression[] = [];
|
|
1350
|
+
const take = (node: AstNode): void => {
|
|
1351
|
+
if (node.type === "CallExpression") out.push(node as CallExpression);
|
|
1352
|
+
};
|
|
1353
|
+
take(body);
|
|
1354
|
+
walkSkipFunctions(body, take);
|
|
1355
|
+
return out;
|
|
1356
|
+
}
|
|
1357
|
+
|
|
1358
|
+
function nestedFunctions(body: AstNode): AstNode[] {
|
|
1359
|
+
const out: AstNode[] = [];
|
|
1360
|
+
walkSkipFunctions(body, (node) => {
|
|
1361
|
+
if (isFunctionNode(node)) out.push(node);
|
|
1362
|
+
});
|
|
1363
|
+
return out;
|
|
1364
|
+
}
|
|
1365
|
+
|
|
1366
|
+
function walkSkipFunctions(root: AstNode, visit: (node: AstNode) => void): void {
|
|
1367
|
+
const rec = (node: AstNode): void => {
|
|
1368
|
+
for (const value of Object.values(node)) {
|
|
1369
|
+
if (Array.isArray(value)) {
|
|
1370
|
+
for (const item of value) {
|
|
1371
|
+
if (!isAstNode(item)) continue;
|
|
1372
|
+
visit(item);
|
|
1373
|
+
if (!isFunctionNode(item)) rec(item);
|
|
1374
|
+
}
|
|
1375
|
+
} else if (isAstNode(value)) {
|
|
1376
|
+
visit(value);
|
|
1377
|
+
if (!isFunctionNode(value)) rec(value);
|
|
1378
|
+
}
|
|
1379
|
+
}
|
|
1380
|
+
};
|
|
1381
|
+
rec(root);
|
|
1382
|
+
}
|
|
1383
|
+
|
|
1384
|
+
function walkStatements(node: AstNode, visit: (node: AstNode) => void): void {
|
|
1385
|
+
if (node.type === "BlockStatement" || node.type === "Program" || node.type === "StaticBlock") {
|
|
1386
|
+
const body = (node as AstNode & { body?: AstNode[] }).body ?? [];
|
|
1387
|
+
for (const stmt of body) walkStatement(stmt, visit);
|
|
1388
|
+
return;
|
|
1389
|
+
}
|
|
1390
|
+
walkSkipFunctions(node, (child) => {
|
|
1391
|
+
if (child.type === "VariableDeclarator" || child.type === "FunctionDeclaration") visit(child);
|
|
1392
|
+
});
|
|
1393
|
+
}
|
|
1394
|
+
|
|
1395
|
+
function walkStatement(stmt: AstNode, visit: (node: AstNode) => void): void {
|
|
1396
|
+
if (stmt.type === "FunctionDeclaration") {
|
|
1397
|
+
visit(stmt);
|
|
1398
|
+
return;
|
|
1399
|
+
}
|
|
1400
|
+
if (stmt.type === "VariableDeclaration") {
|
|
1401
|
+
const declarations = (stmt as AstNode & { declarations?: AstNode[] }).declarations ?? [];
|
|
1402
|
+
for (const decl of declarations) visit(decl);
|
|
1403
|
+
return;
|
|
1404
|
+
}
|
|
1405
|
+
if (stmt.type === "BlockStatement") {
|
|
1406
|
+
walkStatements(stmt, visit);
|
|
1407
|
+
return;
|
|
1408
|
+
}
|
|
1409
|
+
if (
|
|
1410
|
+
stmt.type === "IfStatement" ||
|
|
1411
|
+
stmt.type === "ForStatement" ||
|
|
1412
|
+
stmt.type === "ForInStatement" ||
|
|
1413
|
+
stmt.type === "ForOfStatement" ||
|
|
1414
|
+
stmt.type === "WhileStatement" ||
|
|
1415
|
+
stmt.type === "DoWhileStatement" ||
|
|
1416
|
+
stmt.type === "LabeledStatement"
|
|
1417
|
+
) {
|
|
1418
|
+
const consequent = (stmt as AstNode & { consequent?: AstNode }).consequent;
|
|
1419
|
+
const alternate = (stmt as AstNode & { alternate?: AstNode }).alternate;
|
|
1420
|
+
const body = (stmt as AstNode & { body?: AstNode }).body;
|
|
1421
|
+
if (consequent) walkStatement(consequent, visit);
|
|
1422
|
+
if (alternate) walkStatement(alternate, visit);
|
|
1423
|
+
if (body && body !== consequent) walkStatement(body, visit);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (stmt.type === "TryStatement") {
|
|
1427
|
+
const block = (stmt as AstNode & { block?: AstNode }).block;
|
|
1428
|
+
const handler = (stmt as AstNode & { handler?: AstNode }).handler;
|
|
1429
|
+
const finalizer = (stmt as AstNode & { finalizer?: AstNode }).finalizer;
|
|
1430
|
+
if (block) walkStatement(block, visit);
|
|
1431
|
+
const handlerBody = (handler as (AstNode & { body?: AstNode }) | undefined)?.body;
|
|
1432
|
+
if (handlerBody) walkStatement(handlerBody, visit);
|
|
1433
|
+
if (finalizer) walkStatement(finalizer, visit);
|
|
1434
|
+
return;
|
|
1435
|
+
}
|
|
1436
|
+
if (stmt.type === "SwitchStatement") {
|
|
1437
|
+
const cases = (stmt as AstNode & { cases?: AstNode[] }).cases ?? [];
|
|
1438
|
+
for (const item of cases) {
|
|
1439
|
+
const consequent = (item as AstNode & { consequent?: AstNode[] }).consequent ?? [];
|
|
1440
|
+
for (const child of consequent) walkStatement(child, visit);
|
|
1441
|
+
}
|
|
1442
|
+
}
|
|
1443
|
+
}
|
|
1444
|
+
|
|
1445
|
+
function functionParams(fn: AstNode): AstNode[] {
|
|
1446
|
+
return (fn as AstNode & { params?: AstNode[] }).params ?? [];
|
|
1447
|
+
}
|
|
1448
|
+
|
|
1449
|
+
function paramIdentifierName(param: AstNode): string | undefined {
|
|
1450
|
+
if (param.type === "Identifier") return identifierName(param);
|
|
1451
|
+
if (param.type === "AssignmentPattern" || param.type === "RestElement") {
|
|
1452
|
+
const inner =
|
|
1453
|
+
(param as AstNode & { left?: AstNode }).left ??
|
|
1454
|
+
(param as AstNode & { argument?: AstNode }).argument;
|
|
1455
|
+
return identifierName(inner);
|
|
1456
|
+
}
|
|
1457
|
+
return undefined;
|
|
1458
|
+
}
|
|
1459
|
+
|
|
1460
|
+
function isFunctionNode(node: AstNode): boolean {
|
|
1461
|
+
return (
|
|
1462
|
+
node.type === "ArrowFunctionExpression" ||
|
|
1463
|
+
node.type === "FunctionExpression" ||
|
|
1464
|
+
node.type === "FunctionDeclaration"
|
|
1465
|
+
);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
function unwrapValue(node: AstNode | undefined): AstNode | undefined {
|
|
1469
|
+
if (!node) return undefined;
|
|
1470
|
+
return unwrapValueNode(node);
|
|
1471
|
+
}
|
|
1472
|
+
|
|
1473
|
+
function unwrapValueNode(node: AstNode | undefined): AstNode | undefined {
|
|
1474
|
+
if (!node) return undefined;
|
|
1475
|
+
let current = node;
|
|
1476
|
+
for (let i = 0; i < 8; i++) {
|
|
1477
|
+
if (!VALUE_WRAPPERS.has(current.type)) break;
|
|
1478
|
+
const next =
|
|
1479
|
+
(current as AstNode & { expression?: AstNode }).expression ??
|
|
1480
|
+
(current as AstNode & { argument?: AstNode }).argument;
|
|
1481
|
+
if (!next) break;
|
|
1482
|
+
current = next;
|
|
1483
|
+
}
|
|
1484
|
+
return current;
|
|
1485
|
+
}
|
|
1486
|
+
|
|
1487
|
+
function isAstNode(value: unknown): value is AstNode {
|
|
1488
|
+
return typeof value === "object" && value !== null && typeof (value as AstNode).type === "string";
|
|
1489
|
+
}
|